AI

深度学习之卷积神经网络

动手深度学习

Posted by LXG on March 23, 2026

深度学习之卷积神经网络

概念

卷积神经网络(convolutional neural network,CNN)是一类强大的、为处理图像数据而设计的神经网络。 基于卷积神经网络架构的模型在计算机视觉领域中已经占主导地位,当今几乎所有的图像识别、目标检测或语义分割相关的学术竞赛和商业应用都以这种方法为基础。

平移不变性

不管检测对象出现在图像中的哪个位置,神经网络的前面几层应该对相同的图像区域具有相似的反应,即为“平移不变性”

局部性

神经网络的前面几层应该只探索输入图像中的局部区域,而不过度在意图像中相隔较远区域的关系,这就是“局部性”原则。最终,可以聚合这些局部特征,以在整个图像级别进行预测

多层感知机的限制

1920×1080 的彩色图像来具体说明多层感知机(MLP)处理图像的局限

参数太多

cnn_01

忽略空间结构

多层感知机 MLP 对每个像素都是平等对待,没有考虑“邻近像素可能属于同一个物体”这一空间关系

无法提取局部特征

多层感知机 MLP 直接看整张 1920×1080 的图 → 每个神经元同时处理全图信息,无法专注局部细节

卷积

案例理解


图像:
[10 20 30]
[40 50 60]
[70 80 90]

卷积核:
[1 0]
[0 -1]

  • 把卷积核放在图像左上角的 2×2 区域
  • 做加权求和:

(10*1 + 20*0 + 40*0 + 50*(-1)) = 10 - 50 = -40

  • 然后滑动卷积核到下一步位置,重复同样操作 → 得到输出特征图(feature map)

cnn_02

多角度理解

  • 算子逻辑:它就是一个“滑动加权求和”器
  • 物理直觉:它是一把“特征形状的模具”
  • 数学本质:局部特征的聚合

为什么要叫“卷积”?

“卷”和“积”这两个字其实非常形象:

  • :乘积、累加(就是对应位置相乘加起来)。
  • :在信号处理中,这涉及函数的翻转。但在深度学习中,它更多指代这种“滑动遍历”的过程,即将一个局部窗口在全局空间上不断地翻卷、移动。

通道

理解“通道”(Channel)是深度学习从二维图像迈向高维特征提取的关键

输入层的通道:数据的“颜色分量”

一张 1080p 的图片在 Android 内存中通常是 [1080, 1920, 3] 的张量

cnn_dog_channels

运算逻辑:它是如何“同时”扫描的?

当这个 $3 \times 3 \times 3$ 的绿色小方块(卷积核)扣在图像的某个位置时,它会执行以下动作:

  1. 分层对齐:核的 R 层对准图像的 R 层,G 对 G,B 对 B。
  2. 局部乘法:每个通道内的 9 个像素点分别与核内对应的 9 个权重相乘。
  3. 跨通道累加:这是最核心的一步! 系统会将 R、G、B 三个通道算出来的结果全部加在一起,再加上一个偏置项(Bias)。
  4. 输出单值:最终只吐出一个数值。

结论:卷积核在滑动时,是一次性“吃”掉了纵向深度的所有通道信息,然后把它们“压缩”成了一个特征点。

图像卷积

互相关运算


import torch
from torch import nn
from d2l import torch as d2l

# 卷积核
def corr2d(X, K):
    # X: 输入图像,K: 卷积核
    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 = 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]])
print(corr2d(X, K))

运行结果


tensor([[19., 25.],
        [37., 43.]])

卷积层


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 # 卷积操作加上偏置

卷积层(Convolutional Layer)的核心任务是扫描输入数据,寻找特定的模式(Pattern)

图像中目标的边缘检测


X = torch.ones((6, 8)) # 输入图像
X[:, 2:6] = 0 # 中间部分为0,形成一个白色矩形
K = torch.tensor([[1.0, -1.0]]) # 卷积核,检测水平边缘
Y = corr2d(X, K) # 卷积操作
print("原始图像X:")
print(X)
print("卷积结果Y:") 
print(Y)

运行结果

原始图像在视觉上就像一张黑纸中间贴了一块白色矩形。


原始图像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.]])
卷积结果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.]])

  • $Y$ 的内容:大部分是 0,但在原本 $X$ 的颜色交界处,会出现一列 1 和一列 -1。
  • 结论:卷积层成功地把“像素”变成了“边缘特征”。 神经网络后续层不再关心像素是黑是白,只关心“哪里有边界”。

学习卷积核


X = torch.ones((6, 8)) # 输入图像
X[:, 2:6] = 0 # 中间部分为0,形成一个白色矩形
Y = corr2d(X, K) # 卷积操作
print("原始图像X:")
print(X)
print("卷积结果Y:") 
print(Y)

conv2d = nn.Conv2d(1, 1, kernel_size=(1,2), bias = False) # 卷积层,输入通道数为1,输出通道数为1,卷积核大小为1x2

X = X.reshape((1, 1, 6, 8)) # 将输入图像调整为4D张量,形状为(批量大小, 输入通道数, 高度, 宽度)
Y = Y.reshape((1, 1, 6, 7)) # 将卷积结果调整为4D张量,形状为(批量大小, 输出通道数, 高度, 宽度)

lr = 3e-2 # 学习率

for i in range(10):
    Y_hat = conv2d(X) # 卷积层的前向传播,得到预测结果Y_hat
    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}')

J = conv2d.weight.data.reshape((1, 2)) # 将卷积核权重调整为2D张量,形状为(1, 2)
print('学习到的卷积核权重:')
print(J)

运行结果


原始图像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.]])
卷积结果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.]])
epoch 2, loss 9.599
epoch 4, loss 1.777
epoch 6, loss 0.366
epoch 8, loss 0.089
epoch 10, loss 0.026
学习到的卷积核权重:
tensor([[ 0.9701, -0.9988]])

我们只需要给模型成千上万张 $X$(原图)和对应的 $Y$(标签),模型就能自动在几十个卷积层中,学出成千上万个极其复杂的卷积核,这些核能捕捉到人类甚至无法理解的深层特征。

如果“不知道 Y”怎么办?

让模型自己去观察 100 万张图,它可能会发现某些像素经常一起出现,从而自己归纳出“边缘”的概念。但这种学习效率在目前的工程应用中远不如监督学习

卷积核维度的标准公式

一个标准卷积核的维度通常是一个 4D 张量,表示为:

\[[Out\_Channels, In\_Channels, Kernel\_H, Kernel\_W]\]

在 PyTorch 中,当你调用 nn.Conv2d(in_channels, out_channels, kernel_size) 时,系统会自动帮你初始化这个 4D 权重矩阵

  • 输入通道数 ($In_Channels$): 卷积核的“深度”必须等于输入的“深度”, 如果你处理的是 1080p 的 RGB 图像(3 通道),那么你的每一个卷积核必须有 3 层
  • 输出通道数 ($Out_Channels$): 你想提取多少种特征,就初始化多少个“立体”卷积核, 你想同时检测“垂直边缘”、“水平边缘”和“颜色突变”,那么你就初始化 3 个卷积核
  • 卷积核尺寸 ($Kernel_Size$): 在移动端(边缘 AI),$3 \times 3$ 是最常用的,因为现代芯片(如高通骁龙的 Hexagon DSP)对 $3 \times 3$ 的卷积运算有专门的硬件指令集优化

从现实世界到张量 Y

第一步:原始采集 (Raw Data)

你拿着安卓手机拍了 1000 张小狗的照片。这些是原始的 Bitmap 或 JPEG,对应张量 $X$。

第二步:人工标注 (Manual Labeling)

人类使用标注工具(如 LabelImg, CVAT, 或者华为/阿里的标注平台)对图片进行处理:

  • 分类任务:标注员点击“狗”这个按钮。
  • 检测任务:标注员用鼠标在小狗位置画一个矩形框。
  • 分割任务:标注员用多边形把小狗的轮廓抠出来。

第三步:数字化转化 (Vectorization)

这一步由脚本完成,将人工的操作转化为数学张量 $Y$

做边缘 AI 时,会发现最难的不是写卷积层,而是获取高质量的 $Y$

特征映射和感受野

特征映射 (Feature Map) —— “处理后的图层”

卷积层的输出不再是原始像素,而是经过卷积核“过滤”后生成的二维数组。因为它映射了输入图像中某些特定特征(如边缘、颜色、纹理)出现的位置,所以叫“特征映射”。

输出的那个 $6 \times 7$ 的矩阵 $Y$ 就是特征映射。它上面的数值(1 或 -1)告诉下一层:“嘿,我在这个坐标点发现了垂直边缘!”

感受野 (Receptive Field)—— “视野范围”

在卷积神经网络中,输出特征图上的某一个像素点,是由输入层中多大面积的区域计算出来的?这个区域的大小就是感受野。