ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

从零掌握CNN核心:前向传播、反向传播与ResNet残差网络

2026/8/30 4:36:49 拓冰建站 浏览量
从零掌握CNN核心:前向传播、反向传播与ResNet残差网络 计算机视觉这几年真正被广泛使用靠的并不是某一个花哨技巧而是三个基础能力被逐一解决网络怎么把图像变成特征误差怎么沿着网络传回去更新权重网络怎么在不退化的情况下堆得足够深。如果你正处在“看论文能看懂公式但自己动手训练总出问题”的阶段这篇文章就是把这三块地基重新打一遍。这篇文章不做高屋建瓴的总结核心是计算。我会从前向传播的矩阵计算开始一步一步推导反向传播的链式法则再进入卷积层、池化层和特征图的尺寸计算最后详细拆解 ResNet 的残差块到底改了什么、为什么能几十层上百层地训练。每一步都配 PyTorch 代码或可直接复现的最小实验你可以在本地 CPU 上跑通。1. 知识模块速览与前置要求先把整篇文章的知识点结构列出来方便你按需阅读。知识模块核心问题配套实验前向传播神经网络如何把输入变成预测结果PyTorch 手动搭建全连接网络反向传播损失函数如何倒推出每个参数的梯度自动求导与手写梯度对比卷积层卷积操作如何提取图像局部特征输出尺寸计算与可视化池化层特征图降采样为什么能提升鲁棒性不同池化方式对比经典 CNN 架构LeNet 到 VGG 的演进逻辑模型定义与参数量统计ResNet残差连接如何解决深层网络退化普通网络与 ResNet 收敛对比前置条件很低会 Python 基础语法装了 PyTorch机器有 4GB 以上内存就行。显存不是必须的本文所有实验都可以用 CPU 跑只是训练时间会慢一些。如果你用的是 NVIDIA 显卡装上 CUDA 版 PyTorch 后训练会快很多。2. 神经网络基础从线性变换到非线性拟合2.1 单个神经元做了什么神经网络的基本单位是神经元它可以理解为一次线性变换加一次非线性激活$$ z W^T x b $$$$ a f(z) $$其中 $x$ 是输入向量$W$ 是权重$b$ 是偏置$f$ 是激活函数。如果只有线性变换那么无论堆多少层最终表达能力都等价于一层线性变换这显然无法处理图像分类这类复杂问题。非线性的激活函数才让网络有了拟合复杂函数的能力。最常见的激活函数有三个Sigmoid$\sigma(z) 1/(1e^{-z})$输出范围 0 到 1但两端梯度趋近于 0容易出现梯度消失。Tanh输出范围 -1 到 1同样存在饱和区梯度消失的问题。ReLU$ReLU(z)max(0,z)$正区间梯度恒为 1负区间梯度为 0是目前 CNN 中最常用的激活函数。ReLU 之所以成为默认选择原因很直接正区间梯度不衰减反向传播时梯度可以顺利穿过很多层缓解深层网络训练困难。负区间梯度为 0 虽然会让部分神经元死亡但在实践中通常不会影响整体性能反而带来稀疏性。2.2 全连接网络的矩阵形式一个两层的全连接网络可以写成$$h_1 ReLU(W_1 x b_1)$$$$h_2 ReLU(W_2 h_1 b_2)$$$$\hat{y} softmax(W_3 h_2 b_3)$$这里 $W_1$ 的维度是[输入维度, 隐藏层维度]$W_2$ 的维度是[隐藏层维度, 隐藏层2维度]$W_3$ 的维度是[隐藏层2维度, 类别数]。在 PyTorch 中这个过程可以这样表示import torch import torch.nn as nn class MLP(nn.Module): def __init__(self, input_dim784, hidden_dim128, num_classes10): super().__init__() self.fc1 nn.Linear(input_dim, hidden_dim) self.fc2 nn.Linear(hidden_dim, hidden_dim) self.fc3 nn.Linear(hidden_dim, num_classes) self.relu nn.ReLU() def forward(self, x): x self.relu(self.fc1(x)) x self.relu(self.fc2(x)) x self.fc3(x) return x2.3 输出层与损失函数分类任务中输出层通常接 softmax把网络输出变成概率分布$$p_i \frac{e^{z_i}}{\sum_{j} e^{z_j}}$$然后使用交叉熵损失衡量预测分布和真实标签的差异$$L -\log p_{y}$$其中 $y$ 是真实类别。交叉熵的梯度形式非常简洁对于 softmax 加交叉熵的组合输出层梯度等于预测概率减 one-hot 标签。这个结论在面试中经常出现建议自己推导一遍。3. 反向传播误差如何变成权重更新3.1 链式法则才是核心训练神经网络的目标是让损失 $L$ 最小化。损失是网络输出的函数网络输出又是每一层权重和输入的复合函数所以求梯度时要用到微积分中的链式法则。以两层网络为例假设损失对输出层 $z_3$ 的梯度已知记为 $\delta_3 \partial L / \partial z_3$那么$$\frac{\partial L}{\partial W_3} \delta_3 h_2^T$$$$\frac{\partial L}{\partial h_2} W_3^T \delta_3$$$$\delta_2 \frac{\partial L}{\partial h_2} \odot ReLU(z_2)$$$$\frac{\partial L}{\partial W_2} \delta_2 h_1^T$$这里 $\odot$ 表示逐元素相乘。整个过程就是从输出层开始逐层向前计算误差项 $\delta$再根据误差项计算每个权重的梯度。3.2 一个手算例子假设输入 $x2$权重 $w3$偏置 $b1$真实标签 $y1$使用均方误差损失没有激活函数。前向传播$$z 3 \times 2 1 7$$损失$$L \frac{1}{2}(7-1)^2 18$$反向传播$$\frac{\partial L}{\partial z} z - y 6$$$$\frac{\partial L}{\partial w} \frac{\partial L}{\partial z} \cdot x 6 \times 2 12$$$$\frac{\partial L}{\partial b} 6$$如果学习率设为 0.1那么 $w$ 更新为 $3 - 0.1 \times 12 1.8$$b$ 更新为 $1 - 0.1 \times 6 0.4$。重复这个步骤损失会逐渐下降。3.3 PyTorch 中的自动求导PyTorch 通过autograd自动完成上述过程但你仍然需要理解背后的计算逻辑否则无法排查梯度异常。import torch x torch.tensor(2.0, requires_gradTrue) w torch.tensor(3.0, requires_gradTrue) b torch.tensor(1.0, requires_gradTrue) y torch.tensor(1.0) z w * x b loss 0.5 * (z - y) ** 2 loss.backward() print(grad w:, w.grad.item()) # 12.0 print(grad b:, b.grad.item()) # 6.03.4 梯度下降与优化器拿到梯度后最简单的更新方式是梯度下降$$w_{t1} w_t - \eta \frac{\partial L}{\partial w_t}$$实际训练中很少直接用原始梯度下降而是使用携带动量或自适应学习率的优化器SGD Momentum累积历史梯度方向减少震荡加速收敛。RMSProp按梯度平方的滑动平均缩放学习率。Adam同时使用一阶矩和二阶矩估计是日常训练中默认选择。3.5 梯度消失与梯度爆炸反向传播是连乘过程。如果每一层梯度的模都小于 1乘很多层后梯度会指数级衰减这就是梯度消失如果每一层梯度都大于 1梯度会指数级增大这就是梯度爆炸。Sigmoid 激活函数是梯度消失的典型推手因为它的导数最大只有 0.25。ReLU 正区间导数为 1天然缓解了这个问题。这就是为什么现代 CNN 几乎不再使用 Sigmoid 作为隐藏层激活函数。4. 卷积神经网络从全连接到局部连接4.1 为什么图像不能用全连接直接处理一幅 224×224 的 RGB 图像展开后是 150528 维向量。如果第一层隐藏层有 1024 个神经元这一层的参数数量就接近 1.5 亿。这个参数量在训练时不仅计算量大还非常容易过拟合。更关键的是全连接层破坏了图像的空间结构——图像中相邻像素的关系是最重要的信息展开成向量后这种邻居关系就丢失了。卷积神经网络的核心思想是局部连接 权重共享。卷积核只查看输入的一个局部窗口同时同一个卷积核在整张图上滑动参数数量大幅下降而且能提取到平移不变的特征。4.2 卷积操作的计算过程二维卷积的核心公式可以理解为把卷积核和输入图像的局部区域做逐元素乘法并求和。假设输入特征图是 5×5卷积核是 3×3步幅为 1不做填充则输出尺寸为$$output_size \frac{input - kernel}{stride} 1 \frac{5 - 3}{1} 1 3$$输出是 3×3 的特征图。计算第一个位置时卷积核与输入左上角 3×3 区域逐元素相乘再求和得到输出第一个元素然后向右滑动步幅继续计算。4.3 填充与步幅填充padding的作用是控制输出尺寸并保留边缘信息。加一圈零填充后5×5 输入在 3×3 核、步幅 1 的情况下输出仍为 5×5$$output_size \frac{input 2 \times padding - kernel}{stride} 1 \frac{5 2 - 3}{1} 1 5$$步幅增大可以降低特征图分辨率例如输入 5×5、核 3×3、步幅 2 时$$output_size \frac{5 - 3}{2} 1 2$$在实际网络中padding 通常设为kernel_size // 2保证输出尺寸不缩小。步幅通常只在网络的前几层设为 2用于快速降分辨率。4.4 多通道卷积与输出通道一张彩色图像有 3 个通道卷积核也需要有相同的通道数。假设输入是 $H \times W \times C_{in}$一组卷积核维度是 $k \times k \times C_{in}$该组卷积核在输入所有通道上做卷积并求和得到一个通道的输出。如果有 $C_{out}$ 组这样的卷积核输出通道数就是 $C_{out}$。PyTorch 中定义卷积层import torch.nn as nn # 输入 3 通道输出 64 通道卷积核 3x3padding 1 conv nn.Conv2d(in_channels3, out_channels64, kernel_size3, stride1, padding1)这个卷积层的参数量是$$3 \times 64 \times 3 \times 3 64 1792$$即使输入图像是 224×224卷积层参数也不变因为卷积核只在局部滑动参数量与输入尺寸无关。这就是权重共享带来的直接收益。4.5 池化层池化层是下采样操作常用两种最大池化取窗口内的最大值保留最显著特征适合边缘和纹理提取。平均池化取窗口内平均值保留整体信息。池化层没有需要学习的参数只改变特征图尺寸。例如 2×2 最大池化会把特征图高和宽各缩小一半。PyTorch 定义如下nn.MaxPool2d(kernel_size2, stride2)池化的作用是降低计算量、扩大感受野并提供一定平移不变性。现代 CNN 中池化层的地位有所下降部分架构使用步幅为 2 的卷积层代替池化但理解池化仍然是掌握 CNN 结构的基础。4.6 感受野感受野指的是输出特征图上某个点能“看到”输入图像上的多大区域。第一层卷积输出的每个点看到的是卷积核大小区域例如 3×3。第二层卷积输出的每个点看到的是第一层特征图的 3×3 区域对应原图就是 5×5 区域。感受野越大高层特征包含的上下文信息越多。这也是为什么分类网络会在浅层保留较高分辨率、在深层逐步降低分辨率并扩大通道数。5. 经典 CNN 架构演进从 LeNet 到 VGG5.1 LeNet-5卷积网络的起点LeNet-5 是 Yann LeCun 在 1998 年提出的手写数字识别网络结构非常清晰卷积层5×5 卷积核输出 6 个通道池化层2×2 平均池化卷积层5×5 卷积核输出 16 个通道池化层2×2 平均池化全连接层120 → 84 → 10LeNet-5 确立了“卷积提取特征 全连接分类”的基本范式后续的 CNN 架构基本都遵循这个思路。5.2 AlexNet深度学习引爆点AlexNet 在 2012 年 ImageNet 竞赛中大幅领先它的关键改进包括使用 ReLU 激活函数加速收敛并缓解梯度消失。使用 Dropout 防止过拟合。使用数据增强扩充训练样本。使用两块 GPU 并行训练。AlexNet 证明了深层卷积网络在大规模图像识别任务上的有效性是深度学习热潮的起点。5.3 VGG简洁的堆叠范式VGG 的核心贡献是用多个连续的小卷积核替换一个大卷积核。两个 3×3 卷积核堆叠等价于一个 5×5 卷积核的感受野但参数量更少$$2 \times (C \times C \times 3 \times 3) 18C^2$$$$1 \times (C \times C \times 5 \times 5) 25C^2$$同时连续的小卷积核之间插入 ReLU 非线性网络表达能力更强。VGG 的结构非常简洁多个 3×3 卷积块加最大池化特征图尺寸逐级减半通道数逐级翻倍。缺点是参数主要集中在最后几个全连接层总参数量约 1.38 亿存储开销较大。5.4 GoogLeNet多尺度并行GoogLeNet 提出了 Inception 模块在同一层并联 1×1、3×3、5×5 卷积和池化再把结果拼接。1×1 卷积先降维大幅减少计算量。它证明了可以通过精巧的结构设计在降低参数量的同时提升表达效果。到这里CNN 的基本设计范式已经完整卷积提取局部特征池化降采样扩大感受野堆叠增加深度最终全连接或全局池化分类。但单纯增加深度很快遇到了瓶颈这就是 ResNet 出现的原因。6. ResNet残差网络如何解决深层训练问题6.1 深层的困境退化问题直觉上网络越深表达能力越强但实验发现当网络深度超过一定范围后训练误差和测试误差反而升高。这个现象不是过拟合而是退化问题深层网络难以找到恒等映射导致优化困难。换句话说如果 20 层网络已经能达到较好的效果理论上 56 层网络应该至少不比 20 层差因为多出的 36 层完全可以学习恒等映射。但梯度下降算法很难优化出这种恒等映射深层网络在训练过程中会陷入局部最优。6.2 残差学习改变学习目标ResNet 的思路是不直接让每一层学习目标映射 $H(x)$而是学习残差 $F(x) H(x) - x$然后通过短路连接把输入 $x$ 直接加到输出上$$y F(x, {W_i}) x$$其中 $F$ 通常是两层卷积加 ReLU。如果目标映射 $H(x)$ 接近恒等映射那么残差 $F(x)$ 接近 0网络只需要学习一个接近 0 的输出优化难度大幅降低。这就是残差块的核心思想。PyTorch 中一个基础残差块可以这样实现import torch 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.conv2 nn.Conv2d(out_channels, out_channels, kernel_size3, stride1, padding1, biasFalse) self.bn2 nn.BatchNorm2d(out_channels) self.relu nn.ReLU(inplaceTrue) # 输入输出通道或尺寸不一致时用 1x1 卷积调整 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): identity self.shortcut(x) out self.conv1(x) out self.bn1(out) out self.relu(out) out self.conv2(out) out self.bn2(out) out identity out self.relu(out) return out6.3 ResNet 的网络结构ResNet 由多个残差块堆叠而成常见变体如下变体层数残差块类型典型使用场景ResNet-1818BasicBlock两层 3×3快速实验、轻量任务ResNet-3434BasicBlock两层 3×3中等规模任务ResNet-5050Bottleneck1×1、3×3、1×1大规模分类、迁移学习ResNet-101101Bottleneck更高精度任务ResNet-152152Bottleneck极限精度任务Bottleneck 结构先用 1×1 卷积降低通道数再用 3×3 卷积提取特征最后用 1×1 卷积恢复通道数。这样可以在增加深度的同时控制计算量。以 ResNet-18 为例整体结构是7×7 卷积64 通道步幅为 2接 3×3 最大池化4 个残差阶段每阶段包含 2 个 BasicBlock阶段间特征图尺寸减半通道数依次为 64、128、256、512最后接全局平均池化和全连接层关键设计每个阶段第一个残差块的 stride 为 2负责降采样通过 1×1 卷积调整短路连接保持输入输出形状一致。6.4 ResNet 为什么有效从反向传播角度看残差连接让梯度多了一条“高速公路”。普通网络反向传播时梯度要经过每一层的卷积和激活函数连乘ResNet 中恒等映射路径上梯度可以直接回传到浅层避免了连乘导致的梯度消失。从优化角度看残差结构让网络层与层之间有了“保底”能力。即使新增的层没有学到有用的特征输入也能通过短路连接直接传递到后面至少不劣化。这相当于把深层网络分解成多个浅层网络的集成降低了优化难度。从理论角度看ResNet 加宽了有效路径的多样性。每一步都可以选择通过卷积路径或短路路径网络实际上是在学习一条最优路径组合。6.5 ResNet 的改进方向ResNet 之后出现了很多变体主要是围绕解决它的两个问题编码信息能力不足DenseNet 把每一层特征图都传给后面的所有层特征复用更强。通道信息利用不充分ResNeXt 在残差块中引入分组卷积增加基数而不过多增加参数量。动态路径选择SENet 对通道加注意力权重决定每个通道的重要程度。训练稳定性改进PreAct ResNet 把 BN 和 ReLU 移到卷积前进一步改善梯度流动。这些变体的基础仍然是对残差结构的理解所以学会 ResNet 后再看它们会轻松很多。7. 实验验证普通网络与 ResNet 的收敛对比下面给出一个可以在 CPU 上运行的最小实验用 CIFAR-10 数据集对比一个普通 18 层卷积网络和一个带残差连接的 18 层网络。import torch import torch.nn as nn import torchvision import torchvision.transforms as transforms device cuda if torch.cuda.is_available() else cpu transform transforms.Compose([ transforms.RandomCrop(32, padding4), transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2470, 0.2435, 0.2616)) ]) train_dataset torchvision.datasets.CIFAR10( root./data, trainTrue, downloadTrue, transformtransform ) train_loader torch.utils.data.DataLoader( train_dataset, batch_size64, shuffleTrue, num_workers0 ) def conv_block(n_in, n_out, stride1): return nn.Sequential( nn.Conv2d(n_in, n_out, kernel_size3, stridestride, padding1, biasFalse), nn.BatchNorm2d(n_out), nn.ReLU(inplaceTrue) ) # 不带残差的普通 18 层网络 class Plain18(nn.Module): def __init__(self, num_classes10): super().__init__() self.features nn.Sequential( conv_block(3, 64), conv_block(64, 64), conv_block(64, 64, stride2), conv_block(64, 64), conv_block(64, 128, stride2), conv_block(128, 128), conv_block(128, 128, stride2), conv_block(128, 256), conv_block(256, 256, stride2), conv_block(256, 512), conv_block(512, 512), ) self.pool nn.AdaptiveAvgPool2d((1, 1)) self.fc nn.Linear(512, num_classes) def forward(self, x): x self.features(x) x self.pool(x) x x.view(x.size(0), -1) return self.fc(x) # 带残差的 ResNet-18 简化版 class ResNet18(nn.Module): def __init__(self, num_classes10): super().__init__() self.stem conv_block(3, 64) self.layer1 nn.ModuleList([BasicBlock(64, 64) for _ in range(2)]) self.layer2 nn.ModuleList([BasicBlock(64, 128, stride2), BasicBlock(128, 128)]) self.layer3 nn.ModuleList([BasicBlock(128, 256, stride2), BasicBlock(256, 256)]) self.layer4 nn.ModuleList([BasicBlock(256, 512, stride2), BasicBlock(512, 512)]) self.pool nn.AdaptiveAvgPool2d((1, 1)) self.fc nn.Linear(512, num_classes) def forward(self, x): x self.stem(x) for layer in [self.layer1, self.layer2, self.layer3, self.layer4]: for block in layer: x block(x) x self.pool(x) x x.view(x.size(0), -1) return self.fc(x)训练时分别打印两个模型的训练损失你会观察到前几个 epochResNet 的损失下降并不一定比 Plain18 快很多。随着训练推进Plain18 的训练损失下降变慢或者震荡ResNet 的损失能持续下降。最终验证集准确率上ResNet 通常比 Plain18 高 3 到 5 个百分点。这个实验在 CPU 上跑 10 个 epoch 可能需要 20 到 40 分钟建议先把 batch size 调小到 32并只验证前 5000 张训练图片先确认代码跑通再跑完整训练。8. 实践操作训练过程可视化与调试手段8.1 观察梯度是否消失只看损失曲线很难定位问题。更有效的办法是直接查看每个参数的梯度范数。训练循环中加入for name, param in model.named_parameters(): if param.grad is not None: print(name, param.grad.norm().item())如果浅层参数的梯度范数明显小于深层参数甚至接近 0说明梯度传播受阻。此时优先检查是否使用了合适的激活函数、是否添加了 BatchNorm、是否需要引入残差连接。8.2 观察中间特征图把模型某一层输出保存下来用 matplotlib 可视化卷积后的特征图。浅层特征图通常保留边缘、纹理等结构信息深层特征图则更抽象难以直观解释。这个观察对判断卷积核是否学到有效特征很有帮助。8.3 控制变量排查问题训练过程中如果损失不下降按照以下顺序排查先跑一批数据检查输出形状和 loss 是否有限值。用一个 batch 过拟合测试把训练步数设为 200观察 loss 是否降到接近 0。如果连一个小批量都过拟合不了说明代码或模型结构有问题。检查学习率学习率过大导致 loss 震荡甚至变成 NaN学习率过小导致收敛极慢。如果使用预训练模型检查预处理是否一致特别是 mean 和 std 的归一化数值。9. 常见问题与高频考点下面是学习计算机视觉基础时最常遇到的概念问题和排查思路也是面试笔试中经常出现的考点。问题核心解释排查/回答要点为什么 ReLU 比 Sigmoid 更适合深层网络Sigmoid 导数最大 0.25连乘后梯度消失ReLU 正区间导数为 1从梯度消失角度解释卷积层参数量怎么计算$C_{in} \times C_{out} \times k \times k C_{out}$注意 bias 是否使用输出特征图尺寸怎么算$\frac{input 2 \times padding - kernel}{stride} 1$注意 padding 和 stride 的数值池化层的作用是什么降采样、扩大感受野、提供平移不变性池化层无可学习参数ResNet 解决什么问题网络加深导致的退化问题即训练误差升高用恒等映射和梯度流动解释残差块中为什么要用 1×1 卷积做 shortcut调整通道数或尺寸让恒等映射路径能够相加通道不一致时必备梯度消失怎么解决使用 ReLU、BatchNorm、残差连接、合理初始化从激活函数和网络结构角度回答为什么 50 层以上用 Bottleneck减少计算量先降维再升维计算 FLOPs 差异预训练 ResNet 如何迁移到新任务去掉全连接层保留卷积层作为特征提取器替换分类头冻结浅层、微调深层10. 最佳实践与学习路径建议10.1 理论学习建议不要跳过数学公式直接看代码。前向传播和反向传播的推导是后续阅读论文、调试网络、理解优化器都无法绕开的基础。建议至少完整手推一次两层网络的反向传播把链式法则每一步写清楚。动手推导之后再看 PyTorch 的autograd就会觉得它是替你做了这些计算而不是一个黑盒。10.2 实验验证建议先用 MNIST 跑通一个最小全连接分类网络确认前向传播和反向传播理解正确。再用 CIFAR-10 跑一个 CNN观察卷积核输出和池化效果。最后用普通 18 层网络和 ResNet-18 对比训练曲线。每次修改只改一个变量激活函数、卷积核大小、padding、stride、学习率、batch size、优化器。10.3 工程化建议在单机上训练时先把数据加载器num_workers调大减少 GPU 等待时间。使用torch.utils.tensorboard或wandb记录训练指标避免只靠终端输出判断模型效果。固定随机种子保证实验结果可复现。保存模型时同时保存 optimizer state方便断点续训。加载预训练 ResNet 进行迁移学习时先冻结整个 backbone只训练分类头确认流程跑通后再解冻浅层或者全量微调。10.4 下一步学习方向掌握反向传播、CNN 和 ResNet 之后可以继续深入以下方向目标检测Faster R-CNN、YOLO 系列理解 anchor 和特征金字塔。语义分割U-Net、DeepLab理解编码器-解码器结构。自监督学习SimCLR、MoCo理解对比学习和预训练范式。Vision TransformerViT 用 attention 替代卷积理解为什么它在某些大规模任务上能超越 CNN。这篇文章的核心内容建议反复阅读两遍第一次整体了解概念第二次边读边在笔记本上跑代码。反向传播的数学推导、卷积输出尺寸的计算、残差块的实现逻辑这三件事搞清楚了计算机视觉的基础就立住了。