卷积神经网络(Convolutional Neural Network,CNN)是受生物学上感受野机制的启发而提出的。目前的卷积神经网络一般是由卷积层、汇聚层和全连接层交叉堆叠而成的前馈神经网络,有三个结构上的特性:局部连接、权重共享以及汇聚。这些特性使得卷积神经网络具有一定程度上的平移、缩放和旋转不变性。
深度学习通常都会从学习卷积神经网络(Convolutional Neural Network, 简称CNN)开始。很大程度上,是由于CNN的基本组成部分与前馈神经网络有很紧密的关联,甚至可以说,CNN就是一种特殊的前馈神经网络。
这两者的主要区别在于,CNN在前馈神经网络的基础上加入了卷积层和池化层(后面会讲到),以便更好地处理图像等具有空间结构的数据。
现在画图说明一下。对于前馈神经网络,我们可以将简化后的网络结构如下图表示:

可以变换为:

当然,【全连接层-ReLU】可以有多个,此时网络结构可以表示为:

简单地说,CNN就是在此基础上,将全连接层换成卷积层,并在ReLU层之后加入池化层(非必须),那么一个基本的CNN结构就可以表示成这样:

好了,现在现在问题已经简化为理解卷积层和池化层了。
考虑到使用全连接前馈网络来处理图像时,会出现如下问题:
卷积神经网络有三个结构上的特性:局部连接、权重共享和汇聚。这些特性使得卷积神经网络具有一定程度上的平移、缩放和旋转不变性。和前馈神经网络相比,卷积神经网络的参数也更少。因此,通常会使用卷积神经网络来处理图像信息。
卷积是分析数学中的一种重要运算,常用于信号处理或图像处理任务。本节以二维卷积为例来进行实践。
在机器学习和图像处理领域,卷积的主要功能是在一个图像(或特征图)上滑动一个卷积核,通过卷积操作得到一组新的特征。在计算卷积的过程中,需要进行卷积核的翻转,而这也会带来一些不必要的操作和开销。因此,在具体实现上,一般会以数学中的互相关(Cross-Correlatio)运算来代替卷积。 在神经网络中,卷积运算的主要作用是抽取特征,卷积核是否进行翻转并不会影响其特征抽取的能力。特别是当卷积核是可学习的参数时,卷积和互相关在能力上是等价的。因此,很多时候,为方便起见,会直接用互相关来代替卷积。

上面这个数学公式似乎很难理解。下面给出了卷积计算的示例。

实现一个简单的二维卷积算子,代码实现如下:
import torch
import torch.nn as nn
# 定义输入矩阵和卷积核
input_matrix = torch.tensor([
[1., 2., 3.],
[4., 5., 6.],
[7., 8., 9.]
], dtype=torch.float32) # shape: (3, 3)
kernel = torch.tensor([
[0., 1.],
[2., 3.]
], dtype=torch.float32) # shape: (2, 2)
# 将输入扩展为四维张量(batch_size=1, channels=1, height=3, width=3)
input_tensor = input_matrix.view(1, 1, 3, 3) # shape: [1, 1, 3, 3]
# 创建卷积层(in_channels=1, out_channels=1, kernel_size=2x2)
conv_layer = nn.Conv2d(
in_channels=1,
out_channels=1,
kernel_size=2,
bias=False # 禁用偏置项
)
# 手动设置卷积核权重(权重形状:[out_channels, in_channels, height, width])
conv_layer.weight.data = kernel.view(1, 1, 2, 2)
# 执行卷积操作
output = conv_layer(input_tensor)
# 输出结果(四维张量 -> 二维矩阵)
result = output.squeeze().detach() # shape: [2, 2]
print("卷积结果:\n", result)
运行结果:
卷积结果:
tensor([[25., 31.],
[43., 49.]])
权值共享是卷积神经网络(CNN)中一个非常核心的概念,理解它可以帮助你更好地掌握CNN的精髓。权值共享指的是同一个卷积核(或滤波器)在输入数据不同位置上重复使用,而不仅仅是在一个位置上使用。这个卷积核包含了一组权重,用于从输入数据中提取特征。通过权值共享,网络能够在不同的输入位置之间共享计算和参数,从而大大减少模型参数的数量,提高网络的泛化能力。
权值共享有两个关键好处。首先,它显著减少了模型的参数数量,降低了过拟合的风险。其次,由于所有卷积核都使用相同的权重和偏置,CNN实现了平移不变性。这意味着,即使物体在图像中移动了位置,只要其特征保持不变,网络仍能准确识别它。
权值共享是卷积神经网络的一项重要特性。它使得模型更加简洁,提高了模型的泛化能力。理解权值共享不仅有助于深入理解CNN本身,还有助于更好地利用这个强大的机器学习工具。
举个简单的例子,把图像想象成一个有很多小格子(像素)的棋盘,卷积核就像是一个印章。印章在棋盘上移动,每次印章覆盖到的位置,就对这些位置的数值进行一些计算(通常是乘法和加法),得到一个新的数值,这些新数值组成一个新的矩阵,这个过程就是卷积操作。印章的形状(权值)在整个棋盘(图像)上盖章的时候是不会改变的。无论印章滑动到棋盘的哪个位置,印章的形状都是一样的。这样做的好处是大大减少了需要学习的参数数量。
卷积的 “步长” 是指卷积核(或称为滤波器)在输入图像(或特征图)上移动时每次移动的步数,并且不能超过卷积核的大小,也就是在图像的空间维度上滑动的单位距离。
举例说明:
卷积Padding的重要作用应该是保持输入和输出图像尺寸保持一致性。
举例说明: - 假设我们有一张 5×5 的图像(简化为黑白图像,忽略颜色通道)。当我们使用一个 3×3 的卷积核进行卷积操作,并且步长为 1 时,如果没有 padding,输出的特征图尺寸就会变成 3×3。这是因为卷积核只能完全覆盖在原始图像的范围内进行计算。 - 加齐边缘的作用:如果在原始图像的四周添加一层零填充(padding 的大小设为 1),那么整个图像的尺寸就变成了 7×7(5+2×1)。此时,3×3 的卷积核仍然使用步长 1 进行计算,输出的特征图尺寸就会回到 5×5。这样就能确保输出的特征图在尺寸上与输入图像保持一致,从而便于后续的处理和分析。
卷积 Padding 的两种常用方式:

Valid计算方式:
方式一:(输入特征图大小 - 卷积核尺寸 + 2 * P) / 步长 + 1,P是填充0的数量, 向下取整。 方式二:(输入特征图大小 - 卷积核尺寸 + 1) / 步长,向上取整
以上两个公式是等价的。
Same计算方式:
1.输出特征图的大小的计算公式为:
(输入特征图大小 - 卷积核尺寸 + 2 * P) / 步长 + 1向下取整,P是填充0的数量,一般可以使用:(输入特征图大小 / 步长)向上取整来计算。
例如:5x5的输入,3x3的卷积核,步长为3,那么输出特征图的大小是2x2(注意取整)。 为了保持输出特征图的大小,需要在输入特征图的周围补0。
2.计算补0的数量:
需要填充的高度 = (新的高度 – 1) × 步长 + 卷积核大小 - 输入大小,补零先补右和下,然后补左和上。 例如:5x5的输入,3x3的卷积核,步长为3,(2-1)x3+3-5=1,这里只补右和下。
实例1:
import torch
import torch.nn as nn
# 创建一个大小为 28*28 的单通道图像
input_data = torch.randn(1, 1, 5, 5) # 一个大小为28x28的单通道图像
# 创建卷积层,输入通道数为 1
# 输出通道数16
# 步长默认是1
# 卷积核大小3*3
# 0个0填充
conv_layer = nn.Conv2d(in_channels=1, out_channels=16, stride=1, kernel_size=3, padding=0)
# 对输入数据进行卷积操作
output_data = conv_layer(input_data)
# 输出结果
print(output_data.shape)
输出结果:
torch.Size([1, 16, 3, 3])
这个例子里大家可以自行调整out_channels,stride,kernel_size和padding的四个参数来观察输出特征图的变化情况。
实例2: 下面是卷积的Padding实例图的实现代码。
import torch
import torch.nn as nn
import numpy as np
# 创建一个大小为 28*28 的单通道图像
# input_data = torch.randn(1, 1, 5, 5) # 一个大小为28x28的单通道图像
# 创建一个 NumPy 数组
matrix_np = np.array([[[[0.0, 2.0, 4.0, 1.0, 0.0],
[3.0, 1.0, 1.0, 0.0, 1.0],
[2.0, 4.0, 1.0, 0.0, 1.0],
[2.0, 0.0, 5.0, 2.0, 2.0],
[0.0, 1.0, 3.0, 2.0, 1.0]]]])
kernel = torch.tensor([
[1., 0., -1.],
[1., 0., -1.],
[1., 0., -1.]
], dtype=torch.float32)
matrix_np = np.array(matrix_np).astype(np.float32)
# 转换为 PyTorch 张量
input_data = torch.from_numpy(matrix_np)
print(input_data)
# 创建卷积层,输入通道数为 1
# 输出通道数16
# 步长默认是1
# 卷积核大小3*3
# 0个0填充
conv_layer = nn.Conv2d(in_channels=1, out_channels=1, stride=1, kernel_size=3, padding=0)
conv_layer.weight.data = kernel.view(1, 1, 3, 3)
# 对输入数据进行卷积操作
output_data = conv_layer(input_data)
# 输出结果
print(output_data.shape)
print(torch.round(output_data))
运行结果:
tensor([[[[0., 2., 4., 1., 0.],
[3., 1., 1., 0., 1.],
[2., 4., 1., 0., 1.],
[2., 0., 5., 2., 2.],
[0., 1., 3., 2., 1.]]]])
torch.Size([1, 1, 3, 3])
tensor([[[[-1., 6., 4.],
[-0., 3., 3.],
[-5., 1., 5.]]]], grad_fn=<RoundBackward0>)
如果调整padding=1,则输出结果如下:
tensor([[[[0., 2., 4., 1., 0.],
[3., 1., 1., 0., 1.],
[2., 4., 1., 0., 1.],
[2., 0., 5., 2., 2.],
[0., 1., 3., 2., 1.]]]])
torch.Size([1, 1, 5, 5])
tensor([[[[-3., -2., 2., 4., 1.],
[-7., -1., 6., 4., 1.],
[-5., -0., 3., 3., 2.],
[-5., -5., 1., 5., 4.],
[-1., -6., -3., 5., 4.]]]], grad_fn=<RoundBackward0>)
运行效果与示例图显示效果完全吻合。
在深度学习中,卷积核的大小一般选择奇数是为了方便处理和避免引入不必要的对称性。当卷积核的大小是奇数时,它具有唯一的一个中心像素,这个中心像素点可以作为滑动的默认参考点,即锚点。这使得在进行卷积操作时,卷积核可以在输入图像的每个像素周围均匀地取样。这样的好处是,在进行卷积操作时,可以保持对称地处理图像的每个位置,从而避免引入额外的偏差和不对称性。相反,如果卷积核的大小是偶数,那么在某些位置上,中心像素会落在两个相邻的像素之间,这可能导致对称性问题。
此外,选择奇数大小的卷积核还有一个重要的优点是,在进行空间卷积时,可以确保卷积核有一个明确的中心像素,这有助于处理图像的边缘和边界像素,避免模糊和信息损失。当然,并不是所有情况下都必须选择奇数大小的卷积核。在某些特定情况下,偶数大小的卷积核也可以使用,并且在某些特定任务中可能表现得更好。但是在大多数情况下,奇数大小的卷积核是一种常见且推荐的选择,因为它可以简化卷积操作,并有助于保持图像处理的对称性和一致性。