动手学深度学习(六):卷积神经网络与现代卷积神经网络
因为在实际例子中如果想要使用全连接层,如果数据维度比较大要耗费的GPU资源很多,而卷积神经网络(CNN)则能够用较少的参数同时有平移不变性.在图像检测中划分局部区域进行预测.
(一) 卷积神经网络概述
卷积神经网络的设计就是用于探索图像数据,下以图像为例.
将图像划分为若干个区域分别与核函数(向量)相乘得到输出向量.给出一个图就能很直观地明白卷积的过程了:
具体的代码实现也很简单:
1 2 3 4 5 6 7 8 class Conv2D (nn.Module): def __init__ (self, kernel_size ): super ().__init__() self.weight = nn.Parameter(torch.rand(kernel_size)) self.bias = nn.Parameter(torch.zeros(1 )) def forward (self, x ): return corr2d(x, self.weight) + self.bias
边缘检测
当卷积核是一个二维张量[1.0,-1.0]
的时候就能检测水平相邻两元素是否相同.如果是相同的才会输出0.
(二) 卷积核学习
有时候一些复杂的卷积核我们不想去手动设计那么我们也就可以通过"学习"(参数递归)来更新卷积核.我们先构造一个卷积层,并将其卷积核初始化为随机张量。接下来,在每次迭代中,我们比较Y
与卷积层输出的平方误差,然后计算梯度来更新卷积核。
以边缘检测为例,我们可以这样更新:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 conv2d = nn.Conv2d(1 ,1 , kernel_size=(1 , 2 ), bias=False ) X = X.reshape((1 , 1 , 6 , 8 )) Y = Y.reshape((1 , 1 , 6 , 7 )) lr = 3e-2 for i in range (10 ): Y_hat = conv2d(X) l = (Y_hat - Y) ** 2 conv2d.zero_grad() l.sum ().backward() conv2d.weight.data[:] -= lr * conv2d.weight.grad if (i + 1 ) % 2 == 0 : print (f'epoch {i+1 } , loss {l.sum ():.3 f} ' )
(三) 填充和步幅
如图,输入是3×3但是输出是2×2.为了能让输出和输入保持同样的形状,可以在周围填充一圈0.此时输出形状与填充满足关系( n h − k h + p h + 1 ) × ( n w − k w + p w + 1 ) \left(n_h-k_h+p_h+1\right) \times\left(n_w-k_w+p_w+1\right) ( n h − k h + p h + 1 ) × ( n w − k w + p w + 1 ) .(k是kernal,p是padding)
卷积也可以跳过中间位置每次滑动多个元素,每次滑动的元素数量称作步幅.此时输出形状与填充满足关系( n h − k h + p h + s h ) / s h × ( n w − k w + p w + s w ) / s w (n_h-k_h+p_h+s_h)/s_h\times(n_w-k_w+p_w+s_w)/s_w ( n h − k h + p h + s h ) / s h × ( n w − k w + p w + s w ) / s w .
(四) 多输入多输出通道
当图像不止有一种颜色的时候就对应了有多个通道,可以用多个核函数进行分别相乘.如图:
实现一个多输入通道卷积函数:
1 2 3 def corr2d_multi_in (X, K ): return sum (d2l.corr2d(x, k) for x, k in zip (X, K))
如果需要一个多个输出通道的卷积函数:
1 2 3 4 5 6 K = torch.stack((K, K + 1 , K + 2 ), 0 ) def corr2d_multi_in_out (X, K ): return torch.stack([corr2d_multi_in(X, k) for k in K], 0 )
此外还有1×1卷积层,通常用于调整网络层的通道数量和控制模型复杂性.
(五) 汇聚层
和之前的卷积过程的区别是对于每一个块不是进行每个位置的元素相乘然后所有元素求和而是找出最大的那个元素(最大汇聚层)或者平均值(平均汇聚层).如图:
实现方式与前面卷积类似:
1 2 3 4 5 6 7 8 9 10 def pool2d (X, pool_size, mode='max' ): p_h, p_w = pool_size Y = torch.zeros((X.shape[0 ] - p_h + 1 , X.shape[1 ] - p_w + 1 )) for i in range (Y.shape[0 ]): for j in range (Y.shape[1 ]): if mode == 'max' : Y[i, j] = X[i: i + p_h, j: j + p_w].max () elif mode == 'avg' : Y[i, j] = X[i: i + p_h, j: j + p_w].mean() return Y
汇聚层同样可以控制填充和步幅,但是默认情况下,深度学习框架中的步幅与汇聚窗口的大小相同.也可以手动设定:
1 pool2d = nn.MaxPool2d(3 , stride=(2 , 3 ), padding=(0 , 1 ))
汇聚层使用多个通道的时候不是像卷积层一样一个卷积核与多个输入通道都进行运算而是在每个输入通道上单独运算得到一个单独的结果.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 X = torch.cat((X, X + 1 ), 1 ) ''' tensor([[[[ 0., 1., 2., 3.], [ 4., 5., 6., 7.], [ 8., 9., 10., 11.], [12., 13., 14., 15.]], [[ 1., 2., 3., 4.], [ 5., 6., 7., 8.], [ 9., 10., 11., 12.], [13., 14., 15., 16.]]]]) ''' pool2d = nn.MaxPool2d(3 , padding=1 , stride=2 ) pool2d(X) ''' tensor([[[[ 5., 7.], [13., 15.]], [[ 6., 8.], [14., 16.]]]]) '''
(六) LeNet卷积神经网络
LeNet神经网络是最早发布的卷积神经网络之一,在计算机视觉任务中有高效性能.它有两个部分:
卷积编码器(两个卷积层)
全连接层密集层(三个全连接层)
通过API我们可以将上图简洁实现如下:
1 2 3 4 5 6 7 8 9 net = nn.Sequential( nn.Conv2d(1 , 6 , kernel_size=5 , padding=2 ), nn.Sigmoid(), nn.AvgPool2d(kernel_size=2 , stride=2 ), nn.Conv2d(6 , 16 , kernel_size=5 ), nn.Sigmoid(), nn.AvgPool2d(kernel_size=2 , stride=2 ), nn.Flatten(), nn.Linear(16 * 5 * 5 , 120 ), nn.Sigmoid(), nn.Linear(120 , 84 ), nn.Sigmoid(), nn.Linear(84 , 10 ))
训练使用了GPU:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 def train_ch6 (net, train_iter, test_iter, num_epochs, lr, device ): def init_weights (m ): if type (m) == nn.Linear or type (m) == nn.Conv2d: nn.init.xavier_uniform_(m.weight) net.apply(init_weights) print ('training on' , device) net.to(device) optimizer = torch.optim.SGD(net.parameters(), lr=lr) loss = nn.CrossEntropyLoss() animator = d2l.Animator(xlabel='epoch' , xlim=[1 , num_epochs], legend=['train loss' , 'train acc' , 'test acc' ]) timer, num_batches = d2l.Timer(), len (train_iter) for epoch in range (num_epochs): metric = d2l.Accumulator(3 ) net.train() for i, (X, y) in enumerate (train_iter): timer.start() optimizer.zero_grad() X, y = X.to(device), y.to(device) y_hat = net(X) l = loss(y_hat, y) l.backward() optimizer.step() with torch.no_grad(): metric.add(l * X.shape[0 ], d2l.accuracy(y_hat, y), X.shape[0 ]) timer.stop() train_l = metric[0 ] / metric[2 ] train_acc = metric[1 ] / metric[2 ] if (i + 1 ) % (num_batches // 5 ) == 0 or i == num_batches - 1 : animator.add(epoch + (i + 1 ) / num_batches, (train_l, train_acc, None )) test_acc = evaluate_accuracy_gpu(net, test_iter) animator.add(epoch + 1 , (None , None , test_acc)) print (f'loss {train_l:.3 f} , train acc {train_acc:.3 f} , ' f'test acc {test_acc:.3 f} ' ) print (f'{metric[2 ] * num_epochs / timer.sum ():.1 f} examples/sec ' f'on {str (device)} ' )
(七) 现代卷积神经网络
1.深度卷积神经网络(AlexNet)
AlexNet与LeNet相似,但是AlexNet有五个卷积层、两个全连接隐藏层和一个全连接层.两者对比如下:
可以看出AlexNet处理的图像像素更大,卷积通道数也更多,最后几个全连接层有4096个输出.并且AlexNet使用了ReLU激活函数比sigmoid更容易训练(LeNet没使用是因为当时还没有ReLU激活函数的提出).
使用API实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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 ))
训练使用与LeNet相同的train_ch6
函数.
2.VGG块
一个VGG块由一系列卷积层组成后面再加上汇聚层.
1 2 3 4 5 6 7 8 9 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)
每一个VGG块都有参数,VGG-11网络就是5个卷积块(8个卷积层+3个全连接层).用conv_arch = ((1, 64), (1, 128), (2, 256), (2, 512), (2, 512))
来定义输入输出通道数就能实现VGG-11如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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 ))
训练与AlexNet还是类似,学习率可以略高一点,VGG-11的计算量比AlexNet计算量大.
3.网络中的网络NiN
NiN使用由一个卷积层和多个1×1卷积层组成的块,去除了容易造成过拟合的全连接层,将它们替换为全局平均汇聚层.对比图如下:
NiN的块如下:
1 2 3 4 5 6 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())
NiN网络:
1 2 3 4 5 6 7 8 9 10 11 12 13 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())
4.并行连结网络(GoogLeNet)
这里的GoogLe应该不是谷歌哈哈…该网络的基本卷积块为Inception块:
网络架构如图:
实现起来与之前的类似,这里不再过多赘述.
5.残差网络(ResNet)
残差块(右)与正常块的对比:
残差块(residual blocks)的核心思想是每个附加层都应该更容易的包含原始函数作为其元素之一.这里加上了原始的输入.如果想要改变通道数,可以引入一个1×1卷积层与输入变换后再相加.
ResNet-18架构网络如下:
6.稠密连接网络(DenseNet)
DenseNet一定程度上是ResNet的逻辑扩展.DenseNet使用了函数展开的思想,比如泰勒展开就能将原函数f ( x ) f(x) f ( x ) 展开为1阶线性和若干高阶非线性函数的和.
而连接的时候也不是直接相加而是独立的,(右边为DenseNet):
稠密网络有稠密块和过渡层.过渡层用于减小通道数,因为每个稠密块都会带来通道数的增加.