先算一笔账,理解 CNN 要解决什么问题。一张 200×200 的灰度图有 4 万个像素。如果用上一章那种全连接网络,第一个隐藏层哪怕只有 1000 个神经元,光这一层就有 40000×1000=400040000 \times 1000 = 4000 万个权重。彩色图三通道再翻三倍。参数多到既算不动也学不好,而且会严重过拟合。

更深的问题是:全连接网络不理解图像的结构。对它来说,像素 (3,3) 和像素 (3,4) 是两个毫不相关的输入,和像素 (199,199) 没有任何区别。可图像不是这样的——相邻像素高度相关,一只猫无论出现在画面左上还是右下都还是一只猫。全连接网络要从零学会“空间局部性”和“平移不变性”这两件事,等于把人类视觉系统几亿年进化出的先验全部丢掉、让它重新发明。

卷积网络的思路是:不让模型从零学这些先验,而是把它们直接设计进网络的连接结构里。这两条先验,最早是从猫的大脑里看出来的。


1950 年代末到 1960 年代,神经生理学家 David Hubel 和 Torsten Wiesel 在猫的初级视觉皮层做了一系列后来获诺贝尔奖的实验。他们发现视觉皮层里的神经元有两个关键特征:第一,每个神经元只对视野中一小块区域(它的“感受野”)里的刺激有反应——这是局部感受野;第二,皮层里存在分工——“简单细胞”(simple cells)对特定朝向的边缘、线条敏感,而“复杂细胞”(complex cells)对同样的朝向敏感、但对刺激的精确位置有一定容忍,即便边缘稍微移动它仍然响应1

简单细胞负责“检测局部特征”,复杂细胞负责“对位置做一定的平移容忍”——把这两类细胞分层堆叠,就能从局部边缘逐步组合出更复杂、更有位置不变性的特征。这套生物结构,正是卷积网络的蓝本。

第一个把它搬进人工神经网络的,是日本计算机科学家 Kunihiko Fukushima。


1979/1980 年,Fukushima 在 NHK 广播科学技术研究所工作期间提出了 Neocognitron(新认知机)——一个分层多层的人工神经网络,直接用人工单元实现了 Hubel-Wiesel 的简单细胞与复杂细胞2。Neocognitron 交替堆叠两类层:S 层(对应简单细胞,做局部特征检测)和 C 层(对应复杂细胞,做下采样、提供平移容忍)。这个“特征检测 + 下采样”交替的骨架,几乎就是后来所有 CNN 的结构原型。

但 Neocognitron 有一个和那个时代相称的局限:它不用反向传播训练。在 Fukushima 设计它的时候,反向传播作为多层网络训练方法还没有被广泛知晓(参见上一章的优先权年表)。Neocognitron 用的是一种无监督的自组织学习方法2。结构对了,但缺少一个高效的、由任务目标驱动的训练引擎。

补上这块拼图的,是 Yann LeCun。


1989 年,当时在 AT&T 贝尔实验室的 Yann LeCun 发表了《反向传播应用于手写邮政编码识别》(Backpropagation applied to handwritten zip code recognition)3。这篇工作的历史意义在于:它第一次把反向传播用到了卷积结构上,并把这套学习方法数学形式化——既继承了 Neocognitron 的局部感受野与权重共享思想,又用上一章那个高效的梯度算法,让卷积核的权重能直接从带标签的数据里端到端地学出来,而不再依赖手工设计或无监督自组织34

到 1990 年代,LeCun 在 AT&T 的团队把这套架构发展成著名的 LeNet(最成熟的版本是 1998 年的 LeNet-5)。它在手写数字识别上达到了 99.3% 的准确率,并被实际部署用于自动读取美国 10–20% 的支票上的数字4。这是神经网络第一次大规模工业落地——不是 demo,是每天处理千万张真实支票的生产系统。

所以“谁发明了 CNN”这个问题,答案是分层的:Fukushima(1980)贡献了受生物启发的卷积式分层结构(局部感受野、特征检测与下采样交替),LeCun(1989)贡献了用反向传播端到端训练这种结构的方法、并将其数学形式化与工业化23。把 CNN 简单归给任何一方都不完整:没有 Fukushima 的结构,LeCun 的训练无处施加;没有 LeCun 的训练,Fukushima 的结构发挥不出威力。


现在把卷积这个核心算子的数学讲清楚。卷积层做的事,是用一个小小的卷积核(kernel,也叫 filter)在整张图上滑动,每滑到一个位置,就把核与其覆盖的图像块做逐元素相乘再求和,得到该位置的输出。对二维图像 IIkh×kwk_h \times k_w 的核 KK,输出特征图为

O[i,j]=u=0kh1v=0kw1I[i+u,j+v]K[u,v]O[i,j] = \sum_{u=0}^{k_h-1}\sum_{v=0}^{k_w-1} I[i+u,, j+v],K[u,v]

(严格的数学卷积要把核翻转,工程实现里普遍用不翻转的“互相关”,因为核是学出来的,翻不翻转只是参数的镜像,对学习没有本质影响。)

这一个简单公式里藏着三个决定性的设计:

局部感受野:每个输出 O[i,j]O[i,j] 只看输入里一个 kh×kwk_h \times k_w 的小窗口,而不是整张图。这对应 Hubel-Wiesel 的局部感受野,也契合“图像里相邻像素才相关”的先验。

权重共享:同一个核 KK 在所有位置滑动时用的是同一组权重。这是参数量暴跌的关键——不管图多大,一个 3×33\times3 的核只有 9 个参数(外加一个偏置)。前面那个 4000 万参数的全连接层,换成若干个 3×33\times3 卷积核,参数量降到几百。权重共享还隐含了一个强假设:一个特征(比如竖直边缘)在图像任何位置都用同样的方式检测——这正是平移不变性的来源。

平移等变与不变:因为同一个核在每个位置都做同样的检测,当输入里的物体平移时,输出特征图里的响应也跟着平移(这叫平移等变);再叠加上池化(pooling,通常是取局部窗口的最大值),对小范围的位置变化产生容忍,逐步走向平移不变5。池化对应的正是 Hubel-Wiesel 的复杂细胞、Neocognitron 的 C 层。


用一段纯 NumPy 代码把这三件事一次看清(完整文件 code/03_convolution.py,可直接运行)。构造一张左暗右亮、中间有一条竖直边缘的小图,用 Sobel 竖直边缘核去卷它:

代码 · 03_convolution.py
展开代码 · 03_convolution.py
"""
第 03 章配套代码:2D 卷积 + 边缘检测 + 最大池化(CNN 的两个核心算子)
Runnable with: numpy only.  python3 03_convolution.py

演示卷积的"权重共享 + 局部感受野 + 平移不变",以及 Sobel 边缘核。
"""
import numpy as np


def conv2d(img, kernel):
    """有效卷积(valid),步长 1,无 padding。
    数学:out[i,j] = sum_{u,v} img[i+u, j+v] * kernel[u,v]
    (工程上常用互相关;卷积是核翻转后的互相关,对学习无本质区别)
    """
    kh, kw = kernel.shape
    H, W = img.shape
    out = np.zeros((H - kh + 1, W - kw + 1))
    for i in range(out.shape[0]):
        for j in range(out.shape[1]):
            out[i, j] = np.sum(img[i:i + kh, j:j + kw] * kernel)
    return out


def max_pool(x, size=2):
    """步长=size 的最大池化:下采样 + 局部平移不变。"""
    H, W = x.shape
    out = np.zeros((H // size, W // size))
    for i in range(out.shape[0]):
        for j in range(out.shape[1]):
            out[i, j] = x[i * size:(i + 1) * size, j * size:(j + 1) * size].max()
    return out


if __name__ == "__main__":
    # 一个 6x6 的"图像":左半暗右半亮,中间有竖直边缘
    img = np.zeros((6, 6))
    img[:, 3:] = 1.0
    print("输入图像:\n", img)

    # Sobel 竖直边缘核(检测水平方向的亮度突变)
    sobel_x = np.array([[-1, 0, 1],
                        [-2, 0, 2],
                        [-1, 0, 1]], dtype=float)
    feat = conv2d(img, sobel_x)
    print("\n卷积后(竖直边缘响应, 边缘处出现大值):\n", feat)

    pooled = max_pool(np.abs(feat), 2)
    print("\n2x2 最大池化后(下采样, 保留最强响应):\n", pooled)

    # 平移不变演示:把边缘右移一列,特征图响应也整体右移,模式不变
    img2 = np.zeros((6, 6)); img2[:, 4:] = 1.0
    print("\n边缘右移一列后的卷积响应(同样的核, 检测到同样的边缘模式):\n", conv2d(img2, sobel_x))

↓ 下载 03_convolution.py

sobel_x = np.array([[-1,0,1],[-2,0,2],[-1,0,1]], dtype=float)
def conv2d(img, kernel):
    kh, kw = kernel.shape; H, W = img.shape
    out = np.zeros((H-kh+1, W-kw+1))
    for i in range(out.shape[0]):
        for j in range(out.shape[1]):
            out[i,j] = np.sum(img[i:i+kh, j:j+kw] * kernel)   # 局部相乘求和
    return out

运行结果:

输入图像 (左暗右亮):        卷积后 (竖直边缘响应):
[0 0 0 1 1 1]               [0 4 4 0]
[0 0 0 1 1 1]      ──►      [0 4 4 0]
...                          [0 4 4 0]

原图里 0/1 交界处(第 3 列附近),卷积输出冒出了大值 4——这个 Sobel 核检测到了竖直边缘,而在均匀区域(全 0 或全 1)输出为 0。这就是“局部特征检测”。

再看平移不变:把边缘右移一列,用同一个核

边缘右移后的卷积响应:
[0 0 4 4]   ← 同样的边缘模式,响应整体右移,核不变、检测能力不变

权重共享让同一个核在新位置自动检测出同样的边缘;池化(max_pool,代码里也实现了)再把局部最强响应保留下来、做下采样。一个核 9 个数,在整张图上反复使用——这就是 CNN 用极少参数捕捉视觉结构的全部秘密。真实的 CNN 只是把这件事堆叠很多层、每层很多个核:浅层的核学出边缘、纹理,中层组合成形状、部件,深层组合成“猫脸”“车轮”这样的语义概念。而且这些核不是手工设计的 Sobel,是反向传播从数据里自己学出来的——这正是 LeCun 1989 那一步的意义。


用一张结构图把 LeNet 式 CNN 的数据流钉住:

输入图像        卷积层(多核)        池化层         卷积层        池化       全连接      输出
 28x28x1  ──►  conv 3x3 ──► ReLU ──► maxpool ──► conv ──► ... ──► flatten ──► fc ──► softmax
              [边缘/纹理]   [非线性]  [下采样]   [部件]            [向量]          [10类概率]
   感受野: 小 ───────────────────────────────────────────────► 大
   语义:   像素/边缘 ──► 纹理 ──► 部件 ──► 物体
   空间分辨率: 高 ──────────────────────────────────► 低(被池化逐步压缩)
   通道数(特征种类): 少 ────────────────────────────► 多(每层学更多种特征)

这张图里有 CNN 设计哲学的全部:随着层数加深,空间分辨率被池化逐步压低,而特征通道数逐步升高——网络在用“放弃精确位置”换取“识别更抽象、更大范围的模式”。浅层每个神经元只看几个像素,深层每个神经元的有效感受野已经覆盖大半张图,能对应到“这是一只猫”这种全局判断。

配套的 manim 动画 assets/manim/ch03_cnn.pyConvolutionPooling 两个 Scene)把卷积的几何含义演出来:一个 3×3 的核在整张图上滑动,每滑到一处就做一次局部加权和、点亮特征图上对应的一个像素——核只有一份、复用到每个位置(权重共享),每次只看一小块(局部感受野);随后 Pooling 演示 2×2 取最大如何把分辨率减半、抽掉精确位置只留下“有没有、强不强”。

CNN 的结构在 1990 年代就基本定型,也在支票识别上证明了工业价值。但它随后陷入了一段相对沉寂——直到 2012 年,三个条件同时成熟:足够大的标注数据(ImageNet)、足够快的并行算力(GPU)、以及一组让深层网络真正训得动的工程技巧(ReLU、dropout)。这三者的合流,造就了一篇让整个领域瞬间转向的论文,AlexNet。那是引爆点的故事——但在讲它之前,得先处理另一条与图像平行的主线:当输入不是一张静止的图、而是一段会随时间展开的序列时,神经网络该怎么记住过去。那是循环网络与 LSTM。


本质

卷积网络的核心是把一条关于图像世界的先验知识直接焊进了网络结构里:一个特征(比如一条边)在图像的哪个位置出现,它的检测方式应该是一样的。基于这个假设,它用“同一个小核滑遍全图”取代了“每个位置一套独立权重”,于是参数量与图像大小脱钩、平移不变性被天然内建、局部感受野逐层堆叠成全局视野。它真正解决的问题不是“如何识别图像”,而是“如何在不靠海量数据死记硬背的前提下识别图像”——把本该由数据去学的不变性,改成由结构去保证。这也是它的代价所在:先验越强,能塞进去的灵活性就越少,这为后来“用更弱的结构先验加更大的数据/算力”的路线(如视觉 Transformer)埋下了张力。


参考文献

  1. Hubel, D. H., & Wiesel, T. N. 关于猫初级视觉皮层简单细胞/复杂细胞与局部感受野的发现(1959/1962),CNN 的生物学起源。综述见 Neocognitron 条目:https://en.wikipedia.org/wiki/Neocognitron

  2. Fukushima, K. (1980). Neocognitron: 受 Hubel-Wiesel 启发的分层多层网络,S 层/C 层交替,无监督自组织训练(非反向传播)。Wikipedia:https://en.wikipedia.org/wiki/Neocognitron

  3. LeCun, Y., et al. (1989). Backpropagation applied to handwritten zip code recognition. 首次将反向传播用于卷积结构。IEEE Milestone “Convolutional Neural Networks, 1989”:https://ethw.org/Milestones:Convolutional_Neural_Networks,_1989

  4. LeNet 谱系与 LeNet-5(1998)、手写数字 99.3%、用于读取美国 10–20% 支票(理论与实现综述)。Pablo Insente, “The Convolutional Network: LeNet-5 and AlexNet”:https://pabloinsente.github.io/the-convolutional-network

  5. 卷积、权重共享、池化与平移不变性的机制综述(教学材料)。ml4a “Convolutional neural networks”:https://ml4a.github.io/ml4a/convnets/

用「中心列正、两侧负」的 3×3 核滑过左暗右亮图:平坦区正负相消≈0、跨越边缘处亮侧加暗侧减→强响应,演示「核为何长这样=边缘检测器」与权重共享(同一份核复用到每个位置、参数与图大小脱钩)。
沿「像素→边缘→纹理/部件→物体」数据流骨架,用亮度逐层点亮特征:浅层边缘组合成环/条纹/角点、再组合成猫脸/车轮,并以三轴标尺(分辨率↓、感受野↑、通道↑)演示「放弃精确位置换取更抽象模式」的层级抽象。