动手学深度学习(五、卷积神经网络基础知识)
一、二维卷积层
- 二维卷积层的核心计算是二维互相关运算。在最简单的形式下,它对二维输入数据和卷积核做互相关运算然后加上偏差。
- 我们可以设计卷积核来检测图像中的边缘。
- 我们可以通过数据来学习卷积核。
import torch
from torch import nn
#import torch.nn as nn
#1二维互相关运算
#数组X、核数组K, 输出数组Y
def corr2d(X, K): # 本函数已保存在d2lzh_pytorch包中方便以后使用
h, w = K.shape
Y = torch.zeros((X.shape[0] - h + 1, X.shape[1] - w + 1))
for i in range(Y.shape[0]):
for j in range(Y.shape[1]):
Y[i, j] = (X[i:i + h, j:j+w]*K).sum()
return Y
X = torch.tensor([[0, 1, 2], [3, 4, 5], [6, 7, 8]])
K = torch.tensor([[0, 1], [2, 3]])
print(corr2d(X, K))
#实现一个自定义的二维卷积层
class Conv2D(nn.Module):
def __init__(self, kernel_size):
super(Conv2D, self).__init__()
self.weight = nn.Parameter(torch.randn(kernel_size))
self.bias = nn.Parameter(torch.randn(1))
def forward(self, x):
return corr2d(x, self.weight) + self.bias
#3.图像中物体边缘检测
X = torch.ones(6, 8)
X[:, 2:6] = 0
K = torch.tensor([[1, -1]])
Y = corr2d(X, K)
print(Y)
#我们可以看出,卷积层可通过重复使用卷积核有效地表征局部空间。
#4.通过数据学习核数组
#使用物体边缘检测中的输入数据X和输出数据Y来学习我们构造的核数组K
#构造一个核数组形状是(1, 2)的二维卷积层
#conv2d = Conv2D(kernel_size = (1, 2))
conv2d = Conv2D(kernel_size=(1, 2))
step = 30
lr = 0.01
for i in range(step):
Y_hat = conv2d(X)
l = ((Y_hat - Y) ** 2).sum()
l.backward()
#梯度下降
conv2d.weight.data -= lr * conv2d.weight.grad
conv2d.bias.data -= lr * conv2d.bias.grad
#梯度清零
conv2d.weight.grad.fill_(0)
conv2d.bias.grad.fill_(0)
if(i + 1) % 5 == 0:
print('Step %d, loss %.3f' % (i+ 1, l.item()))
print("weight", conv2d.weight.data)
print("bias", conv2d.bias.data)
#5.卷积运算均指的是互相关运算
#卷积运算核 即 互相关运算核 的上下左右翻转
#6 特征图 和 感受野
# 我们常使用“元素”一词来描述数组或矩阵中的成员。在神经网络的术语中,这些元素也可称为“单元”。
二维卷积层输出的二维数组可以看作是输入在空间维度(宽和高)上某一级的表征,也叫特征图(feature map)
影响元素xxx的前向计算的所有可能输入区域(可能大于输入的实际尺寸)叫做xxx的感受野(receptive field)
二、填充和步幅
- 填充可以增加输出的高和宽。这常用来使输出与输入具有相同的高和宽。
- 步幅可以减小输出的高和宽,例如输出的高和宽仅为输入的高和宽的1/n1/n1/n(nnn为大于1的整数)。
填充
一般来说,假设输入形状是,卷积核窗口形状是,那么输出形状将会是
卷积层的输出形状由输入形状和卷积核窗口形状决定。介绍卷积层的两个超参数,即填充和步幅。它们可以对给定形状的输入和卷积核改变输出形状。
如果在高的两侧一共填充行,在宽的两侧一共填充列,那么输出形状将会是,也就是说,输出的高和宽会分别增加和.
torch.Size([1, 1, 8, 8]) torch.Size([1, 1, 8, 8]) torch.Size([8, 8])
步幅
为了表述简洁,当输入的高和宽两侧的填充数分别为和时,我们称填充为。特别地,当时,填充为。当在高和宽上的步幅分别为和时,我们称步幅为。特别地,当时,步幅为。在默认情况下,填充为0,步幅为1。
一般来说,当高上步幅为,宽上步幅为时,输出形状为
import torch
from torch import nn
#定义一个函数来计算机卷积层、它对输入和输出做相应的升维和降维
def comp_conv2d(conv2d, X):
# (1, 1)代表批量大小和通道数均为1
X = X.view((1, 1) + X.shape)
# print(X.shape)
Y = conv2d(X)
return Y.view(Y.shape[2:]) #排除不关心的前两维:批量和通道
#输入通道数、输出通道数
conv2d = nn.Conv2d(in_channels = 1, out_channels = 1, kernel_size = 3, padding = 1)
X = torch.rand(8, 8)
print(comp_conv2d(conv2d, X).shape)
#当卷积核的高和宽不同时, 我们也可以通过设置高和宽上不同的填充数使得输出和输入具有相同的高和宽
conv2d_1 = nn.Conv2d(in_channels = 1, out_channels = 1, kernel_size = (5, 3), padding = (2, 1))
print(comp_conv2d(conv2d_1, X).shape)
#步幅
conv2d_2 = nn.Conv2d(1, 1, kernel_size = 3, padding = 1, stride = 2)
print(comp_conv2d(conv2d_2, X).shape)
conv2d_3 = nn.Conv2d(1, 1, kernel_size = (3, 5), padding = (0, 1), stride = (3, 4))#填充高、宽; 步幅高、宽
print(comp_conv2d(conv2d_3, X).shape)
输出: torch.Size([8, 8]) torch.Size([8, 8]) torch.Size([4, 4]) torch.Size([2, 2])
三、多输入通道和多输出通道
- 使用多通道可以拓展卷积层的模型参数。
- 假设将通道维当作特征维,将高和宽维度上的元素当成数据样本,那么1×1卷积层的作用与全连接层等价。
- 1×1卷积层通常用来调整网络层之间的通道数,并控制模型复杂度。
输入多通道
当输入数据含多个通道时,我们需要构造一个输入通道数与输入数据的通道数相同的卷积核,从而能够与含多通道的输入数据做互相关运算。
图5.4展示了含2个输入通道的二维互相关计算的例子。在每个通道上,二维输入数组与二维核数组做互相关运算,再按通道相加即得到输出。
输出多通道
当输入通道有多个时,因为我们对各个通道的结果做了累加,所以不论输入通道数是多少,输出通道数总是为1。设卷积核输入通道数和输出通道数分别为和, 高和宽分别为和。如果希望得到含多个通道的输出,我们可以为每个输出通道分别创建形状为的核数组。将它们在输出通道维上连结,卷积核的形状即。在做互相关运算时,每个输出通道上的结果由卷积核在该输出通道上的核数组与整个输入数组计算而来。
1 * 1 卷积层
对于1*1卷积层的原理、作用、以及全连接层的关系尚需进一步学习
卷积窗口形状为的多通道卷积层。我们通常称之为卷积层,并将其中的卷积运算称为卷积。因为使用了最小窗口,卷积失去了卷积层可以识别高和宽维度上相邻元素构成的模式的功能。实际上,卷积的主要计算发生在通道维上。假设我们将通道维当作特征维,将高和宽维度上的元素当成数据样本,那么1×11\times 11×1卷积层的作用与全连接层等价。
经验证,做卷积时,以上函数与之前实现的互相关运算函数corr2d_multi_in_out
等价。
在之后的模型里我们将会看到卷积层被当作保持高和宽维度形状不变的全连接层使用。于是,我们可以通过调整网络层之间的通道数来控制模型复杂度。
import torch
from torch import nn
import sys
sys.path.append("F:\数据\MRC\动手学深度学习\Dive-into-DL-PyTorch-master\code")
import d2lzh_pytorch as d2l
#计算多输入通道
def corr2d_multi_in(X, K):
# 沿着X和K的第0维(通道维)分别计算再相加
res = d2l.corr2d(X[0, :, :], K[0, :, :])
for i in range(1, X.shape[0]):
res += d2l.corr2d(X[i, :, :], K[i, :, :])
return res
X = torch.tensor([[[0, 1, 2], [3, 4, 5], [6, 7, 8]],
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]])
K = torch.tensor([[[0, 1], [2, 3]], [[1, 2], [3, 4]]])
print(corr2d_multi_in(X, K))
#计算多输出通道
#我们将核数组K同K+1(K中每个元素加一)和K+2连结在一起来构造一个输出通道数为3的卷积核。
def corr2d_multi_in_out(X, K):
return torch.stack([corr2d_multi_in(X, k) for k in K])
K = torch.stack([K, K + 1, K + 2])
print(K.shape)
print(corr2d_multi_in_out(X, K))
#1 * 1 卷积层
def corr2d_multi_in_out_1_1(X, K):
c_i, h, w = X.shape
c_o = K.shape[0]
X = X.view(c_i, h * w)
K = K.view(c_o, c_i)
Y = torch.mm(K, X)#全连接层的矩阵乘法
return Y.view(c_o, h, w)
X = torch.rand(3, 3, 3)
K = torch.rand(2, 3, 1, 1)
Y1 = corr2d_multi_in_out_1_1(X, K)
Y2 = corr2d_multi_in_out(X, K)
print((Y1 - Y2).norm().item() < 1e-6)
输出: tensor([[ 56., 72.], [104., 120.]]) torch.Size([3, 2, 2, 2]) tensor([[[ 56., 72.], [104., 120.]], [[ 76., 100.], [148., 172.]], [[ 96., 128.], [192., 224.]]]) True
四、池化层
- 最大池化和平均池化分别取池化窗口中输入元素的最大值和平均值作为输出。
- 池化层的一个主要作用是缓解卷积层对位置的过度敏感性。
- 可以指定池化层的填充和步幅。
- 池化层的输出通道数跟输入通道数相同。
池化(pooling)层,它的提出是为了缓解卷积层对位置的过度敏感性。
最大池化层 和 平均池化层
二维平均池化的工作原理与二维最大池化类似,但将最大运算符替换成平均运算符。池化窗口形状为的池化层称为池化层,其中的池化运算叫作池化。
import torch
from torch import nn
def pool2d(X, pool_size, mode = 'max'):
X = X.float()
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, 1, 2], [3, 4, 5], [6, 7, 8]])
print(pool2d(X, (2, 2)))
print(pool2d(X, (2, 2), 'avg'))
输出:
tensor([[4., 5.], [7., 8.]]) tensor([[2., 3.], [5., 6.]])
填充 和 步幅
同卷积层一样,池化层也可以在输入的高和宽两侧的填充并调整窗口的移动步幅来改变输出形状。池化层填充和步幅与卷积层填充和步幅的工作机制一样。我们将通过nn
模块里的二维最大池化层MaxPool2d
来演示池化层填充和步幅的工作机制。我们先构造一个形状为(1, 1, 4, 4)的输入数据,前两个维度分别是批量和通道。
import torch
X = torch.arange(16, dtype = torch.float).view((1, 1, 4, 4))
print(X)
pool2d = nn.MaxPool2d(3)
#使用形状为(3, 3)的池化窗口, 默认获得形状为(3, 3)的步幅
print(pool2d(X))
#手动指定步幅和填充
pool2d = nn.MaxPool2d(3, padding = 1, stride = 2)
print(pool2d(X))
#指定非正方形的池化窗口, 并分别指定高和宽上的填充和步幅(先高后宽)
pool2d = nn.MaxPool2d((2, 4), padding = (1, 2), stride = (2, 3))
print(pool2d(X))
tensor([[[[ 0., 1., 2., 3.], [ 4., 5., 6., 7.], [ 8., 9., 10., 11.], [12., 13., 14., 15.]]]]) tensor([[[[10.]]]]) tensor([[[[ 5., 7.], [13., 15.]]]]) tensor([[[[ 1., 3.], [ 9., 11.], [13., 15.]]]])
多通道
import torch
X = torch.arange(16, dtype = torch.float).view((1, 1, 4, 4))
print(X)
pool2d = nn.MaxPool2d(3, padding = 1, stride = 2)
print(pool2d(X))
X = torch.cat((X, X + 1), dim = 1)
print(X)
pool2d = nn.MaxPool2d(3, padding = 1, stride = 2)
print(pool2d(X))
输出:
tensor([[[[ 0., 1., 2., 3.], [ 4., 5., 6., 7.], [ 8., 9., 10., 11.], [12., 13., 14., 15.]]]]) tensor([[[[ 5., 7.], [13., 15.]]]]) 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.]]]]) tensor([[[[ 5., 7.], [13., 15.]], [[ 6., 8.], [14., 16.]]]])
卷积神经网络的基础:卷积层、填充和步幅、多输入输出通道、池化层