卷积神经网络(CNN)是深度学习入门时绕不开的第一座山。图像分类、目标检测、人脸识别、OCR、视频理解,底层几乎都有卷积、池化、激活函数和全连接层这四个组件的影子。很多新手在学到这里时容易陷入两个极端:一是盯着公式推导不放,越推越懵;二是直接复制一段网络代码跑起来,却不知道每一层到底在做什么,后面调参、换任务时完全无从下手。
这篇文章就按最直接的方式把 CNN 讲清楚。你会看到卷积层、池化层、激活函数、全连接层各自的输入输出和代码实现,然后用 LeNet-5 和 AlexNet 两个经典网络把整套知识串起来。最后我给出一个可以完整运行的 PyTorch 手写数字识别工程,涵盖环境安装、模型定义、训练验证、批量推理、ONNX 导出和 HTTP 接口封装。整个流程对 CPU 友好,入门显卡速度更快,适合刚接触深度学习的人照着做。
本文适合三类读者:正在学机器学习基础、需要快速理解 CNN 工作原理的学生;准备面试、需要把网络结构讲清楚的求职者;以及想在本地做一个图像识别小工具、走通“训练—导出—部署”全流程的开发者。文章比较长,建议先收藏,再照着跑一遍。
1. CNN 核心概念速览
先建立整体印象,后续章节再逐个展开。表里列出的都是本次会讲到的重点。
| 概念 | 核心作用 | 一句话理解 |
|---|---|---|
| 卷积层 | 提取局部特征 | 用一个小窗口在图片上滑动,把局部模式提取出来 |
| 池化层 | 降低空间尺寸 | 把相邻区域压缩成一个值,减少计算量 |
| 激活函数 | 引入非线性 | 让网络能拟合复杂映射,常用 ReLU |
| 全连接层 | 特征组合与分类输出 | 把前面提取的特征打平成向量,映射到类别概率 |
| LeNet-5 | 经典手写数字识别网络 | 1998 年提出,结构简洁,适合入门 |
| AlexNet | 深度学习爆发标志 | 2012 年 ImageNet 夺冠,奠定现代 CNN 基本范式 |
本文用到的技术路线如下:
| 项目 | 说明 |
|---|---|
| 任务类型 | 图像分类(手写数字识别) |
| 网络模型 | LeNet-5、AlexNet |
| 开发框架 | PyTorch + torchvision |
| 数据集 | MNIST |
| 运行设备 | CPU 可跑,NVIDIA GPU 加速 |
| 批量预测 | 支持,按目录批量处理 |
| 接口封装 | FastAPI HTTP 服务 |
| 导出格式 | ONNX / TorchScript |
需要说明的是,显存占用和训练速度与 batch size、图片分辨率、网络层数直接相关,没有一个统一数字。后面我会给出观察方法,而不是拍脑袋报一个固定值。机器不同,结果只能以本机为准。
2. CNN 要解决什么问题:适用场景与使用边界
CNN 提出的核心背景,是传统的全连接网络在处理图像时会“参数爆炸”。一张 32x32x3 的 CIFAR 图片,直接拉平后是 3072 维。如果第一层全连接设置 1024 个神经元,这一层就有约 315 万个参数;如果换成 224x224x3 的 ImageNet 图片,第一层全连接的参数量会轻松过亿。更大的问题是,图像里的目标并不会因为你把它整体拉平就改变局部结构,猫的眼睛、耳朵、胡须这些特征应该是“在哪里都认识”的,全连接网络做不到这一点。
CNN 用三个设计解决了这个问题:局部连接,每个神经元只看图片的一个小区域;权值共享,同一个卷积核在整个图上滑动,参数不随位置变化;空间降维,通过池化逐步压缩特征图。这三板斧让图像分类的参数量和过拟合风险大幅下降,也让模型对目标位置有了一定的容忍度。这也是卷积神经网络(CNN)在图像任务中长期占据主导地位的根本原因。
使用边界也要说清楚。CNN 适合图像、视频帧、一维信号等具有明显局部相关性的数据。对于长文本、长序列、需要全局依赖建模的任务,CNN 并不占优势,这也是后来 Transformer 在很多场景里取代 CNN 的原因之一。实际工程中两者经常配合使用,比如先用 CNN 提取局部特征,再用注意力机制做全局建模。CNN 没有“过时”,而是成为基础组件。
3. 卷积层:CNN 的灵魂
卷积层的输入一般是形状为 (N, C, H, W) 的四维张量:N 是 batch size,C 是输入通道数,H 和 W 是高度和宽度。对 MNIST 来说,单张图是 1 通道、28x28,所以形状是 (1, 1, 28, 28);CIFAR-10 则是 3 通道、32x32。卷积核是一个很小的窗口,常见 3x3、5x5,在图上从左上到右下按步长滑动,每次把窗口内的像素和卷积核参数做加权求和,再加上偏置,得到一个输出值。
三个关键参数决定输出尺寸:卷积核大小 K、步长 stride、填充 padding。输出尺寸可以按下面的公式计算:
out = (W - K + 2 * padding) / stride + 1例如输入 28x28,卷积核 5x5,padding=2,stride=1,输出仍是 28x28;如果不做 padding,输出会变成 24x24。这个公式是调试代码和面试时最高频的问题,建议直接记住。
PyTorch 里一行代码就能定义卷积层:
import torch.nn as nn conv = nn.Conv2d( in_channels=1, # 输入通道数,MNIST 为 1 out_channels=6, # 输出通道数,也就是滤波器数量 kernel_size=5, # 卷积核大小 stride=1, padding=2 )卷积层的参数量是 (K * K * C_in + 1) * C_out。以这个例子计算,(5x5x1+1)*6=156 个参数,非常小。真正的大头一般在全连接层,这也是为什么后来出现了全局平均池化和 1x1 卷积来压缩参数。理解了参数量计算方式,你就能解释为什么同一个网络在 28x28 小图上很轻量,换成 224x224 大图后显存会迅速上涨。
4. 池化层:降维不是玄学
池化层不改变通道数,只改变空间尺寸。最常用的是最大池化和平均池化。最大池化取窗口内的最大值,平均池化取平均值。默认做法是 2x2 窗口、stride=2,这样宽高各减半,整体特征图缩小为原来的四分之一。
池化的价值主要有三个。一是降低计算量,后面每一层的数据量都变小;二是扩大感受野,让网络在更靠后的层看到更大的范围;三是提供一定的平移不变性,目标稍微移动几个像素,最大池化的输出通常不会剧烈变化。
PyTorch 里是这样写的:
pool = nn.MaxPool2d(kernel_size=2, stride=2)也有不少人会问:既然卷积层可以用 stride=2 来降采样,为什么还要池化?答案是可以互相替代,现代网络很多用 stride=2 的卷积代替池化,比如 ResNet 系列。原版 LeNet-5 和 AlexNet 用的是池化,所以学习经典结构时还是要把池化理解透。池化本身没有可学习的参数,这也是它和卷积层最明显的区别之一。
5. 激活函数:把非线性加回来
如果网络里只有卷积和全连接,那么无论堆叠多少层,整体都等价于一个线性变换。激活函数的作用就是给网络引入非线性,让模型能拟合复杂的映射关系。常见候选有 Sigmoid、Tanh、ReLU 和 LeakyReLU。
早期网络常用 Sigmoid,它的输出在 0 到 1 之间,有一个明显问题:输入绝对值较大时梯度接近 0,深度网络中误差反向传播时会逐层衰减,导致前面层几乎学不动。ReLU 的出现缓解了这一点:正区间梯度恒为 1,负区间输出为 0,计算简单,让深层网络可以更好地被训练。AlexNet 能顺利训练起来,ReLU 是重要原因之一。
PyTorch 里的定义很简单:
activation = nn.ReLU(inplace=True)使用时需要留意 ReLU 的缺点:负数部分被直接置零,可能造成神经元死亡;学习率过大时尤其明显。遇到这种情况可以换 LeakyReLU、PReLU 或 GELU。激活函数不是一个可以忽略的超参数,不同任务、不同网络深度下,它对收敛速度和最终精度都有直接影响。
6. 全连接层与分类头
全连接层的作用,是把前面卷积和池化得到的特征图压平成一维向量,然后接若干线性层。以 LeNet-5 在 MNIST 上的一个简单变体为例,最后一个池化层输出 16 个 5x5 的特征图,压平后是 16x5x5=400 维。这 400 维向量经过两层线性变换,最后映射到 10 个类别。
分类头最后通常会接 softmax,或者直接配合 CrossEntropyLoss 使用。PyTorch 的 CrossEntropyLoss 内部已经做了 softmax,所以模型最后一层直接输出 logits 即可,不需要手动加 softmax。训练时用 CrossEntropyLoss,推理时想要概率可以再套一层 softmax。
全连接层的问题在于参数多。400 维转到 120 维,参数量约 4.8 万;如果输入是 224x224 的彩色大图,全连接层参数量会暴涨。因此现代分类网络往往用全局平均池化替代部分全连接层。这一变化在从 AlexNet 到 ResNet 的发展中可以看得很清楚。对初学者来说,先掌握全连接层的压平和输出逻辑,再去看全局平均池化,会顺畅很多。
7. LeNet-5 实战:从零实现手写数字识别
7.1 环境准备与数据下载
建议使用 Python 3.8 以上版本。先创建虚拟环境,避免依赖冲突:
python -m venv venv # Windows venv\Scripts\activate # Linux / macOS source venv/bin/activate然后安装 PyTorch。CPU 用户可以直接装 CPU 版,体积小很多:
pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu有 NVIDIA 显卡的用户,先去 PyTorch 官网根据 CUDA 版本选择对应的安装命令,不要照抄上面的 CPU 命令。安装完成后执行下面的脚本确认设备:
import torch print(torch.__version__) print(torch.cuda.is_available()) if torch.cuda.is_available(): print(torch.cuda.get_device_name(0))如果输出 True,说明 GPU 可用;如果输出 False,也没关系,本教程全部步骤都可以在 CPU 上完成,只是训练速度会慢一些。
MNIST 是 60k 训练图、10k 测试图的 28x28 灰度手写数字数据集。用 torchvision 可以自动下载:
import torchvision import torchvision.transforms as T transform = T.Compose([ T.ToTensor(), T.Normalize((0.1307,), (0.3081,)), ]) train_ds = torchvision.datasets.MNIST(root="./data", train=True, transform=transform, download=True) test_ds = torchvision.datasets.MNIST(root="./data", train=False, transform=transform, download=True) print(len(train_ds