机器学习 / A LEAF IN THE GARDEN

卷积神经网络基础

机器学习 · 动手深度学习。来自 Daily Learning 的学习笔记。

从全连接到卷积

MLP 的缺陷

1.参数太多

用 MLP 对猫狗的照片分类

  • 手机像素约 12M
  • RGB 图像 36M 元素
  • 100 维度单隐藏层的 MPL 需要 36M×100=3.6B36M \times 100 = 3.6B 参数

远多于世界上猫狗的数量

  • 不如直接记住所有猫狗,不符合“特征提取”的理念
  • 容易过拟合
  • 训练开销过大

2.忽略空间结构

MLP 将图像展平为向量,忽略了像素间的远近、方向等,丢失信息

图像识别的关键特征

1.平移不变性

无论实体在图像中出现的位置如何,识别的 pattern 应该类似

⇒\quad\Rightarrow 卷积核参数共享

2.局部性

图像中相邻像素之间的相关性通常远大于较远像素之间,并且实体周边的局部信息往往是识别的关键

⇒\quad\Rightarrow 卷积核局部感受野

重新考察全连接层

二阶张量全连接变换

  • 将输入和输出变为矩阵 (w,h)→(w′,h′)(w, h) \rightarrow (w', h')
  • 权重变形为 4d 张量

[!TIP] 两种理解

  • 棍到棍 2d ⇒\Rightarrow 棍到面(多根棍)3d ⇒\Rightarrow 面到面 4d
  • 每个输入元素需要对于所有输出的权重(2d),输入有 2d 的元素,总共 2d+2d=4d2d+2d=4d 的权重

推广到更高阶(只作为补充,下面用不到)

对于从 N 阶输入张量到 M 阶输出张量的完全稠密线性映射

  • 权重张量 N+M 阶
  • 偏置张量 M 阶

[!WARNING] 区分概念

  • 维数 shape: 每个轴的长度
  • 阶数 dim: 轴的数量

索引变化

类似于把 4d 张量变为 2d 张量 hi,j=∑k,lwi,j,k,lxk,l=∑a,bvi,j,a,bxi+a,j+bh_{i,j} = \sum_{k,l} w_{i,j,k,l}x_{k,l} = \sum_{a,b} v_{i,j,a,b}x_{i+a,j+b} vi,j,a,b=wi,j,i+a,j+bv_{i,j,a,b} = w_{i,j,i+a,j+b}

几何理解:

一个四维体 Aa,b,c,dA_{a,b,c,d},摊开为二维面 Ba×c,b×dB_{a\times c,b\times d},其中

  • BB 视为一个大型二维面 Ta,bT_{a,b}
  • 其中从任意一点可建立一个小型二维面 tc,dt_{c,d}
  • 此时对整个棋盘的 Ai,j,k,wA_{i,j,k,w} 索引
    • 先找到 TT 面上坐标 (i,j)(i,j) 的点 OO
    • 从 OO 点建立小二维面 tt,找其中相对坐标(k,w)(k,w) 的点 xx
    • 找到的 xx 就是二维面 BB 中的 Bi+k,j+wB_{i+k,j+w} 元素

上面的索引中,i,ji,j 是大棋盘坐标,a,ba,b 是小棋盘坐标,因此 i+a,j+bi+a, j+b 是展开平面中的真实坐标

原则 1: 平移不变性

xx 的平移导致 hh 的变化 hi,j=∑a,bva,bxi+a,j+bh_{i,j} = \sum_{a,b} v_{a,b}x_{i+a,j+b}

根据平移不变性,vv 不应依赖于 (i,j)(i,j),即 vi,j,a,b=va,bv_{i,j,a,b} = v_{a,b} hi,j=∑a,bva,bxi+a,j+bh_{i,j} = \sum_{a,b} v_{a,b}x_{i+a,j+b}

这就是二维卷积(交叉相关)

几何理解:

分开理解大坐标 (i,j)(i,j) 和小坐标 (a,b)(a,b)

  • 新权重张量 va,bv_{a,b} 依赖于小坐标 (a,b)(a,b),即小棋盘中每个位置有独特权重
  • va,bv_{a,b} 不依赖于大坐标 (i,j)(i,j),即在所有大棋盘中随便取一个小棋盘,整体权重分布是相同的

原则 2: 局部性

hi,j=∑a,bva,bxi+a,j+bh_{i,j} = \sum_{a,b} v_{a,b}x_{i+a,j+b} 当评估 hi,jh_{i,j} 时,不应该使用远离 xi,jx_{i,j} 的参数

即当 ∣a∣,∣b∣>Δ|a|,|b| > \Delta 时,令 va,b=0v_{a,b} = 0 hi,j=∑∣a∣,∣b∣≤Δva,bxi+a,j+b h_{i,j} = \sum_{|a|,|b|\leq \Delta} v_{a,b}x_{i+a,j+b}

*几何理解:

全图尺寸为 (n,m)(n,m) 时,我们不需要 (n,m)(n,m) 的权重张量,而只需要 (2Δ,2Δ)(2\Delta, 2\Delta) 的权重张量,即卷积核

最终结论

hi,j=∑a,bvi,j,a,bxi+a,j+b全连接⇓平移不变性, 局部性hi,j=∑∣a∣,∣b∣≤Δva,bxi+a,j+b卷积\begin{aligned} h_{i,j} &= \sum_{a,b} v_{i,j,a,b}x_{i+a,j+b}\quad \text{全连接} \\ \\ &\Downarrow \text{平移不变性, 局部性} \\ \\ h_{i,j} &= \sum_{|a|,|b|\leq \Delta} v_{a,b}x_{i+a,j+b}\quad \text{卷积} \end{aligned}

卷积层

二维卷积层

形状

  • 输入 XX: nh×nwn_h \times n_w
  • 卷积核 WW: kh×kwk_h \times k_w
  • 偏置 bb: 标量
  • 输出 YY: (nh−kh+1)×(nw−kw+1)(n_h-k_h+1) \times (n_w-k_w+1)

计算

Y=X∗W+bY = X * W + b

  • ∗* 表示二维交叉操作(逐元素相乘求和)
  • W,bW, b 是可学习参数

交叉相关 vs 卷积

  • 二维交叉相关 Yi,j=∑a,bWa,bXi+a,j+bY_{i,j} = \sum_{a,b} W_{a,b}X_{i+a,j+b}
  • 二维卷积 Yi,j=∑a,bWa,bXi−a,j−bY_{i,j} = \sum_{a,b} W_{a,b}X_{i-a,j-b}

卷积核翻转 180°,实际上不用考虑

一维与三维交叉相关

  • 一维交叉相关 Yi=∑aWaXi+aY_i = \sum_a W_a X_{i+a}

    • 文本
    • 时序序列
  • 三维交叉相关 Yi,j,k=∑a,b,cWa,b,cXi+a,j+b,k+cY_{i,j,k} = \sum_{a,b,c} W_{a,b,c} X_{i+a,j+b,k+c}

    • 视频
    • 医学图像
    • 气象地图

图像卷积实现

代码 12 · In [1]
import torch
from torch import nn

实现二维互相关计算

代码 14 · In [2]
def corr2d(X, K):
    """计算二维互相关运算
    @param X: 输入张量,二维
    @param K: 卷积核,二维
    @return: 输出张量,二维
    """
    # 获取卷积核的高度和宽度
    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]):
            # 取输入张量中,以 (i, j) 为左上角,形状为 (h, w) 的区域,与卷积核进行逐元素相乘并求和
            Y[i, j] = (X[i:i+h, j:j+w] * K).sum()
    return Y
代码 15 · In [3]
X = torch.tensor([[0.0, 1.0, 2.0],
                  [3.0, 4.0, 5.0],
                  [6.0, 7.0, 8.0]])
K = torch.tensor([[0.0, 1.0],
                  [2.0, 3.0]])
corr2d(X, K)  # 输出张量
已保存输出
tensor([[19., 25.],
        [37., 43.]])

实现二维卷积层

代码 17 · In [4]
class Conv2D(nn.Module):
    """二维卷积层
    @param kernel_size: 卷积核的大小,整数或元组
    """
    def __init__(self, kernel_size: int | tuple[int, int]):
        super().__init__()
        # 如果 kernel_size 是整数,则将其转换为元组 (kernel_size, kernel_size)
        if isinstance(kernel_size, int):
            kernel_size = (kernel_size, kernel_size)
        # 初始化卷积核参数,形状为 (kernel_height, kernel_width)
        self.weight = nn.Parameter(torch.rand(*kernel_size))
        # 初始化偏置参数,标量
        self.bias = nn.Parameter(torch.zeros(1))

    def forward(self, X):
        """前向传播
        @param X: 输入张量,二维
        @return: 输出张量,二维
        """
        return corr2d(X, self.weight) + self.bias

检测图像中不同颜色边缘

创建一个 6x8 的图像张量,左右为 1 表示白色,中间为 0 表示黑色

代码 19 · In [5]
X = torch.ones((6, 8))
X[:, 2:6] = 0
X
已保存输出
tensor([[1., 1., 0., 0., 0., 0., 1., 1.],
        [1., 1., 0., 0., 0., 0., 1., 1.],
        [1., 1., 0., 0., 0., 0., 1., 1.],
        [1., 1., 0., 0., 0., 0., 1., 1.],
        [1., 1., 0., 0., 0., 0., 1., 1.],
        [1., 1., 0., 0., 0., 0., 1., 1.]])

创建一个卷积核,检测颜色变化

代码 21 · In [8]
K = torch.tensor([[1.0, -1.0]]) # 左右一致则为 0

Y 中的 1 表示白到黑,-1 表示黑到白,0 表示没有变化

代码 23 · In [9]
Y = corr2d(X, K)
Y
已保存输出
tensor([[ 0.,  1.,  0.,  0.,  0., -1.,  0.],
        [ 0.,  1.,  0.,  0.,  0., -1.,  0.],
        [ 0.,  1.,  0.,  0.,  0., -1.,  0.],
        [ 0.,  1.,  0.,  0.,  0., -1.,  0.],
        [ 0.,  1.,  0.,  0.,  0., -1.,  0.],
        [ 0.,  1.,  0.,  0.,  0., -1.,  0.]])

K 只能检测垂直边缘

代码 25 · In [11]
corr2d(X.t(), K)
已保存输出
tensor([[0., 0., 0., 0., 0.],
        [0., 0., 0., 0., 0.],
        [0., 0., 0., 0., 0.],
        [0., 0., 0., 0., 0.],
        [0., 0., 0., 0., 0.],
        [0., 0., 0., 0., 0.],
        [0., 0., 0., 0., 0.],
        [0., 0., 0., 0., 0.]])

学习卷积核

代码 27 · In [14]
# 输入通道数为 1,输出通道数为 1,卷积核大小为 (1, 2),不使用偏置
conv2d = nn.Conv2d(1, 1, kernel_size=(1, 2), bias=False)

# 给 X 和 Y 加入通道和批量维度
X = X.reshape((1, 1, 6, 8))
Y = Y.reshape((1, 1, 6, 7))

for i in range(10):
    # 前向传播
    Y_hat = conv2d(X)
    # 平方和误差损失
    l = ((Y_hat - Y) ** 2).sum()
    # 梯度清零
    conv2d.zero_grad()
    # 反向传播
    l.backward()
    # SGD 更新参数
    conv2d.weight.data[:] -= 3e-2 * conv2d.weight.grad
    if (i + 1) % 2 == 0:
        print(f'epoch {i + 1}, loss {l.item():.3f}')
已保存输出
epoch 2, loss 4.546
epoch 4, loss 0.997
epoch 6, loss 0.263
epoch 8, loss 0.083
epoch 10, loss 0.030

查看学到的卷积核权重,应该接近 (1, -1),即检测颜色变化

代码 29 · In [20]
conv2d.weight.data.reshape(-1, 2) # 去掉没用的通道、批量维度
已保存输出
tensor([[ 0.9731, -1.0072]])

卷积层超参数

填充

之前提到输出形状是 (nh−kh+1)×(nw−kw+1)(n_h-k_h+1) \times (n_w-k_w+1),即输出缩小了

  • 层数越多,输出越被压缩
  • 卷积核越大,输出缩小越快

因此在原始图像的外围填充额外的行/列(一般为全0)

  • 填充 ph,pwp_h, p_w 行/列,输出形状为 (nh−kh+1+ph)×(nw−kw+1+pw)(n_h-k_h+1+p_h) \times (n_w-k_w+1+p_w)
  • 通常取 ph=kh−1,pw=kw−1p_h = k_h - 1, p_w = k_w - 1,即输出形状为 (nh,nw)(n_h, n_w)
    • kk 为奇数,两侧各填充 p2\cfrac{p}{2} 行/列
    • kk 为偶数,左/上填充 ⌈p2⌉\left\lceil \cfrac{p}{2} \right\rceil,右/下填充 ⌊p2⌋\left\lfloor \cfrac{p}{2} \right\rfloor
代码 33 · In [21]
def comp_conv2d(conv2d, X):
    """计算卷积层输出"""
    # 给 X 加入通道和批量维度
    X = X.reshape((1, 1, *X.shape))
    # 前向传播
    Y = conv2d(X)
    # 去掉通道和批量维度
    return Y.reshape(Y.shape[2:])
代码 34 · In [25]
# 创建一个填充的卷积层和一个不填充的卷积层
conv2d_nopad = nn.Conv2d(1, 1, kernel_size=3, padding=0)
conv2d_pad = nn.Conv2d(1, 1, kernel_size=3, padding=1)
X = torch.rand(size=(8, 8))
# 比较输出形状
comp_conv2d(conv2d_nopad, X).shape, comp_conv2d(conv2d_pad, X).shape
已保存输出
(torch.Size([6, 6]), torch.Size([8, 8]))

步幅

步幅指卷积核在行、列方向上每次移动的距离,即滑动步长

  • 给定步幅 sh,sws_h, s_w,输出形状为 ⌊nh−kh+ph+shsh⌋×⌊nw−kw+pw+swsw⌋\left \lfloor \cfrac{n_h-k_h+p_h+s_h}{s_h} \right \rfloor\times \left \lfloor \cfrac{n_w-k_w+p_w+s_w}{s_w} \right \rfloor
  • 如果 ph=kh−1,pw=kw−1p_h = k_h - 1, p_w = k_w - 1,输出形状为 ⌊nh+sh−1sh⌋×⌊nw+sw−1sw⌋\left \lfloor \cfrac{n_h+s_h-1}{s_h} \right \rfloor\times \left \lfloor \cfrac{n_w+s_w-1}{s_w} \right \rfloor
  • 如果 nh,nwn_h, n_w 可被 sh,sws_h, s_w 整除,输出形状为 nhsh×nwsw\cfrac{n_h}{s_h}\times\cfrac{n_w}{s_w}

让图像高宽缩小到一半(步幅取 2,图像高宽是偶数)

代码 38 · In [27]
conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1, stride=2)
X.shape, comp_conv2d(conv2d, X).shape
已保存输出
(torch.Size([8, 8]), torch.Size([4, 4]))

一个稍复杂的例子

  • kh=3,kw=5k_h = 3, k_w = 5
  • ph=0,pw=1p_h = 0, p_w = 1
  • sh=3,sw=4s_h = 3, s_w = 4
输入=(nh,nw)=(8,8)填充后=(8+ph,8+pw)=(8,9)输出=(⌊8−kh+shsh⌋,⌊9−kw+swsw⌋)=(⌊83⌋,2)\begin{aligned} \text{输入} &= (n_h, n_w) &= (8, 8) \\ \text{填充后} &= (8+p_h, 8+p_w) &= (8, 9) \\ \text{输出} &= \left(\left\lfloor \cfrac{8-k_h+s_h}{s_h} \right\rfloor, \left\lfloor \cfrac{9-k_w+s_w}{s_w} \right\rfloor\right) &= (\left\lfloor\cfrac{8}{3}\right\rfloor, 2) \end{aligned}

一般不至于这样,通常用对称的输入

代码 40 · In [29]
conv2d = nn.Conv2d(1, 1, kernel_size=(3, 5), padding=(0, 1), stride=(3, 4))
X.shape, comp_conv2d(conv2d, X).shape
已保存输出
(torch.Size([8, 8]), torch.Size([2, 2]))

通道

多输入通道

彩色图像有 RGB 3 个通道,每个通道拥有一个二维卷积核,将每个通道的卷积结果相加,构成三维卷积核

  • 输入 XX: ci×nh×nwc_i \times n_h \times n_w
  • 卷积核 WW: ci×kh×kwc_i \times k_h \times k_w
  • 偏置 bb: cic_i 维向量
  • 输出 YY: mh×mwm_h \times m_w

Y=∑i=1ciXi∗Wi+bY = \sum_{i=1}^{c_i} X_i * W_i + b

多输出通道

可以有多个三维卷积核,每个卷积核产生一个输出通道,最终输出为多个通道的堆叠

  • 输入 XX: ci×nh×nwc_i \times n_h \times n_w
  • 卷积核 WW: co×ci×kh×kwc_o \times c_i \times k_h \times k_w
  • 偏置 bb: co×cic_o \times c_i
  • 输出 YY: co×mh×mwc_o \times m_h \times m_w

Yj=∑i=1ciXi∗Wj,i+bj,j=1,2,…,coY_j = \sum_{i=1}^{c_i} X_i * W_{j,i} + b_j, \quad j=1,2,\dots,c_o

意义

  • 每个输出通道可以识别特定模式(类似 transformer 的多个注意力头)
  • 输入通道可以识别并组合输入中的模式

深度卷积神经网络中,通常先把 RGB 图像卷积到多个特征图,再把特征图卷积到更多的特征图,逐渐提取更高层次的特征

1x1 卷积层

kh=kw=1k_h = k_w = 1 是一个特殊的卷积层,它不识别空间模式,只是加权融合通道

相当于输入 nhnw×cin_h n_w \times c_i 输出 nhnw×con_h n_w \times c_o,权重为 co×cic_o \times c_i 的全连接层,即每个像素点上的通道线性组合

实现多输入通道互相关运算

代码 44 · In [30]
def corr2d_multi_in(X, K):
    """计算多输入通道的二维互相关运算
    @param X: 输入张量,形状为 (c_i, n_h, n_w)
    @param K: 卷积核,形状为 (c_i, k_h, k_w)
    @return: 输出张量,二维
    """
    # 对每个输入通道和卷积核通道进行互相关运算,并将结果相加
    return sum(corr2d(x, k) for x, k in zip(X, K))
代码 45 · In [35]
# 创建一个输入张量 X,形状为 (2, 3, 3),表示有 2 个输入通道,每个通道是 3x3 的矩阵
X = torch.tensor([[[0.0, 1.0, 2.0],
                   [3.0, 4.0, 5.0],
                   [6.0, 7.0, 8.0]],

                  [[1.0, 2.0, 3.0],
                   [4.0, 5.0, 6.0],
                   [7.0, 8.0, 9.0]]])
# 创建一个卷积核张量 K,形状为 (2, 2, 2),表示有 2 个输入通道,每个通道是 2x2 的卷积核
K = torch.tensor([[[0.0, 1.0],
                   [2.0, 3.0]],

                  [[1.0, 2.0],
                   [3.0, 4.0]]])
corr2d_multi_in(X, K)  # 输出张量
已保存输出
tensor([[ 56.,  72.],
        [104., 120.]])

实现多输出通道互相关运算

代码 47 · In [33]
def corr2d_multi_in_out(X: torch.Tensor, K: torch.Tensor):
    """计算多输入通道和多输出通道的二维互相关运算
    @param X: 输入张量,形状为 (c_i, n_h, n_w)
    @param K: 卷积核,形状为 (c_o, c_i, k_h, k_w)
    @return: 输出张量,形状为 (c_o, m_h, m_w)
    """
    # torch.stack 沿着新维度合并张量,dim 指定新维度插入的位置
    return torch.stack([corr2d_multi_in(X, k) for k in K], dim=0)
代码 48 · In [36]
# 创建一个卷积核张量 K,形状为 (3, 2, 2, 2),表示有 3 个输出通道,每个输出通道有 2 个输入通道,每个输入通道是 2x2 的卷积核
K4d = torch.stack((K, K + 1, K + 2), dim=0)
corr2d_multi_in_out(X, K4d)  # 输出张量
已保存输出
tensor([[[ 56.,  72.],
         [104., 120.]],
    [[ 76., 100.],
     [148., 172.]],

    [[ 96., 128.],
     [192., 224.]]])</pre></details>

验证 1x1 卷积等价于全连接

代码 50 · In [38]
def corr2d_multi_in_out_1x1(X: torch.Tensor, K: torch.Tensor):
    """计算多输入通道和多输出通道的 1x1 卷积互相关运算
    @param X: 输入张量,形状为 (c_i, n_h, n_w)
    @param K: 卷积核,形状为 (c_o, c_i, 1, 1)
    @return: 输出张量,形状为 (c_o, n_h, n_w)
    """
    c_i, h, w = X.shape
    c_o = K.shape[0]
    # X 把图像拉成一维,因为每个像素点自己在多个通道组合,不考虑位置关系
    X = X.reshape((c_i, h * w))
    # K 把两个没用的维度去掉,方便矩阵乘法
    K = K.reshape((c_o, c_i))
    # 直接使用矩阵乘法计算输出,相当于每个像素点上做全连接
    Y = torch.matmul(K, X)
    # 将结果 reshape 回原来的形状
    return Y.reshape((c_o, h, w))
代码 51 · In [50]
X = torch.normal(mean=0, std=1, size=(3, 3, 3))  # 输入张量,形状为 (3, 3, 3)
K = torch.normal(mean=0, std=1, size=(2, 3, 1, 1))  # 卷积核,形状为 (2, 3, 1, 1)

Y1 = corr2d_multi_in_out_1x1(X, K)
Y2 = corr2d_multi_in_out(X, K)
# 验证两个输出张量是否接近
torch.testing.assert_close(Y1, Y2)

池化层

原理

池化层通过在输入特征图上滑动固定大小的滑动窗口,对窗口内的元素进行聚合操作,生成更小的特征图,通常紧随卷积层之后。

类型

  • 最大池化(Max Pooling): 窗口最大值作为输出,保留最强的模式信号

  • 平均池化(Average Pooling): 窗口平均值作为输出,平滑特征,保留更多背景信息

作用

  • 特征降维: 减少特征图尺寸,降低后续计算量和参数量

  • 防止过拟合: 减少参数数量和模型复杂度,降低过拟合风险

  • 缓解位置敏感性: 增强模型对平移、旋转和尺度变化的鲁棒性

超参数

  • 窗口大小、步幅、填充都与卷积层类似
  • 输入通道数 = 输出通道数

实现池化层的正向传播

代码 55 · In [52]
def pool2d(x, pool_size, mode='max'):
    """计算二维池化运算
    @param x: 输入张量,形状为 (n_h, n_w)
    @param pool_size: 池化窗口大小,(h, w) 元组
    @param mode: 池化模式,'max' 或 'avg'
    @return: 输出张量,二维
    """
    h, w = pool_size
    # 按形状公式创建输出张量
    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]):
            # 取输入张量中,以 (i, j) 为左上角,形状为 (h, w) 的区域
            if mode == 'max':
                Y[i, j] = x[i:i+h, j:j+w].max()
            elif mode == 'avg':
                Y[i, j] = x[i:i+h, j:j+w].mean()
    return Y
代码 56 · In [53]
X = torch.tensor([[1.0, 2.0, 3.0],
                  [4.0, 5.0, 6.0],
                  [7.0, 8.0, 9.0]])
print(pool2d(X, (2, 2), 'max'))  # 最大池化
print(pool2d(X, (2, 2), 'avg'))  # 平均池化
已保存输出
tensor([[5., 6.],
        [8., 9.]])
tensor([[3., 4.],
        [6., 7.]])

pytorch 框架中默认步幅与窗口大小相同(窗口不重叠)

代码 58 · In [72]
pool2d = nn.MaxPool2d(3)
X = torch.tensor([[1.0, 2.0, 3.0],
                  [4.0, 5.0, 6.0],
                  [7.0, 8.0, 9.0]])
# nn.MaxPool2d 接收 (N, C, H, W) 形状的输入张量,N 为批量大小,C 为通道数,H 为高度,W 为宽度
pool2d(X.reshape((1, 1, 3, 3))).reshape((1, 1))
已保存输出
tensor([[9.]])

设置一个形状莫名其妙的池化层

代码 60 · In [73]
pool2d = nn.MaxPool2d((2, 3), padding=(1, 1), stride=(2, 3))
pool2d(X.reshape((1, 1, 3, 3))).reshape(-1, 1)
已保存输出
tensor([[2.],
        [8.]])

池化层在每个输入通道单独运算

代码 62 · In [74]
X = torch.stack((X, X + 1), dim=0)
X
已保存输出
tensor([[[ 1.,  2.,  3.],
         [ 4.,  5.,  6.],
         [ 7.,  8.,  9.]],
    [[ 2.,  3.,  4.],
     [ 5.,  6.,  7.],
     [ 8.,  9., 10.]]])</pre></details>
代码 63 · In [76]
pool2d = nn.MaxPool2d(3, padding=1, stride=1)
pool2d(X)
已保存输出
tensor([[[ 5.,  6.,  6.],
         [ 8.,  9.,  9.],
         [ 8.,  9.,  9.]],
    [[ 6.,  7.,  7.],
     [ 9., 10., 10.],
     [ 9., 10., 10.]]])</pre></details>