公司动态
从反向传播到ResNet:打通计算机视觉基础神经网络链路
很多人学计算机视觉都会经历一个非常典型的过程先看一堆教程知道了卷积核、池化、激活函数、全连接这些名词也大致能看懂 CNN 的结构图甚至还跑通过几个分类代码。可一旦打开一篇新论文或者要自己动手调一个模型就会突然发现学的东西好像全用不上。你会卡在想不通“为什么这个网络要这样设计”、搞不明白“loss 为什么不动”、也不知道 ResNet 那个跳连到底解决了什么问题。我早期也是这样。直到后来把“反向传播”和“网络结构演进”这两条线重新串起来很多困扰才真正解开。这篇文章想做一件系统的事从反向传播讲起经过 CNN 的卷积、池化、激活最后落到 ResNet 的残差结构把计算机视觉里最基础的神经网络链路完整过一遍。它不是要替代教材而是想帮你建立一张能长期使用的地图理解数据在网络里怎么流动理解梯度在网络里怎么流动理解不同结构为什么要这么设计。1. 先回答一个扎心问题为什么学了 CNN 基础还是看不动论文1.1 核心问题不是公式是知识链路断了我见过很多初学者包括我自己早期都是按照“模块”来学 CNNs 的。今天学卷积明天学池化后天学全连接每个模块单独看都懂了但组合在一起就不知道发生了什么。问题出在哪里出在你脑子里没有一条完整的链路。神经网络不是一堆独立模块的堆叠它本质上是一个“前向传播—损失计算—反向传播—参数更新”的循环。前向传播时数据从输入层一路流到输出层产出预测反向传播时损失函数的梯度从输出层一路传回每一个参数参数更新完后再来一轮。真正要理解神经网络必须同时掌握两条主线一条是数据流一条是梯度流。为什么这样讲因为绝大多数调参问题、收敛问题、网络设计问题本质上都是这两条流出了问题。loss 不下降是梯度流断了网络太深效果变差也是梯度流传不到浅层过拟合是数据流把噪声也学进去了。如果你只记住卷积核大小这些零散概念却不知道它们在整条链路中的作用那看论文当然会晕。1.2 “数据流动”和“梯度流动”是两条主线数据流容易理解。输入一张图经过卷积层提取特征经过激活函数引入非线性经过池化缩小分辨率经过全连接映射到类别最终输出一个概率分布。这是你训练网络时“看得见”的路径。梯度流则比较隐蔽。训练时损失函数会算出当前预测和真实标签之间的差距然后从最后一层开始用链式法则把损失对每一层参数的“责任”算出来再沿着网络反向传播回去每层参数根据自己分到的那份梯度沿着梯度的反方向更新一小步。这个过程反复进行模型才会逐渐收敛。很多人只关注数据流忽略了梯度流于是会遇到很多迷惑现象。比如一个网络结构看起来没问题数据也能正常 forward但训练时浅层参数几乎不动整个模型学不进去。这种问题不去查梯度就很难定位。再比如为什么 ResNet 要在网络中加一条“捷径”就是为了让梯度能更顺畅地从深层传回浅层。没有这条思路你就只能死记 ResNet 的结构图。所以这篇系统讲解的主线非常明确先把反向传播讲透因为它是神经网络能够学习的根基再讲 CNN 如何针对图像结构设计最后讲 ResNet 如何解决深层网络的训练难题。这三块串起来不只是应付考试或面试而是你以后看检测、分割、跟踪这些方向的基础。2. 反向传播神经网络为什么“学得会”2.1 前向传播数据是怎样穿过网络的先从最简单的结构讲起。一个神经元接收输入做加权求和再加上偏置然后通过一个非线性激活函数输出结果。多个神经元组成一层多层堆叠就构成一个全连接神经网络。前向传播的过程可以这样理解你有一份输入数据它被逐层计算每层都完成一次“线性变换 非线性变换”。线性变换由该层的权重和偏置决定非线性变换由激活函数决定。经过若干层之后网络输出一个预测结果。如果做分类任务最后通常再接一个 softmax把输出变成各类别的概率。听起来不复杂但为什么需要这么多层因为单层的线性变换只能表达直线、平面这样的线性边界加上激活函数之后多层叠加才能逼近非常复杂的非线性函数。深度学习的“深度”价值就在这里层数越多理论上能表达的函数越复杂能学到的特征也越抽象。下面给一个只用 NumPy 实现的前向传播最小结构。它不是一个完整模型而是帮你理解一个线性层加激活函数在做什么import numpy as np def relu(x): return np.maximum(0, x) # 模拟一个单层网络: x - W - b - ReLU x np.array([0.5, -0.3, 1.2]) # 输入 W np.array([[0.2, -0.1, 0.4], [0.3, 0.5, -0.2]]) # 2个神经元, 每个有3个权重 b np.array([0.05, -0.02]) # 每个神经元的偏置 z np.dot(W, x) b # 线性变换 a relu(z) # 激活 print(线性输出 z:, z) print(ReLU输出 a:, a)这段代码虽然短但它包含了前向传播的核心数据经过权重矩阵的线性组合再加上偏置最后经过非线性函数输出。你可以把 W 理解成“这层神经元对输入各维度的关注方式”把 b 理解成“神经元自身的偏移偏好”。2.2 反向传播误差是怎么“传递责任”的有了前向传播神经网络能完成一次“预测”。但预测有对有错怎么让网络认识到错误并调整参数呢这就是反向传播要做的事。反向传播的核心是链式法则。假设最终损失是 L某一层的线性输出是 z参数是 W。我们要知道“W 对 L 有多大影响”也就是计算 ∂L/∂W。这个梯度不能直接得到但可以通过链式法则从最后一层一步步往前推先算损失对输出的梯度再算输出对 z 的梯度再算 z 对 W 的梯度然后连乘起来。用个类比可能更好理解。一条流水线上有多个环节最终产品出了问题。你要追责就得从成品开始往回查看每个环节对最终错误分别贡献了多少。反向传播做的就是这件事误差从输出端往回传每一层根据自己收到的“责任信号”去计算自己参数的梯度然后更新参数。梯度更新公式也很简单一般是param param - learning_rate * grad学习率决定每次更新步子迈多大。学习率太大参数可能在最优值附近震荡学习率太小收敛慢甚至看起来不收敛。为了真正内化反向传播强烈建议自己手推一次只有两三个神经元的例子把链式法则的每一步展开写一遍。不要只看公式而是用具体的数字手算前向传播、算损失、算反向传播到某个参数、再手动更新一次参数。这个练习做完你以后再遇到任何复杂的网络结构都会自然地追问梯度是怎么从 loss 流到这一层参数上的2.3 反向传播极容易出错的三个细节第一维度对齐。反向传播时梯度要从输出层一层层传回浅层每一层的梯度形状必须和该层参数形状一致。很多人写代码报错就是因为中间某个转置或 reshape 弄错了方向。查反向传播问题第一个要查的就是形状。第二链式法则漏项。链式法则里一个参数如果同时影响多个路径比如某些结构中对同一参数有多条依赖链就要把不同路径的梯度加起来。漏掉一条梯度就不完整。这在手推时最常见在自定义算子时也容易踩坑。第三学习率。学习率是反向传播里最敏感的超参数之一。常见的错误包括学习率设太大loss 直接爆炸成 NaN学习率设太小loss 下降极其缓慢。入门时遇到 loss 不下降先调学习率这是最便宜也最有效的排查思路。注意遇到 loss 不下降不要先怀疑模型结构按这个顺序排查数据预处理是否正确、损失函数是否选对、前向传播是否存在除零或 log 越界、学习率是否过大或过小。3. CNN从全连接到卷积改变了什么3.1 卷积层真正解决的是“参数爆炸”和“平移不变性”如果直接用全连接网络处理图像会立刻遇到一个尴尬问题参数太多。一张 256×256 的 RGB 图像输入维度是 256×256×3≈19.6 万。如果第一层有 512 个神经元那这一层的权重就是 19.6万×512≈1 亿。这个量级在计算上不现实而且非常容易过拟合。卷积层改变了思路不再让每个神经元连接到输入的所有位置而是用一个小窗口在输入上滑动共享同一组权重。这个小窗口就是卷积核。以 3×3 卷积核为例它只关注输入上一个小邻域并且在整幅图像上复用同一组参数。于是参数量大幅下降同时还保留了空间结构。另一个关键特性是平移不变性。目标出现在图像左上角和右下角用同一组卷积核去扫描都能检测到对应的局部模式。这个性质让 CNN 对物体的位置变化更鲁棒也让它在图像任务上天然比全连接网络合适。但要注意卷积神经网络也不是万能的。它适合处理具有局部相关性的网格结构数据比如图像、视频帧、一些规则排列的信号。对于没有空间结构的表格数据CNN 的优势并不明显。3.2 从特征图到感受野CNN 为什么适合图像卷积层在输入上滑动输出的是特征图。每一张特征图可以看成是“用某个卷积核在整张图上检测某种特征”的结果。浅层卷积核通常学到边缘、颜色、简单纹理深层卷积核可以组合低级特征学到更复杂的模式比如眼睛、车轮、文字局部结构。为什么深层能学到更复杂的特征因为每一层卷积都会扩大“感受野”。所谓感受野就是输出特征图上某个位置在原图上能看到多大区域。第一层卷积每个位置只能看到 3×3 邻域第二层卷积在前一层特征图基础上能间接看到更大的范围层数越多感受野越大网络就能在更大范围内组合特征。这也是为什么图像任务往往需要堆叠多层卷积而不是只用一层巨宽的卷积。一层大卷积核虽然也能扩大感受野但参数量更大而且难以通过逐层组合来获得丰富的特征层级。堆叠小卷积核在参数量和建模能力之间更平衡。代码示例用 PyTorch 构建一个非常简单的 CNN只用来理解结构不是完整训练代码。import torch import torch.nn as nn class SimpleCNN(nn.Module): def __init__(self): super().__init__() self.features nn.Sequential( nn.Conv2d(3, 16, kernel_size3, stride1, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size2, stride2), nn.Conv2d(16, 32, kernel_size3, stride1, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size2, stride2), ) self.classifier nn.Sequential( nn.Flatten(), nn.Linear(32 * 8 * 8, 10), ) def forward(self, x): x self.features(x) x self.classifier(x) return x这个示例假设输入是 32×32 的 RGB 图经过两次池化后特征图变成 8×8。实际使用时需要根据输入大小调整全连接层的输入维度。3.3 池化、激活函数、步长等常见配置理解很多初学者会纠结各种超参数组合其实关键是要理解每个东西都在干什么。卷积核大小决定一次关注多大局部区域。3×3 是最常用的小卷积核因为它计算量低、堆叠多层能有效扩大感受野很多经典网络的骨干都用它。步长决定卷积核每次滑动多远。步长为 1 时输出大小基本不变步长为 2 时输出分辨率直接减半。步长增大可以下采样但会丢失一些位置信息所以一般用池化或步长 2 的卷积做下采样各有利弊。padding 用来控制输出特征图大小。padding1 配合 3×3 卷积可以保持输入输出大小不变这样网络更容易设计梯度也更容易流动。激活函数引入非线性。没有激活函数多层线性变换可以合并成一次线性变换网络再深也表达不了复杂函数。ReLU 因为计算简单、梯度不容易消失是 CNN 里最常用的基础激活函数。池化的作用是下采样可以减小特征图分辨率、降低计算量并在一定程度上增强平移鲁棒性。池化没有需要学习的参数行为非常固定。这些配置单独看都简单组合在一起时一定要反复确认每经过一层特征图的大小和通道数变成了多少。建议养成一个习惯把每一层输出 shape 写在注释里或者用代码打印出来。这个习惯能帮你避开大量维度不匹配的报错。4. ResNet为什么网络加深反而变差了4.1 退化问题的本质不是过拟合而是梯度流动受阻一个很自然的想法是网络越深表达能力越强效果应该越好。可实际上当网络深到一定程度训练误差和测试误差都会变差而且这种变差并不是过拟合造成的。过拟合的特征是训练误差很低、测试误差高而退化问题的典型表现是网络一深训练误差本身就降不下去。这就不能怪数据不够或模型太复杂了而是深层网络本身很难训练。为什么会这样反向传播需要从损失函数一路把梯度传回浅层。经过很多层的链式求导如果每层梯度都小于 1连乘之后就会指数级衰减传到浅层时梯度已经非常小浅层参数几乎更新不动。这就是梯度消失。反过来如果梯度大于 1又有可能指数级放大导致梯度爆炸。两种问题都会让深层网络难以通过反向传播有效训练。所以退化问题的本质通常不是“深度没有意义”而是“深度让梯度无法顺畅流动”。网络结构做得再复杂如果梯度传不过去浅层学不到东西整体效果必然差。4.2 残差学习让网络学“变化量”而不是“全部”ResNet 的解决方式非常直接在网络里加一条“捷径连接”也叫 short-cut 或 skip connection把上一层的输入直接加到当前层的输出上。一个残差块可以写成输出 F(x) x其中 F(x) 是经过几层卷积、激活、归一化之后得到的“残差”x 是块输入。如果网络已经学得很好希望这一层不改变任何东西那 F(x) 只需要趋近于 0网络就可以近似恒等映射。这个目标比让 F(x) 直接逼近一个完整映射要容易得多。从梯度流的角度看残差连接也极其巧妙。反向传播时梯度不但可以从 F(x) 这条路径传回还可以从 x 这条捷径直接传回。哪怕 F(x) 内部的梯度衰减得很厉害梯度仍然可以通过捷径无损地传到浅层。这相当于给梯度开了一条高速通道缓解了深层网络的训练问题。PyTorch 里一个常见的残差块结构如下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, stride1, padding1, biasFalse) self.bn2 nn.BatchNorm2d(out_channels) self.shortcut nn.Sequential() if stride ! 1 or in_channels ! out_channels: self.shortcut nn.Sequential( nn.Conv2d(in_channels, out_channels, kernel_size1, stridestride, biasFalse), nn.BatchNorm2d(out_channels) ) def forward(self, x): out self.conv1(x) out self.bn1(out) out self.relu(out) out self.conv2(out) out self.bn2(out) out self.shortcut(x) # 残差连接 out self.relu(out) return out注意一个细节如果输入输出通道数不一致或者特征图大小不一致快捷连接就需要通过 1×1 卷积和步长调整来对齐形状。初学者复现 ResNet 时最容易在这里出错。4.3 从 ResNet 看深层网络的工程意义ResNet 真正改变了图像分类模型的深度上限。在它之前常见分类网络做到十几层、二十几层已经很吃力ResNet 出现后ImageNet 上 50 层、101 层甚至更深的网络都能稳定训练并且在分类、检测、分割等任务上全面提升了效果。为什么深层网络在工程上这么重要因为很多任务的难点恰恰在于特征层次不够丰富。浅层网络可能只学到边缘和纹理而深层网络能把边缘组合成部件把部件组合成语义最终形成更有判别力的整体特征。目标检测、语义分割这些任务都要依赖比较深的骨干网络来提取多尺度特征。但“越深越好”是有边界的。结构加深意味着参数量、计算量、显存开销同步上升如果数据量不够深层网络还更容易过拟合。工程上做选型不能只看深度还要看任务复杂度、数据规模、计算资源和延迟要求。ResNet18 在很多中小规模任务中已经足够ResNet50 更常用作中等规模任务的骨干101 层以上通常留给需要大量特征表达能力的大任务。从学习角度来看ResNet 也是最推荐用来深入理解网络结构演进的一个模型。它结构清晰几乎没有花哨操作代码实现非常流行预训练模型到处都能下载从它出发后续看 ResNeXt、DenseNet、EfficientNet甚至 Transformer 类视觉模型你的知识迁移成本会低很多。5. 从学到用一套可复用的学习路径和排查链路5.1 建议的学习顺序先跑通再复现再调参再读代码很多初学者一上来就想把最前沿的论文复现出来结果连数据集加载都调试了半天最后失去信心。我更建议采用“最小闭环”的学习方式一步步来第一步先跑通一个简单的端到端训练流程。数据集就用 MNIST、CIFAR-10 这类经典分类数据集模型就写一个两层或三层的简单 CNN训练脚本可以直接参考 PyTorch 官方示例但一定要自己敲一遍。这一阶段的目标不是上分而是让数据、模型、损失函数、优化器、训练循环这几块代码形成一个能跑通的闭环。第二步复现一个结构明确的常见模型比如 ResNet18。对照结构图把残差块、下采样、全连接层一步一步实现出来。训练时观察准确率和 loss 曲线是否正常。如果效果很差优先检查数据预处理和模型结构而不是马上调超参数。第三步在跑通的基础上做调参实验。固定一个变量比如学习率从 0.1、0.01、0.001 这些数量级分别跑一遍画 loss 曲线对比。再试 batch size、优化器、数据增强。调参不是玄学是有控制的对比实验。第四步再回去读代码。当你已经自己实现过一遍再去看开源库里的模型定义你就能看懂每一行为什么存在了。这个顺序比反过来“先读大量源码再动手”更符合人的认知规律。5.2 模型训练常见问题的排查链路刚开始训练深度学习模型有一类问题几乎人人都会遇到训练 loss 一直不降或者训练没几步就变成 NaN或者验证集准确率始终在随机水平。这时候不要慌按下面的顺序一层一层排查。第一步检查数据。是否归一化到合理范围标签是否正确输入张量的 shape 是否是 N×C×H×W如果数据本身有问题模型再正确也学不出效果。第二步缩小规模。先用少量样本比如 1 个 batch 或几十张图过拟合到训练集上。如果小样本都学不进去说明模型或训练配置有明显问题如果能过拟合再逐步放大样本量看是否出现其他问题。第三步检查模型结构。前向传播里是否存在除零是否误用 softmax 而把 logits 提前规范化了最后一层输出的维度是否和类别数一致损失函数是否用对了第四步检查优化器和学习率。学习率设置过大往往会导致 loss 跳成 NaN设置过小会导致收敛极慢。可以先把学习率设成 0.001 或 0.01再根据曲线调整。第五步检查数值稳定性。分类任务优先使用带 logits 的交叉熵函数避免自己在他方先算 softmax 再算 log否则容易产生 log(0) 或接近 0 的极端值。遇到梯度爆炸时可以考虑梯度裁剪。注意排查问题时不要一次性改多个条件。先固定模型和数据只改学习率再固定学习率和数据只修模型结构。否则出了问题你根本无法定位是哪个改动导致的。5.3 适用边界这些基础什么时候够用什么时候还要补把反向传播、CNN、ResNet 这条链路学清楚之后你能解决什么问题比较典型的有图像分类任务、常见的 backbone 理解、自己搭一个小 CNN 或 ResNet 训练模型、看懂以这些结构为基础的论文。但也要清楚边界。这套基础并不覆盖计算机视觉的全部。目标检测里还有 R-CNN、YOLO 这类结构语义分割要面对像素级预测Transformer 进入视觉之后自注意力机制和价值又带来新的训练范式更靠近工程时还要考虑模型剪枝、量化、部署、服务化。这些方向每个都需要补充额外的知识和技能。不过边界并不意味着前面学的会被推翻。恰恰相反无论是目标检测的 region proposal、Transformer 的自注意力还是各种训练技巧最终都离不开“数据流和梯度流”这一基本框架。一个模型为什么要用这样的特征提取方式梯度能不能有效传回训练时到底哪一环出了问题——这些问题通通都可以用这篇文章讲到的思维方式去推演。所以真正值得长期养成的不是记住某个网络所有的卷积核大小而是形成一种“把任何网络都拆成数据流和梯度流来看”的习惯。有了这个习惯你后面的路会顺畅很多。学习计算机视觉最难的不是某一个公式而是把零散的知识点拼成一条能自己走的链路。反向传播让网络学得动卷积让网络看得见ResNet 让网络学得深。这三样东西绝不是孤立的知识点而是同一个故事的不同章节。先把这条主线吃透再往任何方向深入你都不会迷路。