第6章 卷积神经网络
概述
- 平移不变性
- 局部性
平移不变性
\((i,j):\)当前位置(你正在计算哪个输出像素)
\((a,b):\)卷积核内部的位置(小窗口的偏移)
- x的平移导致h的平移 \(h_{i, j} = \sum_{a, b} v_{i, j, a, b} x_{i + a, j + b}\)
- v不应该依赖与i, j
- 解决方案:\(v_{i, j, a, b} = v_{a, b}\)
这就是二维~卷积~交叉相关
局部性
不需要关注远离\(x_{i, j}\)的数据,所以限制\(|a|, |b| > \delte时,v_{a, b} =0\)
其实就是卷积核是有大小的,在引入a,b这两个变量以后这一点有点像废话?
总结
二维卷积层
- 输入: \(X: n_h \times n_w\)
- 核: \(W: k_h \times k_w\)
- 偏差: \(b\)
- 输出: \(Y = (n_h - k_h + 1) \times (n_w \times k_w + 1)\)
- \(W, b\)为可学习的参数
Info
实际上现在CNN中使用的卷积在数学上都应该叫做交叉相关,卷积应当是把卷积核的\((a, b)\)加个负号,即把卷积核上下左右翻转一下,这才符合卷积的数学定义,但由于这个翻转是可以通过参数学习的,且为了方便,现在都把这个负号省略了
图像卷积 代码实现
互相关运算
def corr2d(X, K):
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
实现二维卷积层
class Conv2D(nn.Moduel):
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
填充和步幅
填充
- 填充:在周围添上额外的行/列
- 填充\(p_h\)行和\(p_w\)列,输出形状为: \((n_h - k_h + p_h + 1) \times (n_w - k_w + p_w + 1)\)
-
通常取\(p_h = k_{h}-1, p_w = k_{w}-1\) (代回去发现输出和输入形状一样)
- \(k_h\)为奇数:在上下两侧填充\(p_h / 2\)
- \(k_h\)为偶数:在上册填充\(floor(p_h / 2)\),下侧填充\(p_h // 2\)
# 请注意,这里每边都填充了1行或1列,因此总共添加了2行或2列
conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1)
conv2d = nn.Conv2d(1, 1, kernel_size=(5, 3), padding=(2, 1)) # 上下填充2 左右填充1
步幅
-
步幅:行列的滑动步长
-
给定高度\(s_h\)和宽度\(s_w\)的步幅,输出形状是:$\(\lfloor(n_h-k_h+p_h+s_h)/s_h\rfloor \times \lfloor(n_w-k_w+p_w+s_w)/s_w\rfloor.\)$
如果我们设置了\(p_h=k_h-1\)和\(p_w=k_w-1\),则输出形状将简化为\(\lfloor(n_h+s_h-1)/s_h\rfloor \times \lfloor(n_w+s_w-1)/s_w\rfloor\)。
更进一步,如果输入的高度和宽度可以被垂直和水平步幅整除,则输出形状将为\((n_h/s_h) \times (n_w/s_w)\)。
conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1, stride=2)
conv2d = nn.Conv2d(1, 1, kernel_size=(3, 5), padding=(0, 1), stride=(3, 4))
Q&A
- 3*3的卷积核虽然很小,但只要有深度,其实他能覆盖到很大的板块
多个输入和输出通道
之前是:1个输入通道1--一个卷积核-->一个输出
现在是:3个输入通道
所以卷积核不再是一个2D kernel,而是每个通道都有一层权重\((3, k_h, k_w)\)
一个卷积核=汇总所有输入通道->得到一个输出通道
再进一步,如果要输出多个特征,那就使用多个卷积核
\(每个卷积核:shape=(c_i, k_h, k_w),共c_o个卷积核\)
- 输入\(X: c_i \times n_h \times n_w\)
- 核\(W: c_o \times c_i \times k_h \times k_w\)
- 偏差\(B: c_o \times c_i\)
- 输出\(Y: c_o \times m_h \times m_w\)
- 计算复杂度\(O(c_i c_o k_h k_w m_h m_w)\)
1*1卷积层
不识别空间模式,只是融合通道
可以理解成把所有通道的图片拉成了一个向量
代码实现
多输入通道互相关运算
def corr2d_multi_in(X, K):
# 先遍历“X”和“K”的第0个维度(通道维度),再把它们加在一起
return sum(d2l.corr2d(x, k) for x, k in zip(X, K))
# corr2d就是让一个核在一张图片是滑动得到的输出
多输出通道互相关运算
def corr2d_multi_in_out(X, K):
# 迭代“K”的第0个维度,即输出通道维度,每次都对输入“X”执行互相关运算。
# 最后将所有结果都叠加在一起
return torch.stack([corr2d_multi_in(X, k) for k in K], 0)
1*1卷积
# K: (输出通道数, 输入通道数, 1, 1)
def corr2d_multi_in_out_1x1(X, K):
c_i, h, w = X.shape # 输入通道数、高、宽
c_o = K.shape[0] # 输出通道数
X = X.reshape((c_i, h * w)) # (c_i, h, w)->(c_i, h*w)
K = K.reshape((c_o, c_i)) # (c_o, c_i, 1, 1)->(c_o, c_i)
# 全连接层中的矩阵乘法
Y = torch.matmul(K, X)
return Y.reshape((c_o, h, w))
池化层
最大池化层、平均池化层
代码实现
Q&A
- 池化层作用可以减少计算量,现在算力越来越高,池化层用的越来越少
LeNet
import torch
from torch import nn
# 输入(1, 1, 28, 28)
net = nn.Sequential(
# 卷积层 输入通道1 输出通道6 大小5*5 输出形状(1, 6, 28, 28)
# 从原图中提取6种特征
nn.Conv2d(1, 6, kernel_size=5, padding=2),
# 加一层激活函数
nn.Sigmoid(),
# 池化层 输出形状(1, 6, 14, 14)
# 压缩信息
nn.AvgPool2d(kernel_size=2, stride=2),
# 卷积层2 输出形状(1, 16, 10, 10)
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)) # 最后分成10类,代表数字0-9