文章目录
十九、卷积层基础知识
1、全连接层
( f ∗ g ) ( t ) = ∫ − ∞ ∞ f ( τ ) g ( t − τ ) d τ (f \ast g)(t) = \int_{-\infty}^{\infty} f(\tau) g(t - \tau) \, d\tau (f∗g)(t)=∫−∞∞f(τ)g(t−τ)dτ
二维是w,b,四维是【Wi,Wj,Wk,Wl】
之前输入输出都是一维,权重就是二维
现在输入输出都是二维,权重就是四维
全连接层的权重W是个矩阵,Wij表示第i行第j列的值(标量),对他推广, Wij表示一个二维矩阵(引入宽高kl)就变成了4D
原则一:平移不变性
原则二 :局部性
全连接层
(Fully Connected Layers)在神经网络中通常用于连接前一层的所有神经元到当前层的每一个神经元。然而,当处理图像数据时,全连接层可能会遇到两个问题:参数数量庞大和缺乏平移不变性(Translation Invariance)与局部性(Locality)。
平移不变性意味着无论图像中的目标对象移动到哪里,神经网络都应该能够识别它。局部性则表明神经网络在初始阶段应该只关注图像的局部区域,而不是整个图像。
-
平移不变性:
- 在全连接层中,每个神经元都与前一层的所有神经元相连,这导致网络对图像中的位置变化非常敏感。
- 卷积层通过使用共享的卷积核(也称为滤波器或特征检测器)在整个图像上滑动来解决这个问题。由于卷积核是共享的,并且以相同的方式应用于图像的不同部分,因此网络对图像中的位置变化具有不变性。
-
局部性:
- 在全连接层中,每个神经元都考虑图像中的所有像素,这导致大量的参数和计算复杂性。
- 卷积层通过限制每个神经元只查看图像的一个局部区域(即卷积核的大小)来实现局部性。这使得网络能够在初始阶段关注图像的局部特征,然后在后续层中将这些特征组合起来以形成更高级别的表示。
2、卷积层
下标 h w 分布代表高和宽
-
一维卷积:
一维卷积通常用于处理时间序列数据或一维信号。假设有两个一维向量f
和h
,其长度分别为n
和k
,则一维卷积的结果g
是一个长度为n-k+1
的向量。一维卷积的LaTeX表达式为:g [ i ] = ∑ j = 0 k − 1 f [ i − j ] ⋅ h [ j ] g[i] = \sum_{j=0}^{k-1} f[i-j] \cdot h[j] g[i]=j=0∑k−1f[i−j]⋅h[j]
注意:这里假设
i
的范围从k-1
到n-1
。 -
二维卷积:
二维卷积在计算机视觉和图像处理中非常常见。假设有两个二维矩阵F
和H
(通常称为输入图像和卷积核),则二维卷积的结果G
是一个新的二维矩阵。G [ i ] [ j ] = ∑ m = 0 M − 1 ∑ n = 0 N − 1 F [ i − m ] [ j − n ] ⋅ H [ m ] [ n ] G[i][j] = \sum_{m=0}^{M-1} \sum_{n=0}^{N-1} F[i-m][j-n] \cdot H[m][n] G[i][j]=m=0∑M−1n=0∑N−1F[i−m][j−n]⋅H[m][n]
其中,
M
和N
分别是卷积核H
的高度和宽度。注意:这里假设i
和j
的范围考虑了边界条件(如填充或步长)。
-
三维卷积:
三维卷积在处理立体数据如医学图像中的CT扫描。假设有两个三维数组F
和H
,则三维卷积的结果G
是一个新的三维数组。三维卷积的LaTeX表达式为:G [ i ] [ j ] [ k ] = ∑ m = 0 M − 1 ∑ n = 0 N − 1 ∑ p = 0 P − 1 F [ i − m ] [ j − n ] [ k − p ] ⋅ H [ m ] [ n ] [ p ] G[i][j][k] = \sum_{m=0}^{M-1} \sum_{n=0}^{N-1} \sum_{p=0}^{P-1} F[i-m][j-n][k-p] \cdot H[m][n][p] G[i][j][k]=m=0∑M−1n=0∑N−1p=0∑P−1F[i−m][j−n][k−p]⋅H[m][n][p]
其中,
M
、N
和P
分别是卷积核H
在三个维度上的大小。同样,这里假设i
、j
和k
的范围考虑了边界条件。
3、代码实现
def corr2d(X, K): #@save
"""计算二维互相关运算"""
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
和K
,其中X
通常是一个二维输入数据(例如图像),而K
是一个较小的二维核(kernel)。函数的目标是计算X
和K
之间的二维互相关,并将结果存储在Y
中。
- 获取核的大小:通过
K.shape
获取核的高度h
和宽度w
。 - 初始化输出:使用
torch.zeros
初始化一个全零的二维张量Y
,其大小基于X
和K
的大小来确定。因为核K
会在X
上滑动,所以Y
的高度和宽度会分别是X
的高度和宽度减去核的高度和宽度再加1。 - 双重循环遍历:使用两个嵌套的
for
循环遍历Y
的所有位置。对于每个位置(i, j)
,从X
中提取一个与K
大小相同的子块,并计算这个子块与K
的元素乘积之和。这个和就是Y
在位置(i, j)
的值。 - 返回结果:返回计算得到的二维互相关结果
Y
。
Y[i, j] = (X[i:i + h, j:j + w] * K).sum()
-
X[i:i + h, j:j + w]
:这部分代码从X 中选取一个子区域。这个子区域的左上角坐标是(i, j),高度是h,宽度是w。因此,这个子区域包含了从(i, j)开始,高h宽w的所有像素或特征值。 -
X[i:i + h, j:j + w] * K
:这里,X的子区域与K进行了逐元素相乘。这意味着X中每个位置(m, n)(其中m在i到i+h-1之间,n在j到j+w-1之间)的值与K中对应位置(m-i, n-j)的值相乘。 -
sum():
最后,这个逐元素相乘的结果数组的所有元素被加起来,产生一个单一的输出值。这个值被存储在输出特征图Y的(i, j)位置。
卷积实现
class Conv2D(nn.Module):
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
学习卷积核实现
# 构造一个二维卷积层,它具有1个输出通道和形状为(1,2)的卷积核
conv2d = nn.Conv2d(1,1, kernel_size=(1, 2), bias=False)
# 这个二维卷积层使用四维输入和输出格式(批量大小、通道、高度、宽度),
# 其中批量大小和通道数都为1
X = X.reshape((1, 1, 6, 8))
Y = Y.reshape((1, 1, 6, 7))
lr = 3e-2 # 学习率
for i in range(10):
Y_hat = conv2d(X)
l = (Y_hat - Y) ** 2
conv2d.zero_grad()
l.sum().backward()
# 迭代卷积核
conv2d.weight.data[:] -= lr * conv2d.weight.grad
if (i + 1) % 2 == 0:
print(f'epoch {i+1}, loss {l.sum():.3f}')
实现简单的二维卷积层训练过程,通过迭代来更新卷积核的参数以最小化预测输出
Y_hat
和真实输出Y
之间的平方误差。
- 定义二维卷积层
conv2d = nn.Conv2d(1,1, kernel_size=(1, 2), bias=False)
使用PyTorch的nn.Conv2d
类来定义一个二维卷积层。该层具有1个输入通道和1个输出通道,卷积核的大小是(1, 2),且没有偏置(bias=False
)。
- 准备输入和输出数据
X = X.reshape((1, 1, 6, 8))
Y = Y.reshape((1, 1, 6, 7))
X
和Y
是之前定义好的数据,它们被重新塑形为四维张量,以匹配PyTorch中卷积层的输入和输出格式。四维张量的形状是(批量大小, 通道数, 高度, 宽度)
。这里批量大小和通道数都为1,而X
的高度和宽度分别是6和8,Y
的高度和宽度是6和7(注意通常卷积后的大小会减小,但由于这里卷积核的特殊情况,输出大小可能不变或仅在一个维度上减小)。
- 设置学习率
lr = 3e-2 # 学习率
定义学习率lr
为0.03,它决定了在梯度下降过程中参数更新的步长。
- 训练循环
for i in range(10):
# ...
这是一个简单的训练循环,只迭代10次。在实际应用中,通常会迭代更多次,并使用验证集来监控过拟合和欠拟合。
- 计算损失
l = (Y_hat - Y) ** 2
计算预测输出Y_hat
和真实输出Y
之间的平方误差,得到损失张量l
。
- 清除梯度
conv2d.zero_grad()
在每次迭代开始时,清除卷积层参数的梯度。这是必要的,因为PyTorch会累积梯度,而我们在每次迭代时只想基于当前迭代计算的梯度来更新参数。
- 反向传播
l.sum().backward()
对损失张量l
的所有元素求和,然后调用.backward()
方法来进行反向传播,计算卷积层参数的梯度。
- 更新参数
conv2d.weight.data[:] -= lr * conv2d.weight.grad
使用梯度下降来更新卷积层的权重参数。这里假设只有权重需要更新(因为设置了bias=False
)。注意这里直接对.data
进行操作来更新权重,而在一些更复杂的训练过程中,可能会使用优化器(如torch.optim.SGD
)来更新参数。
- 打印损失
if (i + 1) % 2 == 0:
print(f'epoch {i+1}, loss {l.sum():.3f}')
每两个迭代打印一次当前迭代的损失值。注意这里打印的是损失张量l
所有元素的和,即整个批量上的总损失。
f-string
: 以 f 或 F 开头的字符串字面量允许你在字符串中嵌入表达式,这些表达式在运行时会被其值替换。
最后输出所学卷积核的权重张量
二十、卷积层里的填充和步幅
这里直接给出以下结论,官网推导链接
O u p u t = ( I n p u t + 2 ∗ p a d d i n g − k e r n e l ) / s t r i d e + 1 Ouput=(Input+2∗padding−kernel)/stride+1 Ouput=(Input+2∗padding−kernel)/stride+1
结果向下取整
1、卷积填充
- 填充(padding)是指在输入高和宽的两侧填充元素(通常是0元素)。下图在原输入高和宽的两侧分别添加了值为0的元素,使得输入高和宽从3变成了5,并导致输出高和宽由2增加到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 / 2 p_h/2 ph/2行。如果 k h k_h kh是偶数,一种可能是在输入的顶端一侧填充 ⌈ p h / 2 ⌉ \lceil p_h/2\rceil ⌈ph/2⌉行,而在底端一侧填充 ⌊ p h / 2 ⌋ \lfloor p_h/2\rfloor ⌊ph/2⌋行。在宽的两侧填充同理。
-
卷积神经网络经常使用奇数高宽的卷积核,如1、3、5和7,所以两端上的填充个数相等。对任意的二维数组
X
,设它的第i
行第j
列的元素为X[i,j]
。当两端上的填充个数相等,并使输入和输出具有相同的高和宽时,我们就知道输出Y[i,j]
是由输入以X[i,j]
为中心的窗口同卷积核进行互相关计算得到的。
2、卷积步幅
-
在卷积神经网络中,卷积步幅(Stride)指的是卷积窗口(即卷积核)在输入图像或特征图上从左往右、从上到下移动的距离。这个距离决定了卷积核每次移动覆盖的像素数量。
-
步幅的设置对于卷积神经网络的性能有着重要影响。步幅越大,卷积核每次移动的距离就越大,从而输出的特征图尺寸就越小,这有助于减少计算量和参数数量,但也可能导致一些细节信息的丢失。相反,步幅越小,输出的特征图尺寸就越大,能够保留更多的细节信息,但计算量和参数数量也会相应增加。
当高上步幅为 s h s_h sh,宽上步幅为 s w s_w sw时,输出形状为
⌊ ( 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)。
总结:
- 填充和步幅是卷积层的超参数
- 填充在输入周围添加额外的行/列,来控制输出形状的减少量
- 步幅是每次滑动核窗口时的行/列的步长,可以成倍的减少输出形状
3、代码实现
import torch
from torch import nn
# 为了方便起见,我们定义了一个计算卷积层的函数。
# 此函数初始化卷积层权重,并对输入和输出提高和缩减相应的维数
def comp_conv2d(conv2d, X):
# 这里的(1,1)表示批量大小和通道数都是1
X = X.reshape((1, 1) + X.shape)
Y = conv2d(X)
# 省略前两个维度:批量大小和通道
return Y.reshape(Y.shape[2:])
# 请注意,这里每边都填充了1行或1列,因此总共添加了2行或2列
conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1)
X = torch.rand(size=(8, 8))
comp_conv2d(conv2d, X).shape
8+2+1-3=8
二十一、卷积层多输入多输出通道
1、多输入通道
-
当输入数据含多个通道时, 需要构造一个输入通道数与输入数据的通道数相同的卷积核,从而能够与含多通道的输入数据做互相关运算。假设输入数据的通道数为 c i c_i ci,那么卷积核的输入通道数同样为 c i c_i ci。设卷积核窗口形状为 k h × k w k_h\times k_w kh×kw。当 c i = 1 c_i=1 ci=1时,我们知道卷积核只包含一个形状为 k h × k w k_h\times k_w kh×kw的二维数组。当 c i > 1 c_i > 1 ci>1时,我们将会为每个输入通道各分配一个形状为 k h × k w k_h\times k_w kh×kw的核数组。把这 c i c_i ci个数组在输入通道维上连结,即得到一个形状为 c i × k h × k w c_i\times k_h\times k_w ci×kh×kw的卷积核。由于输入和卷积核各有 c i c_i ci个通道,我们可以在各个通道上对输入的二维数组和卷积核的二维核数组做互相关运算,再将这 c i c_i ci个互相关运算的二维输出按通道相加,得到一个二维数组。这就是含多个通道的输入数据与多输入通道的卷积核做二维互相关运算的输出。
-
下图展示了含2个输入通道的二维互相关计算的例子。在每个通道上,二维输入数组与二维核数组做互相关运算,再按通道相加即得到输出。图5.4中阴影部分为第一个输出元素及其计算所使用的输入和核数组元素: ( 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。
总结:
输出通道数是卷积层的超参数
每个输入通道有独立的二维卷积核,所有通道结果相加得到一个输出通道结果
每个输出通道有独立的三维卷积核
2、1x1 卷积层
1x1 卷积层的特点:
- 通道数变换:1x1 卷积的主要功能是改变输入数据的通道数。假设输入特征图有
M
个通道,你可以使用一个有N
个滤波器的 1x1 卷积层来将其转换为N
个通道的输出特征图。 - 增加非线性:在1x1卷积之后,通常会添加激活函数(如ReLU),这增加了网络的非线性。
- 降维和升维:由于可以改变通道数,1x1 卷积可以用于减少或增加特征图的维度。这在某些情况下是有益的,例如,在需要减少参数数量或计算成本时。
- 跨通道信息交互:通过1x1卷积,不同通道的信息可以相互交互,这在某些应用中可能是有益的。
应用:
- Inception 模块:在Google的Inception架构中,1x1 卷积被用于在多个不同尺度的卷积操作之间降低维度,从而减少计算成本。
- ResNet 中的瓶颈层:在ResNet架构中,1x1 卷积被用作瓶颈层(bottleneck layer)的一部分,以减少计算量和参数数量。
- MobileNet:MobileNet是一种轻量级的CNN架构,它大量使用了1x1和3x3的卷积来减少计算量和参数数量,从而实现高效的移动端和嵌入式设备上的推理。
- 特征融合:在某些情况下,可以使用1x1卷积来融合来自不同层或不同来源的特征图。
优点:
- 灵活性:可以改变通道数,增加或减少特征图的维度。
- 计算效率高:由于滤波器的大小为1x1,因此计算成本相对较低。
- 引入非线性:在卷积之后添加激活函数可以增加网络的非线性。
缺点:
- 如果过度使用,可能会增加网络的参数数量和计算成本(尽管与更大的滤波器相比,这种增加通常是较小的)。
总之,1x1 卷积层是一个强大而灵活的工具,可以在许多不同的CNN架构中找到其用途。
3、二维卷积层
4、代码实现
(1)多输入通道互相关运算
def corr2d_multi_in(X, K):
# 先遍历“X”和“K”的第0个维度(通道维度),再把它们加在一起
return sum(d2l.corr2d(x, k) for x, k in zip(X, K))
- 函数通过
zip(X, K)
遍历X
和K
的通道维度(即第1个维度,在Python中索引从0开始)。 - 对于每一对对应的通道
x
和k
(它们都是三维张量,形状为(height, width, ...)
),函数使用d2l.corr2d(x, k)
计算二维相关性(或卷积)。 - 最后,使用
sum()
函数将所有通道的相关性(或卷积)结果相加,得到一个二维张量(形状为(height_out, width_out)
,具体大小取决于K
的大小和步长等参数,但这里并未明确给出)。
关于zip 函数的解释:
- 在Python中,
zip
函数用于将可迭代的对象作为参数,将对象中对应的元素打包成一个个元组,然后返回由这些元组组成的对象。如果各个迭代器的元素个数不一致,则返回列表长度与最短的对象相同。
以下是一个简化的例子,展示了如何使用 zip
来遍历两个三维数组(模拟通道维度、高度和宽度)的对应元素:
import numpy as np
# 假设 X 和 K 是两个三维数组,表示两个不同通道的图像或特征图
X = np.random.rand(3, 4, 4) # 3个通道,4x4大小
K = np.random.rand(3, 3, 3) # 3个通道,3x3大小的卷积核
# 使用zip遍历对应通道
for x, k in zip(X, K):
# 在这里,x 和 k 分别是 X 和 K 的一个通道(二维数组)
# 执行二维卷积或其他操作...
print(x.shape, k.shape) # 输出: (4, 4) (3, 3)
stack:沿着一个新的维度串联一连串的张量。
(2)多输出通道互相关运算
def corr2d_multi_in_out(X, K):
# 迭代“K”的第0个维度,每次都对输入“X”执行互相关运算。
# 最后将所有结果都叠加在一起
return torch.stack([corr2d_multi_in(X, k) for k in K], 0)
- 使用列表推导式和
torch.stack
函数来迭代K
中的每个卷积核k
,并对输入X
执行corr2d_multi_in
函数,然后将结果沿着第一个维度(通道维度)堆叠起来。
corr2d_multi_in
函数定义如上,处理多通道输入和一个卷积核的二维卷积(或互相关)操作。
# corr2d_multi_in_out 函数,用于处理多通道输入和多个卷积核的二维卷积
def corr2d_multi_in_out(X, K):
# 迭代“K”的第0个维度(即卷积核的维度)
# 对每个卷积核“k”,执行 corr2d_multi_in(X, k)
# 然后将所有结果沿着第0个维度(即新的通道维度)堆叠起来
return torch.stack([corr2d_multi_in(X, k) for k in K], dim=0)
# 示例使用
# 假设 X 是一个四维张量:(batch_size, in_channels, height, width)
# 假设 K 是一个四维张量:(out_channels, in_channels, kernel_height, kernel_width)
X = torch.randn(1, 3, 10, 10) # 例如,一个批次中的一个3通道10x10的图像
K = torch.randn(2, 3, 3, 3) # 例如,两个3x3的卷积核,每个都用于3通道的输入
# 调用 corr2d_multi_in_out 函数
Y = corr2d_multi_in_out(X, K)
# Y 将是一个四维张量:(out_channels, batch_size, output_height, output_width)
# 注意:output_height 和 output_width 需要根据卷积核大小和步长等参数计算得出
print(Y.shape) # 输出应该是 torch.Size([2, 1, output_height, output_width])
实验输出结果如下:
K = torch.stack((K, K + 1, K + 2), 0)
torch.stack((K, K + 1, K + 2), 0)
是在沿着一个新的维度(即第一个维度,索引为0)堆叠三个张量。 K
是一个四维张量,形如 (C_out, C_in, H_k, W_k)
(分别代表输出通道数、输入通道数、卷积核的高度和宽度),那么 K + 1
和 K + 2
将会对 K
中的每个元素分别加1和加2。
注意
-
数据类型:确保
K
的数据类型支持加法操作,通常是浮点数(如torch.float32
)或整数(如torch.int64
,但加法后可能会产生溢出)。 -
广播机制:PyTorch 中的加法操作支持广播(broadcasting)。如果两个张量的形状不完全相同,PyTorch 会尝试将它们扩展为相同的形状,以便执行元素级操作。如果
K
的形状是(C_out, C_in, H_k, W_k)
,那么K + 1
和K + 2
实际上是在对K
中的每个元素进行加法操作,而不是沿着某个维度进行广播。 -
结果形状:
torch.stack((K, K + 1, K + 2), 0)
的结果将是一个新的五维张量,其形状为(3, C_out, C_in, H_k, W_k)
。这是因为沿着第一个维度(索引为0的维度)堆叠了三个形状相同的四维张量。
import torch
# 假设 K 是一个四维卷积核张量
C_out, C_in, H_k, W_k = 2, 3, 3, 3
K = torch.randn(C_out, C_in, H_k, W_k)
# 沿着第一个维度堆叠三个张量:K, K+1, K+2
K_stacked = torch.stack((K, K + 1, K + 2), 0)
print(K_stacked.shape) # 输出:torch.Size([3, 2, 3, 3, 3])
torch.stack((K, K + 1, K + 2), 0)
表示将三个张量K、K+1和K+2沿着第0个维度(即行)合并成一个三行的二维张量。例如,如果K=[1, 2, 3],则输出的张量为[[1, 2, 3], [2, 3, 4], [3, 4, 5]]。🚀🚀
5、1×1 卷积层
- 卷积窗口形状为 1 × 1 1\times 1 1×1( k h = k w = 1 k_h=k_w=1 kh=kw=1)的多通道卷积层。我们通常称之为 1 × 1 1\times 1 1×1卷积层,并将其中的卷积运算称为 1 × 1 1\times 1 1×1卷积。因为使用了最小窗口, 1 × 1 1\times 1 1×1卷积失去了卷积层可以识别高和宽维度上相邻元素构成的模式的功能。实际上, 1 × 1 1\times 1 1×1卷积的主要计算发生在通道维上。下图展示了使用输入通道数为3、输出通道数为2(输入三个片,输出两个片)的 1 × 1 1\times 1 1×1卷积核的互相关计算。值得注意的是,输入和输出具有相同的高和宽。输出中的每个元素来自输入中在高和宽上相同位置的元素在不同通道之间的按权重累加。假设我们将通道维当作特征维,将高和宽维度上的元素当成数据样本,那么 1 × 1 1\times 1 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))
K = K.reshape((c_o, c_i))
# 全连接层中的矩阵乘法
Y = torch.matmul(K, X)
return Y.reshape((c_o, h, w))
normal 前的参数表示 均值为0,方差为1的张量
-
输入参数:
X
:输入数据,形状为(c_i, h, w)
,其中c_i
是输入通道数,h
和w
是输入数据的高度和宽度。K
:卷积核,形状为(c_o, c_i, 1, 1)
,但由于卷积核大小为1x1,所以通常只提供前两个维度(c_o, c_i)
,其中c_o
是输出通道数,c_i
是输入通道数(与X
的通道数相同)。
-
数据重塑:
X
被重塑为(c_i, h*w)
,这样输入数据就被转换成了一个二维矩阵,其中每一列对应于X
中的一个空间位置(像素)的所有通道的值。K
被重塑为(c_o, c_i)
,即卷积核矩阵。
-
矩阵乘法:
- 使用
torch.matmul(K, X)
执行矩阵乘法。这里,K
的每一行(即每一个输出通道对应的权重)与X
相乘,生成输出数据Y
的对应行。
- 使用
-
结果重塑:
- 矩阵乘法后的
Y
形状为(c_o, h*w)
,需要被重塑回(c_o, h, w)
以匹配原始的空间维度。
- 矩阵乘法后的
二十二、池化层基础知识
为了缓解卷积层对位置的过度敏感性
1、二维最大和平均池化层
- 同卷积层一样,池化层每次对输入数据的一个固定形状窗口(又称池化窗口)中的元素计算输出。不同于卷积层里计算输入和核的互相关性,池化层直接计算池化窗口内元素的最大值或者平均值。该运算也分别叫做最大池化或平均池化。在二维最大池化中,池化窗口从输入数组的最左上方开始,按从左往右、从上往下的顺序,依次在输入数组上滑动。当池化窗口滑动到某一位置时,窗口中的输入子数组的最大值即输出数组中相应位置的元素。
2、填充,步幅和多个通道
- 池化层与卷积层类似,都具有填充和步幅
- 没有可学习的参数
- 在每个输入通道应用池化层以获得相应的输出通道
- 输出通道数=输入通道数
3、池化层的代码具体实现
def pool2d(X, pool_size, mode='max'):
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.arange(16, dtype=torch.float32).reshape((1, 1, 4, 4))
pool2d = nn.MaxPool2d(3)
pool2d(X)
在pytorch中一般,池化窗口=步幅
多个通道的情况下
cat是现有的维度上连接 stack是在新维度上堆叠
二十三、经典卷积神经网络 LeNet
1、基本概念介绍
- 在多层感知机的从零开始实现里构造了一个含单隐藏层的多层感知机模型来对Fashion-MNIST数据集中的图像进行分类。每张图像高和宽均是28像素。我们将图像中的像素逐行展开,得到长度为784的向量,并输入进全连接层中。然而,这种分类方法有一定的局限性。
- 图像在同一列邻近的像素在这个向量中可能相距较远。它们构成的模式可能难以被模型识别。
- 对于大尺寸的输入图像,使用全连接层容易造成模型过大。假设输入是高和宽均为1000像素的彩色照片(含3个通道)。即使全连接层输出个数仍是256,该层权重参数的形状是 3 , 000 , 000 × 256 3,000,000\times 256 3,000,000×256:它占用了大约3 GB的内存或显存。这带来过复杂的模型和过高的存储开销。
卷积层尝试解决这两个问题。
- 一方面,卷积层保留输入形状,使图像的像素在高和宽两个方向上的相关性均可能被有效识别;
- 另一方面,卷积层通过滑动窗口将同一卷积核与不同位置的输入重复计算,从而避免参数尺寸过大。
注意汇聚层就是池化层
上图各参数的详细介绍如下:
2、LeNet代码实现
net = nn.Sequential(
nn.Conv2d(1, 6, kernel_size=5, padding=2), nn.Sigmoid(),
nn.AvgPool2d(kernel_size=2, stride=2),
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))
加入激活函数是为了不要让整个网络变成一个线性网络
-
定义网络结构:
- 使用
nn.Sequential
来顺序地堆叠网络层。
- 使用
第一层:
nn.Conv2d(1, 6, kernel_size=5, padding=2)
: 一个二维卷积层,输入通道数为1(通常用于灰度图像),输出通道数为6,卷积核大小为5x5,边缘填充为2。填充是为了确保在卷积后,特征图的高度和宽度不会减少。
下面是关于代码中设置参数的推导过程 😃😃
- 输入图像大小为 W × H W \times H W×H。
- 卷积核大小为 K w × K h K_w \times K_h Kw×Kh。
- 边缘填充为 P P P。
没有填充时,卷积操作后特征图的大小为:
Output size without padding
=
(
W
−
K
w
+
1
)
×
(
H
−
K
h
+
1
)
\text{Output size without padding} = (W - K_w + 1) \times (H - K_h + 1)
Output size without padding=(W−Kw+1)×(H−Kh+1)
当添加填充时,输入图像的大小变为
(
W
+
2
P
)
×
(
H
+
2
P
)
(W + 2P) \times (H + 2P)
(W+2P)×(H+2P),卷积后的特征图大小为:
Oput S with pad
=
(
W
+
2
P
−
K
w
+
1
)
×
(
H
+
2
P
−
K
h
+
1
)
\text{Oput S with pad} = (W + 2P - K_w + 1) \times (H + 2P - K_h + 1)
Oput S with pad=(W+2P−Kw+1)×(H+2P−Kh+1)
为了使输出特征图的大小与原始输入图像的大小相同,需要设置填充
P
P
P 满足以下条件:即等于初始输入图像
W
+
2
P
−
K
w
+
1
=
W
W + 2P - K_w + 1 = W
W+2P−Kw+1=W
H
+
2
P
−
K
h
+
1
=
H
H + 2P - K_h + 1 = H
H+2P−Kh+1=H
解这两个方程,得到:
2
P
=
K
w
−
1
2P = K_w - 1
2P=Kw−1
2
P
=
K
h
−
1
2P = K_h - 1
2P=Kh−1
由于
K
w
K_w
Kw 和
K
h
K_h
Kh 通常是相同的(例如
5
×
5
5 \times 5
5×5 卷积核),所以填充
P
P
P 为:
P
=
K
w
−
1
2
P = \frac{K_w - 1}{2}
P=2Kw−1
P
=
K
h
−
1
2
P = \frac{K_h - 1}{2}
P=2Kh−1
在你的案例中,卷积核大小为
5
×
5
5 \times 5
5×5,所以:
P
=
5
−
1
2
=
2
P = \frac{5 - 1}{2} = 2
P=25−1=2
因此,在每个方向上添加2个填充像素,可以确保卷积后特征图的大小与原始输入图像的大小相同。
第二层:
nn.AvgPool2d(kernel_size=2, stride=2)
: 一个平均池化层,池化核大小为2x2,步长也为2。这会将特征图的高度和宽度都减半。
第三、四层: 与第一、二层类似,但输出通道数从6增加到16。
第五层:
nn.Flatten()
: 这是一个将多维张量展平为一维张量的层,通常用于从卷积层过渡到全连接层。🔥
第六、七、八层:
+ 这些是全连接层(或称为线性层)。nn.Linear(in_features, out_features)
创建一个从in_features
个输入到out_features
个输出的线性变换。这里,输入和输出特征的数量是根据网络前面的结构来确定的。同样,这些层后面都跟着Sigmoid激活函数。但请注意,对于分类任务,网络的最后一层通常使用Softmax激活函数(或者没有激活函数,但使用交叉熵损失函数)。
具体来说,
nn.Linear(16 * 5 * 5, 120)
的输入特征数量16 * 5 * 5
是这样计算出来的:
-
卷积层输出:在第二个卷积层之后(
nn.Conv2d(6, 16, kernel_size=5)
),得到了16个特征图(因为输出通道数为16)。 -
池化层输出:接着是一个平均池化层(
nn.AvgPool2d(kernel_size=2, stride=2)
),它将特征图的高和宽都减半。输入的图像大小在第一个池化层之后是 28x28(对于MNIST数据集中的28x28图像,在经过第一个卷积层和池化层后,大小保持不变,仍为28x28,但通道数变为6,),那么第二个卷积层后的特征图大小将是 14x14(因为28/2 = 14)。经过第二个池化层后,特征图的大小会进一步减半,变为 7x7(因为14/2 = 7)。
关于尺寸不变,尺寸减半的问题 🐟
在卷积神经网络中,特征图的尺寸(高度和宽度)在经过卷积层和池化层时可能会发生变化。这种变化取决于卷积核的大小、步长(stride)、填充(padding)以及池化窗口的大小和步长。
(1)尺寸不变情况
- 尺寸不变通常发生在卷积层中,当使用了适当的填充(padding)时。填充是在输入特征图的边界周围添加额外的像素值(通常为0),以增加特征图的空间尺寸。这样做的目的是在卷积操作后保持特征图的空间尺寸不变,从而避免信息的丢失。
具体来说,如果卷积核的大小为 k
,步长为 s
,填充为 p
,那么卷积后的特征图尺寸 N'
可以通过以下公式计算:
N ′ = N − k + 2 p s + 1 N' = \frac{N - k + 2p}{s} + 1 N′=sN−k+2p+1
其中 N
是输入特征图的尺寸。为了保持 N' = N
(即尺寸不变),可以设置填充 p
使得上述公式成立。对于常见的3x3卷积核和步长为1的情况,通常使用 p = 1
的填充来保持尺寸不变。
(2)尺寸减半情况
尺寸减半通常发生在池化层中。池化操作是对输入特征图的一个区域进行下采样,通常选择该区域内的最大值(最大池化)或平均值(平均池化)作为输出。由于池化窗口的大小通常大于1,并且池化操作的步长也通常设置为与池化窗口大小相同,因此池化后的特征图尺寸会减半。
具体来说,如果池化窗口的大小为 k'
,步长为 s'
,那么池化后的特征图尺寸 N''
可以通过以下公式计算:
N ′ ′ = N ′ k ′ N'' = \frac{N'}{k'} N′′=k′N′
由于池化窗口的大小 k'
通常大于1(如2x2),并且步长 s'
也通常等于 k'
,因此 N''
通常会小于 N'
,导致尺寸减半。
(3)关于尺寸总结
- 尺寸不变:通过在卷积层中使用适当的填充,可以保持特征图的空间尺寸不变,从而避免信息的丢失。
- 尺寸减半:在池化层中,由于池化窗口的大小通常大于1并且步长等于池化窗口大小,因此池化后的特征图尺寸会减半。这种下采样操作有助于减少计算量和参数数量,同时保留重要的特征信息。
-
计算全连接层的输入特征数量:因为现在有16个特征图,每个特征图的大小是 7x7,所以全连接层的输入特征数量是 16 * 7 * 7 = 784。但是,在你的代码中,它写成了
16 * 5 * 5
,因为网络设计的一个假设或特定输入大小(不是MNIST的28x28)。如果输入图像的大小不同,或者网络的其他层有所调整,这个数值也会相应变化。 -
第一个全连接层:然后,定义了一个有120个输出单元的全连接层(
nn.Linear(16 * 5 * 5, 120)
)。这个数值(120)是设计选择,可以根据任务的需要和网络的性能进行调整。 -
后续的全连接层:接下来的全连接层(
nn.Linear(120, 84)
)有84个输出单元,这同样是一个设计选择。这些层之后可能还会有其他的全连接层,但在这个例子中只给出了两层。
总结来说,全连接层的输入和输出特征数量是基于网络前面层的输出和设计选择来确定的。在这个例子中,16 * 5 * 5
和 120
、84
等数值都是根据网络架构和可能的输入大小来设定的。
(4)打印测试输出
X = torch.rand(size=(1, 1, 28, 28), dtype=torch.float32)
for layer in net:
X = layer(X)
print(layer.__class__.__name__,'output shape: \t',X.shape)
400 是有16个输出通道,每个通道都是5*5
-
X = torch.rand(size=(1, 1, 28, 28), dtype=torch.float32)
:创建一个随机初始化的张量X
,其形状为(1, 1, 28, 28)
,表示一个批量大小为1,1个通道(灰度图像),28x28像素的图像。数据类型为torch.float32
。 -
X = layer(X)
:将当前层应用于张量X
,更新X
为该层的输出。 -
print(layer.__class__.__name__, 'output shape: \t', X.shape)
:打印当前层的类名和输出张量X
的形状。
(5)使用GPU实现
def evaluate_accuracy_gpu(net, data_iter, device=None): #@save
"""使用GPU计算模型在数据集上的精度"""
if isinstance(net, nn.Module):
net.eval() # 设置为评估模式
if not device:
device = next(iter(net.parameters())).device
# 正确预测的数量,总预测的数量
metric = d2l.Accumulator(2)
with torch.no_grad():
for X, y in data_iter:
if isinstance(X, list):
# BERT微调所需的(之后将介绍)
X = [x.to(device) for x in X]
else:
X = X.to(device)
y = y.to(device)
metric.add(d2l.accuracy(net(X), y), y.numel())
return metric[0] / metric[1]
用于在 GPU 上评估一个神经网络模型在给定数据集上的精度。
-
函数参数:
net
: 需要评估的神经网络模型。data_iter
: 一个迭代器,它产生一批批的数据(X, y)
,其中X
是输入数据,y
是对应的标签。device
: (可选)用于指定模型和数据应在哪个设备(如 CPU 或 GPU)上进行操作。如果未指定,则默认从模型的第一个参数中推断。
-
函数主体:
- 首先,如果
net
是一个nn.Module
的实例(即 PyTorch 中的神经网络模型),则将其设置为评估模式(eval()
)。在评估模式下,某些层(如 Dropout 和 BatchNorm)的行为会发生变化,以确保在评估时得到一致的结果。 - 如果
device
没有被指定,则通过访问模型的第一个参数的.device
属性来推断出它。 - 创建一个
d2l.Accumulator
对象metric
,用于累积正确预测的数量和总预测的数量。 - 使用
torch.no_grad()
上下文管理器来确保在评估过程中不会计算梯度,这可以节省计算资源并加速评估过程。 - 遍历
data_iter
中的每一批数据(X, y)
:- 如果
X
是一个列表(例如,BERT 微调时可能需要多个输入张量),则将列表中的每个张量都移动到指定的设备上。 - 如果
X
不是一个列表,则直接将其移动到指定的设备上。 - 将标签
y
也移动到指定的设备上。 - 使用模型
net
对输入X
进行预测,并使用d2l.accuracy
函数(可能是一个自定义函数,用于计算精度)来计算这一批数据的精度,并将精度和这一批数据的数量累加到metric
中。
- 如果
- 最后,返回累积的正确预测数量除以总预测数量,即整个数据集上的精度。
- 首先,如果
(6)开始训练模型
#@save
def train_ch6(net, train_iter, test_iter, num_epochs, lr, device):
"""用GPU训练模型 """
def init_weights(m):
if type(m) == nn.Linear or type(m) == nn.Conv2d:
nn.init.xavier_uniform_(m.weight)
net.apply(init_weights)
print('training on', device)
net.to(device)
optimizer = torch.optim.SGD(net.parameters(), lr=lr)
loss = nn.CrossEntropyLoss()
animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs],
legend=['train loss', 'train acc', 'test acc'])
timer, num_batches = d2l.Timer(), len(train_iter)
for epoch in range(num_epochs):
# 训练损失之和,训练准确率之和,样本数
metric = d2l.Accumulator(3)
net.train()
for i, (X, y) in enumerate(train_iter):
timer.start()
optimizer.zero_grad()
X, y = X.to(device), y.to(device)
y_hat = net(X)
l = loss(y_hat, y)
l.backward()
optimizer.step()
with torch.no_grad():
metric.add(l * X.shape[0], d2l.accuracy(y_hat, y), X.shape[0])
timer.stop()
train_l = metric[0] / metric[2]
train_acc = metric[1] / metric[2]
if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1:
animator.add(epoch + (i + 1) / num_batches,
(train_l, train_acc, None))
test_acc = evaluate_accuracy_gpu(net, test_iter)
animator.add(epoch + 1, (None, None, test_acc))
print(f'loss {train_l:.3f}, train acc {train_acc:.3f}, '
f'test acc {test_acc:.3f}')
print(f'{metric[2] * num_epochs / timer.sum():.1f} examples/sec '
f'on {str(device)}')
训练变化过程图如下所示,进行加速处理后 😎😎
二十四、深度卷积神经网络 AlexNet
1、AlexNet的介绍
AlexNet与LeNet的设计理念非常相似,但也有显著的区别。
- 第一,与相对较小的LeNet相比,AlexNet包含8层变换,其中有5层卷积和2层全连接隐藏层,以及1个全连接输出层。下面是详细描述这些层的设计。
①AlexNet第一层中的卷积窗口形状是 11 × 11 11\times11 11×11。因为ImageNet中绝大多数图像的高和宽均比MNIST图像的高和宽大10倍以上,ImageNet图像的物体占用更多的像素,所以需要更大的卷积窗口来捕获物体。
②第二层中的卷积窗口形状减小到 5 × 5 5\times5 5×5,之后全采用 3 × 3 3\times3 3×3。此外,第一、第二和第五个卷积层之后都使用了窗口形状为 3 × 3 3\times3 3×3、步幅为2的最大池化层。而且,AlexNet使用的卷积通道数也大于LeNet中的卷积通道数数十倍。
③紧接着最后一个卷积层的是两个输出个数为4096的全连接层。这两个巨大的全连接层带来将近1 GB的模型参数。由于早期显存的限制,最早的AlexNet使用双数据流的设计使一个GPU只需要处理一半模型。幸运的是,显存在过去几年得到了长足的发展,因此通常我们不再需要这样的特别设计了。
-
第二,AlexNet将sigmoid激活函数改成了更加简单的ReLU激活函数。一方面,ReLU激活函数的计算更简单,例如它并没有sigmoid激活函数中的求幂运算。另一方面,ReLU激活函数在不同的参数初始化方法下使模型更容易训练。这是由于当sigmoid激活函数输出极接近0或1时,这些区域的梯度几乎为0,从而造成反向传播无法继续更新部分模型参数;而ReLU激活函数在正区间的梯度恒为1。因此,若模型参数初始化不当,sigmoid函数可能在正区间得到几乎为0的梯度,从而令模型无法得到有效训练。
-
第三,AlexNet通过丢弃法来控制全连接层的模型复杂度。而LeNet并没有使用丢弃法。
-
第四,AlexNet引入了大量的图像增广,如翻转、裁剪和颜色变化,从而进一步扩大数据集来缓解过拟合。
dropout相当于正则化,防止过拟合
总结
- AlexNet是更大更深的LeNet,10x参数个数,260x计算复杂度
- 新进入了丢弃法,RLU,最大池化层,和数据增强
- AlexNet赢下了2012 ImageNet竞赛后,标志着新轮神经网络热潮的开始
2、AlexNet的实现
net = nn.Sequential(
# 这里使用一个11*11的更大窗口来捕捉对象。
# 同时,步幅为4,以减少输出的高度和宽度。
# 另外,输出通道的数目远大于LeNet
nn.Conv2d(1, 96, kernel_size=11, stride=4, padding=1), nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2),
# 减小卷积窗口,使用填充为2来使得输入与输出的高和宽一致,且增大输出通道数
nn.Conv2d(96, 256, kernel_size=5, padding=2), nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2),
# 使用三个连续的卷积层和较小的卷积窗口。
# 除了最后的卷积层,输出通道的数量进一步增加。
# 在前两个卷积层之后,汇聚层不用于减少输入的高度和宽度
nn.Conv2d(256, 384, kernel_size=3, padding=1), nn.ReLU(),
nn.Conv2d(384, 384, kernel_size=3, padding=1), nn.ReLU(),
nn.Conv2d(384, 256, kernel_size=3, padding=1), nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2),
nn.Flatten(),
# 这里,全连接层的输出数量是LeNet中的好几倍。使用dropout层来减轻过拟合
nn.Linear(6400, 4096), nn.ReLU(),
nn.Dropout(p=0.5),
nn.Linear(4096, 4096), nn.ReLU(),
nn.Dropout(p=0.5),
# 最后是输出层。由于这里使用Fashion-MNIST,所以用类别数为10,而非论文中的1000
nn.Linear(4096, 10))
-
输入层:
- 假设输入图像是灰度图(即只有一个颜色通道),大小为任意,但由于第一个卷积层的设置,图像大小至少应该足够大以支持11x11的卷积核。
-
第一个卷积-ReLU-池化层:
nn.Conv2d(1, 96, kernel_size=11, stride=4, padding=1)
: 输入通道数为1,输出通道数为96,卷积核大小为11x11,步长为4,填充为1。nn.ReLU()
: 激活函数,用于增加模型的非线性。nn.MaxPool2d(kernel_size=3, stride=2)
: 最大池化层,池化核大小为3x3,步长为2。
-
第二个卷积-ReLU-池化层:
nn.Conv2d(96, 256, kernel_size=5, padding=2)
: 输入通道数为96,输出通道数为256,卷积核大小为5x5,填充为2(以保持输入和输出的空间维度相同)。- 接下来的ReLU和MaxPool2d层与上一个相同。
-
三个连续的卷积-ReLU层:
- 这三层卷积层使用3x3的卷积核和1的填充,以在不减小空间维度的同时增加特征的复杂性。输出通道数分别为384、384和256。
-
另一个池化层:
nn.MaxPool2d(kernel_size=3, stride=2)
: 与前面的池化层相似,但在此处,它减小了空间维度。
-
展平层:
nn.Flatten()
: 将卷积层输出的多维张量展平为一维,以便可以将其传递给全连接层。这里假设在展平之前的张量形状是(batch_size, 256, 8, 8)
(基于前面的层进行估计),所以展平后的形状是(batch_size, 6400)
。
-
全连接层:
- 第一个全连接层将6400个输入特征转换为4096个输出特征,并使用ReLU激活函数。
nn.Dropout(p=0.5)
: 随机丢弃50%的神经元,以减少过拟合。- 第二个全连接层与第一个相似,但输出仍然是4096个特征。
- 另一个Dropout层。
-
输出层:
nn.Linear(4096, 10)
: 由于Fashion-MNIST数据集有10个类别,所以输出层有10个神经元,每个神经元对应一个类别的分数。
输出每一层的形状
二十五、使用块的网络 VGG
1、 VGG的介绍
-
VGG块的组成规律是:连续使用数个相同的填充为1、窗口形状为 3 × 3 3\times 3 3×3的卷积层后接上一个步幅为2、窗口形状为 2 × 2 2\times 2 2×2的最大池化层。卷积层保持输入的高和宽不变,而池化层则对其减半。我们使用
vgg_block
函数来实现这个基础的VGG块,它可以指定卷积层的数量和输入输出通道数。 -
VGGNet(Visual Geometry Group Network) VGGNet的主要特点是使用了多个连续的3×3卷积核的卷积层,以构建更深的网络结构,并通过实验证明了增加网络深度可以有效提高性能。
-
VGG块是VGGNet的核心组成部分,它由一系列卷积层和一个最大池化层组成。每个卷积层都使用3×3的卷积核,并且填充为1(以保持输入的高度和宽度不变)。最大池化层则使用2×2的窗口形状,步幅为2(每次池化后,分辨率减半)。这种设计使得VGG块能够在保持特征图尺寸的同时,有效地增加网络的深度。
演化进度
VGGNet的结构非常清晰,它包含多个VGG块,后面接着一系列全连接层。原始VGG网络有5个卷积块,其中前两个块各有一个卷积层,后三个块各包含两个卷积层。每个卷积块的输出通道数逐渐增加,从第一个模块的64个输出通道开始,每个后续模块将输出通道数量翻倍,直到达到512个输出通道。
使用块的网络VGG的主要优势在于其深度结构,通过增加网络深度,VGGNet能够学习到更加复杂的特征表示,从而提高模型的性能。此外,使用多个3×3的卷积核代替较大的卷积核,不仅可以减少参数量,还能够增加网络的非线性映射能力,使模型具有更强的表达能力。
总的来说,VGGNet是一种非常经典的深度卷积神经网络模型,它通过使用多个连续的3×3卷积核和最大池化层构建深度网络结构,证明了增加网络深度可以有效提高性能。同时,VGGNet也为后续的深度学习研究提供了重要的参考和启示。
更多3x3比更少5x5效果更好
总结:
- VGG使用可重复使用的卷积块来构建深度卷积神经网络
- 不同的卷积块个数和超参数可以得到不同复杂度的变种
2、 VGG的实现
f vgg_block(num_convs, in_channels, out_channels):
layers = []
for _ in range(num_convs):
layers.append(nn.Conv2d(in_channels, out_channels,
kernel_size=3, padding=1))
layers.append(nn.ReLU())
in_channels = out_channels
layers.append(nn.MaxPool2d(kernel_size=2,stride=2))
return nn.Sequential(*layers)
该函数用于创建一个类似于VGG网络中的卷积块(Convolutional Block)。VGG网络是一种深度卷积神经网络,以其简单的架构(仅包含卷积层、ReLU激活函数和池化层)和多个变体(如VGG16、VGG19等)而闻名。
-
函数定义:
f vgg_block(num_convs, in_channels, out_channels):
num_convs
:表示该块中卷积层的数量。in_channels
:输入通道数,即上一层的输出通道数或图像的通道数(如RGB为3)。out_channels
:输出通道数,即该块中每个卷积层的输出通道数。
-
初始化layers列表:
layers = []
:创建一个空的列表,用于存储将要添加的层。
-
添加卷积层和ReLU:
- 使用一个for循环,根据
num_convs
的值重复以下操作:- 添加一个卷积层,其输入通道数为
in_channels
,输出通道数为out_channels
,卷积核大小为3x3,并带有1的填充(以保持输入的空间尺寸)。 - 添加一个ReLU激活函数。
- 更新
in_channels
为out_channels
,以便下一个卷积层有正确的输入通道数。
- 添加一个卷积层,其输入通道数为
- 使用一个for循环,根据
-
添加最大池化层:
- 在所有卷积层和ReLU之后,添加一个最大池化层,其核大小为2x2,步长也为2。这通常用于减小特征图的空间尺寸。
-
返回层序列:
- 使用
nn.Sequential(*layers)
将layers列表中的层组合成一个序列模型,并返回该模型。
- 使用
*相当于解包 将layers列表中的元素取出
def vgg(conv_arch):
conv_blks = []
in_channels = 1
# 卷积层部分
for (num_convs, out_channels) in conv_arch:
conv_blks.append(vgg_block(num_convs, in_channels, out_channels))
in_channels = out_channels
return nn.Sequential(
*conv_blks, nn.Flatten(),
# 全连接层部分
nn.Linear(out_channels * 7 * 7, 4096), nn.ReLU(), nn.Dropout(0.5),
nn.Linear(4096, 4096), nn.ReLU(), nn.Dropout(0.5),
nn.Linear(4096, 10))
net = vgg(conv_arch)
输出测试结构
参数每次翻倍,减半 即通道数翻倍,高宽减半
二十六、网络中的网络 NiN
1、NiN的基本概念介绍
- 卷积层的输入和输出通常是四维数组(
样本,通道,高,宽
),而全连接层的输入和输出则通常是二维数组(样本,特征)。如果想在全连接层后再接上卷积层,则需要将全连接层的输出变换为四维。在多输入通道和多输出通道里介绍的 1 × 1 1\times 1 1×1卷积层。它可以看成全连接层,其中空间维度(高和宽)上的每个元素相当于样本,通道相当于特征。因此,NiN使用 1 × 1 1\times 1 1×1卷积层来替代全连接层,从而使空间信息能够自然传递到后面的层中去。下图对比了NiN同AlexNet和VGG等网络在结构上的主要区别。
关于全连接层的问题
-
NiN的设计思想是在传统的卷积神经网络(CNN)结构中引入“网络中的网络”概念,以提高模型的效率和泛化能力。
-
相比于传统的卷积神经网络,NiN网络采用了一种称为==“1x1卷积”==的技术。在传统的卷积神经网络中,卷积核的大小通常为3x3或5x5,这使得网络参数数量非常庞大,容易造成过拟合。而1x1卷积可以将通道数进行压缩或扩张,从而减少网络参数,提高模型的效率和泛化能力。
-
NiN网络的结构主要包括以下几个部分:输入层、多个卷积层(包括1x1卷积层和3x3卷积层)、池化层、随机失活层、全连接层和输出层。其中,1x1卷积层使用多个1x1的卷积核进行特征提取,增加模型的非线性能力并减少特征图的尺寸,从而减少计算量。3x3卷积层则使用常规的3x3卷积核进行特征提取。全局平均池化层对每个特征图进行全局平均池化,得到一个特征向量。随机失活层可以有效地防止过拟合。全连接层使用ReLU激活函数,Dropout正则化进行训练。输出层使用Softmax激活函数进行多分类任务。
-
总的来说,NiN网络通过引入“网络中的网络”概念和1x1卷积技术,提高了模型的效率和泛化能力,使其在处理图像识别等任务时表现出更好的性能。
NiN块
NiN 架构
- 无全连接层
- 交替使用NN块和步幅为2的最大池化层
- 逐步减小小高宽和增大通道数
- 最后使用全局平均池化层得到输出
- 其输入通道数是类别数
NiN无全连接层,最后全局池化
2、VGG与NiN网络对比
总结:
- NiN就是替换全连接,有多少个类,就用多少个核的1*1
- NiN块使用卷积层加两个1x1卷积层,后者对每个像素增加了非线性性
- NiN使用全局平均池化层来替代VGG和AlexNet中的全连接层
- 不容易过以合,更少的参数个数
3、NiN网络的简易实现
def nin_block(in_channels, out_channels, kernel_size, strides, padding):
return nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernel_size, strides, padding),
nn.ReLU(),
nn.Conv2d(out_channels, out_channels, kernel_size=1), nn.ReLU(),
nn.Conv2d(out_channels, out_channels, kernel_size=1), nn.ReLU())
- 函数定义:
def nin_block(in_channels, out_channels, kernel_size, strides, padding):
该函数接收以下参数:
in_channels
:输入张量的通道数。out_channels
:输出张量的通道数,也是中间卷积层的输出通道数。kernel_size
:第一个卷积层的卷积核大小。strides
:第一个卷积层的步长。padding
:第一个卷积层的填充。
-
构建模块:
nn.Conv2d(in_channels, out_channels, kernel_size, strides, padding)
:这是一个二维卷积层,它将输入张量的通道数从in_channels
转换为out_channels
。它使用kernel_size
大小的卷积核,步长为strides
,填充为padding
。nn.ReLU()
:这是一个激活函数,用于在卷积层之后引入非线性。- 接下来的两个
nn.Conv2d(out_channels, out_channels, kernel_size=1)
和它们之后的nn.ReLU()
构成了两个连续的 1x1 卷积层,每个后面都跟着一个 ReLU 激活函数。这些 1x1 的卷积层可以看作是对特征图的线性变换,用于增强网络的表示能力。
-
返回值:
- 该函数返回一个
nn.Sequential
模块,这是一个按顺序包含上述层的容器。
- 该函数返回一个
net = nn.Sequential(
nin_block(1, 96, kernel_size=11, strides=4, padding=0),
nn.MaxPool2d(3, stride=2),
nin_block(96, 256, kernel_size=5, strides=1, padding=2),
nn.MaxPool2d(3, stride=2),
nin_block(256, 384, kernel_size=3, strides=1, padding=1),
nn.MaxPool2d(3, stride=2),
nn.Dropout(0.5),
# 标签类别数是10
nin_block(384, 10, kernel_size=3, strides=1, padding=1),
nn.AdaptiveAvgPool2d((1, 1)),
# 将四维的输出转成二维的输出,其形状为(批量大小,10)
nn.Flatten())
打印输出结构
二十七、并行连结网络GoogLeNet
1、GoogLeNet 基本介绍
-
在此之前,AlexNet、VGG等结构都是通过增大网络的深度(层数)来获得更好的训练效果,但层数的增加会带来很多负作用,比如过拟合、梯度消失、梯度爆炸等。而GoogLeNet则采用了不同的策略来提升训练结果。
-
GoogLeNet引入了“Inception”模块,该模块使用不同尺度的卷积核来同时捕获不同尺度的特征,有助于网络更好地适应不同大小的对象和结构。每个Inception模块包含多个并行的卷积层和池化层,然后将它们的输出在通道维度上连接起来。此外,Inception模块还使用了1x1的卷积来进行升降维,从而能更高效的利用计算资源,在相同的计算量下能提取到更多的特征。
GoogLeNet的特点还包括:
- 提升了对网络内部计算资源的利用。
- 增加了网络的深度和宽度,网络深度达到22层(不包括池化层和输入层),但没有增加计算代价。
Inception模块图如下
从上图可知
- Inception块里有4条并行的线路。前3条线路使用窗口大小分别是 1 × 1 1\times 1 1×1、 3 × 3 3\times 3 3×3和 5 × 5 5\times 5 5×5的卷积层来抽取不同空间尺寸下的信息,其中中间2个线路会对输入先做 1 × 1 1\times 1 1×1卷积来减少输入通道数,以降低模型复杂度。第四条线路则使用 3 × 3 3\times 3 3×3最大池化层,后接 1 × 1 1\times 1 1×1卷积层来改变通道数。4条线路都使用了合适的填充来使输入与输出的高和宽一致。最后我们将每条线路的输出在通道维上连结,并输入接下来的层中去。
卷积核个数就是通道数
具体过程:
首先,输入被复制成了四块(之前所遇到的都是一条路直接到最后):
第一条路先进入一个1 *1的卷积层再输出
第二条路先通过一个1 * 1的卷积层对通道做变换,再通过一个pad为1的3 * 3的卷积层,使得输入和输出的高宽相同
第三条路先通过一个1 * 1的卷积层对通道数做变换,不改变高宽,但是再通过一个pad为2的5 * 5的卷积层提取空间信息特征,输入和输出还是等高等宽的
第四条路先通过一个pad为1的3 * 3的最大池化层,再通过一个1 * 1的卷积层
因为这四条路都没有改变高宽,最后用一个contact的操作将它们的输出合并起来(不是将四张图片放在一起形成一张更大的图片,而是在输出的通道数上做合并,最终的输出和输入等同高宽,但是输出的通道数更多,因为是四条路输出的通道数合并在一起的),因此,输出的高宽是不变的,改变的只有它的通道数
在这个结构中,基本上各种形状的卷积层和最大池化层等都有了,所以就不用过多地纠结于卷积层尺寸的选择
假设输入的通道数是192,高宽是28 * 28
因为在Inception块中,高宽是不变的,所以上图中只标出了通道数的变化:
-
通过第一条路时,经过第一个卷积层直接将通道数压缩到了64
-
经过第二条路时,先经过一个1 * 1的卷积层将通道数从192压缩到了96(这里为什么要压缩到96?因为想要把后一层3 * 3的卷积层的输入数降低,通过降低输入通道数来降低模型的复杂度,因为模型复杂度可以认为是可以学习的参数的个数,卷积层可学习参数的个数是输入通道 * 输出通道 * 卷积核的大小(3 * 3),所以这里要将192压缩为96),然后再经过一个3 * 3的卷积层后,通道数增加到128
-
经过第三条路时,先通过一个3 * 3的卷积层将通道数压缩到16,再经过一个5 * 5的卷积层(这里分配的通道数并不多)增加到32
-
经过第四条路时,首先经过一个3 * 3的最大池化层,这里并不会改变通道数,然后经过一个1 * 1的卷积层之后,通道数直接由192降到了32
-
总的来说,上图中标记为白色的卷积层可以认为是用来改变通道数的,要么改变输入要么改变输出;标记为蓝色的卷积层可以认为是用来抽取信息的,第1条路中标记为蓝色的卷积层不抽取空间信息,只抽取通道信息,第2、3条路中标记为蓝色的卷积层是用来抽取空间信息的,第4条路中标记为蓝色的最大池化层也是用来抽取空间信息的,增强鲁棒性
-
经过Inception块之后,最后输出的通道数由输入的192变成了64+128+32+32=256,每个通道都会识别一些特定的模型,所以应该把重要的通道数留给重要的通道(这里的意思应该是类似于:输入进来之后被复制成了四份,然后经过四条不同的路,最终进行通道数的合并,在输出通道数固定的情况下,四条路的最终输出的通道数是不一样的,所以可以将有限的输出通道数分配给不同的路径,有一点像权重,就比如上图中给第二条路分配了128个输出通道数,接近一半的通道数都留给了3 * 3的卷积层,因为3 * 3的卷积层计算量不大同时能够很好地抽取信息,剩下通道数的一半分给了1 * 1的卷积层,然后再剩下给第3、4条路平分),大致的设计思路就是这样,但是具体所使用的数值也是调出来的
2、GoogLeNet 架构介绍
- GoogLeNet一共使用了9个Inception块和全局平均汇聚层(避免在最后使用全连接层)的堆叠来生成估计值,第一个模块类似于AlexNet和LeNet,Inception块的组合从VGG继承,Inception块之间的最大汇聚层可以降低维度
- GoogLeNet由大量的Inception块组成,如下图所示,总共分成了5个stage(有点类似于VGG,高宽减半一次为1个stage)
- GoogLeNet中总共有9个Inception块,主要集中在stage 3(2个)、stage 4(5个)和stage 5(2个)
- GoogLeNet中大量地使用了1 * 1的卷积,把它当成全连接来使用,来做通道数的变换(受到NiN的影响)
- GoogLeNet中也使用了全局平均池化层,因为最后没有设置Inception块使得最后的输出通道数等于标签的类别数,所以在倒数第二步做完全局平均池化之后会拿到一个长为通道数的向量,最后再通过一个全连接层映射到标号所要的类别数(这里并没有强求最后的输出通道数一定要等于标签类别数,做了简化,更加灵活)
具体结构:
1、Stage 1:
第一个模块使用了一个卷积层和一个最大池化层:
第一个模块先使用了一个64通道7 * 7的卷积层(stride = 2,padding = 3)
然后使用了一个3 * 3的最大池化层(stride = 2,padding = 1)
2、Stage 2:
第二个模块使用了两个卷积层和一个最大池化层:
第一个卷积层是64通道1 * 1的卷积层
第二个卷积层是192通道的3 * 3的卷积层(stride = 3,padding = 1)
最大池化层的窗口大小为3 * 3(stride = 2,padding = 1)
3、Stage 3:
第三个模块串联了两个完整的Inception块和一个最大池化层:
第一个Inception块的输出通道数为:64 + 128 + 32 + 32 = 256,四条路经之间的输出通道数量比是:64 :128 :32 :32 = 2 :4 :1 :1。第二条和第三条路径首先将输入通道的数量分别由192减少到96和16,然后连接第二个卷积层(Inception(192,64,(96,128),(16,32),32))
第二个Inception块的输出通道数增加到128 + 192 + 96 + 64 = 480,四条路经之间的输出通道数量比为128 :192 :96 :64 = 4 :6 :3 :2,第二条和第三条路径首先将输入通道数量分别由256减少到128和32,然后连接第二个卷积层(Inception(256,128,(128,192),(32,96),64))
最大池化层的窗口大小为3 * 3(stride = 2,padding = 1)
4、Stage 4:
第四个模块串联了5个Inception块和一个最大池化层:
第一个Inception块的输出通道数为:192 + 208 + 48 + 64 = 512(Inception(480,192,(96,208),(16,48),64))
第二个Inception块的输出通道数为:160 + 224 + 64 + 64 =512(Inception(512,160,(112,224),(24,64),64))
第三个Inception块的输出通道数为:128 + 256 + 64 + 64 = 512(Inception(512,128,(128,256),(24,64),64))
第四个Inception块的输出通道数为:112 + 288 + 64 + 64 =528 (Inception(512,112,(144,288),(32,64),64))
第五个Inception块的输出通道数为:256 + 320 +128 + 128 = 832(Inception(528,256,(160,320),(32,128),128 ))
以上这些路径的通道数分配和和第三模块中的类似
第一条路经仅含1 * 1的卷积层
含3 * 3卷积层的第二条路径输出最多通道
含5 * 5卷积层的第三条路经
含3 * 3最大汇聚层的第四条路经
第二、第三条路径都会先按比例减小通道数
5、Stage 5:
第五个模块中有两个Inception块和一个输出层:
第一个Inception块的输出通道数为:256 + 320 + 128 +128 = 832(Inception(832,256,(160,320),(32,128),128))
第二个Inception块的输出通道数为:384 + 384 + 128 +128 = 1024(Inception(832,384,(192,384),(48,128),128))
输出层和NiN一样使用全局平均汇聚层,将每个通道的高和宽变成1
最后将输出变成二维数组,再接上一个输出个数为标签类别数的全连接层 nn.Linear(1024,10)
以上这些路径通道数的分配思路和第三、第四模块一致
总结
- Inception块有四条不同超参数的卷积层和池化层的路来抽取不同的信息(等价于一个有4条路径的子网络,通过不同窗口形状的卷积层和最大汇聚层来并行抽取信息,并使用1 * 1卷积层减少每像素级别上的通道维数从而降低模型的复杂度),它的一个主要优点是模型参数小,计算复杂度低
- GoogLeNet使用了9个Inception块(每个Inception块中有6个卷积层,所有Inception块中一共有54个卷积层),这些Inception块与其他层(卷积层、全连接层)串联起来,其中Inception块的通道数分配之比是在Imagenet数据集上通过大量的实验得来的
- GoogLeNet是第一个达到上百层的网络,但是不是深度是100,直到ResNet的出现才达到了模型的深度达到100层,这里的上百层指的是通过设计并行的通道来使得模型达到数百层
- Inception后续也有一系列的改进,GoogLeNet V3和GoogLeNet V4目前依旧在被使用,GoogLeNet一开始的精度其实不高,在BN、V3、V4之后精度才慢慢提升上去了,现在也是比较常用的模块,它以较低的计算复杂度提供了类似的测试精度
- GoogLeNet的问题是特别复杂,通道数的设置没有一定的选择依据,以及内部构造比较奇怪,这也是GoogLeNet不那么受欢迎的原因所在
2、GoogLeNet 简易实现
class Inception(nn.Module):
# c1--c4是每条路径的输出通道数
def __init__(self, in_channels, c1, c2, c3, c4, **kwargs):
super(Inception, self).__init__(**kwargs)
# 线路1,单1x1卷积层
self.p1_1 = nn.Conv2d(in_channels, c1, kernel_size=1)
# 线路2,1x1卷积层后接3x3卷积层
self.p2_1 = nn.Conv2d(in_channels, c2[0], kernel_size=1)
self.p2_2 = nn.Conv2d(c2[0], c2[1], kernel_size=3, padding=1)
# 线路3,1x1卷积层后接5x5卷积层
self.p3_1 = nn.Conv2d(in_channels, c3[0], kernel_size=1)
self.p3_2 = nn.Conv2d(c3[0], c3[1], kernel_size=5, padding=2)
# 线路4,3x3最大汇聚层后接1x1卷积层
self.p4_1 = nn.MaxPool2d(kernel_size=3, stride=1, padding=1)
self.p4_2 = nn.Conv2d(in_channels, c4, kernel_size=1)
def forward(self, x):
p1 = F.relu(self.p1_1(x))
p2 = F.relu(self.p2_2(F.relu(self.p2_1(x))))
p3 = F.relu(self.p3_2(F.relu(self.p3_1(x))))
p4 = F.relu(self.p4_2(self.p4_1(x)))
# 在通道维度上连结输出
return torch.cat((p1, p2, p3, p4), dim=1)
Inception模块是一个并行结构,它使用了不同尺度的卷积和池化操作来捕获输入图像的不同尺度的特征。
-
初始化(
__init__
):-
接收输入通道数
in_channels
和四个路径的输出通道数c1, c2, c3, c4
作为参数。其中,c2
和c3
是列表,因为它们分别有两个卷积层。 -
对于每个路径,它定义了相应的卷积层或池化层。
- 路径1: 一个1x1的卷积层。
- 路径2: 一个1x1的卷积层后接一个3x3的卷积层。
- 路径3: 一个1x1的卷积层后接一个5x5的卷积层。
- 路径4: 一个3x3的最大池化层后接一个1x1的卷积层。
-
-
前向传播(
forward
):- 输入
x
被传递给每个路径。 - 每个路径的输出都经过ReLU激活函数。
- 四个路径的输出在通道维度(即
dim=1
)上被连结起来。
- 输入
输出测试数据结构
X = torch.rand(size=(1, 1, 96, 96))
for layer in net:
X = layer(X)
print(layer.__class__.__name__,'output shape:\t', X.shape)
二十八、批量归一化
1、批量归一化基本概念
-
批量归一化层, 能让较深的神经网络的训练变得更加容易 。标准化处理输入数据使各个特征的分布相近:这往往更容易训练出有效的模型。
-
通常来说,数据标准化预处理对于浅层模型就足够有效 。随着模型训练的进行,当每层中参数更新时,靠近输出层的输出较难出现剧烈变化。但对深层神经网络来说,即使输入数据已做标准化,训练中模型参数的更新依然很容易造成靠近输出层输出的剧烈变化。这种计算数值的不稳定性通常令我们难以训练出有效的深度模型。
-
批量归一化的提出正是为了应对深度模型训练的挑战。在模型训练时,批量归一化利用小批量上的均值和标准差,不断调整神经网络中间输出,从而使整个神经网络在各层的中间输出的数值更稳定。批量归一化和残差网络为训练和设计深度模型提供了两类重要思路。
梯度在上面比较大 越到下面梯度越小
关于批量化我在之前文章提过
2、全连接层批量化处理
- 通常将批量归一化层置于全连接层中的仿射变换和激活函数之间。设全连接层的输入为 u \boldsymbol{u} u,权重参数和偏差参数分别为 W \boldsymbol{W} W和 b \boldsymbol{b} b,激活函数为 ϕ \phi ϕ。设批量归一化的运算符为 BN \text{BN} BN。那么,使用批量归一化的全连接层的输出为
ϕ ( BN ( x ) ) , \phi(\text{BN}(\boldsymbol{x})), ϕ(BN(x)),
其中批量归一化输入 x \boldsymbol{x} x由仿射变换
x = W u + b \boldsymbol{x} = \boldsymbol{W\boldsymbol{u} + \boldsymbol{b}} x=Wu+b
得到。考虑一个由 m m m个样本组成的小批量,仿射变换的输出为一个新的小批量 B = { x ( 1 ) , … , x ( m ) } \mathcal{B} = \{\boldsymbol{x}^{(1)}, \ldots, \boldsymbol{x}^{(m)} \} B={x(1),…,x(m)}。它们正是批量归一化层的输入。对于小批量 B \mathcal{B} B中任意样本 x ( i ) ∈ R d , 1 ≤ i ≤ m \boldsymbol{x}^{(i)} \in \mathbb{R}^d, 1 \leq i \leq m x(i)∈Rd,1≤i≤m,批量归一化层的输出同样是 d d d维向量
y ( i ) = BN ( x ( i ) ) , \boldsymbol{y}^{(i)} = \text{BN}(\boldsymbol{x}^{(i)}), y(i)=BN(x(i)),
并由以下几步求得。首先,对小批量 B \mathcal{B} B求均值和方差:
μ
B
←
1
m
∑
i
=
1
m
x
(
i
)
,
\boldsymbol{\mu}_\mathcal{B} \leftarrow \frac{1}{m}\sum_{i = 1}^{m} \boldsymbol{x}^{(i)},
μB←m1i=1∑mx(i),
σ
B
2
←
1
m
∑
i
=
1
m
(
x
(
i
)
−
μ
B
)
2
,
\boldsymbol{\sigma}_\mathcal{B}^2 \leftarrow \frac{1}{m} \sum_{i=1}^{m}(\boldsymbol{x}^{(i)} - \boldsymbol{\mu}_\mathcal{B})^2,
σB2←m1i=1∑m(x(i)−μB)2,
其中的平方计算是按元素求平方。接下来,使用按元素开方和按元素除法对 x ( i ) \boldsymbol{x}^{(i)} x(i)标准化:
x ^ ( i ) ← x ( i ) − μ B σ B 2 + ϵ , \hat{\boldsymbol{x}}^{(i)} \leftarrow \frac{\boldsymbol{x}^{(i)} - \boldsymbol{\mu}_\mathcal{B}}{\sqrt{\boldsymbol{\sigma}_\mathcal{B}^2 + \epsilon}}, x^(i)←σB2+ϵx(i)−μB,
这里 ϵ > 0 \epsilon > 0 ϵ>0是一个很小的常数,保证分母大于0。在上面标准化的基础上,批量归一化层引入了两个可以学习的模型参数,拉伸(scale)参数 γ \boldsymbol{\gamma} γ 和偏移(shift)参数 β \boldsymbol{\beta} β。这两个参数和 x ( i ) \boldsymbol{x}^{(i)} x(i)形状相同,皆为 d d d维向量。它们与 x ( i ) \boldsymbol{x}^{(i)} x(i)分别做按元素乘法(符号 ⊙ \odot ⊙)和加法计算:
y ( i ) ← γ ⊙ x ^ ( i ) + β . {\boldsymbol{y}}^{(i)} \leftarrow \boldsymbol{\gamma} \odot \hat{\boldsymbol{x}}^{(i)} + \boldsymbol{\beta}. y(i)←γ⊙x^(i)+β.
至此,我们得到了 x ( i ) \boldsymbol{x}^{(i)} x(i)的批量归一化的输出 y ( i ) \boldsymbol{y}^{(i)} y(i)。
值得注意的是,可学习的拉伸和偏移参数保留了不对 x ^ ( i ) \hat{\boldsymbol{x}}^{(i)} x^(i)做批量归一化的可能:此时只需学出 γ = σ B 2 + ϵ \boldsymbol{\gamma} = \sqrt{\boldsymbol{\sigma}_\mathcal{B}^2 + \epsilon} γ=σB2+ϵ和 β = μ B \boldsymbol{\beta} = \boldsymbol{\mu}_\mathcal{B} β=μB。我们可以对此这样理解:如果批量归一化无益,理论上,学出的模型可以不使用批量归一化。
可学习的参数为gamma和 beta 作用在全连接层和卷积层输出上,激活函数前,全连接层和卷积层输入上
对全连接层,作用在特征维
对于卷积层,作用在通道维
- 跟1x1的卷积一个道理,把同一像素的各个通道上的值作为特征
- 在每个批量里,1个像素是1个样本。与像素(样本)对应的通道维,就是特征维
总结:
- 批量归一化固定小批量中的均值和方差,然后学习出适合的偏移和缩放
- 可以加速收敛速度,但一般不改变模型精度
3、批量归一化代码实现
def batch_norm(X, gamma, beta, moving_mean, moving_var, eps, momentum):
# 通过is_grad_enabled来判断当前模式是训练模式还是预测模式
if not torch.is_grad_enabled():
# 如果是在预测模式下,直接使用传入的移动平均所得的均值和方差
X_hat = (X - moving_mean) / torch.sqrt(moving_var + eps)
else:
assert len(X.shape) in (2, 4)
if len(X.shape) == 2:
# 使用全连接层的情况,计算特征维上的均值和方差
mean = X.mean(dim=0)
var = ((X - mean) ** 2).mean(dim=0)
else:
# 使用二维卷积层的情况,计算通道维上(axis=1)的均值和方差。
# 这里我们需要保持X的形状以便后面可以做广播运算
mean = X.mean(dim=(0, 2, 3), keepdim=True)
var = ((X - mean) ** 2).mean(dim=(0, 2, 3), keepdim=True)
# 训练模式下,用当前的均值和方差做标准化
X_hat = (X - mean) / torch.sqrt(var + eps)
# 更新移动平均的均值和方差
moving_mean = momentum * moving_mean + (1.0 - momentum) * mean
moving_var = momentum * moving_var + (1.0 - momentum) * var
Y = gamma * X_hat + beta # 缩放和移位
return Y, moving_mean.data, moving_var.data
这里2对应全连接层两个维度(batch,全连接层大小) ,4对应卷积4个维度(batch,通道数,长,宽)
-
函数定义:
X
: 输入数据,通常是一个batch的数据。gamma
和beta
: 可学习的缩放和移位参数。moving_mean
和moving_var
: 在训练过程中计算的移动平均值和移动方差,用于预测时标准化数据。eps
: 一个小的常数,用于防止除零错误。momentum
: 用于更新moving_mean
和moving_var
的动量值。
-
检查是否处于训练模式:
- 如果当前没有启用梯度计算(即处于评估或预测模式),则使用
moving_mean
和moving_var
进行标准化。
- 如果当前没有启用梯度计算(即处于评估或预测模式),则使用
-
在训练模式下:
- 首先检查
X
的形状,确保其是一个二维或四维张量(对应于全连接层和卷积层)。 - 计算当前batch的均值和方差:
- 对于全连接层(二维张量),沿特征维(dim=0)计算均值和方差。
- 对于卷积层(四维张量),沿通道维(dim=(0, 2, 3))计算均值和方差,并使用
keepdim=True
来保持输出的形状与输入相同,以便进行广播操作。
- 使用当前batch的均值和方差进行标准化。
- 更新
moving_mean
和moving_var
以在后续批次中使用。
- 首先检查
-
标准化后的缩放和移位:
- 使用可学习的
gamma
和beta
参数对标准化后的数据进行缩放和移位。
- 使用可学习的
-
返回值:
- 返回标准化并缩放/移位后的数据
Y
。 - 返回更新后的
moving_mean
和moving_var
(注意,这里只返回了它们的.data
部分,这意味着 只返回了它们的值,而不包括梯度信息)。
- 返回标准化并缩放/移位后的数据
创建一个正确的BatchNorm层
class BatchNorm(nn.Module):
# num_features:完全连接层的输出数量或卷积层的输出通道数。
# num_dims:2表示完全连接层,4表示卷积层
def __init__(self, num_features, num_dims):
super().__init__()
if num_dims == 2:
shape = (1, num_features)
else:
shape = (1, num_features, 1, 1)
# 参与求梯度和迭代的拉伸和偏移参数,分别初始化成1和0
self.gamma = nn.Parameter(torch.ones(shape))
self.beta = nn.Parameter(torch.zeros(shape))
# 非模型参数的变量初始化为0和1
self.moving_mean = torch.zeros(shape)
self.moving_var = torch.ones(shape)
def forward(self, X):
# 如果X不在内存上,将moving_mean和moving_var
# 复制到X所在显存上
if self.moving_mean.device != X.device:
self.moving_mean = self.moving_mean.to(X.device)
self.moving_var = self.moving_var.to(X.device)
# 保存更新过的moving_mean和moving_var
Y, self.moving_mean, self.moving_var = batch_norm(
X, self.gamma, self.beta, self.moving_mean,
self.moving_var, eps=1e-5, momentum=0.9)
return Y
训练过程如下图所示 🌴🌴🌴🌴🌴🌴🌴🌴🌴🌴🌴🌴🌴🌴
再看从第一个批量规范化层中学到的拉伸参数gamma和偏移参数beta。
二十九、残差网络 ResNet ❗️❗️
1、ResNet 基本概念
- ResNet(深度残差网络)是CNN(卷积神经网络)图像史上的一项重要里程碑,由何凯明等人提出,并因其在公开数据上展现的优势而广受关注。ResNet是残差网络(Residual Network)的缩写,该系列网络广泛用于目标分类、对象检测、语义分割、人脸识别等领域,以及作为计算机视觉任务主干经典神经网络的一部分。
ResNet的主要特点包括:
- 残差连接:ResNet引入了残差连接,将输入的特征直接与输出的特征相加,形成残差块。这种连接方式允许信息直接通过
跳跃连接
(skip connection)绕过网络中的某些层,从而避免了梯度在传播过程中的衰减。通过残差连接,网络能够更轻松地学习残差部分,从而提高了网络的性能和训练效果。 - 深度网络设计:ResNet可以构建非常深的网络,甚至超过百层的网络,而且仍然能够有效训练。这是因为残差连接的引入有效地解决了梯度消失和梯度爆炸问题。较深的网络可以学习到更多复杂的特征表示,从而提高了网络的表达能力和性能。
- 全局平均池化:ResNet在网络的末尾通常使用全局平均池化层来代替全连接层。全局平均池化将最后一层特征图的每个通道的空间维度进行平均,得到一个固定长度的特征向量,作为最终的分类器输入。
2、ResNet 的残差块
- 聚焦于神经网络局部。如下图所示,设输入为 x \boldsymbol{x} x。假设希望学出的理想映射为 f ( x ) f(\boldsymbol{x}) f(x),从而作为图上方激活函数的输入。左图虚线框中的部分需要直接拟合出该映射 f ( x ) f(\boldsymbol{x}) f(x),而右图虚线框中的部分则需要拟合出有关恒等映射的残差映射 f ( x ) − x f(\boldsymbol{x})-\boldsymbol{x} f(x)−x。残差映射在实际中往往更容易优化。 f ( x ) f(\boldsymbol{x}) f(x)。
- 将下图中右图虚线框内上方的加权运算(如仿射)的权重和偏差参数学成0,那么 f ( x ) f(\boldsymbol{x}) f(x)即为恒等映射。实际中,当理想映射 f ( x ) f(\boldsymbol{x}) f(x)极接近于恒等映射时,残差映射也易于捕捉恒等映射的细微波动。下面右图也是ResNet的基础块,即残差块(residual block)。在残差块中,输入可通过跨层的数据线路更快地向前传播。
- ResNet沿用了VGG全 3 × 3 3\times 3 3×3卷积层的设计。残差块里首先有2个有相同输出通道数的 3 × 3 3\times 3 3×3卷积层。每个卷积层后接一个批量归一化层和ReLU激活函数。然后 将输入跳过这两个卷积运算后直接加在最后的ReLU激活函数前。这样的设计要求两个卷积层的输出与输入形状一样,从而可以相加。如果想改变通道数,就需要引入一个额外的 1 × 1 1\times 1 1×1卷积层来将输入变换成需要的形状后再做相加运算。
3、ResNet残差块细节
-
左边是ResNet的第一种实现(不包含1 * 1卷积层的残差块),它直接将输入加在了叠加层的输出上面
-
右边是ResNet的第二种实现(包含1 * 1卷积层的残差块),它先对输入进行了1 * 1的卷积变换通道(改变范围),再加入到叠加层的输出上面
ResNet沿用了VGG完整的3 * 3卷积层设计 -
残差块中首先有2个相同输出通道数的3 * 3卷积层,每个卷积层后面接一个批量归一化层和ReLu激活函数;通过跨层数据通路,跳过残差块中的两个卷积运算,将输入直接加在最后的ReLu激活函数前(这种设计要求2个卷积层的输出与输入形状一样,这样才能使第二个卷积层的输出(也就是第二个激活函数的输入)和原始的输入形状相同,才能进行相加)
-
如果想要改变通道数,就需要引入一个额外的1 * 1的卷积层来将输入变换成需要的形状后再做相加运算(如上图中右侧含1 * 1卷积层的残差块)
总结:
- 残差块使得很深的网络更加容易训练,甚至可以训练一千层的网络
- 残差网络对随后的深层神经网络设计产生了深远影响,无论是卷积类网络还是全连接类网络。
4、ResNet代码的实现
class Residual(nn.Module): #@save
def __init__(self, input_channels, num_channels,
use_1x1conv=False, strides=1):
super().__init__()
self.conv1 = nn.Conv2d(input_channels, num_channels,
kernel_size=3, padding=1, stride=strides)
self.conv2 = nn.Conv2d(num_channels, num_channels,
kernel_size=3, padding=1)
if use_1x1conv:
self.conv3 = nn.Conv2d(input_channels, num_channels,
kernel_size=1, stride=strides)
else:
self.conv3 = None
self.bn1 = nn.BatchNorm2d(num_channels)
self.bn2 = nn.BatchNorm2d(num_channels)
def forward(self, X):
Y = F.relu(self.bn1(self.conv1(X)))
Y = self.bn2(self.conv2(Y))
if self.conv3:
X = self.conv3(X)
Y += X
return F.relu(Y)
1、初始化(__init__
)
-
参数:
input_channels
:输入特征图的通道数。num_channels
:中间卷积层的输出通道数(同时也是该残差块的输出通道数)。use_1x1conv
:一个布尔值,指示是否使用1x1的卷积来在相加之前改变输入特征图的通道数或调整其空间尺寸(即进行下采样)。strides
:卷积层的步长,用于控制空间尺寸的变化(通常是2,用于下采样)。
-
卷积层:
self.conv1
:第一个3x3的卷积层,使用padding=1
以保持空间尺寸不变。self.conv2
:第二个3x3的卷积层,同样使用padding=1
。self.conv3
(可选):一个1x1的卷积层,当use_1x1conv=True
时使用。它通常用于改变输入特征图的通道数或进行下采样。
-
批量归一化层:
self.bn1
和self.bn2
:用于加速训练并增强模型泛化能力的批量归一化层。
2、前向传播(forward
)
- 输入
X
首先通过第一个卷积层self.conv1
和批量归一化层self.bn1
,并应用ReLU激活函数。 - 接着,输出
Y
通过第二个卷积层self.conv2
和批量归一化层self.bn2
,但不应用激活函数。 - 如果
use_1x1conv=True
,则输入X
会通过一个1x1的卷积层self.conv3
,以便与Y
相加(确保两者的通道数和空间尺寸相同)。 - 最后,
Y
与可能经过1x1卷积的X
相加,形成残差连接。相加后的结果再次通过ReLU激活函数,并作为该残差块的输出。
进行输出测试
①测试一
-
在给定的
Residual
类定义和实例化中,blk = Residual(3,3)
创建了一个没有使用1x1卷积(use_1x1conv=False
)且没有改变空间尺寸(strides=1
)的残差块。输入和输出通道数都是3。 -
接下来,创建了一个随机的输入张量
X
,其形状为(4, 3, 6, 6)
,这表示有4个样本,每个样本有3个通道,且每个通道的空间尺寸为6x6。 -
现在,我们把这个张量传递给
blk
残差块进行前向传播:Y = blk(X)
。 -
由于
blk
没有改变空间尺寸(strides=1
),并且没有使用1x1卷积来改变通道数或空间尺寸(use_1x1conv=False
),因此输出Y
的形状将与输入X
的形状相同
②测试二
增加输出通道数的同时,减半输出的高和宽
-
对于给定的
blk = Residual(3,6, use_1x1conv=True, strides=2)
,这是一个使用了1x1卷积并且步长为2的残差块。 -
输入
X
的形状是(4, 3, 6, 6)
,代表4个样本,每个样本有3个通道,且每个通道的空间尺寸为6x6。 -
由于
use_1x1conv=True
,在残差连接中,输入X
会通过一个1x1的卷积层,该层将通道数从3增加到6(与主路径的输出通道数相匹配),并且步长为2,这将导致空间尺寸减半(从6x6变为3x3)。❗️❗️❗️主路径(通过两个3x3卷积层)也将进行步长为2的下采样,因此输出的空间尺寸也会减半。 -
最后,由于两个路径(原始输入通过1x1卷积和主路径通过两个3x3卷积)的输出具有相同的通道数(6)和空间尺寸(3x3),所以它们可以相加。
因此,输出Y
的形状将是(4, 6, 3, 3)
,代表4个样本,每个样本有6个通道,且每个通道的空间尺寸为3x3。
高宽减半 通道数加倍
ResNet模型搭建
b1 = nn.Sequential(nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3),
nn.BatchNorm2d(64), nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1))
def resnet_block(input_channels, num_channels, num_residuals,
first_block=False):
blk = []
for i in range(num_residuals):
if i == 0 and not first_block:
blk.append(Residual(input_channels, num_channels,
use_1x1conv=True, strides=2))
else:
blk.append(Residual(num_channels, num_channels))
return blk
b2 = nn.Sequential(*resnet_block(64, 64, 2, first_block=True))
b3 = nn.Sequential(*resnet_block(64, 128, 2))
b4 = nn.Sequential(*resnet_block(128, 256, 2))
b5 = nn.Sequential(*resnet_block(256, 512, 2))
net = nn.Sequential(b1, b2, b3, b4, b5,
nn.AdaptiveAvgPool2d((1,1)),
nn.Flatten(), nn.Linear(512, 10))
*表示将列表解开,**表示将字典解开
初始的卷积块b1
和四个残差块b2
、b3
、b4
、b5
,最后是一个全局平均池化层、展平层和全连接层。
-
初始卷积块
b1
:- 输入形状:
(batch_size, 1, H, W)
(其中H
和W
是输入图像的高和宽) nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3)
: 卷积操作,输出通道数为64,卷积核大小为7x7,步长为2,填充为3。输出形状为(batch_size, 64, H/2, W/2)
(由于填充,输出尺寸不变)nn.BatchNorm2d(64)
: 批量归一化,不改变形状nn.ReLU()
: ReLU激活函数,不改变形状nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
: 最大池化操作,输出形状为(batch_size, 64, (H/2-2)/2+1, (W/2-2)/2+1)
,即(batch_size, 64, H/4, W/4)
- 输入形状:
-
残差块:
resnet_block
函数用于生成残差块序列。在残差块中,如果first_block=True
或i=0
且不是第一个残差块,则第一个残差块会进行下采样(即改变空间尺寸和通道数)。
first block 指的是整个结构里的第一个 i=0仅仅是这个block里面第一个
-
残差块序列:
b2
:第一个残差块会改变空间尺寸和通道数(从64到64),之后的残差块不会改变。输出形状为(batch_size, 64, H/8, W/8)
(假设H
和W
都是可被8整除的)b3
:第一个残差块会改变空间尺寸和通道数(从64到128),之后的残差块不会改变。输出形状为(batch_size, 128, H/16, W/16)
b4
:第一个残差块会改变空间尺寸和通道数(从128到256),之后的残差块不会改变。输出形状为(batch_size, 256, H/32, W/32)
b5
:第一个残差块会改变空间尺寸和通道数(从256到512),之后的残差块不会改变。输出形状为(batch_size, 512, H/64, W/64)
-
全局平均池化层:
nn.AdaptiveAvgPool2d((1,1))
: 将特征图的空间尺寸调整为1x1。输出形状为(batch_size, 512, 1, 1)
-
展平层和全连接层:
nn.Flatten()
: 展平特征图。输出形状为(batch_size, 512)
nn.Linear(512, 10)
: 全连接层,输出10个神经元的向量(通常用于分类任务的输出)。输出形状为(batch_size, 10)
因此,整个网络net
的输入形状为(batch_size, 1, H, W)
,输出形状为(batch_size, 10)
。这里,H
和W
应该是可被64整除的,因为网络中有多个下采样步骤。
打印输出网络结构
全连接层拟合能力强,因此和真实值的之间的误差就低,由此而来的梯度也就低