【动手学深度学习】卷积神经网络(CNN)入门
- 1,卷积神经网络简介
- 2,卷积层
- 2.1,互相关运算原理
- 2.2,互相关运算实现
- 2.3,实现卷积层
- 3,卷积层的简单应用:边缘检测
- 3.1,初始化输入矩阵
- 3.2,构造卷积核用于边缘检测
- 3.3,通过学习得到卷积核
- 4,填充和步幅
- 4.1,填充
- 4.2,步幅
- 4.3,卷积层输出矩阵形状计算
- 5,多输入通道和多输出通道
- 5.3,多输入通道
- 5.4,多输出通道
- 5.5,1×1卷积层
- 6,池化层
- 6.1,最大池化和平均池化
- 6.2,实现池化操作
- 6.3,池化操作的填充和步幅
1,卷积神经网络简介
卷积神经网络(CNN)的核心原理是通过局部感受野、参数共享和层级化特征提取,模仿生物视觉处理机制,自动从图像或序列数据中学习特征。其应用广泛,尤其在计算机视觉、语音识别、自然语言处理等领域效果显著。CNN通过局部特征提取+层级抽象的原理,将复杂任务(如图像分类)转化为可学习的数学问题。其应用从早期的手写识别发展到今天的自动驾驶、医疗诊断等关键领域,核心优势在于自动特征学习和对空间/时序数据的高效处理,成为深度学习最成功的模型之一。
2,卷积层
2.1,互相关运算原理
卷积层中,输入张量和核张量通过互相关运算生成输出张量。 因此卷积神经网络中的卷积层中表达的运算实际上是互相关运算
。
以二维图像为例解释二维互相关运算:
如上图所示:
- 输入是高度为 3、宽度为 3 的二维张量(即形状为 3×3 );
- 卷积核的高度和宽度都是 2 ;
- 卷积的输出形状取决于输入形状和卷积核形状。
二维互相关运算中,卷积窗口从输入张量的左上角开始,从左到右、从上到下滑动。 当卷积窗口滑动到新一个位置时,包含在该窗口中的部分张量与卷积核张量进行按元素相乘,得到的张量再求和得到一个单一的标量值,由此我们得出了这一位置的输出张量值。在如上例子中,输出张量的四个元素由二维互相关运算得到。
则经过如下计算得到输出矩阵:
0 × 0 + 1 × 1 + 3 × 2 + 4 × 3 = 19 , 1 × 0 + 2 × 1 + 4 × 2 + 5 × 3 = 25 , 3 × 0 + 4 × 1 + 6 × 2 + 7 × 3 = 37 , 4 × 0 + 5 × 1 + 7 × 2 + 8 × 3 = 43. 0\times0+1\times1+3\times2+4\times3=19,\\ 1\times0+2\times1+4\times2+5\times3=25,\\ 3\times0+4\times1+6\times2+7\times3=37,\\ 4\times0+5\times1+7\times2+8\times3=43. 0×0+1×1+3×2+4×3=19,1×0+2×1+4×2+5×3=25,3×0+4×1+6×2+7×3=37,4×0+5×1+7×2+8×3=43.
计算输出矩阵形状:
输出矩阵的大小等于输入大小 n h × n w n_h \times n_w nh×nw减去卷积核大小 k h × k w k_h \times k_w kh×kw,即:
( n h − k h + 1 ) × ( n w − k w + 1 ) (n_h-k_h+1) \times (n_w-k_w+1) (nh−kh+1)×(nw−kw+1)
因此上述例子的输出矩阵形状为: ( 3 − 2 + 1 ) × ( 3 − 2 + 1 ) = 2 × 2 (3-2+1) \times (3-2+1)=2\times2 (3−2+1)×(3−2+1)=2×2
2.2,互相关运算实现
导入相关库:
import torch
from torch import nn
from d2l import torch as d2l
定义corr2d函数实现二维互相关运算:
"""输入矩阵和核矩阵之间进行二维互相关运算"""
def corr2d(X, K): # K是核矩阵,h,w分别是核矩阵的高和宽h, w = K.shape# 先将输出矩阵全部元素初始化为0(输出矩阵的形状由公式计算得到)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 中取出对应的局部区域 X[i:i + h, j:j + w]即取出从输入矩阵中的i行开始往后到(i+h)行,从j列开始往后到(j+w)列这么一个局部区域与核矩阵 K 进行元素级乘法,最后对相乘结果求和,将结果填入输出矩阵对应的位置。此即为二维互相关运算。"""Y[i, j] = (X[i:i + h, j:j + w] * K).sum()return Y
验证二维互相关运算:
# 输入矩阵
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)
运算结果如下:
2.3,实现卷积层
卷积层对输入
和卷积核权重
进行互相关运算,并在添加标量偏置之后产生输出。 所以,卷积层中的两个被训练的参数是卷积核权重和标量偏置。 就像我们之前随机初始化全连接层一样,在训练基于卷积层的模型时,我们也随机初始化卷积核权重。
基于corr2d互相关运算函数实现二维卷积层:
# nn.Module 是 PyTorch 中所有神经网络模块的基类。
# PyTorch 中构建自己的神经网络时,常通过继承 nn.Module 类并定义自己的网络层或整个网络架构
class Conv2D(nn.Module):def __init__(self, kernel_size):super().__init__()# 随机初始化权重(生成一个kernel_size型的随机张量。如 kernel_size=(3, 3))self.weight = nn.Parameter(torch.rand(kernel_size))# 标量初始化为0self.bias = nn.Parameter(torch.zeros(1))# 重写 nn.Module 的 forward 方法来定义模型的前向传播逻辑def forward(self, x):# corr2d()二维互相关运算return corr2d(x, self.weight) + self.bias
总结:
- 卷积层将输入和核矩阵进行交叉相关,加上偏移后得到输出;
- 核矩阵和偏移是可学习的参数;
- 核矩阵的大小是超参数;
3,卷积层的简单应用:边缘检测
介绍一个卷积层的简单应用:通过找到像素变化的位置,来检测图像中不同颜色的边缘。
3.1,初始化输入矩阵
# 初始化矩阵:构造一个 6×8像素的黑白图像。中间四列为黑色(0),其余像素为白色(1)。
X = torch.ones((6, 8))
# X的所有行和索引为2的列到索引为6的列(不包括索引为6的列)设置为0
X[:, 2:6] = 0
X # 输出查看
运行结果如下:
3.2,构造卷积核用于边缘检测
# 构造一个高度为 1、宽度为 2的卷积核K
# 当进行互相关运算时,如果水平相邻的两元素相同,则输出为零,否则输出为非零
K = torch.tensor([[1.0, -1.0]])
对参数X(输入)和K(卷积核)执行互相关运算:
Y = corr2d(X, K)
# 输出结果查看
Y
运行结果如下:
注意:输出Y中的1代表从白色到黑色的边缘,-1代表从黑色到白色的边缘,其他情况的输出为 0
局限性:
现在我们将输入X的二维图像转置,再进行如上的互相关运算。
# 转置之后图像中的边变为横向,而此核只能检测垂直的
corr2d(X.t(), K)
运行结果如下:之前检测到的垂直边缘消失了。
因此当前手动设计的卷积核 K = torch.tensor([[1.0, -1.0]]) 只可检测垂直边缘,无法检测水平边缘。
3.3,通过学习得到卷积核
复杂数值情形下,手动设计卷积核肯定存在很大局限性。依然是垂直边缘检测的例子,本节介绍通过从输入输出对(X-Y)中学习得到卷积核。
给定X输入和Y输出,学习得到卷积核K
# 构造一个二维卷积层,它具有1个输出通道和形状为(1,2)的卷积核
"""
nn.Conv2d是二维卷积层的构造函数,在pytorch用于执行二维卷积操作
* 第一个参数表示输入通道的数量,1表示输入数据只有一个通道,如灰度图像
* 第二个参数表示输出通道的数量,1表示 卷积层将输出一个通道的数据
* kernel_size=(1, 2)表示卷积核的大小,此处高为1,宽为2
* bias=False,这个参数指定卷积操作后不添加可学习的偏置项
"""
conv2d = nn.Conv2d(1,1, kernel_size=(1, 2), bias=False)# 二维卷积层使用四维输入和输出格式(批量大小、通道、高度、宽度)(XY均被重塑为4维张量)
# 其中批量大小和通道数都为1
# X和Y都使用上面的XY
X = X.reshape((1, 1, 6, 8))"""
输出矩阵高:输入矩阵高-核矩阵高+1=6
输出矩阵宽:输入矩阵的宽-核矩阵宽+1=7
"""
Y = Y.reshape((1, 1, 6, 7))lr = 3e-2 # 学习率for i in range(10): # 迭代十次Y_hat = conv2d(X)# 计算损失,以便通过反向传播算法将损失梯度传播回网络的每一层l = (Y_hat - Y) ** 2# 更新参数之前需要将参数梯度置为0conv2d.zero_grad()# l.sum是计算损失l中所有元素的和# 计算损失函数关于卷积层权重的梯度(此处反向传播,将损失梯度传播回网络的每一层)l.sum().backward()# 迭代卷积核(梯度下降法更新卷积层权重)conv2d.weight.data[:] -= lr * conv2d.weight.grad# 每两个bitch输出一下lossif (i + 1) % 2 == 0:print(f'epoch {i+1}, loss {l.sum():.3f}')
运行结果如下:
根据输出可以看出:十次迭代之后,误差已经降到最低。
查看经过十次迭代之后,学习得到的卷积核的权重张量:
# 输出所学卷积核的权重张量
conv2d.weight.data.reshape((1, 2))
输出结果如下:
此时我们就会发现,此处经过学习得到的卷积核权重非常接近我们之前自定义的卷积核。能够很好地应用到垂直边缘检测。
4,填充和步幅
通过以上学习,我们已知卷积的输出形状取决于输入形状和卷积核的形状。
假设输入形状为 n h × n w n_h\times n_w nh×nw,卷积核形状为 k h × k w k_h\times k_w kh×kw,那么输出形状将是 ( n h − k h + 1 ) × ( n w − k w + 1 ) (n_h-k_h+1) \times (n_w-k_w+1) (nh−kh+1)×(nw−kw+1)。
填充和步幅可用于有效地调整数据的维度。 填充可以增加输出的高度和宽度,步幅可以减小输出的高和宽。接下来详细解释填充和步幅:
4.1,填充
在应用了连续的卷积之后,我们最终得到的输出大小会远小于输入大小。这是由于卷积核的宽度和高度通常大于 1 所导致的。一个 240×240 像素
的图像,经过 10 层 5×5 的卷积
后,将减少到 200×200
像素。如此一来,原始图像的边界丢失了许多有用信息。 解决这个问题的简单方法即为填充(padding):在输入图像的边界填充元素(通常填充元素是 0)。我们将 3×3 输入填充到 5×5,那么它的卷积输出就会增加为到4×4 。
如果我们添加 p h p_h ph行填充(大约一半在顶部,一半在底部)和 p w p_w pw列填充(左侧大约一半,右侧一半),则输出形状将为:
( n h − k h + p h + 1 ) × ( n w − k w + p w + 1 ) 。 (n_h-k_h+p_h+1)\times(n_w-k_w+p_w+1)。 (nh−kh+ph+1)×(nw−kw+pw+1)。
这意味着输出的高度和宽度将分别增加 p h p_h ph和 p w p_w pw。
在许多情况下,我们需要设置 p h = k h − 1 p_h=k_h-1 ph=kh−1和 p w = k w − 1 p_w=k_w-1 pw=kw−1,消去两项,使输入和输出具有相同的高度和宽度。 这样可以在构建网络时更容易地预测每个图层的输出形状。
-
假设 k h k_h kh是奇数(此时 p h p_h ph为偶数),我们将在高度的两侧填充 p h / 2 p_h/2 ph/2行;
-
假设 k h k_h kh是偶数(很少发生,万一发生的话,此时 p h p_h ph为奇数),则一种可能性是在输入顶部填充 ⌈ p h / 2 ⌉ \lceil p_h/2\rceil ⌈ph/2⌉行,在底部填充 ⌊ p h / 2 ⌋ \lfloor p_h/2\rfloor ⌊ph/2⌋行。同理,我们填充宽度的两侧。
比如,在下面的例子中,我们创建一个高度和宽度为3的二维卷积层(即卷积核大小为3*3),并
在所有侧边填充1个像素
。给定高度和宽度为8的输入,则输出的高度和宽度也是8
。
import torch
from torch import nn
"""
comp_conv2d()对二维数据执行二维卷积操作,并返回卷积后的结果,但去除了批量大小和通道数维度,只保留了高度和宽度。此函数初始化卷积层权重,并对输入和输出提高和缩减相应的维数
"""
def comp_conv2d(conv2d, X):# 首先将输入矩阵 X 重新塑形,以适应二维卷积层的输入要求# 这里的(1,1)表示批量大小和通道数都是1# 如下是一个元组拼接操作。将元组 (1, 1) 与 X.shape 返回的元组(height, width)拼接在一起,形成新的形状 (1, 1, height, width)X = X.reshape((1, 1) + X.shape)# 二维卷积层输出格式为:批量大小、通道、高度、宽度。此时 Y四维Y = conv2d(X)"""返回时省略前两个维度:批量大小和通道(只保留索引为2的元素到最后一个元素)若张量 Y 的形状为 (batch_size, in_channels, height, width),即它是一个四维张量,那么 Y.shape[2:] 将返回一个包含 height 和 width 的元组( batch_size 和 in_channels 是前两个维度)"""return Y.reshape(Y.shape[2:])"""
* nn.Conv2d()是二维卷积层的一个构造函数,入参可以是为输入输出通道数、核函数形状、填充数、是否加入偏置项。最终返回一个conv2d实例;
* conv2d(X)表示使用Conv2d实例对输入张量X进行卷积;其中X通常是4维的,四个维度分别代表:批量大小、通道数、高、宽前两个参数1表示输入和输出的通道数都为1;
padding表示上下左右各填充一行;
请注意,这里*每边*都填充了1行或1列,因此总共添加了2行或2列。
"""
conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1)# 随机初始化一个8*8的矩阵
X = torch.rand(size=(8, 8))"""8-3+2+1=8.所以输出Y的形状为8*8"""
comp_conv2d(conv2d, X).shape
运行结果如下:
特殊情况下的填充处理:
卷积核的高度和宽度不相同的情况下,我们可以填充不同的高度和宽度,使得输入和输出具有相同的高度和宽度。
代码示例:
# 输入X大小依然是8*8
# 构造二维卷积层(卷积核大小是5*3;上下各填充2,左右各填充1)
conv2d = nn.Conv2d(1, 1, kernel_size=(5, 3), padding=(2, 1))# 输出经过comp_conv2d计算后的形状
# (8-5+2*2+1)×(8-3+1*2+1)=8*8
comp_conv2d(conv2d, X).shape
运行结果如下:
4.2,步幅
有时,如果我们发现原始的输入分辨率十分冗余,我们可能希望大幅降低图像的宽度和高度。此时步幅则可以在这类情况下提供帮助。
在计算互相关时,卷积窗口从输入张量的左上角开始,从左向右、从上到下滑动。在前面的例子中,我们
默认每次滑动一个元素
。但是,有时候为了高效计算或是缩减采样次数,卷积窗口可以跳过中间位置,每次滑动多个元素。 可以成倍的减少输出形状。我们将 每次滑动元素的数量称为步幅 (stride)。
如上图所示,可以看到,为了计算输出中第一列的第二个元素和第一行的第二个元素,卷积窗口分别向下滑动三行和向右滑动两列。但是,当卷积窗口继续向右滑动两列时,没有输出,除非我们添加另一列填充。
4.3,卷积层输出矩阵形状计算
假设输入形状为 n h × n w n_h\times n_w nh×nw,卷积核形状为 k h × k w k_h\times k_w kh×kw。
无填充、步幅默认为1时,输出矩阵形状为:
( n h − k h + 1 ) × ( n w − k w + 1 ) (n_h-k_h+1) \times (n_w-k_w+1) (nh−kh+1)×(nw−kw+1)
添加 p h p_h ph行填充(大约一半在顶部,一半在底部)和 p w p_w pw列填充(左侧大约一半,右侧一半),步幅为1时。输出矩阵形状为:
( n h − k h + p h + 1 ) × ( n w − k w + p w + 1 ) 。 (n_h-k_h+p_h+1)\times(n_w-k_w+p_w+1)。 (nh−kh+ph+1)×(nw−kw+pw+1)。
添加 p h p_h ph行、 p w p_w pw列填充,设置垂直步幅 s h s_h sh,水平步幅 s w s_w sw时,输出矩阵形状为:
⌊ n h − k h + p h s h + 1 ⌋ × ⌊ n w − k w + p w s w + 1 ⌋ \lfloor\frac{n_h-k_h+p_h}{s_h}+1\rfloor \times \lfloor\frac{n_w-k_w+p_w}{s_w}+1\rfloor ⌊shnh−kh+ph+1⌋×⌊swnw−kw+pw+1⌋
此时输出形状可换算为:
⌊ ( n h − k h + p h + s h ) / s h ⌋ × ⌊ ( n w − k w + p w + s w ) / 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 ⌊(nh−kh+ph+sh)/sh⌋×⌊(nw−kw+pw+sw)/sw⌋
如果我们设置了 p h = k h − 1 p_h=k_h-1 ph=kh−1和 p w = k w − 1 p_w=k_w-1 pw=kw−1,则输出形状将简化为: ⌊ ( n h + s h − 1 ) / s h ⌋ × ⌊ ( n w + s w − 1 ) / s w ⌋ \lfloor(n_h+s_h-1)/s_h\rfloor \times \lfloor(n_w+s_w-1)/s_w\rfloor ⌊(nh+sh−1)/sh⌋×⌊(nw+sw−1)/sw⌋
更进一步,如果输入的高度和宽度可以被垂直和水平步幅整除
,则输出形状将为: ( n h / s h ) × ( n w / s w ) (n_h/s_h) \times (n_w/s_w) (nh/sh)×(nw/sw)
代码演示: 将高度和宽度的步幅设置为2,从而将输入的高度和宽度减半。
# 输入X大小依然是8*8
# stride=2表示将宽度和高度的步幅均设置为2
conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1, stride=2)# (8-3+1*2+2)÷2向下取整后为4
# (8-3+1*2+2)÷2向下取整后为4。因此输出形状为4*4
comp_conv2d(conv2d, X).shape
运行结果如下:
下面是一个更复杂的例子:
# 输入X大小依然是8*8
# 行填充0,列填充1。垂直步幅3,水平步幅4
conv2d = nn.Conv2d(1, 1, kernel_size=(3, 5), padding=(0, 1), stride=(3, 4))# 计算输出矩阵的形状
# (8-3+0+3)÷3向下取整=2
# (8-5+1*2+4)÷4向下取整=2。因此2*2
comp_conv2d(conv2d, X).shape
输出结果如下:
5,多输入通道和多输出通道
到目前为止,我们仅展示了单个输入和单个输出通道的例子。 这使得我们可以将输入、卷积核和输出看作二维张量。实际应用中的大多数场景下并非为单纯的单通道,比如:彩色图像具有标准的RGB通道来代表红、绿和蓝,而RGB图像转为1通道的灰度图像会丢失信息。
当我们添加通道时,我们的输入和隐藏的表示都变成了三维张量。例如,每个RGB输入图像具有 3×ℎ×𝑤 的形状。我们将这个大小为 3 的轴称为
通道(channel)维度
。
5.3,多输入通道
当输入中包含多个通道时,需要构造一个与输入数据具有相同输入通道数的卷积核。以便与输入数据进行互相关运算。
比如,假设输入通道数为 C i C_i Ci,则卷积核的输入通道数也为 C i C_i Ci。卷积核窗口形状设为 k h × k w k_h×k_w kh×kw,那么:
- C i = 1 C_i=1 Ci=1时可以将卷积核看作形状为 k h × K w k_h×K_w kh×Kw的二维张量;
- C i > 1 C_i>1 Ci>1时,卷积核的每个输入通道将包含形状为 k h × K w k_h×K_w kh×Kw的张量,即得到形状为 C i × k h × K w C_i×k_h×K_w Ci×kh×Kw的卷积核;
由于每个输入和卷积核都有 C i C_i Ci个通道,我们可以对每个通道
输入的二维张量
和卷积核的二维张量
进行互相关运算,再对各通道求和得到二维张量。示例如下:
以下是一个具有两个输入通道的二位互相关运算示例:
计算式子为:
( 1 × 1 + 2 × 2 + 4 × 3 + 5 × 4 ) + ( 0 × 0 + 1 × 1 + 3 × 2 + 4 × 3 ) = 56 (1\times1+2\times2+4\times3+5\times4)+(0\times0+1\times1+3\times2+4\times3)=56 (1×1+2×2+4×3+5×4)+(0×0+1×1+3×2+4×3)=56
代码示例如下:
先定义多个输入通道X与多个卷积核K之间的互相关运算函数
import torch
from d2l import torch as d2l# 其中corr2d()是前面节定义的二维互相关操作
def corr2d_multi_in(X, K):# 先遍历“X”和“K”的第0个维度(通道维度),再把它们加在一起# corr2d()二维互相关操作# zip()将元素按位置配对形成一个元组的序列,创建成一个迭代器"""X和K的第一个维度即通道维度,一定是相等的,因此可以通过zip配对成功;for x, k in zip(X, K) 是遍历zip()生成的元组序列:每次迭代中,它解包元组以获取 x 和 k,这两个变量分别绑定到来自 X 和 K 的当前元素。"""return sum(d2l.corr2d(x, k) for x, k in zip(X, K))
构造输入张量X和核张量K,验证相关运算的输出
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 = torch.tensor([[[0.0, 1.0], [2.0, 3.0]], [[1.0, 2.0], [3.0, 4.0]]])print(X.shape) # X形状为(2, 3, 3)
print(K.shape) # K形状为(2, 2, 2)corr2d_multi_in(X, K)
运行结果如下:
5.4,多输出通道
到目前为止,不论有多少输入通道,我们还只有一个输出通道。而实际上,在最流行的神经网络架构中,随着神经网络层数的加深,我们常会增加输出通道的维数,通过减少空间分辨率以获得更大的通道深度。
- 用 c i c_i ci和 c o c_o co 分别表示输入和输出通道的数目, k h k_h kh和 k w k_w kw 为卷积核的高度和宽度;
- 为了获得多个通道的输出,我们可以为每个输出通道创建一个形状为 c i × k h × k w c_i\times k_h\times k_w ci×kh×kw的三维卷积核张量,这样
最终变为四维卷积核
,形状是 c o × c i × k h × k w c_o\times c_i\times k_h\times k_w co×ci×kh×kw; - 简单来说就是:有多个三维卷积核,每个卷积核生成一个输出通道。
可以认为每个输出通道可识别特定的模式
;
# 使用上面定义的K,K形状为(2, 2, 2)则K、K+1、K+2形状均为(2, 2, 2)
# torch.stack 的作用:沿着一个新维度将多个张量拼接起来。
# torch.stack 要求所有输入张量必须具有相同的形状。新维度的索引由参数 dim 指定(dim=0 表示在最前面添加维度)。
K = torch.stack((K, K + 1, K + 2), 0)# 打印K形状
K.shape
三个输出通道的三维卷积核,每个卷积核2个输入通道,每个输入通道都是2*2。因此拼接完是一个四维卷积核,形状为(3,2,2,2)
,运行结果如下:
接下来对输入张量 X 和卷积核张量 K 执行互相关运算。
def corr2d_multi_in(X, K):# 先遍历“X”和“K”的第0个维度(通道维度),再把它们加在一起# corr2d()二维互相关操作# zip()将元素按位置配对形成一个元组的序列,创建成一个迭代器"""X和K的第一个维度即通道维度,一定是相等的,因此可以通过zip配对成功;for x, k in zip(X, K) 是遍历zip()生成的元组序列:每次迭代中,它解包元组以获取 x 和 k,这两个变量分别绑定到来自 X 和 K 的当前元素。"""return sum(d2l.corr2d(x, k) for x, k in zip(X, K))# 打印出X和K
print(X)
print(K)
# 调用函数
corr2d_multi_in_out(X, K)
在互相关运算中,
每个输出通道先获取所有输入通道
,再拿对应该输出通道的卷积核
计算出结果。
- K形状为(3,2,2,2),
三
个形状为(2,2,2)的三维卷积核,对应三个输出通道
;- X形状为(2, 3, 3);
- 可以下图的一个1×1的卷积核为例,类比互相关运算的计算原理;
上述代码运行结果如下:
5.5,1×1卷积层
1×1卷积层的作用等价于全连接层。
因为使用了最小窗口,1×1 卷积失去了卷积层在高度和宽度维度上识别相邻元素间相互作用的能力。 实际上 1×1 卷积的计算发生在通道上。
1×1卷积就像是一个“信息处理器”,但它
只在通道之间做文章
,不改变图片的宽和高
,只调整通道的数量
。实际意义如下:
- 压缩信息(降维):假设你有一本厚厚的书(比如100个章节),但很多章节内容重复或无关。你用1×1卷积就像请一个编辑,把100章的内容压缩成20章,保留核心信息,去掉冗余;
- 扩展信息(升维):假设你有一张黑白照片(1个通道),想生成一张更丰富的艺术画。1×1卷积可以帮你把1个通道扩展成10个通道,每个通道代表不同的“风格特征”(比如线条、阴影、颜色);
- 跨通道交互:让不同通道的信息互相“交流”,生成更综合的特征;
- …
实现1×1卷积
# 采用卷积核大小为1x1的卷积。这样的卷积操作可以简化为输入通道和输出通道之间的线性变换(或称为全连接层,但在空间维度上独立应用)。
def corr2d_multi_in_out_1x1(X, K):# 获取:输入通道数、高、宽 c_i, h, w = X.shape # 获取输出通道数,输出通道数和卷积核的第一个维度相等c_o = K.shape[0]# 多输入通道X形状为:𝑐_𝑖×𝑛_ℎ×𝑛_𝑤。此处每个输入通道的所有元素都被展平成一个长向量。X = X.reshape((c_i, h * w))# 多输出通道核矩阵形状为𝑐_𝑜×𝑐_𝑖×𝑘_ℎ×𝑘_𝑤。此处卷积核大小为1x1,不需要额外的高宽维度。因此核矩阵形状为𝑐_𝑜×𝑐_𝑖# K就变成了一个标准的权重矩阵,可以用于矩阵乘法K = K.reshape((c_o, c_i))# 全连接层中的矩阵乘法。由于 K 的形状是 (c_o, c_i) 和 X 的形状是 (c_i, h * w),结果 Y 的形状将是 (c_o, h * w)。Y = torch.matmul(K, X)# 再reshape成形状(c_o, h, w)return Y.reshape((c_o, h, w))
初始化输入张量 X 和卷积核张量 K
# X 是一个形状为 (3, 3, 3) 的张量,每个元素的值服从标准正态分布(均值 0,标准差 1)。
X = torch.normal(0, 1, (3, 3, 3))# 卷积核张量K的形状为(2, 3, 1, 1)(分别对应输出通道数,输入通道数,卷积核的高宽)
K = torch.normal(0, 1, (2, 3, 1, 1))# 打印输出查看
print(X)
print(K)
运行结果如下:
# corr2d_multi_in_out_1x1。专用于1*1的卷积核
Y1 = corr2d_multi_in_out_1x1(X, K)
Y2 = corr2d_multi_in_out(X, K)# 打印输出查看
# 由于 K 的形状是 (c_o, c_i) 和 X 的形状是 (c_i, h * w),结果 Y 的形状将是 (c_o, h , w)。即二者相乘之后形状为2×3×3
print(Y1)
print(Y2)
"""
assert 是一个内置的关键字,它用于在代码中设置检查点,以确保程序在运行时满足特定的条件。
如果条件评估为 True,则程序会继续执行;
如果条件评估为 False,则会触发一个 AssertionError 异常,并且程序会中断执行(除非异常被捕获和处理)。
"""
# 没有报错说明二者结果几乎完全一样
assert float(torch.abs(Y1 - Y2).sum()) < 1e-6
运行结果如下:
6,池化层
正如我们在第三节,垂直边缘检测的例子中讲的那样:卷积对位置信息敏感,但我们理想的效果是让卷积层兼具平移不变性。
因此引入池化层(或汇聚层):降低卷积层对位置的敏感性,同时降低对空间降采样表示的敏感性。
- 池化层的超参数有:窗口大小、填充、步幅。
6.1,最大池化和平均池化
与卷积层类似,汇聚层也由一个固定形状的窗口组成,该窗口根据其步幅大小从输入张量的左上角开始,从左往右、从上往下的在输入张量内滑动。遍历的每个位置计算一个输出。 在汇聚窗口到达的每个位置,它计算该窗口中
输入子张量的最大值或平均值
。计算最大值或平均值是取决于使用了最大池化层还是平均池化层。
比如:输出张量的高度为2,宽度为2。每个汇聚窗口中的最大值:
max ( 0 , 1 , 3 , 4 ) = 4 , max ( 1 , 2 , 4 , 5 ) = 5 , max ( 3 , 4 , 6 , 7 ) = 7 , max ( 4 , 5 , 7 , 8 ) = 8. \max(0, 1, 3, 4)=4,\\ \max(1, 2, 4, 5)=5,\\ \max(3, 4, 6, 7)=7,\\ \max(4, 5, 7, 8)=8.\\ max(0,1,3,4)=4,max(1,2,4,5)=5,max(3,4,6,7)=7,max(4,5,7,8)=8.
6.2,实现池化操作
导入相关库
import torch
from torch import nn
from d2l import torch as d2l
自定义池化操作函数
# 自定义池化操作函数。X是输入;pool_size:池化窗口大小;mode决定最大池化还是平均池化
# 假设是单通道场景下。代码实现和卷积操作类似
def pool2d(X, pool_size, mode='max'):p_h, p_w = pool_size# 初始化输出YY = 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]])
# 打印输入张量X的形状
print(X.shape)# 调用函数
pool2d(X, (2, 2))
运行结果如下:
同理,验证平均汇聚层
pool2d(X, (2, 2), 'avg')
运行结果如下:
6.3,池化操作的填充和步幅
与卷积层一样,汇聚层也可以改变输出形状。和以前一样,我们可以通过填充和步幅以获得所需的输出形状。
# torch.arange(16, dtype=torch.float32)生成一个从 0 到 15(包含0,不包含16)的一维张量
# .reshape((1, 1, 4, 4)):这个方法将一维张量重新塑形为一个四维张量(批量大小、通道数、高度、宽度),实际上是一个4*4矩阵(因为前两个参数为1)
X = torch.arange(16, dtype=torch.float32).reshape((1, 1, 4, 4))# 输出X查看
X
运行结果如下:
特别注意:默认情况下,深度学习框架中的步幅与汇聚窗口的大小相同(窗口没有重叠) 。因此,如果我们使用形状为(3, 3)的汇聚窗口,那么默认情况下,我们得到的步幅形状为(3, 3)。
# MaxPool2d:是 torch.nn 模块下的一个类,用于执行二维空间上的最大池化操作。
# pool2d = nn.MaxPool2d(3) 创建了一个 MaxPool2d 的实例,其中池化窗口的大小被设置为 3x3。
pool2d = nn.MaxPool2d(3)# 默认情况下,深度学习框架中的步幅与汇聚窗口的大小相同(窗口没有重叠)
pool2d(X)
运行结果如下:
如果不想使用默认的填充和步幅,也可以手动设定。
# 3表示池化窗口大小3*3,stride步幅为2
# padding=1可以在输入张量的每一个边界上填充 1
pool2d = nn.MaxPool2d(3, padding=1, stride=2)# 函数调用
pool2d(X)
运行结果如下:
设定一个任意大小的矩形汇聚窗口,并分别设定填充和步幅的高度和宽度
# stride=(2, 3)表示步长在高度方向上是2,在宽度方向上是3。
# padding=(0, 1)表示高度方向上没有添加填充(即填充为0),而宽度方向上添加了1个像素的填充。
pool2d = nn.MaxPool2d((2, 3), stride=(2, 3), padding=(0, 1))
pool2d(X)
以上为单输入通道的池化情况,接下来我们看一下多通道场景
在处理多通道输入数据时,池化层在每个输入通道上单独运算,而不是像卷积层一样在通道上对输入进行汇总。汇聚层的输出通道数与输入通道数相同。
下面,我们将在通道维度上连结张量X和X + 1,以构建具有2个通道的输入。
# 通过cat操作将X和X+1沿指定维度1(第二个维度)进行拼接
X = torch.cat((X, X + 1), 1)
# 输出查看
X
运行结果如下:
执行汇聚操作:汇聚后输出通道的数量仍然是2。
pool2d = nn.MaxPool2d(3, padding=1, stride=2)
pool2d(X)