对于CNN(卷积神经网络)最早可以追溯到1986年BP算法的提出,然后1989年LeCun将其用到多层神经网络中,直到1998年LeCun提出LeNet-5模型,神经网络的雏形完成。第一个典型的CNN就是LeNet5网络结构,但是今天我们要讲的主角是AlexNet也就是文章《ImageNet Classification with Deep Convolutional Neural Networks》所介绍的网络结构。Alex等人在2012年提出的AlexNet网络结构模型在ILSVRC-2012上以巨大的优势获得第一名,引爆了神经网络的应用热潮,使得卷积神经网络CNN成为在图像分类上的核心算法模型。这篇论文阐述了一个多层卷积网络,目标是将120万高分辨率的图像分成1000类。
Net Structure-
AlexNet首先用一张227×227×3的图片作为输入,实际上原文中使用的图像是224×224×3,但是如果你尝试去推导一下,你会发现227×227这个尺寸更好一些。
-
第一层我们使用96个11×11的过滤器,步幅为4,由于步幅是4,因此尺寸缩小到55×55,缩小了4倍左右。然后用一个3×3的过滤器构建最大池化层,f=3,步幅s为2,卷积层尺寸缩小为27×27×96。接着再执行一个5×5的卷积,padding之后,输出是27×27×256。然后再次进行最大池化,尺寸缩小到13×13。再执行一次same卷积,相同的padding,得到的结果是13×13×384,用384个过滤器再做一次same卷积,再做一次同样的操作,最后再进行一次最大池化,尺寸缩小到6×6×256。6×6×256等于9216,将其展开为9216个单元,
-
然后是三层全连接层,最后使用softmax函数输出识别的结果,看它究竟是1000个可能的对象中的哪一个。
-
第2,4,5卷积层的神经元只与位于同一GPU上的前一层的核映射相连接。第3卷积层的核与第2层的所有核映射相连。全连接层的神经元与前一层的所有神经元相连。第1,2卷积层之后是响应归一化层。最大池化层加在了响应归一化层和第5卷积层之后。ReLU非线性应用在每个卷积层和全连接层的输出上。
-
最终整个网络包含5个卷积层和3个全连接层,深度似乎是非常重要的:移除任何卷积层(每个卷积层包含的参数不超过模型参数的1%)都会导致更差的性能。
-
ReLU
-
激活函数采用ReLU,而不是tanh和sigmoid函数,与饱和神经元相比,有ReLU的网络学习速度要快好几倍
-
-
Training on Multiple GPUs
-
在写这篇论文的时候,GPU的处理速度还比较慢,所以AlexNet采用了非常复杂的方法在两个GPU上进行训练。大致原理是,这些层分别拆分到两个不同的GPU上,同时还专门有一个方法用于两个GPU进行交流。
-
Local Response Normalization
-
“局部响应归一化层”(Local Response Normalization),即LRN层。局部响应归一层的基本思路是,假如这是网络的一块,比如是13×13×256,LRN要做的就是选取一个位置,从这个位置穿过整个通道,能得到256个数字,并进行归一化。进行局部响应归一化的动机是,对于这张13×13的图像中的每个位置来说,我们可能并不需要太多的高激活神经元。但是后来,很多研究者发现LRN起不到太大作用,因此我们现在并不用LRN来训练网络。
-
-
Overlapping Pooling
-
一般的池化层因为没有重叠,所以poolsize 和 stride一般是相等的,例如8×8的一个图像,如果池化层的尺寸是2×2 ,那么经过池化后的操作得到的图像是 4×4 大小,这种设置叫做不覆盖的池化操作,如果 stride < poolsize, 那么就会产生覆盖的池化操作,这种有点类似于convolutional化的操作,这样可以得到更准确的结果。论文指出,在训练模型过程中,覆盖的池化层更不容易过拟合。
-
-
Data Augmentation 数据扩充是防止过拟合的一种简单方法,只需要对原始的数据进行合适的变换,就会得到更多有差异的数据集,防止过拟合。本文中主要采用了两种方法来进行数据扩充,这两种方式都是从原始图像通过非常少的计算量产生变换的图像,因此变换图像不需要存储在硬盘上。
-
第一种数据增强方式包括产生图像平移和水平翻转。我们从256× 256图像上通过随机提取224 × 224的图像块(以及这些图像块的水平翻转)实现了这种方式,然后在这些提取的图像块上进行训练。在测试时,网络会提取5个224 × 224的图像块(四个角上的图像块和中心的图像块)和它们的水平翻转(因此总共10个图像块)进行预测,然后对网络在10个图像块上的softmax层的预测结果进行平均。
-
第二种数据增强方式是改变训练图像的RGB通道的强度。在整个ImageNet训练集上对RGB像素值集合执行主成分分析(PCA)。对于每幅训练图像,我们加上多倍找到的主成分,大小成正比的对应特征值乘以一个随机变量,这个随机变量通过均值为0,标准差为0.1的高斯分布得到。这个方案近似抓住了自然图像的一个重要特性,即光照的强度和颜色发生变化时,物体本身没有发生变化。
-
-
Dropout
-
以0.5的概率对每个隐层神经元的输出设为0。那些用这种方式“丢弃”的神经元不再进行前向传播并且不参与反向传播。因此每次输入时,神经网络会采样一个不同的架构,但所有架构共享权重。这个技术减少了复杂的神经元互适应,因为一个神经元不能依赖特定的其它神经元的存在。
-
在前两层全连接层使用Dropout。
-
该技术大致上使要求收敛的迭代次数翻了一倍。
-
-
Details of Learning
-
使用随机梯度下降来训练模型,样本的batch size为128,动量为0.9,权重衰减率为0.0005。少量的权重衰减对于模型的学习是重要的。换句话说,权重衰减不仅仅是一个正则项:而且它减少了模型的训练误差。权重的更新规则:
-
i是迭代索引,v是动量变量,ϵ是学习率, 是目标函数对w,在wi上的第i批微分Di的平均。
-
使用均值为0,标准差为0.01的高斯分布对每一层的权重进行初始化。在第2,4,5卷积层和全连接隐层将神经元偏置初始化为常量1。这个初始化通过为ReLU提供正输入加速了早期阶段的学习,对剩下的层的神经元偏置初始化为0。
-
当验证误差在当前的学习率下停止改善时,遵循启发式的方法将学习率除以10。
-
-
class AlexNet(object): def __init__(self, x, keep_prob, num_classes, skip_layer, weights_path='DEFAULT'): """ Create the graph of the AlexNet model. Args: x: Placeholder for the input tensor. keep_prob: Dropout probability. num_classes: Number of classes in the dataset. skip_layer: List of names of the layer, that get trained from scratch weights_path: Complete path to the pretrained weight file, if it isn't in the same folder as this code """ # Parse input arguments into class variables self.X = x self.NUM_CLASSES = num_classes self.KEEP_PROB = keep_prob self.SKIP_LAYER = skip_layer if weights_path == 'DEFAULT': self.WEIGHTS_PATH = 'bvlc_alexnet.npy' else: self.WEIGHTS_PATH = weights_path # Call the create function to build the computational graph of AlexNet self.create() def create(self): # 1st Layer: Conv (w ReLu) -> Lrn -> Pool conv1 = conv(self.X, 11, 11, 96, 4, 4, padding='VALID', name='conv1') norm1 = lrn(conv1, 2, 1e-04, 0.75, name='norm1') pool1 = max_pool(norm1, 3, 3, 2, 2, padding='VALID', name='pool1') # 2nd Layer: Conv (w ReLu) -> Lrn -> Pool with 2 groups conv2 = conv(pool1, 5, 5, 256, 1, 1, groups=2, name='conv2') norm2 = lrn(conv2, 2, 1e-04, 0.75, name='norm2') pool2 = max_pool(norm2, 3, 3, 2, 2, padding='VALID', name='pool2') # 3rd Layer: Conv (w ReLu) conv3 = conv(pool2, 3, 3, 384, 1, 1, name='conv3') # 4th Layer: Conv (w ReLu) splitted into two groups conv4 = conv(conv3, 3, 3, 384, 1, 1, groups=2, name='conv4') # 5th Layer: Conv (w ReLu) -> Pool splitted into two groups conv5 = conv(conv4, 3, 3, 256, 1, 1, groups=2, name='conv5') pool5 = max_pool(conv5, 3, 3, 2, 2, padding='VALID', name='pool5') # 6th Layer: Flatten -> FC (w ReLu) -> Dropout flattened = tf.reshape(pool5, [-1, 6*6*256]) fc6 = fc(flattened, 6*6*256, 4096, name='fc6') dropout6 = dropout(fc6, self.KEEP_PROB) # 7th Layer: FC (w ReLu) -> Dropout fc7 = fc(dropout6, 4096, 4096, name='fc7') dropout7 = dropout(fc7, self.KEEP_PROB) # 8th Layer: FC and return unscaled activations self.fc8 = fc(dropout7, 4096, self.NUM_CLASSES, relu=False, name='fc8') def load_initial_weights(self, session): # Load the weights into memory weights_dict = np.load(self.WEIGHTS_PATH, encoding='bytes').item() # Loop over all layer names stored in the weights dict for op_name in weights_dict: # Check if layer should be trained from scratch if op_name not in self.SKIP_LAYER: with tf.variable_scope(op_name, reuse=True): # Assign weights/biases to their corresponding tf variable for data in weights_dict[op_name]: # Biases if len(data.shape) == 1: var = tf.get_variable('biases', trainable=False) session.run(var.assign(data)) # Weights else: var = tf.get_variable('weights', trainable=False) session.run(var.assign(data))