一、AlexNet网络
(1)学习特征:在具有一定复杂度的神经网络中,图像特征应该是由多个共同学习的神经网络组成,每一层都会有可学参数,例如在机器视觉中,最底层可能会检测出边缘颜色等信息,更高层则建立在这些底层表示的基础上提取更负责的特征,最终学习出图像的综合表示。
(2)数据:lenet训练的数据集共有7万,对于深度神经网络训练明显不足,能够提取许多特征的深度模型需要大量的有标签数据。ImageNet数据集发布提供了深度模型发展的条件。
(3)网络模型:与lenet网络相似,ALexNet由五个卷积三个全连接组成。实现与结构如图所示:
net = nn.Sequential(
nn.Conv2d(1, 96, kernel_size=11, stride=4, padding=1), nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2),
nn.Conv2d(96, 256, kernel_size=5, padding=2), nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2),
nn.Conv2d(256, 384, kernel_size=3, padding=1), nn.ReLU(),
nn.Conv2d(384, 384, kernel_size=3, padding=1), nn.ReLU(),
nn.Conv2d(384, 256, kernel_size=3, padding=1), nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2),
nn.Flatten(),
nn.Linear(6400, 4096), nn.ReLU(),
nn.Dropout(p=0.5),
nn.Linear(4096, 4096), nn.ReLU(),
nn.Dropout(p=0.5),
nn.Linear(4096, 10))
(4)激活函数、容量控制和预处理
与lenet不同,该模型选用的relu函数,ReLU激活函数的计算更简单,当使用不同的参数初始化方法时,ReLU激活函数使训练模型更加容易。
为了控制模型复杂度,该模型在全连接层后加入了Dropout层。
为了使得模型更健壮,AlexNet在训练时增加了大量的图像增强数据,如裁剪等,更大的样本量有效地减少了过拟合。
二、VGG神经网络
基于AlexNet的后三层卷积层和模块化的思想设计出VGG网络。
(1)VGG块:由指定卷积层数的网络块最后加窗口为2X2、步幅为2的最大值池化层构成。每一个网络块由一个3X3的卷积核、padding为1的卷积层再加relu激活函数组成。实现与结构如下:
def vgg_block(num_convs, in_channels, out_channels):
layers = []
for _ in range(num_convs):
layers.append(nn.Conv2d(in_channels, out_channels,
kernel_size=3, padding=1))
layers.append(nn.ReLU())
in_channels = out_channels
layers.append(nn.MaxPool2d(kernel_size=2,stride=2))
return nn.Sequential(*layers)
(2)VGG网络:由指定数的VGG块组成,最后加三个全连接层。实现与结构如下:
conv_arch = ((1, 64), (1, 128), (2, 256), (2, 512), (2, 512))#指定每个块的参数
def vgg(conv_arch):
conv_blks = []#存储卷积层
in_channels = 1
for (num_convs, out_channels) in conv_arch:
conv_blks.append(vgg_block(num_convs, in_channels, out_channels))
in_channels = out_channels
return nn.Sequential(
*conv_blks, nn.Flatten(),
nn.Linear(out_channels * 7 * 7, 4096), nn.ReLU(), nn.Dropout(0.5),
nn.Linear(4096, 4096), nn.ReLU(), nn.Dropout(0.5),
nn.Linear(4096, 10))
net = vgg(conv_arch)
三、NiN神经网络
与VGG相似,NIN使用了1X1的卷积神经网络,更加多的表示了空间结构。
(1)NiN块:在每个像素位置应用一个全连接层( 1×1 卷积层),做到将空间维度中的每个像素视为单个样本,将通道维度视为不同特征。与VGG块设计思路相同,其由一个指定卷积核大小的卷积层和两个1×1 卷积层组成。实现与结构如下:
def nin_block(in_channels, out_channels, kernel_size, strides, padding):
return nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernel_size, strides, padding),
nn.ReLU(),
nn.Conv2d(out_channels, out_channels, kernel_size=1), nn.ReLU(),
nn.Conv2d(out_channels, out_channels, kernel_size=1), nn.ReLU())
(2)NiN网络:由指定NiN块组成,在每一块(除最后)后加上窗口形状为 3×3,步幅为 2的最大值池化层,最后一块的输出通道必须与标签类别数相同,最后使用全局平均池化层。实现与结构如下:
net = nn.Sequential(
nin_block(1, 96, kernel_size=11, strides=4, padding=0),
nn.MaxPool2d(3, stride=2),
nin_block(96, 256, kernel_size=5, strides=1, padding=2),
nn.MaxPool2d(3, stride=2),
nin_block(256, 384, kernel_size=3, strides=1, padding=1),
nn.MaxPool2d(3, stride=2),
nn.Dropout(0.5),
nin_block(384, 10, kernel_size=3, strides=1, padding=1),
nn.AdaptiveAvgPool2d((1, 1)),
nn.Flatten())
四、GoogLeNet神经网络
结合NiN中串联网络的思想,有时不同的卷积核大小效果不同,该模型在每一层则使用了不同的卷积核。
(1) Inception块:由四条并行路径组成,前三条路径使用窗口大小为 1×1、3×3 和 5×5 的卷积层,从不同空间大小中提取信息。中间的两条路径在输入上执行 1×1 卷积,以减少通道数,从而降低模型的复杂性。第四条路径使用 3×3 最大池化层,然后使用 1×1 卷积层来改变通道数。 最后四条路在通道维度合并。实现与结构如下:
class Inception(nn.Module):
def __init__(self, in_channels, c1, c2, c3, c4, **kwargs):
super(Inception, self).__init__(**kwargs)
self.p1_1 = nn.Conv2d(in_channels, c1, kernel_size=1)
self.p2_1 = nn.Conv2d(in_channels, c2[0], kernel_size=1)
self.p2_2 = nn.Conv2d(c2[0], c2[1], kernel_size=3, padding=1)
self.p3_1 = nn.Conv2d(in_channels, c3[0], kernel_size=1)
self.p3_2 = nn.Conv2d(c3[0], c3[1], kernel_size=5, padding=2)
self.p4_1 = nn.MaxPool2d(kernel_size=3, stride=1, padding=1)
self.p4_2 = nn.Conv2d(in_channels, c4, kernel_size=1)
def forward(self, x):
p1 = F.relu(self.p1_1(x))
p2 = F.relu(self.p2_2(F.relu(self.p2_1(x))))
p3 = F.relu(self.p3_2(F.relu(self.p3_1(x))))
p4 = F.relu(self.p4_2(self.p4_1(x)))
return torch.cat((p1, p2, p3, p4), dim=1)
(2)GoogLeNet模型: 9 个Inception块和全局平均池化层,最后使用全连接层输出。实现与结构如下:
b1 = nn.Sequential(nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3),
nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1))
b2 = nn.Sequential(nn.Conv2d(64, 64, kernel_size=1),
nn.ReLU(),
nn.Conv2d(64, 192, kernel_size=3, padding=1),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1))
b3 = nn.Sequential(Inception(192, 64, (96, 128), (16, 32), 32),
Inception(256, 128, (128, 192), (32, 96), 64),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1))
b4 = nn.Sequential(Inception(480, 192, (96, 208), (16, 48), 64),
Inception(512, 160, (112, 224), (24, 64), 64),
Inception(512, 128, (128, 256), (24, 64), 64),
Inception(512, 112, (144, 288), (32, 64), 64),
Inception(528, 256, (160, 320), (32, 128), 128),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1))
b5 = nn.Sequential(Inception(832, 256, (160, 320), (32, 128), 128),
Inception(832, 384, (192, 384), (48, 128), 128),
nn.AdaptiveAvgPool2d((1,1)),
nn.Flatten())
net = nn.Sequential(b1, b2, b3, b4, b5, nn.Linear(1024, 10))
五、批量归一化
在深层的神经网络中,反向传播过程中顶层网络梯度合适能够有效收敛,但是底层网络因为梯度的传递在不断缩小导致可能会梯度消失,最终导致底层网络收敛过慢,而且底层网络一边,顶层就会跟着一起变。批量归一化就是解决这个问题:批量归一化利用小批量的均值和标准差,不断调整神经网络的中间输出,使整个神经网络各层的中间输出值更加稳定。
(1)原理:在每次训练迭代中,我们首先归一化输入,即通过减去其均值并除以其标准差,其中两者均基于当前小批量处理。 接下来,我们应用比例系数和比例偏移。BN(x)=γ*(x−μ^B)σ^B+β.注意的是使用大小为 1 的小批量应用批量归一化,我们将无法学到任何东西。 这是因为在减去均值之后,每个隐藏单元将为 0。所以,只有使用足够大的小批量,批量归一化这种方法才是有效且稳定的。
(2)作用在全连接层和卷积层的输出上,激活函数前,也可作用在全连接层和卷积层输入上。对于全连接层作用在特征维,对与卷积层作用在通道维。
(3)实现
#从零实现
# batch_norm函数
def batch_norm(X, gamma, beta, moving_mean, moving_var, eps, momentrun):
# momentrun更新mean和var,eps为了避免除0
if not torch.is_grad_enabled():
X_hat = (X - moving_mean) / torch.sqrt(moving_var + eps)
else:
assert len(X.shape) in (2, 4) # 要么全连接层,要么卷积层
if len(X.shape) == 2:
mean = X.mean(dim=0)
var = ((X - mean) ** 2).mean(dim=0)
else:
mean = X.mean(dim=(0, 2, 3), keepdim=True) # 每个通道求,得出(1,n,1,1)
var = ((X - mean) ** 2).mean(dim=(0, 2, 3), keepdim=True)
X_hat = (X - mean) / torch.sqrt(var + eps)
moving_mean = momentrun * moving_mean + (1.0 - momentrun) * mean
moving_var = momentrun * moving_var + (1.0 - momentrun) * var
Y = gamma * X_hat + beta
return Y, moving_mean.data, moving_var.data
# batch_norm层
class BatchNorm(nn.Module):
def __init__(self, num_features, num_dims):
super().__init__()
if num_dims == 2:
shape = (1, num_features)
else:
shape = (1, num_features, 1, 1)
self.gamma = nn.Parameter(torch.ones(shape))
self.beta = nn.Parameter(torch.zeros(shape))
self.moving_mean = torch.zeros(shape)
self.moving_var = torch.ones(shape)
def forward(self, X):
if self.moving_mean.device != X.device:
self.moving_mean = self.moving_mean.to(X.device)
self.moving_var = self.moving_var.to(X.device)
Y, self.moving_mean, self.moving_var = batch_norm(X, self.gamma, self.beta, self.moving_mean, self.moving_var,
eps=1e-5, momentrun=0.9)
return Y
net = nn.Sequential(
nn.Conv2d(1, 6, kernel_size=5), BatchNorm(6, num_dims=4),
nn.Sigmoid(), nn.MaxPool2d(kernel_size=2, stride=2),
nn.Conv2d(6, 16, kernel_size=5), BatchNorm(16, num_dims=4),
nn.Sigmoid(), nn.MaxPool2d(kernel_size=2, stride=2),
nn.Flatten(), nn.Linear(16 * 4 * 4, 120), BatchNorm(120, num_dims=2),
nn.Sigmoid(), nn.Linear(120, 84), BatchNorm(84, num_dims=2),
nn.Sigmoid(), nn.Linear(84, 10)
)
#简洁实现
net = nn.Sequential(
nn.Conv2d(1, 6, kernel_size=5), nn.BatchNorm2d(6),
nn.Sigmoid(), nn.MaxPool2d(kernel_size=2, stride=2),
nn.Conv2d(6, 16, kernel_size=5), nn.BatchNorm2d(16),
nn.Sigmoid(), nn.MaxPool2d(kernel_size=2, stride=2),
nn.Flatten(), nn.Linear(16 * 4 * 4, 120), nn.BatchNorm2d(120),
nn.Sigmoid(), nn.Linear(120, 84), nn.BatchNorm2d(84),
nn.Sigmoid(), nn.Linear(84, 10)
)
六、残差网络ResNet
(1)原理:对于一个具有更深层次的卷积神经网络,因为模型过于复杂导致过拟合,训练效果不如浅一些的神经网络,根据这一问题提出残差网络,使用嵌套模式增加一条线路使得当前训练效果不低于之前的训练。
(2)残差快:2 个有相同输出通道数的 3×3 卷积层,每个卷积层后接一个批量归一化层和 ReLU 激活函数。然后通过跨层数据通路,跳过这 2 个卷积运算,将输入直接加在最后的 ReLU 激活函数前。如果想改变通道数,就需要引入一个额外的 1×1 卷积层来将输入变换成需要的形状后再做相加运算。实现与结构如下:
class Residual(nn.Module):
def __init__(self, input_channels, num_channels,
use_1x1conv=False, strides=1):
#use_1x1conv=False是否改变通道数
super().__init__()
self.conv1 = nn.Conv2d(input_channels, num_channels,
kernel_size=3, padding=1, stride=strides)
self.conv2 = nn.Conv2d(num_channels, num_channels,
kernel_size=3, padding=1)
if use_1x1conv:
self.conv3 = nn.Conv2d(input_channels, num_channels,
kernel_size=1, stride=strides)
else:
self.conv3 = None
self.bn1 = nn.BatchNorm2d(num_channels)
self.bn2 = nn.BatchNorm2d(num_channels)
self.relu = nn.ReLU(inplace=True)
def forward(self, X):
Y = F.relu(self.bn1(self.conv1(X)))
Y = self.bn2(self.conv2(Y))
if self.conv3:
X = self.conv3(X)
Y += X
return F.relu(Y)
(3)模型:前两层跟之前介绍的 GoogLeNet 中的一样: 在输出通道数为 64、步幅为 2 的 7×7 卷积层后,接步幅为 2 的 3×3 的最大池化层。 不同之处在于 ResNet 每个卷积层后增加了批量归一化层。 然后使用 4 个由残差块组成的模块,每个模块使用若干个同样输出通道数的残差块。 第一个模块的通道数同输入通道数一致。之后的每个模块在第一个残差块里将上一个模块的通道数翻倍,并将高和宽减半。最后,与 GoogLeNet 一样,在 ResNet 中加入全局平均汇聚层,以及全连接层输出。实现与结构如下:
def resnet_block(input_channels, num_channels, num_residuals,
first_block=False):
blk = []
for i in range(num_residuals):
if i == 0 and not first_block:
blk.append(Residual(input_channels, num_channels,
use_1x1conv=True, strides=2))
else:
blk.append(Residual(num_channels, num_channels))
return blk
b2 = nn.Sequential(*resnet_block(64, 64, 2, first_block=True))
b3 = nn.Sequential(*resnet_block(64, 128, 2))
b4 = nn.Sequential(*resnet_block(128, 256, 2))
b5 = nn.Sequential(*resnet_block(256, 512, 2))
(4)resnet能训练深层次的原因:传统的深度网络计算梯度时,如图中y所示,是通过相乘,这就导致有一个模块梯度特别小时,会导致之后的梯度也会消失。resnet则将乘法换成了加法,如图y'所示,当某一块特别小时后边的任然可以由一个相对合适的梯度用来更新。
七、猫狗大战
在上一次的基础上,修改网络模型,因为gpu内存太小,batchsize改为32,参照resnet18使用了14层网络,取消了第二个残差块(调试使用前10层时,网络效果不好,猜测时因为前10层最终通道数只有128,提取特征不够)。修改代码如下:
class Residual(nn.Module):
def __init__(self, input_channels, num_channels,
use_1x1conv=False, strides=1):
super().__init__()
self.conv1 = nn.Conv2d(input_channels, num_channels,
kernel_size=3, padding=1, stride=strides)
self.conv2 = nn.Conv2d(num_channels, num_channels,
kernel_size=3, padding=1)
if use_1x1conv:
self.conv3 = nn.Conv2d(input_channels, num_channels,
kernel_size=1, stride=strides)
else:
self.conv3 = None
self.bn1 = nn.BatchNorm2d(num_channels)
self.bn2 = nn.BatchNorm2d(num_channels)
self.relu = nn.ReLU(inplace=True)
def forward(self, X):
Y = F.relu(self.bn1(self.conv1(X)))
Y = self.bn2(self.conv2(Y))
if self.conv3:
X = self.conv3(X)
Y += X
return F.relu(Y)
def resnet_block(input_channels, num_channels, num_residuals,
first_block=False):
blk = []
for i in range(num_residuals):
if i == 0 and not first_block:
blk.append(Residual(input_channels, num_channels,
use_1x1conv=True, strides=2))
else:
blk.append(Residual(num_channels, num_channels))
return blk
class net(nn.Module):
def __init__(self):
super().__init__()
self.b1 = nn.Sequential(
nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3),
nn.BatchNorm2d(64),
nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
)
# self.b2 = nn.Sequential(*resnet_block(64, 64, 2, first_block=True))
self.b3 = nn.Sequential(*resnet_block(64, 128, 2))
self.b4 = nn.Sequential(*resnet_block(128, 256, 2))
self.b5 = nn.Sequential(*resnet_block(256, 512, 2))
self.bf = nn.Sequential(
nn.AdaptiveAvgPool2d((1, 1)),
nn.Flatten(),
nn.Linear(512, 2)
)
def forward(self, X):
X = self.b1(X)
# X = self.b2(X)
X = self.b3(X)
X = self.b4(X)
X = self.b5(X)
X = self.bf(X)
return X
如果图:跑了50次,最终准确率在90%上下