从卷积到ResNet
从卷积到 ResNet
一、卷积基础
1.1 滑动窗口与离散卷积
卷积是一种数学运算,在信号处理、图像处理和深度学习中有着广泛应用。我们可以从最简单的滑动窗口操作来理解卷积的基本思想。
假设我们有一个一维数组[1, 2, 3, 4, 5]
和一个核[0, 1, 0]
,我们想要对这个数组进行卷积操作。具体步骤如下:
-
翻转核:将核
[0, 1, 0]
翻转得到[0, 1, 0]
(这里因为核是对称的,翻转后不变) -
滑动窗口:将翻转后的核在原数组上滑动,每次滑动一个元素的位置
-
元素相乘并求和:在每个位置,将窗口内的元素与核对应位置的元素相乘,然后将结果相加
例如,当窗口位于数组起始位置时:
原数组: \[1, 2, 3, 4, 5]核: \[0, 1, 0]相乘: 1\*0 + 2\*1 + 3\*0 = 2
当窗口向右滑动一位:
原数组: \[1, 2, 3, 4, 5]核: \[0, 1, 0]相乘: 2\*0 + 3\*1 + 4\*0 = 3
以此类推,最终得到的卷积结果是[2, 3, 4, 5]
。
这个简单的例子展示了卷积的核心思想:通过滑动窗口对局部区域进行加权求和。这种操作在图像处理中可以用于模糊、边缘检测等任务。
1.2 卷积的数学定义
在数学上,卷积是两个函数或序列之间的一种运算。根据输入是连续函数还是离散序列,卷积有不同的定义形式。
连续函数的卷积定义为:
(f∗g)(t)=∫−∞∞f(τ)g(t−τ)dτ(f * g)(t) = \int_{-\infty}^{\infty} f(\tau)g(t-\tau)d\tau(f∗g)(t)=∫−∞∞f(τ)g(t−τ)dτ
其中,f(t)f(t)f(t)和g(t)g(t)g(t)是输入的两个函数,τ\tauτ是积分变量,ttt是卷积结果的自变量。
离散序列的卷积定义为:
(f∗g)[n]=∑m=−∞∞f[m]g[n−m](f * g)[n] = \sum_{m=-\infty}^{\infty} f[m]g[n-m](f∗g)[n]=∑m=−∞∞f[m]g[n−m]
这里,f[m]f[m]f[m]和g[n]g[n]g[n]是两个离散序列,nnn是卷积结果的自变量。
从数学角度看,卷积可以理解为函数f与翻转并平移后的函数g的重叠部分的积分(或求和)。这个过程既包含了 “卷”(翻转和平移)的操作,也包含了 “积”(积分或求和)的操作,这就是卷积名称的由来。
1.3 卷积的性质
卷积运算具有以下重要性质:
-
交换律:f∗g=g∗ff * g = g * ff∗g=g∗f
-
结合律:(f∗g)∗h=f∗(g∗h)(f * g) * h = f * (g * h)(f∗g)∗h=f∗(g∗h)
-
分配律:f∗(g+h)=f∗g+f∗hf * (g + h) = f * g + f * hf∗(g+h)=f∗g+f∗h
-
与冲激函数的卷积:f(t)∗δ(t)=f(t)f(t) * \delta(t) = f(t)f(t)∗δ(t)=f(t),其中δ(t)\delta(t)δ(t)是冲激函数
这些性质使得卷积在信号处理和系统分析中非常有用。例如,交换律意味着我们可以交换两个函数的顺序而不改变结果;与冲激函数的卷积性质则表明冲激函数是卷积运算的单位元。
1.4 二维卷积
在图像处理中,我们通常需要处理二维信号(如图像),因此需要用到二维卷积。二维连续卷积的定义为:
(f∗g)(x,y)=∫−∞∞∫−∞∞f(ξ,η)g(x−ξ,y−η)dξdη(f * g)(x, y) = \int_{-\infty}^{\infty}\int_{-\infty}^{\infty} f(\xi, \eta)g(x-\xi, y-\eta)d\xi d\eta(f∗g)(x,y)=∫−∞∞∫−∞∞f(ξ,η)g(x−ξ,y−η)dξdη
二维离散卷积的定义为:
(f∗g)[i,j]=∑m=−∞∞∑n=−∞∞f[m,n]g[i−m,j−n](f * g)[i, j] = \sum_{m=-\infty}^{\infty}\sum_{n=-\infty}^{\infty} f[m, n]g[i-m, j-n](f∗g)[i,j]=∑m=−∞∞∑n=−∞∞f[m,n]g[i−m,j−n]
二维卷积在图像处理中有着广泛应用。例如,我们可以通过设计不同的卷积核来实现图像的模糊、锐化、边缘检测等操作。例如,一个简单的平均模糊核是:
19[111111111]\frac{1}{9}\begin{bmatrix} 1 & 1 & 1 \\ 1 & 1 & 1 \\ 1 & 1 & 1 \end{bmatrix}91111111111
而用于检测垂直边缘的 Sobel 核是:
[−101−202−101]\begin{bmatrix} -1 & 0 & 1 \\ -2 & 0 & 2 \\ -1 & 0 & 1 \end{bmatrix}−1−2−1000121
二、快速傅里叶变换与卷积
2.1 傅里叶变换基础
傅里叶变换是一种将信号从时域(或空域)转换到频域的数学工具。对于连续函数f(t)f(t)f(t),其傅里叶变换定义为:
F{f(t)}=F(ω)=∫−∞∞f(t)e−iωtdt\mathcal{F}\{f(t)\} = F(\omega) = \int_{-\infty}^{\infty} f(t)e^{-i\omega t}dtF{f(t)}=F(ω)=∫−∞∞f(t)e−iωtdt
逆傅里叶变换定义为:
F−1{F(ω)}=f(t)=12π∫−∞∞F(ω)eiωtdω\mathcal{F}^{-1}\{F(\omega)\} = f(t) = \frac{1}{2\pi}\int_{-\infty}^{\infty} F(\omega)e^{i\omega t}d\omegaF−1{F(ω)}=f(t)=2π1∫−∞∞F(ω)eiωtdω
对于离散序列f[n]f[n]f[n],其离散傅里叶变换(DFT)定义为:
F[k]=∑n=0N−1f[n]e−i2πkn/NF[k] = \sum_{n=0}^{N-1} f[n]e^{-i2\pi kn/N}F[k]=∑n=0N−1f[n]e−i2πkn/N
逆离散傅里叶变换(IDFT)定义为:
f[n]=1N∑k=0N−1F[k]ei2πkn/Nf[n] = \frac{1}{N}\sum_{k=0}^{N-1} F[k]e^{i2\pi kn/N}f[n]=N1∑k=0N−1F[k]ei2πkn/N
傅里叶变换的重要性在于它将复杂的卷积运算转化为简单的乘法运算,这就是接下来要介绍的卷积定理。
2.2 卷积定理
卷积定理是连接卷积运算和傅里叶变换的桥梁,它表明:两个函数在时域(空域)的卷积等于它们在频域的傅里叶变换的乘积。数学表达式为:
F{f∗g}=F{f}⋅F{g}\mathcal{F}\{f * g\} = \mathcal{F}\{f\} \cdot \mathcal{F}\{g\}F{f∗g}=F{f}⋅F{g}
同样,逆傅里叶变换也有类似的性质:
F−1{F⋅G}=F−1{F}∗F−1{G}\mathcal{F}^{-1}\{F \cdot G\} = \mathcal{F}^{-1}\{F\} * \mathcal{F}^{-1}\{G\}F−1{F⋅G}=F−1{F}∗F−1{G}
这个定理的证明相对简单。对于连续情况,我们可以写出:
F{f∗g}=∫−∞∞eiωt[∫−∞∞f(t−τ)g(τ)dτ]dt\mathcal{F}\{f * g\} = \int_{-\infty}^{\infty} e^{i\omega t}\left[\int_{-\infty}^{\infty} f(t-\tau)g(\tau)d\tau\right]dtF{f∗g}=∫−∞∞eiωt[∫−∞∞f(t−τ)g(τ)dτ]dt
交换积分顺序并进行变量替换,可得:
=∫−∞∞g(τ)[∫−∞∞f(t−τ)eiωtdt]dτ=∫−∞∞g(τ)eiωτdτ⋅∫−∞∞f(t)eiωtdt=F{f}⋅F{g}= \int_{-\infty}^{\infty} g(\tau)\left[\int_{-\infty}^{\infty} f(t-\tau)e^{i\omega t}dt\right]d\tau = \int_{-\infty}^{\infty} g(\tau)e^{i\omega \tau}d\tau \cdot \int_{-\infty}^{\infty} f(t)e^{i\omega t}dt = \mathcal{F}\{f\} \cdot \mathcal{F}\{g\}=∫−∞∞g(τ)[∫−∞∞f(t−τ)eiωtdt]dτ=∫−∞∞g(τ)eiωτdτ⋅∫−∞∞f(t)eiωtdt=F{f}⋅F{g}
卷积定理的重要性在于,它提供了一种计算卷积的高效方法:先对两个函数进行傅里叶变换,然后在频域相乘,最后进行逆傅里叶变换得到卷积结果。这种方法在计算大尺寸数据的卷积时尤为高效。
2.3 快速傅里叶变换(FFT)
快速傅里叶变换(FFT)是计算离散傅里叶变换(DFT)的高效算法,它能将 DFT 的计算复杂度从O(N2)O(N^2)O(N2)降低到O(NlogN)O(N\log N)O(NlogN),这是一个巨大的进步。
FFT 的基本思想是将长序列的 DFT 分解为多个短序列的 DFT,利用对称性和周期性来减少计算量。具体来说,FFT 通过将序列按奇偶位置分组,递归地将 DFT 分解为更小的子问题,从而大幅提高计算效率。
在实际应用中,当需要计算两个长序列的卷积时,使用 FFT 方法通常比直接计算卷积快得多。计算步骤如下:
-
对两个序列分别进行 FFT,得到它们的频域表示
-
将两个频域表示相乘
-
对乘积结果进行逆 FFT,得到时域的卷积结果
数学表达式为:
a(n)∗b(n)=IFFT[FFT(a(n))⋅FFT(b(n))]a(n) * b(n) = \text{IFFT}[\text{FFT}(a(n)) \cdot \text{FFT}(b(n))]a(n)∗b(n)=IFFT[FFT(a(n))⋅FFT(b(n))]
这种方法的时间复杂度为O(NlogN)O(N\log N)O(NlogN),而直接计算卷积的复杂度为O(N2)O(N^2)O(N2),因此当NNN较大时,FFT 方法明显更高效。
2.4 利用 FFT 计算卷积的示例
假设我们有两个序列a=[2,3,1,0]a = [2, 3, 1, 0]a=[2,3,1,0]和b=[1,0,−1]b = [1, 0, -1]b=[1,0,−1],我们可以使用 FFT 来计算它们的卷积。
首先,计算两个序列的 FFT:
A(k)=FFT(a(n))A(k) = \text{FFT}(a(n))A(k)=FFT(a(n))
B(k)=FFT(b(n))B(k) = \text{FFT}(b(n))B(k)=FFT(b(n))
然后,在频域将两个结果相乘:
C(k)=A(k)⋅B(k)C(k) = A(k) \cdot B(k)C(k)=A(k)⋅B(k)
最后,进行逆 FFT 得到卷积结果:
c(n)=IFFT(C(k))c(n) = \text{IFFT}(C(k))c(n)=IFFT(C(k))
计算结果如下:
c(0)=1,c(1)=3,c(2)=1c(0) = 1, \quad c(1) = 3, \quad c(2) = 1c(0)=1,c(1)=3,c(2)=1
这个结果与直接计算卷积的结果一致,证明了 FFT 方法的正确性。这种方法在处理大尺寸数据时效率更高,特别是在图像处理和信号处理领域。
三、高斯模糊与卷积应用
3.1 高斯函数与高斯核
高斯模糊是图像处理中常用的一种操作,它通过对图像进行加权平均来实现模糊效果,其中权重由高斯函数确定。高斯函数的一维形式为:
G(x)=12πσ2e−x22σ2G(x) = \frac{1}{\sqrt{2\pi\sigma^2}}e^{-\frac{x^2}{2\sigma^2}}G(x)=2πσ21e−2σ2x2
其中,σ\sigmaσ是标准差,控制着高斯函数的宽度。σ\sigmaσ越大,模糊效果越明显。
在图像处理中,我们通常使用二维高斯函数来生成高斯核:
G(x,y)=12πσ2e−x2+y22σ2G(x, y) = \frac{1}{2\pi\sigma^2}e^{-\frac{x^2 + y^2}{2\sigma^2}}G(x,y)=2πσ21e−2σ2x2+y2
为了计算方便,我们通常将高斯核离散化并归一化,使得核内所有元素的和为 1。例如,一个 3x3 的高斯核可能如下所示:
116[121242121]\frac{1}{16}\begin{bmatrix} 1 & 2 & 1 \\ 2 & 4 & 2 \\ 1 & 2 & 1 \end{bmatrix}161121242121
3.2 高斯模糊的实现原理
高斯模糊的实现方式是用高斯核对图像进行卷积操作。具体步骤如下:
-
生成高斯核:根据指定的核大小和标准差,计算高斯核的各个元素值,并进行归一化处理。
-
边界处理:由于卷积核在图像边缘会超出图像范围,需要对图像边缘进行处理,常见的方法有零填充、镜像填充等。
-
卷积运算:将高斯核在图像上滑动,对每个位置进行加权求和,得到模糊后的像素值。
高斯模糊的核心在于将高斯卷积核与图像进行卷积。这个过程可以用数学表达式表示为:
Iblur(x,y)=∑i=−kk∑j=−kkI(x+i,y+j)⋅G(i,j)I_{\text{blur}}(x, y) = \sum_{i=-k}^{k}\sum_{j=-k}^{k} I(x+i, y+j) \cdot G(i, j)Iblur(x,y)=∑i=−kk∑j=−kkI(x+i,y+j)⋅G(i,j)
其中,I(x,y)I(x, y)I(x,y)是原始图像,G(i,j)G(i, j)G(i,j)是高斯核,kkk是核大小的一半。
3.3 高斯模糊的可分离性
高斯核具有一个重要性质:可分离性。这意味着二维高斯核可以分解为两个一维高斯核的乘积:
G(x,y)=G(x)⋅G(y)G(x, y) = G(x) \cdot G(y)G(x,y)=G(x)⋅G(y)
其中,G(x)G(x)G(x)和G(y)G(y)G(y)分别是沿 x 轴和 y 轴的一维高斯函数。
这个性质非常有用,因为它允许我们将二维卷积分解为两个一维卷积,从而大幅减少计算量。具体来说,我们可以先对图像进行水平方向的一维卷积,然后对结果进行垂直方向的一维卷积,得到最终的模糊效果。
数学上,这可以表示为:
Iblur(x,y)=((I(x,y)∗Gx)∗Gy)I_{\text{blur}}(x, y) = \left(\left(I(x, y) * G_x\right) * G_y\right)Iblur(x,y)=((I(x,y)∗Gx)∗Gy)
其中,GxG_xGx是水平方向的一维高斯核,GyG_yGy是垂直方向的一维高斯核。
这种方法的计算复杂度从O(N2M2)O(N^2M^2)O(N2M2)(直接二维卷积)降低到O(N2M)O(N^2M)O(N2M)(两次一维卷积),其中NNN是图像尺寸,MMM是核大小。这在实际应用中带来了显著的效率提升。
3.4 高斯模糊的代码实现示例
下面是一个用 Python 实现高斯模糊的示例代码,包括生成高斯核和对图像进行卷积的过程:
import numpy as np
from scipy.ndimage import convolve
def gaussian_kernel(size, sigma=1):kernel = np.fromfunction(lambda x, y: (1/(2*np.pi*sigma**2)) * np.exp(-(x**2 + y**2)/(2*sigma**2)), (size, size))return kernel / kernel.sum()
def gaussian_blur(image, kernel_size, sigma):kernel = gaussian_kernel(kernel_size, sigma)blurred_image = convolve(image, kernel, mode='constant', cval=0.0)
blurred = gaussian_blur(image, 3, 1.0)
print(blurred)
这个示例首先定义了一个生成高斯核的函数gaussian_kernel
,然后定义了gaussian_blur
函数,使用生成的高斯核对输入图像进行卷积操作。convolve
函数来自 SciPy 库,用于高效地执行卷积运算。
由于高斯核的可分离性,我们还可以实现更高效的版本,将二维卷积分解为两个一维卷积:
def separable_gaussian_blur(image, kernel_size, sigma):kernel_x = np.fromfunction(lambda x: (1/(np.sqrt(2*np.pi)*sigma)) * np.exp(-x**2/(2*sigma**2)), (kernel_size,))kernel_x = kernel_x / kernel_x.sum()kernel_y = kernel_x.copy()# 水平方向卷积blurred_x = convolve(image, kernel_x[:, np.newaxis], mode='constant', cval=0.0)# 垂直方向卷积blurred_xy = convolve(blurred_x, kernel_y[np.newaxis, :], mode='constant', cval=0.0)return blurred_xy
这个版本将二维高斯核分解为两个一维核,分别进行水平和垂直方向的卷积,大大提高了计算效率,特别是对于大尺寸的核和图像。
四、卷积神经网络与卷积层
4.1 卷积神经网络(CNN)概述
卷积神经网络(Convolutional Neural Network, CNN)是一类专门为处理具有网格结构数据(如图像、音频)而设计的深度学习模型。CNN 的核心思想是利用卷积操作来提取输入数据中的局部特征,同时通过参数共享减少模型的参数量。
CNN 的基本组成部分包括:
-
卷积层:执行卷积操作,提取输入数据中的特征
-
激活层:对卷积结果应用非线性激活函数
-
池化层:对特征图进行下采样,减少数据量
-
全连接层:将提取的特征映射到输出空间
CNN 的优势在于:
-
局部连接:每个神经元只与输入数据的一个局部区域相连,减少参数量
-
参数共享:同一卷积核的参数在整个输入数据上共享,进一步减少参数量
-
平移不变性:对输入数据的平移具有不变性,提高模型的泛化能力
CNN 在图像分类、目标检测、语义分割等计算机视觉任务中取得了巨大成功,是深度学习领域的重要模型之一。
4.2 卷积层的数学定义
在 CNN 中,卷积层的操作与数学中的卷积略有不同,更准确地说,是互相关(cross-correlation)操作,但通常仍称为卷积。对于输入张量X∈RH×W×CinX \in \mathbb{R}^{H \times W \times C_{in}}X∈RH×W×Cin和卷积核K∈Rk×k×Cin×CoutK \in \mathbb{R}^{k \times k \times C_{in} \times C_{out}}K∈Rk×k×Cin×Cout,卷积层的输出Y∈RH′×W′×CoutY \in \mathbb{R}^{H' \times W' \times C_{out}}Y∈RH′×W′×Cout定义为:
Y[i,j,l]=∑c=0Cin−1∑m=0k−1∑n=0k−1X[i+m,j+n,c]⋅K[m,n,c,l]+b[l]Y[i, j, l] = \sum_{c=0}^{C_{in}-1} \sum_{m=0}^{k-1} \sum_{n=0}^{k-1} X[i+m, j+n, c] \cdot K[m, n, c, l] + b[l]Y[i,j,l]=∑c=0Cin−1∑m=0k−1∑n=0k−1X[i+m,j+n,c]⋅K[m,n,c,l]+b[l]
其中,HHH和WWW是输入的高度和宽度,CinC_{in}Cin是输入通道数,kkk是卷积核的大小,CoutC_{out}Cout是输出通道数,bbb是偏置项。
卷积层的关键参数包括:
-
卷积核大小(k):通常为奇数,如 3x3、5x5
-
步幅(stride):卷积核在滑动时的步长,默认为 1
-
填充(padding):在输入周围添加的零值或其他值,通常有 “valid”(不填充)和 “same”(填充后输出尺寸与输入相同)两种方式
输出尺寸的计算公式为:
H′=H+2P−kS+1H' = \frac{H + 2P - k}{S} + 1H′=SH+2P−k+1
W′=W+2P−kS+1W' = \frac{W + 2P - k}{S} + 1W′=SW+2P−k+1
其中,P 是填充大小,S 是步幅。
4.3 卷积层的参数共享与局部连接
卷积层的两个核心特性是参数共享和局部连接,这使得 CNN 能够高效地处理图像数据。
参数共享是指同一卷积核的参数在整个输入数据上共享。例如,如果有一个 3x3 的卷积核,它在输入图像的每个位置都使用相同的权重。这一特性大大减少了模型的参数量。假设输入图像大小为 224x224x3,使用一个 3x3x3 的卷积核,那么总共有 3x3x3=27 个参数,而不是 224x224x3=150,528 个参数(如全连接层)。
局部连接是指每个神经元只与输入数据的一个局部区域(如 3x3 区域)相连,而不是与整个输入相连。这反映了视觉信息的局部相关性,即相邻像素通常比远处像素更相关。局部连接使得模型能够捕捉局部特征,如边缘、角点等。
这两个特性的结合使得卷积层能够在减少参数量的同时,有效地提取图像的局部特征,提高模型的泛化能力和计算效率。
4.4 卷积层的代码实现示例
下面是一个使用 PyTorch 实现卷积层的示例代码:
import torch
import torch.nn as nn# 定义一个卷积层
conv_layer = nn.Conv2d(in_channels=3, # 输入通道数(如RGB图像为3)out_channels=64, # 输出通道数(卷积核数量)kernel_size=3, # 卷积核大小stride=1, # 步幅padding=1, # 填充大小bias=True # 是否使用偏置
)# 输入张量:(batch_size, channels, height, width)
x = torch.randn(1, 3, 224, 224)# 执行卷积
output = conv_layer(x)# 输出张量形状:(1, 64, 224, 224)
print(output.shape)
在 PyTorch 中,nn.Conv2d
类实现了二维卷积层。输入张量的形状为 (batch_size, in_channels, height, width),输出张量的形状为 (batch_size, out_channels, height’, width’),其中 height’ 和 width’ 由输入尺寸、卷积核大小、步幅和填充决定。
卷积层的参数可以通过conv_layer.weight
和conv_layer.bias
访问。conv_layer.weight
的形状为 (out_channels, in_channels, kernel_size, kernel_size),每个卷积核对应一个输出通道。
4.5 卷积层的反向传播
在训练过程中,卷积层需要计算梯度以更新参数。卷积层的反向传播涉及两个主要部分:计算损失对卷积核的梯度(权重梯度)和损失对输入的梯度(误差梯度)。
权重梯度计算:对于每个卷积核KKK,其梯度∂L∂K\frac{\partial L}{\partial K}∂K∂L通过将误差图与输入图进行卷积得到:
∂L∂K[m,n,c,l]=∑i=0H′−1∑j=0W′−1∂L∂Y[i,j,l]⋅X[i+m,j+n,c]\frac{\partial L}{\partial K[m, n, c, l]} = \sum_{i=0}^{H'-1} \sum_{j=0}^{W'-1} \frac{\partial L}{\partial Y[i, j, l]} \cdot X[i+m, j+n, c]∂K[m,n,c,l]∂L=∑i=0H′−1∑j=0W′−1∂Y[i,j,l]∂L⋅X[i+m,j+n,c]
误差梯度计算:损失对输入的梯度∂L∂X\frac{\partial L}{\partial X}∂X∂L通过将误差图与卷积核进行翻转后的卷积得到:
∂L∂X[i,j,c]=∑l=0Cout−1∑m=0k−1∑n=0k−1∂L∂Y[i−m,j−n,l]⋅K[m,n,c,l]\frac{\partial L}{\partial X[i, j, c]} = \sum_{l=0}^{C_{out}-1} \sum_{m=0}^{k-1} \sum_{n=0}^{k-1} \frac{\partial L}{\partial Y[i-m, j-n, l]} \cdot K[m, n, c, l]∂X[i,j,c]∂L=∑l=0Cout−1∑m=0k−1∑n=0k−1∂Y[i−m,j−n,l]∂L⋅K[m,n,c,l]
卷积层的反向传播计算量较大,但可以通过高效的矩阵运算(如使用 FFT 或专用硬件加速)来优化。现代深度学习框架(如 PyTorch、TensorFlow)会自动处理这些计算,用户只需定义模型结构和损失函数,框架会自动计算梯度并更新参数。
五、ResNet 详解
5.1 ResNet 的提出背景
随着深度学习的发展,研究人员发现网络深度是影响模型性能的关键因素。然而,随着网络深度的增加,出现了两个主要问题:
-
梯度消失 / 爆炸问题:随着层数的增加,梯度在反向传播过程中可能变得非常小或非常大,导致训练困难。
-
退化问题:当网络深度增加到一定程度后,模型的性能可能饱和甚至下降,这不是由于过拟合,而是由于优化困难导致的。
为了解决这些问题,微软研究院的 Kaiming He 等人在 2015 年提出了残差网络(Residual Network, ResNet),并在 ImageNet 图像分类任务中取得了显著的成功。ResNet 的核心创新是引入了残差块(residual block)和跳跃连接(skip connection),使得极深的网络能够被有效训练。
5.2 残差块的数学定义
ResNet 的基本构建块是残差块,它通过引入跳跃连接来绕过一个或多个层。残差块的数学定义如下:
y=F(x,{Wi})+xy = F(x, \{W_i\}) + xy=F(x,{Wi})+x
其中,xxx是输入,F(x,{Wi})F(x, \{W_i\})F(x,{Wi})是残差函数(通常由几个卷积层组成),yyy是输出。
残差块的核心思想是让网络学习残差映射F(x)=H(x)−xF(x) = H(x) - xF(x)=H(x)−x,而不是直接学习原始映射H(x)H(x)H(x)。这样,如果恒等映射是最优解,网络可以很容易地通过将残差设为零来接近恒等映射,这在实践中被证明更容易优化。
残差块有两种主要类型:基本块(Basic Block)和瓶颈块(Bottleneck Block)。基本块用于 ResNet-18 和 ResNet-34,而瓶颈块用于更深的网络如 ResNet-50、ResNet-101 和 ResNet-152。
5.3 基本块(Basic Block)的结构与代码
基本块由两个 3x3 的卷积层组成,每个卷积层后接批量归一化(Batch Normalization)和 ReLU 激活函数。跳跃连接直接将输入加到输出上,不需要额外的参数。
基本块的结构如下:
输入 -> [3x3卷积 + BN + ReLU] -> [3x3卷积 + BN] -> + -> ReLU -> 输出|_____________________________________________________|
在 PyTorch 中,基本块的实现如下:
class BasicBlock(nn.Module):expansion = 1 # 输出通道数与输入通道数的比例def __init__(self, inplanes, planes, stride=1, downsample=None):super(BasicBlock, self).__init__()self.conv1 = nn.Conv2d(inplanes, planes, kernel_size=3, stride=stride, padding=1, bias=False)self.bn1 = nn.BatchNorm2d(planes)self.relu = nn.ReLU(inplace=True)self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, stride=1, padding=1, bias=False)self.bn2 = nn.BatchNorm2d(planes)self.downsample = downsample # 用于调整维度或步幅的下采样层self.stride = stridedef forward(self, x):identity = x # 保存输入作为跳跃连接out = self.conv1(x)out = self.bn1(out)out = self.relu(out)out = self.conv2(out)out = self.bn2(out)# 如果存在下采样,调整输入维度以匹配输出维度if self.downsample is not None:identity = self.downsample(x)out += identity # 跳跃连接out = self.relu(out)return out
在forward
方法中,输入x
首先被保存为identity
,然后经过两次卷积和批量归一化,接着与调整后的identity
相加,最后通过 ReLU 激活函数。如果输入和输出的维度或步幅不匹配,downsample
层会对identity
进行调整,通常由 1x1 卷积和批量归一化组成。
5.4 瓶颈块(Bottleneck Block)的结构与代码
瓶颈块通过使用 1x1 卷积来减少计算量,适用于更深的网络。它由三个卷积层组成:1x1(降维)、3x3(特征提取)和 1x1(升维)。跳跃连接同样将输入直接加到输出上。
瓶颈块的结构如下:
输入 -> [1x1卷积 + BN + ReLU] -> [3x3卷积 + BN + ReLU] -> [1x1卷积 + BN] -> + -> ReLU -> 输出|________________________________________________________________________|
在 PyTorch 中,瓶颈块的实现如下:
class Bottleneck(nn.Module):expansion = 4 # 输出通道数是输入通道数的4倍def __init__(self, inplanes, planes, stride=1, downsample=None):super(Bottleneck, self).__init__()self.conv1 = nn.Conv2d(inplanes, planes, kernel_size=1, bias=False)self.bn1 = nn.BatchNorm2d(planes)self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, stride=stride, padding=1, bias=False)self.bn2 = nn.BatchNorm2d(planes)self.conv3 = nn.Conv2d(planes, planes * self.expansion, kernel_size=1, bias=False)self.bn3 = nn.BatchNorm2d(planes * self.expansion)self.relu = nn.ReLU(inplace=True)self.downsample = downsampleself.stride = stridedef forward(self, x):identity = xout = self.conv1(x)out = self.bn1(out)out = self.relu(out)out = self.conv2(out)out = self.bn2(out)out = self.relu(out)out = self.conv3(out)out = self.bn3(out)if self.downsample is not None:identity = self.downsample(x)out += identityout = self.relu(out)return out
瓶颈块的第一个 1x1 卷积将输入通道数减少到planes
,第二个 3x3 卷积进行特征提取,第三个 1x1 卷积将通道数恢复到planes * expansion
(通常为 4 倍)。这样的设计减少了计算量,同时保持了足够的表达能力。
5.5 ResNet 的整体结构
ResNet 的整体结构由多个残差块堆叠而成,通常包括以下几个部分:
-
输入层:7x7 卷积层,步幅为 2,用于下采样
-
最大池化层:3x3 最大池化,步幅为 2,进一步下采样
-
残差层:由多个残差块组成的四个阶段,每个阶段的通道数逐渐增加
-
全局平均池化层:将特征图转换为向量
-
全连接层:输出层,用于分类或其他任务
ResNet 的具体结构根据深度不同而有所变化。常见的 ResNet 变体包括:
-
ResNet-18:由基本块组成,总层数 18
-
ResNet-34:由基本块组成,总层数 34
-
ResNet-50:由瓶颈块组成,总层数 50
-
ResNet-101:由瓶颈块组成,总层数 101
-
ResNet-152:由瓶颈块组成,总层数 152
5.6 ResNet 的代码实现
下面是 ResNet 的完整 PyTorch 实现:
class ResNet(nn.Module):def __init__(self, block, layers, num_classes=1000):super(ResNet, self).__init__()self.inplanes = 64self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3, bias=False)self.bn1 = nn.BatchNorm2d(64)self.relu = nn.ReLU(inplace=True)self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)self.layer1 = self._make_layer(block, 64, layers[0])self.layer2 = self._make_layer(block, 128, layers[1], stride=2)self.layer3 = self._make_layer(block, 256, layers[2], stride=2)self.layer4 = self._make_layer(block, 512, layers[3], stride=2)self.avgpool = nn.AdaptiveAvgPool2d((1, 1))self.fc = nn.Linear(512 * block.expansion, num_classes)# 参数初始化for m in self.modules():if isinstance(m, nn.Conv2d):nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')elif isinstance(m, nn.BatchNorm2d):nn.init.constant_(m.weight, 1)nn.init.constant_(m.bias, 0)# 特殊初始化:将残差块最后一个BN的权重初始化为0if isinstance(block, Bottleneck):for m in self.modules():if isinstance(m, Bottleneck):nn.init.constant_(m.bn3.weight, 0)elif isinstance(block, BasicBlock):for m in self.modules():if isinstance(m, BasicBlock):nn.init.constant_(m.bn2.weight, 0)def _make_layer(self, block, planes, blocks, stride=1):downsample = None# 如果输入和输出维度不匹配,创建下采样层if stride != 1 or self.inplanes != planes * block.expansion:downsample = nn.Sequential(nn.Conv2d(self.inplanes, planes * block.expansion,kernel_size=1, stride=stride, bias=False),nn.BatchNorm2d(planes * block.expansion),)layers = []layers.append(block(self.inplanes, planes, stride, downsample))self.inplanes = planes * block.expansionfor _ in range(1, blocks):layers.append(block(self.inplanes, planes))return nn.Sequential(*layers)def forward(self, x):x = self.conv1(x)x = self.bn1(x)x = self.relu(x)x = self.maxpool(x)x = self.layer1(x)x = self.layer2(x)x = self.layer3(x)x = self.layer4(x)x = self.avgpool(x)x = x.view(x.size(0), -1)x = self.fc(x)return x
ResNet
类的构造函数接受block
(基本块或瓶颈块)和layers
(每个阶段的块数)作为参数。_make_layer
方法用于创建每个阶段的残差块序列。forward
方法定义了数据的前向传播路径。
ResNet 的初始化策略包括:
-
使用 Kaiming 正态分布初始化卷积层权重
-
将批量归一化层的权重初始化为 1,偏置初始化为 0
-
特殊初始化:将残差块最后一个批量归一化层的权重初始化为 0,这有助于训练初期让网络更容易学习恒等映射
5.7 跳跃连接与梯度反向传播
ResNet 的关键创新是跳跃连接,它允许梯度在反向传播过程中直接跳过某些层,这对于训练极深的网络至关重要。
在反向传播过程中,梯度可以通过两条路径传播:一条是通过常规的卷积层路径,另一条是通过跳跃连接路径。数学上,可以证明残差块的梯度传播满足:
∂L∂x=∂L∂y⋅(1+∂F∂x)\frac{\partial \mathcal{L}}{\partial x} = \frac{\partial \mathcal{L}}{\partial y} \cdot (1 + \frac{\partial F}{\partial x})∂x∂L=∂y∂L⋅(1+∂x∂F)
其中,L\mathcal{L}L是损失函数,xxx是输入,yyy是输出,FFF是残差函数。
这个公式表明,梯度不仅通过残差路径∂L∂y⋅∂F∂x\frac{\partial \mathcal{L}}{\partial y} \cdot \frac{\partial F}{\partial x}∂y∂L⋅∂x∂F传播,还通过直接路径∂L∂y\frac{\partial \mathcal{L}}{\partial y}∂y∂L传播。这使得梯度能够更容易地反向传播到前面的层,避免了梯度消失问题。
在极端情况下,如果残差函数FFF的梯度为零,梯度仍然可以通过直接路径∂L∂y\frac{\partial \mathcal{L}}{\partial y}∂y∂L传播,这保证了梯度不会完全消失。这种特性使得 ResNet 能够训练比传统网络深得多的架构。
5.8 ResNet 的训练与性能
ResNet 的训练通常使用随机梯度下降(SGD)及其变体,学习率策略通常包括:
-
初始学习率设为 0.1,在训练过程中按一定比例衰减
-
使用动量(momentum)加速收敛
-
使用权重衰减(weight decay)进行正则化
ResNet 在 ImageNet 图像分类任务上取得了显著的性能提升。例如,ResNet-152 在 ImageNet 测试集上达到了 3.57% 的错误率,显著优于之前的模型。此外,ResNet 的深度可以扩展到 1000 层以上,而性能不会下降,这证明了残差块和跳跃连接的有效性。
ResNet 的成功不仅限于图像分类,还被广泛应用于其他计算机视觉任务,如目标检测、语义分割、姿态估计等,成为现代深度学习中最重要的架构之一。
参考文章
[1] https://datawhalechina.github.io/thorough-pytorch/%E7%AC%AC%E5%9B%9B%E7%AB%A0/4.1%20ResNet.html
[2] https://www.bilibili.com/video/BV1Vd4y1e7pj/?spm_id_from=333.337.search-card.all.click&vd_source=e7424398ef5ae0830b0a55abc35b2197