
读《神经网络与深度学习》这类书第一遍最容易卡住的其实不是part1里的池化和padding而是part2里这些听起来很基础的问题LeNet-5为什么长成那样ReLU、Dropout、BatchNorm这些组件到底各自解决什么为什么图像处理放着现成的前馈神经网络不用非要用卷积这些东西教材里都提到了但很多书只给了定义和公式没有把“当时为什么非要这么设计”讲透。这篇文章就围绕第五章第二部分的内容把卷积神经网络从经典架构到训练细节再到实战用法完整串一遍适合刚学完CNN基础运算、正准备接触真实网络的读者。1. 从LeNet-5到ResNet五个经典架构解决的真实问题1.1 LeNet-5CNN基础范式的定形1998年LeCun提出的LeNet-5基本确定了卷积神经网络的标准骨架卷积层提取特征、池化层降采样、最后接全连接层做分类。这个结构在今天看来简单但在当时是颠覆性的因为它完全抛弃了“手工设计特征”的思路让网络自己从像素里学出有用的模式。LeNet-5有一个容易被忽略的细节就是C3层的特征图没有和S2层的所有特征图全连接而是只连了其中一部分。这个设计是当年为了减少参数量同时打破对称性避免同一组特征被重复学出来。后来GPU和算力上来了VGG之后的网络基本都是全连接方式了但这个小细节说明了设计CNN的一个核心思路连接方式本身就是一种正则化。1.2 AlexNet深度学习的引爆点2012年AlexNet把CNN带到了ImageNet上Top-5错误率一下子降到了15.3%比第二名低了近10个百分点。这个差距让整个学术界意识到深度卷积网络在同规模数据上可以甩开传统方法一整条街。AlexNet的贡献不是某个单一技巧而是一套组合拳用ReLU替代sigmoid/tanh解决深层网络梯度衰减的问题用Dropout在训练时随机丢弃神经元来抑制过拟合用GPU并行训练把网络规模提上去同时配合大量数据增强把训练样本变多。当年文章里还用了LRN局部响应归一化后来实践发现这个模块收益很小后续模型基本都弃用了。AlexNet真正的历史意义在于证明了“足够深的网络足够多的数据足够强的算力”三者结合起来能产生质变。1.3 VGG小卷积核堆叠的范式VGG改变了整个CNN的结构审美。在VGG之前模型喜欢用大的卷积核比如AlexNet第一层就是11×11。VGG仔细比算了一下感受野发现两个3×3卷积堆叠起来感受野等于一个5×5卷积三个3×3堆叠等于7×7但参数量却小得多而且中间多了两次非线性变换表达能力反而更强。当时的实际计算也说明问题一个7×7卷积的参数是49C²三个3×3是27C²差了将近一倍。所以VGG之后“用小卷积核堆叠”成了视觉网络的事实标准一直延续到现在。VGG真正的问题在于参数量集中在最后的三层全连接上占了大头但收益有限这也为后面全局平均池化的流行埋下了伏笔。1.4 GoogLeNet的Inception多尺度并行与1×1卷积GoogLeNet走了一条和VGG完全不同的路。它的Inception模块把1×1、3×3、5×5的卷积和3×3的池化并行放在一起让网络自己决定用多大感受野的特征最后把各分支的结果在通道维上拼接起来。用“宽度换深度”的思路来增强表达能力而不是一味堆层数。Inception里1×1卷积的作用值得单独拎出来说。它可以在融合通道信息的同时压缩通道数从而大幅降低后续3×3、5×5卷积的计算量。举个直观的例子输入是256通道先用64个1×1把通道降到64再做3×3卷积计算量直接降到原来的四分之一。这种通道维上的“瓶颈”设计后来的SENet、MobileNet也一直在用。1.5 ResNet残差学习解决退化问题ResNet解决的退化问题是深度学习史上一道分水岭。实验发现网络堆到五六十层之后深度继续增加训练误差反而上升而且这不是过拟合因为训练集上的误差也变大了。这说明深层网络的优化本身出现了障碍梯度在反向传播中反复相乘后趋近于零前面的层根本得不到有效更新。残差连接的做法非常朴素不再让网络直接拟合目标映射H(x)而是学习残差F(x)H(x)-x最后用shortcut把输入x加回来。这样一来网络最差也可以学成恒等映射梯度也能通过shortcut直达浅层。从今天回看ResNet能成功核心就在“梯度有一条高速公路可以走”。实际写一个简化版BasicBlock就是这个逻辑import torch.nn as nn class BasicBlock(nn.Module): def __init__(self, in_channels, out_channels, stride1): super().__init__() self.conv1 nn.Conv2d(in_channels, out_channels, kernel_size3, stridestride, padding1, biasFalse) self.bn1 nn.BatchNorm2d(out_channels) self.relu nn.ReLU(inplaceTrue) self.conv2 nn.Conv2d(out_channels, out_channels, kernel_size3, padding1, biasFalse) self.bn2 nn.BatchNorm2d(out_channels) self.downsample None if stride ! 1 or in_channels ! out_channels: self.downsample nn.Sequential( nn.Conv2d(in_channels, out_channels, kernel_size1, stridestride, biasFalse), nn.BatchNorm2d(out_channels), ) def forward(self, x): identity x out self.conv1(x) out self.bn1(out) out self.relu(out) out self.conv2(out) out self.bn2(out) if self.downsample is not None: identity self.downsample(x) out identity out self.relu(out) return out这五个架构几乎每一个都是被某个具体问题逼出来的。LeNet要解决手写数字识别AlexNet要解决大规模图像分类VGG在探深度的上限GoogLeNet在探宽度的可能ResNet则直面深度化带来的优化难题。理解“它当时在对抗什么”比记住网络层数和参数更重要。架构核心贡献最值得学习的技巧遗留问题LeNet-5确立卷积池化全连接范式特征图部分连接减少参数网络太浅表达力有限AlexNet深度学习大规模验证成功ReLU、Dropout、数据增强组合依赖强算力结构粗糙VGG小卷积核堆叠的深度范式感受野等价分析全连接层参数冗余GoogLeNet多尺度并行与通道降维1×1卷积做瓶颈压缩分支结构工程落地复杂ResNet解决深层退化问题残差连接提供梯度捷径加深到极限后收益递减2. 图像任务为什么不用前馈神经网络计算量与先验的双重约束2.1 一张图拉平后的参数灾难很多人初学时会有一个疑问普通全连接前馈神经网络也能做分类为什么图像处理非要绕一圈搞卷积直接算一笔账就明白了。假设输入是一张224×224的彩色图拉平成向量就是224×224×3150528个输入维度。如果第一个隐藏层只有1000个神经元那这一层就产生了1.5亿个权重参数。在实际训练里这么一层的参数量就已经可以让网络在中小数据集上瞬间过拟合更别提后面还要堆多层。全连接网络对图像是“有多少像素就买多少权重”成本高到没法用。2.2 空间局部性先验图像数据和普通表格数据不一样它的每个像素和周围像素之间有极强的空间关联。猫的眼睛旁边大概率是猫的脸而不是背景墙一条直线附近的像素也大概率在同一条边上。这种局部相关性是图像最基本的先验结构而全连接网络完全没有利用这一点把距离十万八千里的像素和紧挨着的像素同等看待。卷积网络天然假设“局部相邻的特征最有关系”通过一个固定大小的卷积核在整张图上滑动只对局部区域做加权求和。这个归纳偏置和生物视觉系统的感受野机制有一致性。人眼也不是每看一个物体都从头扫描全部像素而是先看到局部轮廓再拼出整体结构。CNN做的正是这件事。2.3 权重共享与平移等变性如果只有一个局部连接而权重每个位置都不同参数量依然爆炸。CNN再往前走了一步同一个卷积核在整张图上共享。这样一来学到的“猫眼特征”无论在图像左上角出现还是右下角出现都会被同一个卷积核捕捉到。这就是卷积带来的平移等变性。物体在图像里移动几个像素卷积输出的特征图也跟着移动几个像素特征本身不会消失。池化层在此基础上再进一步通过对局部区域取最大或平均让特征具备更松弛的平移不变性物体稍微挪动一点池化后的输出变化很小。全连接网络没有这种性质。输入中猫的位置挪一下所有像素重新拼成一个完全不同的向量网络需要重新学一遍。这是本质上不擅长图像的原因不是数据量能弥补的。2.4 全连接与卷积的边界当然不是说全连接一无是处。CNN的标准做法是前面前面用卷积提取二维结构特征最后再加一个全局平均池化或全连接层做分类。全连接层的价值在于对高层特征做全局关系建模但在图像任务里它应该放在最后而不是一上来就面对原始像素。现在的趋势是即使是分类头也有不少模型用全局平均池化加1×1卷积来替代全连接进一步减少参数。网络的容量给了前面卷积部分最后的映射只需要很轻的“翻译层”把特征张量翻成分类概率就够了。3. 训练CNN的工程细节权重初始化、BatchNorm、数据增强与学习率调度3.1 权重初始化决定你从哪个斜坡下山训练CNN的第一个坑往往不是网络结构而是权重初始化。ReLU激活函数并不像sigmoid那样有天然的饱和区但它在x0时输出是0这个“砍半”行为让深层网络前向传播的方差每过一层都会缩小。如果初始化不当几十层之后激活值可能小到完全失去区分度反向传播的梯度也微乎其微网络干脆学不动。早期Xavier初始化假设激活函数近似线性适合sigmoid/tanh后来He初始化专门针对ReLU做了修正把每层输出的方差维持在可控范围。PyTorch里调用torch.nn.init.kaiming_normal_就是干这个用的。实际写代码时我建议自定义网络先用默认初始化训练一个浅层版本如果loss不降或梯度异常再手动检查各层输出的标准差而不是盲目换学习率。3.2 BatchNorm让深度网络真正可训练的关键如果说残差连接解决了深层的优化道路BatchNorm解决的是每层数据分布的稳定问题。简单理解它在每一层激活之后把数据标准化到均值0方差1再通过两个可学习参数做缩放和平移。这样一来即使前一层参数抖动后一层的输入分布也不会剧烈变化。但BN有两个必须留意的地方。第一训练和推理行为不同训练时用当前batch的均值方差推理时用训练期间累积的running_mean和running_var。所以模型从train模式切到eval模式时如果batch很小或者running stats还没积累够推理结果可能变差。第二batch大小非常影响BN效果batch1的时候统计量根本没有意义。之前用一个小模型在视频帧上做在线推理batch1BN直接把输出搞成噪声。后来换成了GroupNorm或冻结BN的running stats才算稳定。BN还有一个隐藏的正则化效果。因为每个batch的均值方差都在变化相当于给网络加了随机扰动所以很多现代网络里Dropout的位置被BN顶掉了。这也是为什么很多时候加了BN就不需要再叠Dropout。3.3 数据增强把一张图拆成十张来用数据增强是CNN训练里性价比最高的一环。常用组合是一套“随机裁剪翻转颜色抖动”的流程RandomResizedCrop随机裁剪图像的一部分再缩放到固定尺寸让网络学会忽略物体位置和大小的变化RandomHorizontalFlip水平翻转等于把样本翻倍ColorJitter调整亮度、对比度、饱和度让网络对光照变化更鲁棒RandomAffine随机旋转和平移增强几何鲁棒性Cutout或RandomErasing随机遮掉一块区域强迫网络不要只盯一个局部特征测试阶段必须关闭这些随机增强只做Resize和CenterCrop。否则推理结果每次都在变没法稳定评估。这里最需要警惕的是任务是否兼容。做文字识别时水平翻转会生成一个完全镜像的字等于制造错误标签医学影像里左右翻转可能改变解剖结构的方位约定遥感图、卫星图通常对方向不敏感但对手性敏感。我的教训是每加一种增强手动看几十张增强后的图确认人眼还能认出原任务的目标再放进训练管线。3.4 优化器与学习率调度loss曲线最诚实的镜子优化器选择我的建议是分场景看。从头训练大模型SGD加momentum0.9是最稳定的baseline配合0.1这样相对激进的基础学习率再加上warmup和cosine退火微调预训练模型则用AdamW学习率从1e-4到3e-4起步。Adam收敛快但泛化性通常略逊于调好的SGDAdamW把weight decay做了解耦比Adam的L2正则行为更可控。warmup这个操作在微调和Transformer类模型里几乎是必须的。它的意义在于模型刚初始化或刚加载预训练权重时特征分布还很陌生直接上大学习率容易把权重一脚踹到坏区域。前几个epoch先用很小或线性增长的学习率让更新幅度温和之后再把学习率提到峰值。下面是我常用的一个warmup加cosine退火函数import math def warmup_cosine_lr(epoch, warmup_epochs5, total_epochs100, lr_max0.1, lr_min1e-4): if epoch warmup_epochs: return lr_max * (epoch 1) / warmup_epochs progress (epoch - warmup_epochs) / (total_epochs - warmup_epochs) return lr_min 0.5 * (lr_max - lr_min) * (1 math.cos(math.pi * progress))实际训练中我观察loss曲线的经验是如果前几步loss突然冲高又猛烈下降通常学习率偏大如果loss从头到尾纹丝不动可能是初始化有问题或者数据管线出错了先别急着调优化器打印一下输入标签、检查增强后的图是不是黑块再回来调参。哪一种情况都先别碰warmup先把基础的学习率和数据链路确认清楚。3.5 一套可以直接抄的训练配置场景优化器学习率batch size关键操作从头训练ResNetSGD momentum 0.9峰值0.1256warmup cosineweight decay 1e-4微调预训练模型AdamW1e-4到3e-432到128低学习率backbone层更小小数据类别不均衡AdamW5e-5到1e-416到32加权采样或Focal Loss这个表不是公式但作为起点能省很多时间。我见过太多人上来就纠结某个超参数差0.001会怎样结果一次正经实验都没跑完。先把这套配置跑通再加改动效率高得多。4. 走向现实场景的变体3D卷积、空洞卷积、深度可分离卷积与图卷积4.1 3D卷积从图片到视频和时间序列视频数据的标准输入不再是一张静态图而是一段连续帧通常组织成(B, C, T, H, W)其中T是时间维。3D卷积直接把2D的卷积核沿着时间方向也扩展一维让网络能同时学习空间和时间两个维度的特征。3D卷积最直接的代价是计算量膨胀。一个3×3×3的卷积核权重参数只比2D多一倍多但特征图多了时间维整体计算量可能是同尺寸2D网络的数倍。实际做视频理解任务时常见做法是用(21)D分解把3D卷积拆成空间2D卷积加时间1D卷积既减少计算量又多了一层非线性。医学CT序列和视频本质上是同一个问题一层一层切片叠起来是天然的三维结构非常适合3DCNN处理。4.2 空洞卷积不降分辨率却扩大感受野传统CNN扩大感受野靠堆卷积层或大步长池化但代价是特征图分辨率逐渐变小。语义分割这类任务需要逐像素精确分类分辨率丢了信息就回不来了。空洞卷积的解法很直接卷积核的权重之间插入空洞让同样大小的卷积核在输入上覆盖更大的范围。一个3×3卷积rate1就是普通卷积rate2时覆盖范围等于5×5rate3时覆盖7×7。感受野变大了但参数还是那9个feature map分辨率也能保持。DeepLab系列语义分割里就是靠这种策略控制多尺度上下文信息的。空洞卷积有个著名的坑叫gridding artifact如果所有层都用相同的rate小物体上的特征信息会像网格一样被漏掉。缓解手段有两个思路要么把rate设置成递增序列比如1,2,3,1,2,3循环要么使用混合空洞卷积HDC来设计rate组合保证感受野覆盖均匀。4.3 深度可分离卷积移动端的轻量化主力MobileNet系列的核心是把一个标准卷积拆成两个阶段。第一阶段是depthwise卷积每个输入通道单独用一个2D卷积核去卷积不跨通道第二阶段是pointwise卷积用1×1卷积把各个通道的信息重新融合起来。计算量的差距很大。设输入和输出通道都是256卷积核3×3标准卷积的参数量是256×256×9约59万深度可分离卷积只需要depthwise的256×9加pointwise的256×256×1约6.8万压缩了将近9倍。这个结构在端侧设备上非常实用。不过要提醒的是深度可分离卷积并不是免费的午餐。通道之间的交互被推迟到了pointwise阶段特征表达能力比标准卷积弱一些。在计算资源允许的服务器端标准卷积往往效果更好。MobileNet那套结构的价值在于在功耗和性能之间找一个可用的平衡点。4.4 从卷积到注意力局部归纳偏置的边界CNN能成功很大程度上是因为它内置了“局部相关”这个先验。但局部先验在某些任务上有边界比如一张图里相距很远的目标之间也需要交互。SENet用全局平均池化加一个轻量网络来建模通道间的依赖CBAM在空间和通道两个维度上做注意力加权这些都是在不放弃卷积的前提下补充全局建模能力。Transformer里的窗口自注意力WSA更直白把图像切成一个个窗口先在窗口内部做自注意力再通过滑动或跨窗口交互。这个方法某种程度上和CNN的局部滑窗有异曲同工之处只是把“定权重”换成了“动态算权重”。理解CNN局部连接的人再去看这些结构会发现它的演变逻辑其实是延续的从固定局部到动态局部从局部到全局。4.5 图卷积网络CNN思路的非欧延伸CNN处理的是规整的方格像素。但很多数据天然是图结构社交网络的好友关系、分子里的原子连接、流量网络里的节点链路。这些数据没有规则的网格没法直接套用卷积核平移。图卷积网络GCN的核心思路是把“卷积”重新解释为“对邻居信息的聚合”。每个节点用自身特征和邻居特征的加权和来更新表示权重可以来自邻接矩阵也可以由注意力机制动态生成。这个思路和CNN本质上是同一套哲学节点的特征应该由它的局部邻域共同决定。图神经网络的实现路径不同但设计动机和卷积完全同源局部先验加参数共享换了一块不规则的地形而已。5. 迁移学习与微调实战里最常用的CNN使用方式5.1 为什么不建议从零训练现在你随便拿一个视觉任务第一反应都应该是找预训练模型微调而不是搭网络随机初始化从头训。原因很直接ImageNet上训练的模型已经把“边缘、纹理、颜色、局部形状”这些底层特征学出来了这些特征对绝大多数视觉任务都是通用的。从零训练需要的不是几万张图而是几十万、上百万张图。我见过一个项目想识别工业零件缺陷手里只有8000个样本硬要自己从零训ResNet50结果验证集准确率停在70%不到。换用torchvision里在ImageNet上预训练的ResNet18冻结backbone只训最后的分类头第一轮就能到75%。这就是迁移学习的真实差距。5.2 一套可以直接套用的微调流程我用PyTorch的话微调流程固定如下几步。先加载预训练模型替换分类头再决定哪些层参与训练。冻结backbone时只更新全连接层计算量小、速度快适合数据和算力都有限的情况数据量足够时再分层解冻从后往前慢慢放开让高层特征适应新任务。import torch import torch.nn as nn import torchvision.models as models model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) for param in model.parameters(): param.requires_grad False num_features model.fc.in_features model.fc nn.Linear(num_features, 10) # 替换成你自己的类别数 # 如果要做分层解冻把后两层的 requires_grad 设为 True for param in model.layer4.parameters(): param.requires_grad True optimizer torch.optim.AdamW( filter(lambda p: p.requires_grad, model.parameters()), lr3e-4, )这套流程不需要改动太多代码。唯一建议是新接的分类头第一轮loss偏高是正常的。因为新头是随机初始化的输出概率一开始混乱等backbone的特征经过几个batch的适应后loss会快速降下来。如果第一个epoch结束loss还高得离谱再检查数据标签映射有没有问题。5.3 微调中的常见坑第一个坑是BN层。如果冻结了backbone但backbone里的BN层还在用batch计算统计量小batch下抖动会非常明显。我的做法是冻backbone时把BN也切到eval模式让它们沿用预训练时的running_mean和running_var。如果数据分布和ImageNet差太远就不适合完全冻结BN应该把BN的统计量放开来重新适应但这要求batch size至少大于16。第二个坑是分类头改得过大。预训练模型的fc层输入特征数是固定的比如ResNet18是512ResNet50是2048。替换新头时不要为了增加容量在中间插很多全连接层。数据量不够的情况下一个线性层往往就已经够用加了复杂度反而过拟合。第三个坑是数据集很小的时候解冻太多层。一般几千张图规模的任务只解冻最后layer4甚至只解冻fc层就够了。解冻越多层需要的数据和算力就越多而且容易把预训练学好的底层特征破坏掉。第四个坑是样本不均衡。我做过一个缺陷检测场景正常样本占了90%缺陷只有10%直接微调出来的模型会把所有样本都判成正常。解决方式是给样本加权重或者用Focal Loss。这个和模型结构无关但优先级比调网络超参数高得多。5.4 实测效果与心得回到教材的part2本身。我的感觉是教科书里的每个模型其实都是当年工程约束和理论直觉碰撞后的产物数据不够于是有了数据增强训练不稳定于是有了BatchNorm和残差连接。真正读透这章不是背下结构表而是每看到一个组件都问一句它到底在对抗什么问题带着这个问题做实验再回去看论文那些结构设计背后的理由自然就浮出来了。