跳转至

第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}\)
\[h_{i, j} = \sum_{a, b} v_{a, b} x_{i + a, j + b}\]

这就是二维~卷积~交叉相关

局部性

\[h_{i, j} \sum_{a, b} v_{a, b} x_{i + a, j + b}\]

不需要关注远离\(x_{i, j}\)的数据,所以限制\(|a|, |b| > \delte时,v_{a, b} =0\)

其实就是卷积核是有大小的,在引入a,b这两个变量以后这一点有点像废话?

总结

\[h_{i,j}= \sum_{a,b}v_{i,j,a,b} x_{i+a, j+b} \rightarrow h_{i,j} = \sum_{a= - \delta}^{\delta} \sum_{b= - \delta}^{delta} v_{a,b} x_{i+a,j+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)\)
\[Y = X \cdot W + b\]
  • \(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