卷积神经网络基础
二维卷积层
二维互相关运算
二维互相关(cross-correlation)运算的输入是一个二维输入数组和一个二维核(kernel)数组,输出也是一个二维数组,其中核数组通常称为卷积核或过滤器(filter)。卷积核的尺寸通常小于输入数组,卷积核在输入数组上滑动,在每个位置上,卷积核与该位置处的输入子数组按元素相乘并求和,得到输出数组中相应位置的元素。
二维卷积层
二维卷积层将输入和卷积核做互相关运算,并加上一个标量偏置来得到输出。卷积层的模型参数包括卷积核和标量偏置。
互相关运算与卷积运算
卷积层得名于卷积运算,但卷积层中用到的并非卷积运算而是互相关运算。我们将核数组上下翻转、左右翻转,再与输入数组做互相关运算,这一过程就是卷积运算。由于卷积层的核数组是可学习的,所以使用互相关运算与使用卷积运算并无本质区别。
特征图与感受野
二维卷积层输出的二维数组可以看作是输入在空间维度(宽和高)上某一级的表征,也叫特征图(feature map)。影响元素的前向计算的所有可能输入区域(可能大于输入的实际尺寸)叫做的感受野(receptive field)。
填充和步幅
填充(padding)
填充(padding)是指在输入高和宽的两侧填充元素(通常是0元素)。
如果原输入的高和宽是和,卷积核的高和宽是和,在高的两侧一共填充行,在宽的两侧一共填充列,则输出形状为:
对于高度(或宽度)为大小为的卷积核,令步幅为1,在高(或宽)两侧选择大小为的填充,便可保持输入与输出尺寸相同。
步幅(stride)
在互相关运算中,卷积核在输入数组上滑动,每次滑动的行数与列数即是步幅(stride)。
一般来说,当高上步幅为,宽上步幅为时,输出形状为:
多输入通道和多输出通道
多输入通道
彩色图像有RGB(红、绿、蓝)3个颜色通道。
假设输入数据的通道数为,卷积核形状为,我们为每个输入通道各分配一个形状为的核数组,将个互相关运算的二维输出按通道相加,得到一个二维数组作为输出。我们把个核数组在通道维上连结,即得到一个形状为的卷积核。
多输出通道
卷积层的输出也可以包含多个通道,设卷积核输入通道数和输出通道数分别为和,高和宽分别为和。如果希望得到含多个通道的输出,我们可以为每个输出通道分别创建形状为的核数组,将它们在输出通道维上连结,卷积核的形状即。
对于多输出通道卷积核的一种理解:不同的核数组提取的是不同的特征。
卷积层与全连接层的对比
1.全连接层把图像展平成一个向量,在输入图像上相邻的元素可能因为展平操作不再相邻,网络难以捕捉局部信息。而卷积层的设计,天然地具有提取局部信息的能力。
2.卷积层的参数量更少。不考虑偏置的情况下,一个形状为的卷积核的参数量是,与输入图像的宽高无关。假如一个卷积层的输入和输出形状分别是和,如果要用全连接层进行连接,参数数量就是。使用卷积层可以以较少的参数数量来处理更大的图像。
卷积层的简洁实现
我们使用Pytorch中的nn.Conv2d
类来实现二维卷积层,主要关注以下几个构造函数参数:
-
in_channels
(python:int) – 图像输入通道数 -
out_channels
(python:int) – 输出通道数 -
kernel_size
(python:int or tuple) – 卷积核大小(如果参数类型是整形就是高和宽一样,如果类型是元组就分别是高和宽,下同) -
stride
(python:int or tuple, optional) – 步长 -
padding
(python:int or tuple, optional) – 每条边的填充数,填充的内容默认为0 -
bias
(bool, optional) – 若为真,给输出添加一个可学习的偏置项,默认为真
forward
函数的参数为一个四维张量,形状为,返回值也是一个四维张量,形状为,其中是批量大小,分别表示通道数、高度、宽度。
池化
池化层直接计算池化窗口内元素的最大值或者平均值,该运算也分别叫做最大池化或平均池化。
池化层也可以在输入的高和宽两侧填充并调整窗口的移动步幅来改变输出形状。池化层填充和步幅与卷积层填充和步幅的工作机制一样。
在处理多通道输入数据时,池化层对每个输入通道分别池化,但不会像卷积层那样将各通道的结果按通道相加。这意味着池化层的输出通道数与输入通道数相等。
池化层的简洁实现
我们使用Pytorch中的nn.MaxPool2d
实现最大池化层,关注以下构造函数参数:
-
kernel_size
– 池化窗口大小 -
stride
– 池化窗口的步长,默认值是池化窗口的大小。 -
padding
– 每条边的0填充数
forward
函数的参数为一个四维张量,形状为,返回值也是一个四维张量,形状为,其中是批量大小,分别表示通道数、高度、宽度。
平均池化层使用的是nn.AvgPool2d
,使用方法与nn.MaxPool2d
相同。
LeNet
LeNet模型
LeNet分为卷积层块和全连接层块两个部分。
卷积层块里的基本单位是卷积层后接平均池化层:卷积层用来识别图像里的空间模式,如线条和物体局部,之后的平均池化层则用来降低卷积层对位置的敏感性。
卷积层块由两个这样的基本单位重复堆叠构成。在卷积层块中,每个卷积层都使用的窗口,并在输出上使用sigmoid激活函数。第一个卷积层输出通道数为6,第二个卷积层输出通道数则增加到16。
全连接层块含3个全连接层。它们的输出个数分别是120、84和10,其中10为输出的类别个数。
LeNet模型实现
#net
class Flatten(torch.nn.Module): #展平操作
def forward(self, x):
return x.view(x.shape[0], -1)
class Reshape(torch.nn.Module): #将图像大小重定型
def forward(self, x):
return x.view(-1,1,28,28) #(B x C x H x W)
net = torch.nn.Sequential( #Lelet
Reshape(),
nn.Conv2d(in_channels=1, out_channels=6, kernel_size=5, padding=2), #b*1*28*28 =>b*6*28*28
nn.Sigmoid(),
nn.AvgPool2d(kernel_size=2, stride=2), #b*6*28*28 =>b*6*14*14
nn.Conv2d(in_channels=6, out_channels=16, kernel_size=5), #b*6*14*14 =>b*16*10*10
nn.Sigmoid(),
nn.AvgPool2d(kernel_size=2, stride=2), #b*16*10*10 => b*16*5*5
Flatten(), #b*16*5*5 => b*400
nn.Linear(in_features=16*5*5, out_features=120),
nn.Sigmoid(),
nn.Linear(120, 84),
nn.Sigmoid(),
nn.Linear(84, 10)
)
计算准确率的函数
#计算准确率
'''
(1). net.train()
启用 BatchNormalization 和 Dropout,将BatchNormalization和Dropout置为True
(2). net.eval()
不启用 BatchNormalization 和 Dropout,将BatchNormalization和Dropout置为False
'''
def evaluate_accuracy(data_iter, net,device=torch.device('cpu')):
"""Evaluate accuracy of a model on the given data set."""
acc_sum,n = torch.tensor([0],dtype=torch.float32,device=device),0
#data_iter:测试集,acc_sum:模型预测正确的总数,n:预测时的总数目
for X,y in data_iter:
# If device is the GPU, copy the data to the GPU.
X,y = X.to(device),y.to(device)
net.eval()
with torch.no_grad(): #该区域所涉及的计算数据不需要计算梯度,也不会进行反向传播
y = y.long()
acc_sum += torch.sum((torch.argmax(net(X), dim=1) == y)) #[[0.2 ,0.4 ,0.5 ,0.6 ,0.8] ,[ 0.1,0.2 ,0.4 ,0.3 ,0.1]] => [ 4 , 2 ]
n += y.shape[0]
return acc_sum.item()/n
训练函数
#训练函数
def train_ch5(net, train_iter, test_iter,criterion, num_epochs, batch_size, device,lr=None):
"""Train and evaluate a model with CPU or GPU."""
print('training on', device)
net.to(device)
optimizer = optim.SGD(net.parameters(), lr=lr)
for epoch in range(num_epochs):
train_l_sum = torch.tensor([0.0],dtype=torch.float32,device=device)
train_acc_sum = torch.tensor([0.0],dtype=torch.float32,device=device)
n, start = 0, time.time()
for X, y in train_iter:
net.train() #训练过程,启用 BatchNormalization 和 Dropout
optimizer.zero_grad()
X,y = X.to(device),y.to(device)
y_hat = net(X)
loss = criterion(y_hat, y) #下边一个cell中初始化criterion为交叉熵损失函数
loss.backward()
optimizer.step()
with torch.no_grad():
y = y.long()
train_l_sum += loss.float()
train_acc_sum += (torch.sum((torch.argmax(y_hat, dim=1) == y))).float()
n += y.shape[0]
test_acc = evaluate_accuracy(test_iter, net,device)
print('epoch %d, loss %.4f, train acc %.3f, test acc %.3f, '
'time %.1f sec'
% (epoch + 1, train_l_sum/n, train_acc_sum/n, test_acc,
time.time() - start))
卷积神经网络进阶
深度卷积神经网络(AlexNet)
AlexNet首次证明了学习到的特征可以超越⼿⼯设计的特征。
LeNet使用MNIST数据集而AlexNet使用IMAGENET数据集。
AlexNet的特征:
- 8层变换,其中有5层卷积和2层全连接隐藏层,以及1个全连接输出层。
- 将LeNet中的sigmoid激活函数改成了更加简单的ReLU激活函数。
- 用Dropout来控制全连接层的模型复杂度。
- 引入数据增强,如翻转、裁剪和颜色变化,从而进一步扩大数据集来缓解过拟合。
代码实现
class AlexNet(nn.Module):
def __init__(self):
super(AlexNet, self).__init__()
self.conv = nn.Sequential(
nn.Conv2d(1, 96, 11, 4), # in_channels, out_channels, kernel_size, stride, padding ,这里最后一项没有写,默认的padding是0
nn.ReLU(),
nn.MaxPool2d(3, 2), # kernel_size, stride
# 减小卷积窗口,使用填充为2来使得输入与输出的高和宽一致,且增大输出通道数
nn.Conv2d(96, 256, 5, 1, 2),
nn.ReLU(),
nn.MaxPool2d(3, 2),
# 连续3个卷积层,且使用更小的卷积窗口。除了最后的卷积层外,进一步增大了输出通道数。
# 前两个卷积层后不使用池化层来减小输入的高和宽
nn.Conv2d(256, 384, 3, 1, 1),
nn.ReLU(),
nn.Conv2d(384, 384, 3, 1, 1),
nn.ReLU(),
nn.Conv2d(384, 256, 3, 1, 1),
nn.ReLU(),
nn.MaxPool2d(3, 2)
)
# 这里全连接层的输出个数比LeNet中的大数倍。使用丢弃层来缓解过拟合
self.fc = nn.Sequential(
nn.Linear(256*5*5, 4096), #这里的256是最后输入全连层的通道数,5*5是特征图的宽和高,需要自己根据每一层推算出来
nn.ReLU(),
nn.Dropout(0.5),
#由于使用CPU镜像,精简网络,若为GPU镜像可添加该层
#nn.Linear(4096, 4096),
#nn.ReLU(),
#nn.Dropout(0.5),
# 输出层。由于这里使用Fashion-MNIST,所以用类别数为10,而非论文中的1000
nn.Linear(4096, 10),
)
def forward(self, img): #注意调用全连层前要先resize,把后几个维度展平
feature = self.conv(img)
output = self.fc(feature.view(img.shape[0], -1))
return output
使用重复元素的网络(VGG)
AlexNet的缺点:并没有提供简单的规则来指导后来的研究者如何设计新的网络,也就是说它的网络结构比较死板。而VGG可以重复使用简单的基础块。
VGG:通过重复使⽤简单的基础块来构建深度模型。
每个VGG Block:数个相同的填充为1、窗口形状为的卷积层(保持输入的高和宽不变);接上一个步幅为2、窗口形状为的最大池化层(让特征图高和宽减半)。
每个VGG block中卷积层个数,输入通道数,输出通道数是自己设置的,可以不一致。
代码实现
-
pytorch知识点:
named_children()
:返回包含模型当前子模块的迭代器,可迭代模块名字和模块本身。
代码示例:for name, module in model.named_children(): if name in ['conv4', 'conv5']: print(module)
VGG11的简单实现
def vgg_block(num_convs, in_channels, out_channels): #卷积层个数,输入通道数,输出通道数
blk = []
for i in range(num_convs):
if i == 0:
blk.append(nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1))
else:
blk.append(nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1))
blk.append(nn.ReLU())
blk.append(nn.MaxPool2d(kernel_size=2, stride=2)) # 这里会使宽高减半
return nn.Sequential(*blk)
conv_arch = ((1, 1, 64), (1, 64, 128), (2, 128, 256), (2, 256, 512), (2, 512, 512))
# 经过5个vgg_block, 宽高会减半5次, 变成 224/32 = 7
fc_features = 512 * 7 * 7 # c * w * h
fc_hidden_units = 4096 # 任意
def vgg(conv_arch, fc_features, fc_hidden_units=4096):
net = nn.Sequential()
# 卷积层部分
for i, (num_convs, in_channels, out_channels) in enumerate(conv_arch):
# 每经过一个vgg_block都会使宽高减半
net.add_module("vgg_block_" + str(i+1), vgg_block(num_convs, in_channels, out_channels))
# 全连接层部分
net.add_module("fc", nn.Sequential(d2l.FlattenLayer(),
nn.Linear(fc_features, fc_hidden_units),
nn.ReLU(),
nn.Dropout(0.5),
nn.Linear(fc_hidden_units, fc_hidden_units),
nn.ReLU(),
nn.Dropout(0.5),
nn.Linear(fc_hidden_units, 10)
))
return net
net = vgg(conv_arch, fc_features, fc_hidden_units)
X = torch.rand(1, 1, 224, 224)
# named_children获取一级子模块及其名字(named_modules会返回所有子模块,包括子模块的子模块)
for name, blk in net.named_children():
X = blk(X)
print(name, 'output shape: ', X.shape) #可输出每个VGG block及最后的全连层的输出shape
#还可用print(net)直接输出网络中各层的详细信息
网络中的网络(NiN)
NiN:串联多个由卷积层和“全连接”层构成的小⽹络来构建⼀个深层⽹络。
上一句话中带引号的“全连接层”,其实指的是NiN中的卷积层,使用它代替真正的全连接层(因为如果把卷积层和真正的全连接层连起来很不方便,要进行flatten操作,影响了整个的结构)。
每个NiN block中第一个卷积层参数可设置,后两个的卷积层参数一般是固定的。几个卷积层及最大池化层的设置都是和AlexNet一致的。
在LeNet,AlexNet,VGG里,通过全连接层来调整类别数,让输出张量的尺寸与类别数一致;NiN去掉了VGG最后的三个全连接层,而是用一个平均池化层来做代替,因此需要将最后一个NiN block的输出通道数设为与类别数一致。
NiN最后的全局平均池化层是对每个通道中所有元素求平均并直接⽤于分类。
作用:可显著减少模型参数尺寸,缓解过拟合,但会造成训练时间的增加。
- 一般来说,卷积核可能会有如下作用:
- 放缩通道数:通过控制卷积核的数量达到通道数的放缩。
- 增加非线性。1×1卷积核的卷积过程相当于全连接层的计算过程,并且还加入了非线性激活函数,从而可以增加网络的非线性。
- 计算参数少(相比全连层)。
代码实现
def nin_block(in_channels, out_channels, kernel_size, stride, padding): #kernel_size, stride, padding指的都是第一个卷积层的参数
blk = nn.Sequential(nn.Conv2d(in_channels, out_channels, kernel_size, stride, 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())
return blk
# 已保存在d2lzh_pytorch
class GlobalAvgPool2d(nn.Module):
# 全局平均池化层可通过将池化窗口形状设置成输入的高和宽实现
def __init__(self):
super(GlobalAvgPool2d, self).__init__()
def forward(self, x):
return F.avg_pool2d(x, kernel_size=x.size()[2:]) #把kernel_size改成特征图的宽和高([2:]表示x的后两个维度,就是宽和高)(这样就是对一个通道的所有值求平均了)
net = nn.Sequential(
nin_block(1, 96, kernel_size=11, stride=4, padding=0),
nn.MaxPool2d(kernel_size=3, stride=2),
nin_block(96, 256, kernel_size=5, stride=1, padding=2),
nn.MaxPool2d(kernel_size=3, stride=2),
nin_block(256, 384, kernel_size=3, stride=1, padding=1),
nn.MaxPool2d(kernel_size=3, stride=2),
nn.Dropout(0.5),
# 标签类别数是10
nin_block(384, 10, kernel_size=3, stride=1, padding=1),
GlobalAvgPool2d(), #这里的输出还是四维的
# 将四维的输出转成二维的输出,其形状为(批量大小, 10)
d2l.FlattenLayer())
GoogLeNet
GoogLeNet由Inception基础块组成。
Inception块相当于⼀个有4条线路的⼦⽹络。它通过不同窗口形状的卷积层和最⼤池化层来并⾏抽取信息,并使⽤1×1卷积层减少通道数从而降低模型复杂度(注意这里的1×1卷积层作用和之前的NiN的1×1卷积层是不同的,可以将输入通道和输出通道数值设为不一样的,起到放缩通道数的作用)。4条线路都保证了特征图的宽高不变。
最后使用torch.cat()
将四条线路的输出在通道维上拼接。
可以⾃定义的超参数是每个层的输出通道数,我们以此来控制模型复杂度。
代码实现
class Inception(nn.Module):
# c1 - c4为每条线路里的层的输出通道数
def __init__(self, in_c, c1, c2, c3, c4):
super(Inception, self).__init__()
# 线路1,单1 x 1卷积层
self.p1_1 = nn.Conv2d(in_c, c1, kernel_size=1)
# 线路2,1 x 1卷积层后接3 x 3卷积层
self.p2_1 = nn.Conv2d(in_c, c2[0], kernel_size=1)
self.p2_2 = nn.Conv2d(c2[0], c2[1], kernel_size=3, padding=1)
# 线路3,1 x 1卷积层后接5 x 5卷积层
self.p3_1 = nn.Conv2d(in_c, c3[0], kernel_size=1)
self.p3_2 = nn.Conv2d(c3[0], c3[1], kernel_size=5, padding=2)
# 线路4,3 x 3最大池化层后接1 x 1卷积层
self.p4_1 = nn.MaxPool2d(kernel_size=3, stride=1, padding=1)
self.p4_2 = nn.Conv2d(in_c, 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) # 在通道维上连结输出
#这里用的输入shape是1*96*96
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.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), #256 = 64+128+32+32
nn.MaxPool2d(kernel_size=3, stride=2, padding=1))
b4 = nn.Sequential(Inception(480, 192, (96, 208), (16, 48), 64), #480 = 128+192+96+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),
d2l.GlobalAvgPool2d())
net = nn.Sequential(b1, b2, b3, b4, b5,
d2l.FlattenLayer(), nn.Linear(1024, 10))
net = nn.Sequential(b1, b2, b3, b4, b5, d2l.FlattenLayer(), nn.Linear(1024, 10))
X = torch.rand(1, 1, 96, 96)
for blk in net.children():
X = blk(X)
print('output shape: ', X.shape)
#batchsize=128
batch_size = 16
# 如出现“out of memory”的报错信息,可减小batch_size或resize
#train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=96)
lr, num_epochs = 0.001, 5
optimizer = torch.optim.Adam(net.parameters(), lr=lr)
d2l.train_ch5(net, train_iter, test_iter, batch_size, optimizer, device, num_epochs)