ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

从LeNet-5到AlexNet:CNN卷积神经网络入门与PyTorch实践

从LeNet-5到AlexNet:CNN卷积神经网络入门与PyTorch实践 CNN 卷积神经网络是入门深度学习图像识别时绕不开的第一课。不管是图像分类、目标检测、人脸识别还是风格迁移底层基础基本都围绕卷积层、池化层、激活函数和全连接层展开。网上讲 CNN 的材料很多但最常见的两个问题是要么只讲名词不讲流程要么一上来就贴 ResNet、Transformer 这类复杂结构新手看三天还是不知道 LeNet-5 为什么长这样。这篇内容适合已经会一点 Python、但还没系统学过深度学习的读者。我会把卷积层、池化层、激活函数、全连接层放进 LeNet-5 和 AlexNet 两条主线里先讲每个模块到底在干什么再给能直接跑的代码最后把训练时最容易翻车的地方逐条讲清楚。先说结论理解 CNN 不需要死记硬背所有公式最好的路径是这几个动作——先搞清楚输入是一张什么样的图再看它在每一层经过什么变换变成什么形状最后在代码里打印每一层的输出亲眼看到尺寸变化。能做到这一步经典网络结构基本就通了。1. 先想清楚CNN 到底在解决什么问题1.1 普通全连接网络处理图像的致命问题很多人学 CNN 之前先接触的是全连接神经网络也就是把每个像素都当成一个特征全部连起来。这种思路在处理图像时有两个明显的问题。第一个问题是参数爆炸。一张 32x32 的彩色图片有 3 个通道展平之后是 3072 个数。这个规模还能接受但如果换成 224x224 的彩色图片展平之后就是 150528 个数。再往下接一个隐藏层参数量立刻变成千万级甚至亿级普通机器根本训练不动。第二个问题更关键位置信息被破坏了。图片里的猫眼睛在两侧鼻子在中间嘴巴在下方。这种空间关系非常重要。但展平成向量之后像素之间的远近关系就变成了一维顺序网络需要自己去重新学习这种空间结构而且学得很低效。同一只猫平移几个像素、旋转一点角度像素层面的差异可能很大但语义上它还是同一只猫。全连接网络处理这种局部形变非常吃力。CNN 的思路完全不一样它模仿人看图的习惯先看局部细节再把局部特征组合成整体。判断一张图是不是猫不需要盯着每个像素看而是看毛发的纹理、耳朵的轮廓、眼睛的形状这些局部特征。卷积层专门做这件事。1.2 CNN 的三个核心机制局部连接、权值共享、下采样CNN 能同时缓解参数爆炸和位置信息丢失问题靠的是三个核心机制。局部感受野。每个神经元只连接输入图像的一个局部区域而不是全部像素。这个局部区域的大小就是卷积核大小常见的是 3x3、5x5、7x7。这样每个神经元只负责一小块区域不需要关心整张图。权值共享。同一个卷积核会在整张图上滑动不管图像多大这个卷积核的参数只有一份。参数数量不会随着输入尺寸变大而爆炸同时还能自动提取出“图像任何位置出现某种边缘或纹理”这样的特征。下采样也就是池化层。它把特征图缩小保留主要信息丢掉多余细节同时让网络对轻微的平移和形变更不敏感。把三个机制串起来就形成了 CNN 的基本骨架卷积层提取特征激活函数引入非线性池化层降低尺寸全连接层做分类中间再穿插 Dropout、归一化之类的辅助手段。下面按顺序拆开看。2. 卷积层CNN 特征提取的真正核心2.1 卷积核是如何在图像上滑动的卷积层是 CNN 里最核心、也最容易让人迷糊的部分。一个卷积核本质上是一个小的参数矩阵比如 3x3。它像手电筒一样在输入图像上逐步滑动每个滑到的位置都和图像上对应区域的像素做逐点相乘再求和得到一个数。整个过程结束之后就得到一张新的特征图。举个例子输入是 1 通道、5x5 的灰度图卷积核是 3x3步长为 1不填充。卷积核从左上角开始每次向右移动一格移动到最右再回到下一行。每移动一次就做一次乘加运算最终得到一张 3x3 的特征图。特征图上每个位置的值可以理解成“原始图像这个局部区域和卷积核模式的匹配程度”。如果某一块区域恰好包含水平边缘某个专门检测水平边缘的卷积核在这里就会得到较大的响应。这里要特别提醒一个容易搞混的点卷积核里的参数不是人手工设计的而是训练出来的。刚开始可能是随机值随着损失函数不断反向传播网络会自己调整这些参数让它学会提取边缘、纹理、颜色渐变等特征。越靠前的卷积层学到的是越基础的边缘纹理越靠后的层会把基础特征组合成更复杂的语义特征。2.2 步长、填充、输出通道到底怎么理解步长决定卷积核每次滑动几格。步长为 1 就是每次移动一格步长为 2 就一次跳两格。步长越大输出特征图越小计算量越小但也可能丢掉细节。第一次做实验步长通常先用 1。填充是指在图像边缘补一圈 0。为什么不填充不行因为每次卷积都会让尺寸变小网络多叠几层特征图就缩没了。补 0 之后可以让输出和输入尺寸保持一致也才能让边缘位置的像素获得足够次数的计算。输出通道数要这样理解用多少个卷积核就得到多少张特征图。每张特征图代表网络从一个“视角”观察输入。比如输出通道是 16就相当于网络从 16 个不同角度观察这张图。通道数越大表达能力越强但计算量也越大。2.3 特征图尺寸怎么算直接看公式特征图尺寸计算是排查报错时最常用的工具。公式如下H_out (H_in 2 * padding - kernel_size) / stride 1W_out 也一样把高度换成宽度就行。举个例子输入 32x32卷积核 5x5stride1padding0那么 H_out (32 0 - 5) / 1 1 28。LeNet-5 第一层从 32x32 变成 28x28就是这么算出来的。如果除法除不尽说明参数搭配不合理。PyTorch 这类框架通常会向下取整但更好的做法是直接回头检查输入尺寸和网络设计是否匹配。可以用代码验证一下import torch import torch.nn as nn x torch.randn(1, 1, 32, 32) # batch1, 通道1, 高32, 宽32 conv nn.Conv2d(in_channels1, out_channels6, kernel_size5, stride1, padding0) y conv(x) print(y.shape) # torch.Size([1, 6, 28, 28])输出是 [1, 6, 28, 28]说明这一层把 1 通道的 32x32 图片变成了 6 张 28x28 的特征图。3. 池化层和激活函数压缩尺寸与引入非线性3.1 最大池化、平均池化该怎么选池化层的作用是压缩特征图。它没有可学习参数只做固定操作把一个区域内的多个像素合并成一个数。最常见的两种是最大池化和平均池化。最大池化取区域内最大值适合保留最明显的特征响应。比如边缘检测之后某个位置出现强烈响应最大池化会把这种信号保留下来。平均池化取平均值更偏向平滑信息、保留整体特征。LeNet-5 刚提出时用的是 2x2 的平均池化当时的叫法是子采样。到了 AlexNet已经普遍使用最大池化。现在的实践里最大池化更常见因为它在保留强特征、抑制噪声方面表现更好。PyTorch 里一行代码就能用pool nn.MaxPool2d(kernel_size2, stride2)池化层没有可学习参数所以它不会增加模型体积只会改变特征图尺寸。3.2 激活函数为什么不能省激活函数的作用是给神经网络引入非线性。如果没有激活函数不管网络叠多少层本质上都还是线性变换的组合表达能力非常有限。ReLU 是最常用的激活函数之一表达式是 max(0, x)。大于 0 的时候导数为 1小于 0 的时候导数为 0。它有两个明显好处正区间梯度不会消失训练更容易收敛计算非常简单。现在的实践里隐藏层基本从 ReLU 或者它的变体开始比如 LeakyReLU、SiLU。最后一层根据任务选择 softmax 或者 sigmoid中间层一般不会再碰 sigmoid 和 tanh。3.3 从 Sigmoid 到 ReLU经典网络怎么变LeNet-5 时代激活函数用的是 sigmoid 或 tanh。这两个函数的问题是输入稍微大一点或者小一点梯度就接近 0网络训练非常慢这就是梯度饱和现象。AlexNet 把激活函数换成了 ReLU。这个改动看似简单效果却很直接在深层网络里ReLU 的正区间梯度恒为 1信号能更顺畅地反向传播训练速度明显提升。当时论文里专门强调过用 ReLU 训练深度卷积网络比用 tanh 要快好几倍。还有一个容易被忽略的点池化操作本身没有非线性。所以激活函数必须由卷积层或全连接层来提供。实际代码里标准顺序通常是卷积 - 激活 - 池化。顺序不要搞反否则池化之后的信息还没有经过非线性变换特征表达会弱很多。4. 全连接层把特征图变成分类结果4.1 展平操作做了什么全连接层放在网络的最后阶段。卷积层和池化层已经把原始图像逐步压缩成若干张较小但语义更丰富的特征图。这些特征图还是二维结构而全连接层处理的是向量所以中间需要一个展平操作。展平本质上就是改数据形状没有学习参数。假设最后一层卷积输出的是 16 张 5x5 的特征图展平之后就是 16 x 5 x 5 400 个数。这 400 个数按顺序排成一个向量再接入全连接层。4.2 输出层怎么设计全连接层把展平后的向量逐步映射到类别空间。比如 400 - 120 - 84 - 10最后输出 10 个值对应 MNIST 手写数字的 0 到 9。多分类任务最后一层一般接 softmax让每个类别的概率加起来等于 1。二分类可以用 sigmoid。如果是回归任务输出层可能就不加激活函数。4.3 为什么全连接层参数量占比大全连接层的参数量通常占整个网络的很大比重。原因是每个输入节点都要和每个输出节点相连。输入维度和输出维度一旦放大参数量会快速增长。这也是现代很多网络喜欢用全局平均池化代替全连接层的原因。但学习经典结构时全连接层仍然是必须理解的部分LeNet-5 和 AlexNet 都靠它完成最终分类。5. LeNet-5 逐层拆解最干净的 CNN 入门结构5.1 七层结构的输入输出尺寸LeNet-5 是 1998 年提出的卷积神经网络用来做手写数字识别。它结构非常干净适合作为理解 CNN 完整流程的第一份图纸。输入是 32x32 的灰度图整体结构是 7 层C1 卷积层6 个 5x5 卷积核输出 6 张 28x28 特征图。 S2 池化层2x2 平均池化输出 6 张 14x14 特征图。 C3 卷积层16 个 5x5 卷积核输出 16 张 10x10 特征图。 S4 池化层2x2 平均池化输出 16 张 5x5 特征图。 C5 卷积层120 个 5x5 卷积核输出 120 张 1x1 特征图。 F6 全连接层84 个神经元。 输出层10 个类别。把这张表记下来很有帮助。因为很多网络结构图都是从这种尺寸变化推导出来的。5.2 C3 到输出层从局部特征到全局分类C3 层是 LeNet-5 里比较特殊的一层。它没有采用完全连接的方式而是部分连接目的是控制参数数量和打破对称性。但对于新手来说可以先不纠结这个细节把它当成一个普通卷积层理解即可。S4 之后特征图是 16 张 5x5。C5 用 120 个 5x5 的卷积核去卷积输出 120 张 1x1 的特征图。由于此时每个特征图已经变成 1x1所以 C5 在实际实现里经常被直接当成全连接层来写。F6 再接一个 84 维的全连接层最后输出 10 个类别得分。5.3 用 PyTorch 复现一遍 LeNet-5在 PyTorch 里复现 LeNet-5可以这样写import torch import torch.nn as nn class LeNet5(nn.Module): def __init__(self, num_classes10): super().__init__() self.conv1 nn.Conv2d(1, 6, kernel_size5) # 32 - 28 self.conv2 nn.Conv2d(6, 16, kernel_size5) # 14 - 10 self.pool nn.AvgPool2d(kernel_size2, stride2) self.fc1 nn.Linear(16 * 5 * 5, 120) self.fc2 nn.Linear(120, 84) self.fc3 nn.Linear(84, num_classes) def forward(self, x): x self.pool(torch.tanh(self.conv1(x))) # 32 - 28 - 14 x self.pool(torch.tanh(self.conv2(x))) # 14 - 10 - 5 x x.view(x.size(0), -1) # 展平 x torch.tanh(self.fc1(x)) x torch.tanh(self.fc2(x)) x self.fc3(x) return x model LeNet5() fake torch.randn(2, 1, 32, 32) out model(fake) print(out.shape) # torch.Size([2, 10])这里用了 tanh 激活函数是为了贴近原始 LeNet-5 的结构。如果你只是想练习识别能力把 tanh 换成 ReLU 通常收敛更快。这个替换不影响理解结构。6. AlexNet从 8 层结构看深度网络的工程改进6.1 AlexNet 的核心改动AlexNet 是 2012 年 ImageNet 图像分类比赛的冠军模型把当时的错误率大幅降低也因此成为深度学习热潮的引爆点之一。它的贡献不在于把网络做得特别复杂而在于证明“更大、更深 正确的训练技巧”确实有效。AlexNet 的整体结构是 8 层5 个卷积层加 3 个全连接层。输入是 224x224 的 RGB 图片。第一层用 96 个 11x11 的卷积核步长为 4在原始图像上做较大范围的卷积。后面的卷积层逐渐缩小卷积核尺寸、增加通道数最后接 4096、4096、1000 的全连接层。相比 LeNet-5AlexNet 有几点关键改进。第一个是 ReLU 激活函数解决了深层网络里的梯度消失问题。第二个是 Dropout训练时随机丢弃部分神经元强迫网络不依赖单个节点是缓解过拟合的常用手段。第三个是数据增强对训练图片做随机裁剪、翻转、颜色扰动相当于免费扩大数据集。第四是重叠池化池化窗口大于步长让池化区域有部分重叠。原文里还提到局部响应归一化LRN但在后续网络中被证明作用不大现在已经很少使用。学习的时候知道有这样一个历史改进即可不用在它身上花太多精力。6.2 LeNet-5 和 AlexNet 的关键对比把两个网络放在一起对比能明显看出 CNN 的发展脉络。LeNet-5 总共 7 层输入是 32x32 灰度图激活函数用 tanh参数量大概几十万级别适合处理小尺寸手写数字。AlexNet 总共 8 层输入是 224x224 彩色图激活函数用 ReLU训练时使用 Dropout 和数据增强参数量超过 6000 万需要较强算力支持。结构上两者其实遵循同一套思路卷积层提取特征池化层压缩尺寸全连接层做分类。AlexNet 不是发明了新结构而是把规模做大、把训练技巧做足最后用实际效果证明了深度网络的价值。6.3 单卡环境怎么学 AlexNet如果手头只有一块普通显卡直接训练完整的 AlexNet 跑 ImageNet 基本不现实但不代表不能学。一种方式是拿 AlexNet 的结构改一改跑 MNIST 或 CIFAR-10。把第一层输入通道改成对应图片通道数把最后全连接输出的类别数改成自己的分类数同时把图片缩放到合适尺寸。重点不是追求完整复现训练效果而是理解结构里的尺寸计算和参数变化。如果连 GPU 都没有建议直接用 LeNet-5 类结构跑 MNIST先把训练流程跑通。AlexNet 这种规模在纯 CPU 环境下训练会非常煎熬不值得为了练手硬跑。7. 第一次完整训练MNIST 和猫狗数据集怎么选7.1 不同数据集的学习价值MNIST 是最经典的入门数据集28x28 灰度手写数字6 万张训练图。用 LeNet-5 变体跑 MNIST 很合适数据集小CPU 都能跑。但要注意原版 LeNet-5 输入是 32x32MNIST 是 28x28需要先做一次补零缩放或者把第一层输入尺寸调整成 28否则特征图尺寸对不上。CIFAR-10 是 32x32 彩色图10 个类别。它比 MNIST 更有挑战性因为是 3 通道输入物体更复杂一个简单的 LeNet 变体只能跑到 70% 左右。这个阶段特别适合观察过拟合和调参。猫狗数据集Cats vs Dogs更接近真实场景图片尺寸不统一、背景复杂、样本量有两万多张。它适合用来练习真实图片预处理包括缩放、归一化、数据增强。但第一次不建议拿它做完整训练因为图片缩放到 224x224 之后三通道输入对显存和训练时间的要求会明显上升。7.2 训练一个模型的最小闭环完整训练流程可以拆成这几步第一步加载数据把图片转成 Tensor归一化到 [0,1] 或 [-1,1]。 第二步构建模型用前面写的 LeNet5。 第三步定义损失函数和优化器。分类任务用 CrossEntropyLoss优化器可以先用 Adam学习率从 1e-3 开始。 第四步循环多个 epoch每个 batch 做前向传播、计算损失、反向传播、更新参数。 第五步每个 epoch 结束后在验证集上计算准确率观察训练是否正常。一个最小训练循环大概是这样的import torch import torch.nn as nn model LeNet5() criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr1e-3) for epoch in range(5): for images, labels in train_loader: scores model(images) loss criterion(scores, labels) optimizer.zero_grad() loss.backward() optimizer.step() print(fepoch {epoch 1}, loss {loss.item():.4f})这段代码能跑通之后再考虑调整 batch_size、学习率、网络深度不要一上来就堆复杂度。7.3 训练过程中怎么判断网络是否正常判断训练是否正常不能只看 loss 有没有降要看几个指标一起判断。loss 是否逐渐下降。前几个 batch 损失明显下降之后缓慢下降说明网络在学习。训练准确率和验证准确率是否都在上升。如果训练准确率很高、验证准确率停滞甚至下降大概率是过拟合。输出是否集中在某一个类别。如果网络总是输出 0 或者某个固定类先检查数据标签是否错位、损失函数是否选对、学习率是否太大。训练耗时是否稳定。每个 epoch 的耗时应该在一个稳定范围内。如果突然变慢看看是不是数据加载环节出了问题。8. 训练报错时按这个顺序排查8.1 形状不匹配问题新手遇到最多的一类报错是形状不匹配报错信息一般是类似 mat1 and mat2 shapes cannot be multiplied 或者 size mismatch。原因通常是卷积或池化之后的特征图尺寸和你写进全连接层的维度数字对不上。排查方法是先喂一张假数据打印每一层的输出形状逐层核对x torch.randn(1, 1, 32, 32) print(x.shape) x model.conv1(x) print(x.shape) x model.pool(x) print(x.shape)看到哪一层尺寸和你预期不一致就改哪里。常见做法是调整图片输入尺寸或者调整全连接层的输入维度。8.2 Loss 变成 NaN 或梯度爆炸Loss 变成 NaN常见原因有几个学习率过大、数据没有归一化、标签类别数设置错误、损失函数里出现 log(0)。排查顺序是先调低学习率从 1e-4 重新试。再看输入数据是不是还在 0 到 255 的整数范围如果是先归一化。最后看标签确认类别数从 0 开始编号并且和模型输出维度一致。8.3 准确率一直不涨准确率一直不涨不要急着换复杂模型。先做一个小实验只取几百条数据让模型在这几百条数据上反复训练。如果能过拟合到接近 100%说明网络本身没问题问题出在数据或者超参。如果连小样本都过拟合不了依次检查数据标签是否错位、损失函数是否选对、模型输出维度是否匹配、学习率是不是太大或太小。SGD 不涨的时候换 Adam 试试学习率从 1e-3 开始。8.4 训练慢和显存不足的处理训练慢先看单个 epoch 耗了多少时间。如果太慢优先降低 batch_size再降低图片分辨率最后考虑减少卷积层通道数。显存不足也就是常见的 CUDA out of memory同样先把 batch_size 调小然后减小图片尺寸再不行就换更小的网络。低配机器能跑通 MNIST不代表能跑通大尺寸彩色图这一点要提前有预期。9. 学完 CNN 基础之后下一步怎么走9.1 现代卷积网络往哪个方向演化LeNet-5 和 AlexNet 只是起点。后续的 VGG 证明了更小的卷积核和更深的层数有效GoogLeNet 提出了 Inception 结构在同一个层里用不同尺寸的卷积核ResNet 用残差连接解决了深层网络难以训练的问题把网络做到了上百层。这些结构虽然更复杂但底层思路没有变依旧是卷积层提取特征、池化层降维、全连接层或全局平均池化做分类。学完 LeNet-5 和 AlexNet 之后再看 ResNet 会容易很多因为你已经知道怎么跟踪一张图在网络里的形状变化了。9.2 为什么后来 Transformer 也进入了视觉领域后来出现的 Vision Transformer 一类结构确实在很多任务上取得了不错的效果。它不再用卷积核滑动而是把图片切成 patch用自注意力机制处理全局关系。很多人会问既然 CNN 已经很好用了为什么还会出现 Transformer。我的理解是CNN 强在局部特征提取和归纳偏置在数据量不够大的时候表现稳定Transformer 强在全局建模和规模扩展数据量非常大的时候上限更高。两者不是简单的替代关系。入门阶段先把 CNN 的经典结构吃透再去看注意力机制和 Transformer路径会顺很多。9.3 适合新手的后续路线给一个我比较推荐的学习顺序你参考即可。第一步把 LeNet-5 在 MNIST 上跑通目标准确率 95% 以上。第二步把同一个网络改成三通道输入跑 CIFAR-10观察准确率上限和过拟合现象。第三步换一个真实场景数据集比如猫狗识别练习图片缩放、数据增强和训练日志管理。第四步再看 ResNet 的残差连接复现一个 18 层的 ResNet跑 CIFAR-10。真正把这些结构跑通之后再回头看你会发现 CNN 的核心并不神秘局部连接提取特征权值共享控制参数量池化压缩尺寸全连接做分类。只要能把一张 tensor 从输入流到输出你就已经过了第一道坎。剩下的就是在数据、损失函数和超参里反复做实验了。建议先把 LeNet-5 在 MNIST 上跑出 95% 以上的准确率再谈其他。
返回列表