FEATURED · 精选文章

动手学深度学习24-50集:CNN主线学习笔记与代码复现踩坑

发布时间 / 2026/9/18 8:42:54
来源 / 创域科博编辑部
栏目 / 资讯中心
动手学深度学习24-50集:CNN主线学习笔记与代码复现踩坑 把《动手学深度学习》第 24 到 50 集逐集刷完并且把里面的代码亲手敲一遍、跑通、再故意改坏看它会怎么报错前后花了我差不多三周的下班时间。这套课程在深度学习入门圈子里口碑一直很稳但不少人卡在同一个位置前面十几集讲线性代数、微积分、概率跟着抄公式还跟得上一到第 24 集往后代码密度陡然上升各种名词像暴雨一样砸过来——权重衰减、dropout、批量归一化、残差连接每一个单独拎出来都够写一篇论文。我写这份学习笔记目的不是复述课件而是把第 2450 集这段最陡的爬坡路按我自己的理解重新串一遍哪几集是必须咬牙啃透的骨架哪些细节第一遍可以先跳过代码里那些看起来能跑但结果不对的坑又都在哪里。这篇笔记适合两类人一类是刚看完前 20 集、正准备往后冲的初学者另一类是做过一些深度学习项目但基础属于拼装式的想回头把 CNN 这条主线补扎实的开发者。全程我会给出可复现的代码、参数计算过程和踩坑记录你完全可以边看边在本地或云上跑。1. 第 2450 集的内容地图与学习顺序1.1 这段内容到底覆盖了哪几块知识先把范围说清楚不然后面容易迷路。以 PyTorch 版课程的主线来看第 24 集左右正好卡在线性回归从零实现附近到第 50 集基本收在 ResNet 和批量归一化这一带。中间大致可以切成四块基础模型收尾线性回归的从零实现与简洁实现、softmax 回归、多层感知机MLP这部分是把一个模型怎么训练起来这件事彻底讲透。泛化与正则化模型选择、欠拟合与过拟合、权重衰减、丢弃法、数值稳定性与参数初始化外加一个 Kaggle 房价预测的完整实战。卷积神经网络基础从卷积层、填充与步幅、多输入多输出通道、池化层一路搭到 LeNet。现代经典架构演进AlexNet、VGG、NiN、GoogLeNet、批量归一化、ResNet这条线是深度学习真正起飞的路线图。这四块不是并列关系而是层层依赖。你在正则化那一块没弄明白训练误差和泛化误差的区别到了 AlexNet 看到 dropout 就只会当成一个超参数照抄你不理解卷积输出尺寸怎么算搭 ResNet 时残差分支和主分支对不上尺寸就只能靠试错凑。1.2 为什么建议按顺序啃别跳着看我第一遍刷的时候自作聪明觉得卷积层简单直接跳到 AlexNet结果在 Inception 块那里彻底卡住——1×1 卷积降维、多分支拼接后通道数怎么变全靠猜。后来老老实实退回第 30 多集重看卷积基础二十分钟就把问题解决了。这种回去补课的时间浪费比一开始按顺序看要多得多。提示第 2450 集是一条强依赖链跳集的代价通常不是看不懂一集而是后面连续好几集都在猜。宁可慢别跳。至于学习节奏我给自己的安排是每集先 1.5 倍速看一遍听懂大意然后关掉视频自己照着讲义敲代码跑通之后再做两件事——把关键超参数改掉看结果怎么变把某个模块注释掉看它到底有没有用。这两步做完一集的内容才算真正落到手上。第 2450 集里代码量最大的是 Kaggle 房价预测和 ResNet 那两集我在这两集上各花了一个完整的周末。2. 从线性模型跨到多层感知机基础篇的收尾2.1 线性回归从零实现和简洁实现的差别在哪第 24 集附近会同时给出线性回归的两种写法很多人觉得从零实现是多余的重复其实这一集的价值恰恰在从零实现里。简洁实现用的是nn.Linear、nn.MSELoss、torch.optim.SGD这些封装好的组件三行代码就搭完了从零实现则是手动定义w、b参数手动写平方损失手动写小批量随机梯度下降的更新循环。真正要理解的是这三件事参数为什么要初始化。从零实现里w用正态分布随机初始化b置零。如果你把w也全置零所有神经元的梯度完全相同网络永远学不到东西——这个问题在后面的多层感知机里会以对称性无法打破的形式再出现一次。小批量随机梯度下降的三个超参数。batch size、学习率、迭代轮数课程里给的经验值是 batch size 取 10 左右、学习率 0.03这个组合对线性回归够用但换到 CNN 就完全不够看。损失怎么下降才算正常。课程里画的那条训练损失曲线前几十轮下降很快后面趋缓。如果你的曲线是震荡的八成是学习率太大如果几乎不动检查一下是不是忘了zero_grad()。注意optimizer.zero_grad()一定要放在loss.backward()之前、每个 batch 的开头。忘了它梯度会累加loss 会以一种看着在降但越来越离谱的方式崩掉。这个坑我替你先踩了。2.2 softmax 回归里最容易被忽略的两个细节softmax 回归解决多分类课程用 Fashion-MNIST 做例子。表面上是线性层加 softmax 再取交叉熵但有两个点必须抠清楚。第一个是为什么不直接用平方损失。softmax 输出的向量经过交叉熵梯度形式非常干净预测概率减去真实标签的 one-hot。如果用平方损失配 softmax梯度里会多出一个 softmax 导数的因子当预测概率接近 0 或 1 时这个因子趋近于零梯度消失训练几乎推不动。这不是理论洁癖是我实际改成 MSE 试过之后准确率卡在 60% 上不去的原因。第二个是标签的表示。课程后面统一用整数标签09配合CrossEntropyLoss而不是 one-hot 向量。PyTorch 的CrossEntropyLoss内部把 log-softmax 和负对数似然合在了一起数值上比先 softmax 再取 log更稳。我一开始自己写log(softmax(x))在 logits 很大的时候就会出-inf换成官方损失函数立刻安静。2.3 多层感知机补上的那块拼图多层感知机这一集的关键词是隐藏层和激活函数。线性模型堆多少层都还是线性的加了非线性激活才有表达力这句话课程里反复讲但真正让我记住的是自己动手把那层 ReLU 换成恒等映射结果模型退化成线性回归在异或这类数据上直接摆烂。激活函数的选择上有一些朴素的经验激活函数特点适用场景ReLU计算快、正区间不饱和隐藏层默认首选Sigmoid输出 01两端饱和二分类输出层隐藏层已少用Tanh输出 -11零中心早期网络、RNN 里常见隐藏层单元数也不是越大越好。课程里试过把隐藏单元从 256 加到 1024训练集拟合更快但验证集没提升反而开始过拟合。这个现象直接引出了下一块内容——模型选择与正则化。3. 模型选择与正则化三件套过拟合的对手戏3.1 训练误差、泛化误差和 K 折交叉验证这一集看似是概念课其实是整段学习里最重要的一集。核心就一句话训练误差低不代表模型好泛化误差才决定它在没见过的数据上能不能打。课程把误差拆成三块——欠拟合、正常拟合、过拟合配合多项式拟合的例子一眼就能看出三次多项式刚好、九次多项式在训练点上完美但曲线乱扭。实操里落地这件事靠的是验证集。我的习惯是拿到数据先切 6:2:2 或者 8:1:1训练集训练、验证集调超参数、测试集只在最后摸一次。很多人包括早期的我反复用测试集调参等于在测试集上过拟合报出来的数字很好看上线就拉胯。K 折交叉验证是应对数据量小的情况。课程在 Kaggle 房价预测里就用它把训练数据切成 K 份轮流留一份当验证集。K 一般取 5 或 10取太大比如等于样本数计算成本爆炸取太小比如 2方差又太大。我实测下来数据量几万条时 5 折是性价比最高的。3.2 权重衰减L2 惩罚到底在惩罚什么权重衰减L2 正则的直觉解释是让权重别太大。加在损失函数里的那一项是 $\frac{\lambda}{2}|w|^2$$\lambda$ 越大模型越倾向于把权重压小。权重小意味着函数更平滑对输入的微小扰动不那么敏感泛化更稳。PyTorch 里用torch.optim.SGD(..., weight_decay...)就能开启。这里有几个我踩过的点偏置和权重通常分开处理。偏置大一点不会导致函数剧烈扭曲所以很多实现里偏置不参与权重衰减。如果你用一个优化器统管所有参数就享受不到这个细节。$\lambda$ 不是越大越好。课程里试过几个值太大会让模型欠拟合训练误差和验证误差一起高。我的经验是从 1e-4 到 1e-2 之间扫一遍。BN 层出现后卷积层上加权重衰减的必要性下降这是后话但刷到 ResNet 时会回头验证。3.3 丢弃法 Dropout训练和推理是两个世界Dropout 是这一块最容易被用错的技术。它的机制是训练时以概率 $p$ 随机把某些神经元的输出置零推理时全部保留。很多人不知道的是为了让训练和推理的输出期望一致框架在训练时会做反向缩放也就是保留下来的值除以 $(1-p)$。以nn.Dropout(p)为例训练模式net.train()下才真正丢弃net.eval()下原样通过。我曾经把评估忘在train()模式里验证准确率忽高忽低排查了半天才想起来。这个坑非常隐蔽因为它只表现为结果不稳定不报错。提示任何评估、推理之前先敲net.eval()训练前敲net.train()。养成习惯能省下大量诡异的调试时间。我试过把 dropout 放在隐藏层之后、输出层之前效果最稳放在卷积层之间要谨慎卷积层的空间信息被随机丢弃后小网络上掉点比较明显。课程里也提到dropout 是深度网络的正则化主力但它和权重衰减不是二选一可以叠着用。3.4 数值稳定性、参数初始化和 Kaggle 实战数值稳定性那一集讲的是梯度消失和梯度爆炸深层网络里梯度在反向传播中连乘权重稍微大一点就指数爆炸稍微小一点就指数消失。解决办法之一是合理的参数初始化。课程给了 Xavier 初始化适配 sigmoid、tanh和 He 初始化适配 ReLU核心思想都是让每一层的输出方差保持一致。附一段我自己常用的初始化写法方便直接抄import torch.nn as nn def init_weights(m): if isinstance(m, nn.Linear): # He 初始化适配 ReLU nn.init.kaiming_normal_(m.weight, nonlinearityrelu) if m.bias is not None: nn.init.zeros_(m.bias) net nn.Sequential(nn.Linear(784, 256), nn.ReLU(), nn.Linear(256, 10)) net.apply(init_weights)Kaggle 房价预测那一集是这段内容里唯一的完整实战。我总结下来最有价值的三个处理是对数值型特征做标准化减均值除标准差、对标签取log1p再训练把长尾分布压平均方误差更稳、用 K 折把多折的预测取平均。第三条很多教程一笔带过但它对最终名次的提升往往比调网络结构还大。4. 卷积神经网络基础卷积、通道、池化和 LeNet4.1 卷积输出尺寸到底怎么算卷积这一块是整段内容的分水岭。先把最硬的那个公式记住能省掉无数次试错$$H_{out} \left\lfloor \frac{H_{in} 2p - k}{s} \right\rfloor 1$$其中 $H_{in}$ 是输入高$p$ 是填充$k$ 是卷积核大小$s$ 是步幅。举个例子输入 32×32卷积核 5×5填充 2步幅 1输出就是 $(324-5)/11 32$尺寸不变。如果步幅改成 2输出变成 $(324-5)/21 16$向下取整。这个式子在搭 VGG、ResNet 时会反复用到尤其是隐藏层通道翻倍、特征图减半的那几次变换。填充存在的意义是让边缘像素被公平对待。不做填充时角落的像素只参与一次卷积中间像素参与多次边缘信息被稀释。步幅则是用来降采样的但降采样现在更多交给池化或带步幅的卷积。4.2 多输入输出通道和 1×1 卷积的妙用输入通常是 3 通道RGB所以卷积核也得是 3 通道每个通道一个核最后相加。输出通道数等于卷积核的个数——这一点第一遍看很容易懵可以用一句话记一个卷积核产生一个输出通道。通道之间信息的混合方式就是那句常被忽略的话1×1 卷积等价于在通道维度上做全连接。它不改变空间尺寸只做通道融合还顺带减少参数量。GoogLeNet 里的 Inception 块先用 1×1 把通道压下来再用大卷积核算力能省掉一大半。我实测在一个小数据集上把 Inception 里的 1×1 降维去掉显存直接爆掉训练时间翻了三倍。4.3 池化层降采样但不带参数池化层最反直觉的一点是它没有可学习参数。最大池化取窗口里的最大值平均池化取平均窗口大小和步幅是超参数。它带来的好处是特征图变小、计算量下降、对位置的小扰动更鲁棒。我踩过的一个坑是把池化窗口设成 3、步幅设成 1结果特征图几乎没变小还引入了大量重叠计算纯粹浪费。常见配置是 2×2 窗口配 2 步幅尺寸正好减半。另外池化层不要滥用现代网络里很多地方用带步幅的卷积替代池化理由是卷积有参数、能学池化是固定的。4.4 LeNet第一个完整的卷积网络LeNet 是这段内容里第一个能端到端跑通的完整 CNN结构简单到可以背下来卷积 → 池化 → 卷积 → 池化 → 全连接 → 全连接 → 输出。在 Fashion-MNIST 上它能把准确率从 softmax 回归的 80% 出头推到接近 90%这个提升第一次让我直观感受到卷积提取局部特征的威力。实现上有个细节值得注意LeNet 当年用的激活函数是 sigmoid 或 tanh不是 ReLU。你把它换成 ReLU收敛会更快。这不是课程让你改而是我实际动手对比之后发现的——换 ReLU 后前几个 epoch 的 loss 下降明显更陡。import torch.nn as nn net nn.Sequential( nn.Conv2d(1, 6, kernel_size5, padding2), nn.Sigmoid(), nn.AvgPool2d(kernel_size2, stride2), nn.Conv2d(6, 16, kernel_size5), nn.Sigmoid(), nn.AvgPool2d(kernel_size2, stride2), nn.Flatten(), nn.Linear(16 * 5 * 5, 120), nn.Sigmoid(), nn.Linear(120, 84), nn.Sigmoid(), nn.Linear(84, 10) )注意那个nn.Flatten()之前的特征图大小是 16×5×5这是前面两次池化把 28×28 降下来的结果。你如果改了输入尺寸或者池化参数这里的全连接输入维度必须跟着改否则会报维度不匹配的错误。这个报错在第 2450 集里出现的频率极高基本是新手第一个卡点。5. 现代 CNN 架构演进从 AlexNet 走到 ResNet5.1 AlexNet 和 VGG深度开始起作用AlexNet 的意义在于它证明了深 大 数据 GPU这条路线可行。相比 LeNet它的变化是更深的网络、ReLU 替代 sigmoid、加入 dropout、数据增强、用最大池化。我第一次在 Fashion-MNIST 上复现简易版 AlexNet训练时间比 LeNet 长了三四倍但准确率能到 92% 上下。VGG 的核心贡献是把深度这件事模块化了。它用重复的卷积块多个 3×3 卷积后接一个 2×2 池化拼接通道数按 64、128、256、512 翻倍。两个 3×3 卷积堆叠的感受野等于一个 5×5但参数更少、非线性更多这是 VGG 块设计背后的关键理由。我建议把 VGG 块单独抽成一个函数写起来清爽def vgg_block(num_convs, in_channels, out_channels): layers [] for _ in range(num_convs): layers.append(nn.Conv2d(in_channels, out_channels, kernel_size3, padding1)) layers.append(nn.ReLU()) in_channels out_channels layers.append(nn.MaxPool2d(kernel_size2, stride2)) return nn.Sequential(*layers)5.2 NiN 和 GoogLeNet在通道维度上做文章NiN 提出了两个后来被广泛使用的想法用 1×1 卷积替代全连接层以及用全局平均池化把特征图直接压成类别数。全局平均池化的好处是参数量骤减还顺带缓解过拟合。GoogLeNet 则把用多大卷积核这个问题交给网络自己选Inception 块里 1×1、3×3、5×5 分支和池化分支并行输出在通道维拼接。这一块最容易搞错的是拼接后的通道数。假设四个分支各自输出 24、64、64、32 个通道那拼接后就是 184 个通道下一层输入必须写 184。我建议在 Inception 块里用变量记下每个分支的通道数或者干脆写个小函数算别硬编码。5.3 批量归一化让深层网络能训得动批量归一化BatchNorm是整段内容里我最喜欢的一集。它做的事很朴素在每个 mini-batch 上把某一层的输入按 batch 维度算均值和方差归一化到均值 0、方差 1再用两个可学习参数 $\gamma$、$\beta$ 做缩放平移。训练时用的是当前 batch 的统计量推理时用的是训练过程中累积的移动平均。它带来的好处有三层允许更大的学习率、降低对初始化的敏感度、本身带一点正则效果所以用 BN 后可以适当减小 dropout。位置上有讲究一般放在卷积层之后、激活函数之前。放错了顺序效果会打折扣。注意batch size 特别小时比如 2、4BN 的统计量估计非常不准训练会不稳定。这种情况考虑用 LayerNorm 或 GroupNorm但那是后面的内容了。5.4 ResNet残差连接为什么能救深层网络ResNet 解决的是越深越差的退化问题。它引入残差块输出等于 $f(x) x$$f(x)$ 是几层卷积$x$ 是恒等映射。关键洞察是——如果这几层什么都学不到至少还能把输入原样传下去最差也就退化成浅网络不会比它更差。实现残差块有几个必须注意的点当输入输出通道数不一致或特征图尺寸因为步幅改变时恒等映射那条路要加一个 1×1 卷积把 $x$ 变换到相同形状否则加法会因为维度不匹配报错。BN 通常放在每个卷积之后、ReLU 之前。残差块的输出经过加法后再过一次 ReLU这条主干是在块外面处理的。我照着课程实现的 ResNet-18 简易版在没有 GPU 的机器上跑 Fashion-MNIST第一次明显感觉到深网络能训起来了。它和 dropout、BN、He 初始化配合起来才构成一个稳定的训练组合。6. 实操踩坑备忘与问题排查6.1 环境和显存相关的几个现实问题第 2450 集里涉及的模型从几万参数涨到上千万参数环境的压力是逐步上来的。我用过的方案有本地 CPU、本地单卡和云上租的实例各自都有脾气。CPU 跑 LeNet 还行跑到 ResNet 就慢到没法调参云上实例方便但要注意断开前把 checkpoint 存下来我就丢过一次跑了两个小时的训练结果。显存不够的报错几乎人人都会遇到。常见的缓解手段按性价比排序调小 batch size、用混合精度、把不需要梯度的验证阶段裹进torch.no_grad()、及时把临时张量移出显存。这里最容易被忽略的是第三条验证阶段如果忘了关闭梯度显存占用可能是训练阶段的好几倍。6.2 loss 不降、准确率不动的排查顺序我把这段内容里遇到过的训练异常整理成一张速查表按从最可能到最不可能排序能省时间现象可能原因排查动作loss 完全不动学习率为 0、梯度被断开打印几个梯度看是否全 0loss 先降后爆学习率过大、梯度爆炸调小学习率、加梯度裁剪训练准但验证差过拟合加 dropout、权重衰减、数据增强训练和验证都差欠拟合或实现有 bug检查维度、损失函数、标签范围准确率卡在随机水平标签没对齐、输出层写错用小批量数据手动验证一遍我印象最深的一次是验证准确率一直卡在 10%十分类等于瞎猜查了一个小时才发现是Flatten()之后全连接层的输入维度写错模型把特征图当成了别的东西但形状恰好能对上不报错只是学不会。这种能跑但错的 bug 最耗时间所以每写一步都建议打印一次张量形状。6.3 我自己的复现节奏和一点经验刷完这 27 集最大的感受是这套课程的代码只占一半价值另一半在那些为什么不这么做的解释里。比如为什么用交叉熵不用 MSE为什么 BN 放在激活之前为什么残差连接能解决退化——这些问题如果你只看代码是看不出来的得跟着讲解自己动手验证一遍。我给自己定的复现节奏是一集一个 notebook代码分三块——原样实现、关键改动实验、踩坑记录。三周下来这些 notebook 比任何笔记软件里的总结都有用因为它们本身就是可运行的。至于 epoch、batch size、学习率这些超参数我给不出通用答案唯一靠谱的做法是拿一小份数据先跑几轮看 loss 曲线的形状陡就调小学习率平就调大然后带着这个直觉去跑全量。最后说一个我经常用的小技巧当你不确定某个层有没有起作用时把它临时换成恒等映射或者干脆删掉看指标变化多大。变化大说明它在承担关键职责变化不大说明它可以被简化。这个消融式的调试思路比盯着代码猜要快得多我在 ResNet 上验证残差分支和 BN 的作用时都靠它。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻