从本节开始,将逐个讲述图像分类模型的发展历程,首个就是 AlexNet,学习视频源于 Bilibili,感谢霹雳吧啦Wz,建议大家去看视频学习哦。
AlexNet 是 2012 年 ISLVRC2012 (Image Large Scale Visual Recognition Challenge) 竞赛的冠军网络,原始论文为 ImageNet Classification with Deep Convolutional Neural Networks。当时传统算法已经达到性能瓶颈,然而 AlexNet 将分类准确率由传统的 70%+ 提升到 80%+。它是由 Hinton 和他的学生 Alex Krizhevsky 设计的。也就是在那年之后,每年的 ImageNet LSVRC 挑战赛都被深度学习模型霸占着榜首,深度学习开始迅速发展。
ISLVRC2012 包括以下三部分:
它有以下几个亮点:
在具体讲解网络结构之前,补充卷积公式。经过卷积后的矩阵尺寸大小计算公式为: N = ( W − F + 2 P ) / S + 1 N = (W - F + 2 P) / S + 1 N=(W−F+2P)/S+1。
AlexNet 网络结构如下所示:
因为原作者使用两块 GPU 并行训练,所以画了一摸一样的两行,其实我们看其中一行就可以了。网络第一层可以看到,原始图像为 224 × 224 × 3 224 \times 224 \times 3 224×224×3 的 RGB 彩色图像。网络第一层为 11 × 11 11 \times 11 11×11 大小卷积核的卷积层,卷积核个数为 96。第一层卷积之后的输出为 55 × 55 × 96 55 \times 55 \times 96 55×55×96,所以我们可以反推出 padding = [1, 2],左边上边加一列 0,右边下边加两列 0。
然后就是经过了一个 Maxpooling 层,kernel size 为 3 × 3 3 \times 3 3×3,padding = 0,stride = 2。此后的输出就是 27 × 27 × 96 27 \times 27 \times 96 27×27×96。(原始论文并没有提到 padding 和 stride 这些细节,但是看官方实现可以了解到)
然后又是一个卷积层,输出通道为 256,kernel size 为 5,padding 为 [2,2],stride = 1。可以计算得到输出特征图为 27 × 27 × 256 27 \times 27 \times 256 27×27×256。
然后就是经过了一个 Maxpooling 层,kernel size 为 3 × 3 3 \times 3 3×3,padding = 0,stride = 2。此后的输出就是 13 × 13 × 256 13 \times 13 \times 256 13×13×256。
接下来是第三个卷积层,输出通道为 384,kernel size 为 3 × 3 3 \times 3 3×3,padding = 1,stride = 1,也就是保大小的卷积。输出特征图为 13 × 13 × 384 13 \times 13 \times 384 13×13×384。
第四个卷积层和第三个卷积层是一模一样的,只不过输入通道从 256 变成了 384。输出特征图为 13 × 13 × 384 13 \times 13 \times 384 13×13×384。
第五个卷积层输出通道为 256,kernel size 为 3 × 3 3 \times 3 3×3,padding = 1,stride = 1,也是保大小的卷积。输出特征图为 13 × 13 × 256 13 \times 13 \times 256 13×13×256。
之后经过了一个 Maxpooling 层,kernel size 为 3 × 3 3 \times 3 3×3,padding = 0,stride = 2。此后的输出就是 6 × 6 × 256 6 \times 6 \times 256 6×6×256。
此后就展平特征图,经过两个 4096 个节点的全连接层,最后再通过一个 1000 个节点(ImageNet 是 1000 分类)的全连接层获得输出就可以了。注意到,所有卷积层后面其实都接了一个激活函数 ReLU 层。在全连接层的前两层中使用了 Dropout 随机失活神经元操作。
本章节参考博文 深入理解AlexNet网络,并加入了部分个人理解。
在神经网络中,我们用激活函数将神经元的输出做一个非线性映射,但是 tanh 和 sigmoid 这些传统的激活函数的值域都是有范围的,但是 ReLU 激活函数得到的值域没有一个区间,所以要对 ReLU 得到的结果进行归一化。也就是 Local Response Normalization。局部响应归一化的方法如下面的公式:
b ( x , y ) i = a ( x , y ) i ( k + α ∑ j = max ( 0 , i − n / 2 ) min ( N − 1 , i + n / 2 ) ( a ( x , y ) j ) 2 ) β b_{(x, y)}^{i}=\frac{a_{(x, y)}^{i}}{\left(k+\alpha \sum_{j=\max (0, i-n / 2)}^{\min (N-1, i+n / 2)}\left(a_{(x, y)}^{j}\right)^{2}\right)^{\beta}} b(x,y)i=(k+α∑j=max(0,i−n/2)min(N−1,i+n/2)(a(x,y)j)2)βa(x,y)i
在公式中, a ( x , y ) i a^i_{(x,y)} a(x,y)i 代表的是 ReLU 在第 i 个 kernel 的 (x, y) 位置的输出,n 表示的是, a ( x , y ) i a^i_{(x,y)} a(x,y)i 的邻居个数,N 表示该 kernel 的总数量。 b ( x , y ) i b^i_{(x,y)} b(x,y)i 表示的是 LRN 的结果。ReLU 输出的结果和它周围一定范围的邻居做一个局部的归一化,这里有点类似域我们的最大最小归一化,即假设有一个向量 X = [ x 1 , x 2 , . . . , x n ] X = [x_1, x_2, ...,x_n] X=[x1,x2,...,xn],那么将所有的数归一化到 0-1 之间的归一化规则是 x i = ( x i − x m i n ) / ( x m a x − x m i n ) x_i = (x_i - x_{min}) / (x_{max} - x_{min}) xi=(xi−xmin)/(xmax−xmin)。而上面的式子则是在通道层面进行归一化,有一些其他的参数 α , β , k \alpha, \beta, k α,β,k。论文中说在验证集中确定,最终确定的结果为: k = 2 , n = 5 , α = 1 0 − 4 , β = 0.75 k = 2 , n = 5 , \alpha = 10^{-4}, \beta = 0.75 k=2,n=5,α=10−4,β=0.75。
上图,每一个矩形表示的一个卷积核生成的 feature map。所有的元素已经经过了 ReLU 激活函数,现在我们都要对具体的 pixel 进行局部的归一化。假设绿色箭头指向的是第 i 个 kernel 对应的特征图,其余的四个蓝色箭头是它周围的邻居 kernel 层对应的特征图,假设矩形中间的绿色的元素的位置为 (x, y),那么我需要提取出来进行局部归一化的数据就是周围邻居 kernel 对应的特征图的 (x, y) 位置的 pixel 的值。也就是上面式子中 a ( x , y ) j a^j_{(x,y)} a(x,y)j 。然后把这些邻居元素的值平方再加和。乘以一个系数 α \alpha α 再加上一个常数 k (防止分母为 0),然后 β \beta β 次幂,最终得到分母。分子就是第 i 个 kernel 对应的特征图的 (x, y) 位置的元素值。
但是我觉得,这有个假设在于连续通道之间有一定的关系,然而我们知道,卷积层中连续通道不一定有”连续“意义,就像全连接层中相邻节点一样。所以这样的领域有待进一步思考!
一般的池化层因为没有重叠,所以 pool size 和 stride 一般是相等的,例如 8 × 8 的一个图像,如果池化层的尺寸是 2 × 2,那么经过池化后的操作得到的图像是 4 × 4 大小的,这种设置叫做不覆盖的池化操作。如果 stride < pool size, 那么就会产生覆盖的池化操作,这种有点类似于 convolutional 化的操作,这样可以得到更准确的结果。甚至在之前讲述的 YOLOv3 的 SPP 模块中,使用 stride 为 1 的 maxpooling,我其实认为获得的是超像素!下图以图像超像素为例:
超像素最直观的解释,便是把一些具有相似特性的像素“聚合”起来,形成一个更具有代表性的大“元素”。而这个新的元素,将作为其他图像处理算法的基本单位。
论文中说,在训练模型过程中,覆盖的池化层更不容易过拟合。在 top-1,和 top-5 中使用覆盖的池化操作分别将 error rate 降低了 0.4% 和 0.3%。
文章使用 Random Crop、flip 从而上千倍的扩充训练样本的数量,也使得随机裁剪成为通用方法。具体做法就是首先将图片 resize 到 256 x 256大小,然后从中随机裁剪出 224 x 224 大小的 patch 训练,测试时就从四个角及中心 crop,然后水平翻转图像形成 10 个测试图片,然后对于这 10 个结果取一下平均值(这种方法在后来的去马赛克任务中也有用,将图片顺时针转动四次,然后恢复结果转动回来取平均,其实有点模型集成投票的意思,避免了训练多个模型)。另一种增强为转换RGB 通道的强度,在之后的模型中很少用到。对于该模型,数据增强大约降低了 1% 错误率。
import torch.nn as nn
import torch
class AlexNet(nn.Module):
def __init__(self, num_classes=1000, init_weights=False):
super(AlexNet, self).__init__()
self.features = nn.Sequential(
nn.Conv2d(3, 96, kernel_size=11, stride=4, padding=2), # input[3, 224, 224] output[48, 55, 55]
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2), # output[96, 27, 27]
nn.Conv2d(96, 256, kernel_size=5, padding=2), # output[256, 27, 27]
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2), # output[256, 13, 13]
nn.Conv2d(256, 384, kernel_size=3, padding=1), # output[384, 13, 13]
nn.ReLU(inplace=True),
nn.Conv2d(384, 384, kernel_size=3, padding=1), # output[384, 13, 13]
nn.ReLU(inplace=True),
nn.Conv2d(384, 256, kernel_size=3, padding=1), # output[256, 13, 13]
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2), # output[256, 6, 6]
)
self.classifier = nn.Sequential(
nn.Dropout(p=0.5),
nn.Linear(256 * 6 * 6, 4096),
nn.ReLU(inplace=True),
nn.Dropout(p=0.5),
nn.Linear(4096, 4096),
nn.ReLU(inplace=True),
nn.Linear(4096, num_classes),
)
if init_weights:
self._initialize_weights()
def forward(self, x):
x = self.features(x)
x = torch.flatten(x, start_dim=1)
x = self.classifier(x)
return x
def _initialize_weights(self):
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
if m.bias is not None:
nn.init.constant_(m.bias, 0)
elif isinstance(m, nn.Linear):
nn.init.normal_(m.weight, 0, 0.01)
nn.init.constant_(m.bias, 0)