与卷积层类似,不过每次只需要返回滑动窗口的最大值
当我们在进行图片颜色垂直边缘检测时,进行的卷积输出对位置非常敏感。看下面例子,真实图片的边缘位置不一定每次都在很好的地方,但是卷积输出Y只要有1像素的移位,就会输出0,所以我们需要一定程度的平移不变性去解决这个问题。
当我们做了最大池化操作,就会容忍一个像素的移位
池化层与卷积层类似,都具有填充和步幅。
没有可学习的参数(kernel)
子啊每个输入通道应用池化层以获得相应的输出通道
输出通道数 = 输入通道数
最大池化层:检测每个窗口最强的模式信号
平均池化层:将最大池化层的“取最大值操作”替换为“求平均值”
总结
最大和平均池化层
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
X = torch.tensor([[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]])
# 最大池化层
print(pool2d(X, (2, 2)))
'''
tensor([[4., 5.],
[7., 8.]])
'''
# 平均池化层
print(pool2d(X, (2, 2), 'avg'))
'''
tensor([[2., 3.],
[5., 6.]])
'''
填充和步幅
X = torch.arange(16, dtype=torch.float32).reshape((1, 1, 4, 4))
'''
tensor([[[[ 0., 1., 2., 3.],
[ 4., 5., 6., 7.],
[ 8., 9., 10., 11.],
[12., 13., 14., 15.]]]])
'''
pool2d = nn.MaxPool2d(3) # 默认步幅与池化窗口的大小相同
print(pool2d(X)) # tensor([[[[10.]]]])
# 自定义填充和步幅
pool2d = nn.MaxPool2d(3, padding=1, stride=2)
# 自定义任意大小的矩阵池化窗口
pool2d = nn.MaxPool2d((2, 3), padding=(1, 1), stride=(2, 3))
汇聚层在每个输入通道上单独运算
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)
print(pool2d(X))
'''
tensor([[[[ 5., 7.],
[13., 15.]],
[[ 6., 8.],
[14., 16.]]]])
'''
先使用卷积层来学习图片空间信息,然后使用全连接层来转换到类别空间。
代码
class Reshape(torch.nn.Module):
def forward(self, x):
return x.view(-1, 1, 28, 28)
net = torch.nn.Sequential(Reshape(), 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))
AlexNet使用了更大的池化窗口,并且使用最大化池化层,由于图片更大,核窗口和步长也变得更大
AlexNet新增了3个卷积层,并且有着更多的输出通道
AlexNet和LeNet都用了两个隐藏层,但是AlexNet的大小增加了很多。
import torch
from torch import nn
from d2l import torch as d2l
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))
AlexNet比LeNet更深更大来得到更好的精度,VGG的思想就是能不能在AlexNet的基础上更深和更大。有以下三种选择
- 更多的全连接层(太贵)
- 更多的卷积层,因为AlexNet使用了3个连续的卷积层,在这个地方改进的话如果是继续在增加卷积层的数量就得不到最终一个很好的设计
- 将卷积层组合成块
使用3 x 3卷积(n 层,m通道):在设计前比较了使用5 x 5卷积和3 x 3卷积的效果,发现在相同计算开销的情况下,使用更多的3 x 3的效果比使用少一点(5 x 5因为计算量大,所以不能做得很深)的5 x 5效果更好
2 x 2最大池化层, 步幅2
VGG与AlexNet主要区别就是把AlexNet不规则的卷积层设计抽取出来成为一个可以重复使用的卷积块(VGG块),然后可以重复的拼接块来得到不同的架构。
import torch
from torch import nn
from d2l import torch as d2l
def vgg_block(num_convs, in_channels, out_channels): # 参数1:需要的卷积层 参数2:输入通道数 参数3:输出通道数
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)
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)
LeNet、AlexNet和VGG都有一个共同的特点:它们通过一系列的卷积层和汇聚层来提取空间结构特征后,再通过全连接层对特征的表征进行处理。但是全连接层存在一个问题,它会占用比较多的参数空间,可能会带来过拟合、占用大量内存以及占用很多计算带宽等影响。
NiN的提出就是为了解决这些问题。它的思想是完全不需要全连接层,在每个像素的通道上分别使用多层感知机,也就是在每个像素位置,应用一个全连接层。这样将权重连接到了每个空间位置,可以将其视为1 * 1的卷积层,或者看作在每个像素位置上独立作用的全连接层,从另一个角度讲,就是将空间维度中的每个像素视为单个样本,将通道维度视为不同特征。
NiN块由一个卷积层和两个全连接层(实际上是窗口大小为1 * 1、步幅为1、无填充的卷积层)组成(因为1 * 1的卷积层等价于全连接层)
NiN块中两个1 * 1的卷积层其实是起到了全连接层的作用,这两个卷积层充当了带有ReLu激活函数的逐像素全连接层。
NiN块可以认为是一个简单的神经网络:一个卷积层+两个全连接层。唯一的不同是这里的全连接层是对每一个像素做全连接,对于每个输入的每个像素的权重都是一样的,不会根据输入的具体情况发生变化,所以可以认为是按照输入的每个像素逐一做全连接
VGG块是由多个卷积层和一个最大池化层组成。而NiN块由一个卷积层和两个大的全连接层,最后通过一个输出通道数为1000的全连接层得到1000类
VGG网络由四个VGG块和两个大的全连接层,最后通过一个输出通道数为1000的全连接层得到1000类。NiN网络由一个NiN块和一个步幅为2、3 * 3的最大池化层不断重复,如果将最后重复部分的NiN块的通道数设置为标签类别的数量的话,则最后就可以用全局平均池化层替全连接层来得到输出。
总结
NiN完全取消了全连接层,这样做的好处是减少了模型所需参数的数量,但是在实践中,这种设计有时会增加模型的训练时间。
import torch
from torch import nn
from d2l import torch as d2l
# NiN块
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模型
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())