PyTorch深度学习实践笔记:从环境搭建到CNN实战的避坑索引
1. 为什么我要整理这份Pytorch实践笔记索引动手写这个目录之前我把刘二大人的《Pytorch深度学习实践》系列前前后后刷了三遍。第一遍1.5倍速过剧情第二遍0.75倍速记代码第三遍直接关掉视频凭记忆复现遇到卡壳的地方再定点回看。三遍下来最深的感受是这套课程的内容密度极高但知识点散落在几十个视频里没有一个统一的索引复习时找某个具体操作比如DataLoader的num_workers到底怎么设要来回翻好几个视频效率很低。所以这份笔记目录不是简单的视频列表搬运而是我按自己的学习路径重新编排的知识地图。每一节都标注了这个知识点在整个深度学习体系里的位置、它解决什么问题、和前后知识点的关联以及我在实操中踩过的坑。对于刚接触Pytorch的朋友你可以把它当作课程学习的导航仪按图索骥地推进对于已经入门的同学这份索引更适合当速查手册哪个参数忘了怎么调、哪个API不确定用法直接定位到对应章节快速回顾。整套笔记覆盖了从环境搭建到CNN实战的完整链路代码全部基于Pytorch 2.x版本实测通过。需要提前说明的是刘二大人的课程偏重“从零手写”来理解底层逻辑而不是直接调库。这个理念贯穿整套笔记——我们不会一上来就用torchvision.models.resnet50()而是先自己用nn.Module把卷积、池化、全连接一层层搭出来理解每一层的输入输出维度怎么算、梯度怎么回传。这个过程会稍微“笨”一点但走完一遍之后再看那些封装好的高级API你会清楚地知道它们在背后做了什么。这套笔记适合的人群有Python基础但没接触过深度学习框架的在校学生、需要快速上手Pytorch做科研的转行开发者、以及上过理论课但不知道怎么把公式变成代码的自学者。如果你已经能熟练用Keras或TensorFlow搭建模型这份索引也可以帮你快速建立起Pytorch的思维方式。2. 笔记整体框架与学习路线设计2.1 从“线性模型”到“CNN”的递进逻辑整套课程的设计思路非常清晰走的是“线性模型 → 梯度下降 → 反向传播 → 神经网络 → CNN”的递进路线。这个顺序不是随便排的背后有很强的教学考量。先看线性模型这一章。它用最简单的y w*x b作为切入点让你先理解“模型”到底是什么——本质上就是一个从输入到输出的映射函数而“训练”就是找到让这个映射最贴合数据的参数。这个阶段代码量极少重点在于建立直觉损失函数怎么定义、为什么用均方误差、参数更新方向怎么确定。接着进入梯度下降。这里开始涉及优化算法的核心——沿着损失函数的梯度反方向走逐步逼近最小值。课程从最朴素的批量梯度下降讲起然后引入随机梯度下降SGD和小批量梯度下降解释为什么实际训练中几乎不用全量梯度下降计算量太大也不用单样本SGD震荡太剧烈。反向传播是整个课程的第一个难点。刘二大人用计算图的方式把链式法则拆解得非常细从最简单的两层网络开始手推每个节点的梯度。这一步绝对不能跳过因为后面所有的loss.backward()都是在做这件事只是Pytorch帮你自动完成了。如果你不理解计算图调backward()的时候就像在盲人摸象。神经网络章节开始引入nn.Module和nn.Linear把前面手写的参数封装成层。这里的关键跃迁是学会用类的方式组织模型结构以及理解forward()函数的作用。到了CNN部分则是把全连接层替换成卷积层和池化层核心要掌握的是特征图尺寸的推导公式、通道数的变化规律以及nn.Conv2d那几个参数到底怎么配。2.2 每个阶段的核心产出物我给自己每个阶段都设定了明确的产出目标有产出才有反馈有反馈才有动力继续往下走学习阶段核心产出物检验标准线性模型手写forward()和loss()函数能解释为什么用(y_pred - y)^2而不是绝对值梯度下降手动实现参数更新循环能画出损失随epoch下降的曲线反向传播用计算图推导两层网络的梯度手推结果与backward()输出一致神经网络用nn.Module搭建三分类网络测试集准确率超过85%CNN搭建含两个卷积层的图像分类器理解每层特征图尺寸的变化这个表格我在笔记本上贴了一份每完成一个阶段就打个勾。实测下来这种“产出物驱动”的学习方式比单纯看视频有效得多因为你看完视频产生的“我懂了”的感觉往往是错觉只有真正把代码写出来跑通了才算真的掌握。2.3 哪些章节可以跳过哪些必须反复看根据我的经验如果你时间有限可以按下面的优先级来分配精力必须反复看的章节反向传播的手动推导对应视频第7-8讲左右这是整个Pytorch体系的地基不懂这个后面全是空中楼阁DataLoader和Dataset的使用实际项目中数据加载占了大量工作量这个不熟会非常痛苦CNN的卷积层参数计算输入通道、输出通道、卷积核大小、padding、stride这五个参数怎么配合直接决定模型能不能跑起来可以快速过的章节线性模型的数学推导如果你有微积分基础这部分看一遍知道思路就行各种优化器的对比介绍知道SGD、Adam、RMSprop的区别即可具体用哪个后面做项目时再深入部分案例的完整代码理解了核心概念后案例代码可以当作练习自己先写再看建议跳过的章节如果你已经有基础Python基础语法复习部分环境搭建的详细步骤除非你卡在某个具体报错上3. 核心知识点索引与深度拆解3.1 环境搭建Anaconda Pytorch IDE的选择环境搭建是劝退新人的第一道门槛我周围至少有五个人卡在这一步超过一天。刘二大人推荐的是Anaconda Pycharm的组合这个方案确实最稳妥但有几个关键决策点需要提前想清楚。第一个决策装CPU版还是GPU版如果你手头没有NVIDIA显卡或者显卡显存小于6GB直接装CPU版别折腾。GPU版需要装CUDA和cuDNN版本匹配非常严格——Pytorch 2.0要求CUDA 11.7或11.8而CUDA版本又受显卡驱动版本限制。我见过太多人在这三个版本之间来回卸载重装浪费一整天。CPU版虽然训练慢但用于课程中的小数据集MNIST、CIFAR-10完全够用等确定了要深入做项目再折腾GPU环境也不迟。第二个决策用conda还是pip安装conda的优势是能自动处理依赖关系特别是科学计算相关的库numpy、scipyconda会一并帮你装好兼容版本。pip的优势是速度快、包更全。我的习惯是创建conda虚拟环境来隔离项目然后在环境里用pip安装Pytorch。具体命令如下# 创建名为pytorch_env的虚拟环境指定Python版本 conda create -n pytorch_env python3.9 -y # 激活环境 conda activate pytorch_env # 用pip安装CPU版PytorchPytorch 2.x版本 pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu # 验证安装 python -c import torch; print(torch.__version__); print(torch.cuda.is_available())最后一行如果输出FalseCPU版或TrueGPU版说明安装成功。踩坑提醒不要在base环境里直接装Pytorch。base环境是Anaconda的根基装坏了修复很麻烦而且不同项目需要的Pytorch版本可能不同全部塞在base里早晚冲突。第三个决策用Pycharm还是VSCode这个纯粹看个人习惯。Pycharm的专业版对科学计算支持更好能直接在IDE里看到变量矩阵的形状VSCode更轻量配合Jupyter插件也能交互式运行。我在课程前期用的是Pycharm因为调试方便可以打断点一步步看张量的变化。后期转向VSCode Jupyter Notebook的模式因为做实验时经常需要改一行跑一次Notebook的即时反馈更顺手。3.2 张量操作Pytorch最基础也最容易翻车的部分张量是Pytorch里一切数据的基本单位你可以把它理解成NumPy的ndarray加上GPU加速和自动求导。课程里涉及张量操作的地方非常多我总结了几个高频操作和对应的注意点创建张量的几种方式区别在于是否初始化以及是否指定数据类型import torch # 从Python列表创建数据类型自动推断 a torch.tensor([[1, 2], [3, 4]]) # 创建全零张量指定形状和数据类型 b torch.zeros(2, 3, dtypetorch.float32) # 创建未初始化的张量速度快但值是随机的 c torch.empty(2, 3) # 创建单位矩阵 d torch.eye(3) # 创建随机张量正态分布 e torch.randn(2, 3)关键区别torch.tensor()会复制数据torch.from_numpy()会共享内存改一个另一个也变。这个在数据预处理阶段非常容易出bug比如你从NumPy数组创建了张量然后修改了原数组发现张量也跟着变了——这就是共享内存导致的。张量的形状变换是另一个重灾区。view()和reshape()在大多数情况下可以互换但有一个关键区别view()要求张量在内存中是连续的reshape()不要求它会自动拷贝一份。所以如果你对一个转置过的张量用view()可能会报错这时候换成reshape()就好了。# 假设有一个4x4的张量 x torch.randn(4, 4) # 展平成16维向量 x_flat x.view(16) # 要求x连续 y x.t() # 转置后不再连续 # z y.view(16) # 这行会报错 z y.reshape(16) # 这样写就没问题广播机制需要特别小心。当你对两个形状不同的张量做运算时Pytorch会自动扩展维度来匹配但这个扩展规则如果不理解很容易得到意料之外的结果a torch.tensor([[1], [2], [3]]) # 形状(3, 1) b torch.tensor([10, 20]) # 形状(2,) c a b # 形状(3, 2) # c [[11, 21], [12, 22], [13, 23]]这里的规则是把形状(3,1)和(2,)从右往左对齐维度为1的可以扩展缺失的维度补1。所以a扩展成(3,2)b扩展成(3,2)然后逐元素相加。实操心得每次做完张量运算养成打印.shape的习惯。我调试模型时80%的报错都是形状不匹配比如把(batch_size, 1, 28, 28)的卷积输出直接喂给了需要(batch_size, 784)的全连接层。3.3 自动求导机制理解backward()到底做了什么Pytorch最核心的功能就是自动求导它让反向传播从“手推公式”变成了“一行代码”。但便利的背后理解它的工作机制能帮你避开很多坑。自动求导的核心是计算图Computation Graph。当你对一个设置了requires_gradTrue的张量做运算时Pytorch会在后台记录每一步操作形成一个有向无环图。调用backward()时它从这个图的末端开始沿着每条边反向传播梯度用链式法则算出每个叶子节点的梯度。# 创建需要求导的张量 x torch.tensor([2.0, 3.0], requires_gradTrue) # 前向计算 y x ** 2 # y [4, 9] z y.sum() # z 13 # 反向传播计算z对x的梯度 z.backward() # 查看梯度dz/dx 2x [4, 6] print(x.grad) # 输出: tensor([4., 6.])这里有个关键概念叫叶子节点。只有通过requires_gradTrue直接创建的张量才是叶子节点它们在反向传播后会累积梯度到.grad属性里。中间计算产生的张量比如上面的y和z不是叶子节点它们的梯度不会保留。梯度累积是另一个容易踩坑的点。每次调用backward()梯度会累加到.grad上而不是替换。所以标准的训练循环里必须手动清零# 标准训练循环的简写 optimizer.zero_grad() # 清零梯度不写这行梯度会一直累加 loss.backward() # 反向传播计算梯度 optimizer.step() # 更新参数如果忘了zero_grad()第二次迭代的梯度会是第一次的两倍损失曲线会直接飞掉。detach()和with torch.no_grad()也是必须掌握的。前者从计算图中分离出一个张量后者在一个代码块内禁用梯度追踪。在模型推理和验证阶段一定要用它们来节省显存和计算时间# 验证阶段不记录梯度 model.eval() with torch.no_grad(): for data, target in test_loader: output model(data) # 计算准确率等指标实测经验如果你在验证阶段忘了加torch.no_grad()显存占用会暴涨小显存显卡直接OOM内存溢出。我有一次用6GB显存的笔记本跑CIFAR-10训练正常但验证时报显存不够排查了半天才发现是这个问题。3.4 数据集与数据加载从Dataset到DataLoader的完整链路数据是深度学习的燃料而Pytorch的数据加载机制是课程中段的核心内容。很多教程只教你调DataLoader但刘二大人会让你先继承Dataset类自己写一个理解数据从磁盘到模型的完整链路。Dataset类的核心接口是两个方法__len__()返回数据集大小__getitem__(index)返回一条数据。下面是一个自定义数据集的示例框架from torch.utils.data import Dataset, DataLoader class MyDataset(Dataset): def __init__(self, data, labels): # 通常在初始化时把数据读进内存或记录文件路径 self.data data self.labels labels def __len__(self): # 返回样本总数 return len(self.labels) def __getitem__(self, idx): # 根据索引返回一条数据和标签 sample self.data[idx] label self.labels[idx] return sample, labelDataLoader的核心参数我整理成了一张表这几个参数直接关系到训练效率和显存占用参数名作用推荐设置注意事项batch_size每次加载多少条数据32/64/128根据显存调整必须是2的幂次过大容易OOMshuffle是否打乱顺序训练集True验证集False验证集打乱会导致指标不稳定num_workers用几个子进程加载数据Windows下设0Linux/Mac设4或8Windows多进程有坑设0最稳drop_last是否丢弃最后不足一个batch的数据训练集视情况验证集False丢弃可能导致最后几条数据没用到pin_memory是否锁页内存加速CPU到GPU的拷贝GPU训练时设TrueCPU训练时设了反而更慢关于num_workers有个反直觉的事实它设得越大不一定越快。当num_workers超过CPU核心数时进程切换的开销会抵消并行加载的收益。我的经验是设为CPU物理核心数的一半左右比较合适。另外在Windows上num_workers 0经常报BrokenPipeError最省心的做法是直接设0用主进程加载速度慢一点但稳定。4. 各模块实操要点与容易踩的坑4.1 手写线性回归时最常见的三个错误线性回归是课程的第一个实操案例代码不到20行但我在做练习时发现周围同学包括我自己几乎都在这三个地方翻过车。错误一损失函数返回的是张量而不是标量。如果你用loss (y_pred - y) ** 2得到的是一个向量对这个向量调用backward()会报错“grad can be implicitly created only for scalar outputs”。正确做法是对batch内所有样本的损失取平均或求和# 错误写法loss是向量 loss (y_pred - y) ** 2 # 正确写法对batch维度取平均 loss ((y_pred - y) ** 2).mean()错误二优化器接收的是参数列表而不是模型本身。用torch.optim.SGD(model, lr0.01)会报错必须传model.parameters()# 错误写法 optimizer torch.optim.SGD(model, lr0.01) # 正确写法 optimizer torch.optim.SGD(model.parameters(), lr0.01)错误三学习率设得太大导致损失震荡甚至发散。线性回归的损失函数是凸的理论上一定能找到全局最优但如果学习率超过临界值与数据尺度有关参数更新会直接飞出去变成NaN。我的经验是先用1e-3或1e-4试观察损失曲线如果震荡就除以10如果下降太慢就乘以3。4.2 逻辑回归与交叉熵为什么不能用均方误差逻辑回归用于二分类输出层通常用Sigmoid把值压到(0,1)区间然后接一个阈值判断类别。这里的关键问题是为什么分类问题要用交叉熵损失而不是均方误差从理论层面解释均方误差配合Sigmoid函数时损失函数关于参数的梯度会包含Sigmoid的导数项sigmoid(z)。当z很大或很小时Sigmoid导数趋近于0导致梯度消失参数几乎不更新。而交叉熵损失配合Sigmoid时梯度中的Sigmoid导数项会被约掉梯度只与预测值和真实值的差有关更新效率高得多。从实操层面看用均方误差做分类时损失下降很慢经常需要调很大的学习率才能动但学习率一大又容易震荡。换成nn.BCELoss或nn.BCEWithLogitsLoss之后收敛速度会有肉眼可见的提升。# 二分类的两种损失写法 # 方式一网络输出经过Sigmoid损失用BCELoss class Net(nn.Module): def forward(self, x): return torch.sigmoid(self.fc(x)) criterion nn.BCELoss() # 方式二推荐网络输出不接Sigmoid损失用BCEWithLogitsLoss class Net(nn.Module): def forward(self, x): return self.fc(x) # 不接激活函数 criterion nn.BCEWithLogitsLoss() # 内部自动做Sigmoid方式二更好因为BCEWithLogitsLoss在内部用了一种数值更稳定的实现方式避免了Sigmoid在极端值处溢出。实操心得如果你在多分类任务中看到损失不下降先检查是不是用错了损失函数。多分类必须用nn.CrossEntropyLoss它内部已经包含了LogSoftmax操作所以网络最后一层不要加Softmax。4.3 多层感知机的维度变换全连接层之间的衔接多层感知机MLP是课程从线性模型到CNN的过渡核心在于理解全连接层之间维度怎么衔接。全连接层的本质是一个矩阵乘法输入向量乘以权重矩阵加上偏置。假设输入是28x28的灰度图像展平后是784维。如果第一层全连接输出256维第二层输出128维最后输出10类那么class MLP(nn.Module): def __init__(self): super().__init__() self.flatten nn.Flatten() # 将(28,28)展平为784 self.fc1 nn.Linear(784, 256) # 784 - 256 self.fc2 nn.Linear(256, 128) # 256 - 128 self.fc3 nn.Linear(128, 10) # 128 - 10 def forward(self, x): x self.flatten(x) x torch.relu(self.fc1(x)) x torch.relu(self.fc2(x)) x self.fc3(x) # 最后一层不加激活CrossEntropyLoss会处理 return x这段代码里有两个容易出问题的地方。第一是nn.Flatten()的位置它默认从第1维开始展平第0维是batch_size所以输入形状是(batch, 1, 28, 28)时会变成(batch, 784)这是正确的。如果你手动用x.view(-1, 784)要注意-1表示自动推断batch维度不要写成固定值。第二个是最后一层的激活函数用nn.CrossEntropyLoss时网络输出不要加Softmax因为它内部会先做LogSoftmax再做NLLLoss加了两遍会导致概率分布异常。4.4 卷积神经网络特征图尺寸计算的万能公式CNN部分是课程的重头戏而特征图尺寸的计算是最核心也最容易算错的地方。我见过太多人因为维度对不上而报错最后发现是卷积层参数没配对。先记住这个万能公式输入特征图尺寸为H_in卷积核大小为K填充为P步长为S那么输出尺寸H_out为H_out floor((H_in 2P - K) / S) 1下面用几个具体例子来验证输入尺寸卷积核PaddingStride输出尺寸计算过程2830126(280-3)/11 262831128(282-3)/11 282852128(284-5)/11 282830213(280-3)/21 13注意第三个例子卷积核5x5padding2时输出尺寸和输入一样这就是所谓的“same padding”在需要保持空间尺寸不变时非常有用。通道数的变化规则更简单输出通道数等于卷积核的个数与输入通道数无关但每个卷积核的深度必须等于输入通道数。比如输入是(3, 32, 32)的彩色图像用16个3x3的卷积核输出就是(16, 30, 30)——通道从3变成了16空间尺寸从32变成了30。# 一个典型的CNN结构 class SimpleCNN(nn.Module): def __init__(self): super().__init__() # 输入: (batch, 1, 28, 28) self.conv1 nn.Conv2d(1, 16, kernel_size3, padding1) # 输出(batch, 16, 28, 28) self.pool1 nn.MaxPool2d(2) # 输出(batch, 16, 14, 14) self.conv2 nn.Conv2d(16, 32, kernel_size3, padding1) # 输出(batch, 32, 14, 14) self.pool2 nn.MaxPool2d(2) # 输出(batch, 32, 7, 7) self.fc nn.Linear(32 * 7 * 7, 10) # 全连接分类 def forward(self, x): x torch.relu(self.conv1(x)) x self.pool1(x) x torch.relu(self.conv2(x)) x self.pool2(x) x x.view(x.size(0), -1) # 展平保留batch维度 x self.fc(x) return x这段代码里x.view(x.size(0), -1)是关键一步。x.size(0)就是batch_size-1让Pytorch自动计算剩余维度的大小这里是32771568。如果你硬编码成x.view(-1, 1568)当batch_size变化时不会出问题因为-1会自动适配但如果特征图尺寸变了比如输入图像从28变成321568这个数字就需要跟着改。避坑技巧在forward()里加一行print(x.shape)来调试是最高效的方法。尤其是在搭建新网络时每一步的维度变化都打印出来和上面表格里的计算结果对照哪里对不上一眼就能看出来。4.5 训练循环从“能跑”到“跑得好”的关键细节训练循环的代码框架看起来很简单——前向传播、计算损失、反向传播、更新参数——但要让模型真正收敛里面有几个细节决定成败。细节一训练和验证的切换。model.train()和model.eval()不只是形式上的调用它们会影响Dropout和BatchNorm层的行为。训练时Dropout随机丢弃神经元验证时保留全部训练时BatchNorm用当前batch的统计量验证时用全局移动平均。忘了切换会导致验证结果波动很大。细节二准确率的计算方式。分类任务的准确率不能直接比较输出向量而要取最大值的索引# 正确做法取输出中最大概率对应的类别 _, predicted torch.max(output, dim1) correct (predicted labels).sum().item()细节三多个epoch的训练节奏。课程中的案例通常训练10个epoch左右但实际项目中要看损失曲线来决定何时停止。我的经验是如果验证损失连续3个epoch不下降就可以停下来了。继续训练只会让模型在训练集上过拟合验证集表现反而变差。# 完整的训练循环框架 for epoch in range(num_epochs): # 训练阶段 model.train() for images, labels in train_loader: optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() # 验证阶段 model.eval() correct 0 total 0 with torch.no_grad(): for images, labels in test_loader: outputs model(images) _, predicted torch.max(outputs, dim1) total labels.size(0) correct (predicted labels).sum().item() print(fEpoch {epoch1}, Accuracy: {100 * correct / total:.2f}%)5. 常见报错与问题排查实录5.1 维度不匹配类报错这应该是出现频率最高的一类错误典型报错信息是RuntimeError: mat1 and mat2 shapes cannot be multiplied或者size mismatch。排查思路是检查全连接层的输入维度是否等于特征图展平后的大小。举个例子如果你定义了self.fc nn.Linear(128, 10)但实际展平后是1568维就会报错。解决方法有两个要么调整前面的卷积层让输出特征图大小匹配要么把全连接层的输入改成正确的维度。我个人的习惯是先用x.view(x.size(0), -1).shape打印出展平后的维度再据此设置全连接层。还有一类维度错误发生在数据加载阶段RuntimeError: Expected 4-dimensional input for 4-dimensional weight。这是因为卷积层需要(batch, channel, height, width)的四维输入但你可能不小心把channel维度丢了。检查DataLoader返回的数据形状如果是(batch, 784)的二维数组需要在模型里加一个nn.Unflatten或手动x.view(-1, 1, 28, 28)。5.2 梯度异常类报错RuntimeError: Trying to backward through the graph a second time是因为你对同一个计算图调用了两次backward()而没有保留图。这种情况通常出现在RNN的训练中或者你在一个迭代里对多个损失分别调用了backward()。解决方法是设置retain_graphTrue或者检查代码逻辑是不是重复调用了。RuntimeError: grad can be implicitly created only for scalar outputs是因为你对非标量张量调用了backward()。前面提过要对损失取平均或求和变成标量再调用backward()。梯度爆炸表现为损失变成NaN解决方法是降低学习率、使用梯度裁剪torch.nn.utils.clip_grad_norm_、或者在网络中加入BatchNorm层。5.3 显存不足类报错CUDA out of memory是GPU训练时的常见报错。解决方法按优先级排序方法操作效果代价减小batch_size从64降到32或16立即见效训练速度变慢梯度噪声变大用torch.no_grad()验证阶段禁用梯度释放大量显存无删除无用变量del x; torch.cuda.empty_cache()释放部分显存无用混合精度训练torch.cuda.amp显存减半需要改少量代码减小模型复杂度减少通道数或层数显存大幅降低模型能力下降我个人的经验是先用torch.cuda.empty_cache()清一下缓存很多时候显存其实没有被真正占用只是碎片化了。如果还不行再降batch_size。5.4 训练不收敛类问题如果损失一直不下降或者震荡剧烈按以下顺序排查学习率是不是太大了。这是最常见的原因。把学习率除以10再试一次如果损失开始下降就说明原来太大。数据有没有归一化。如果输入图像的像素值在0-255之间而不是0-1之间梯度会非常大训练很容易发散。用transforms.ToTensor()会把像素自动归一化到0-1别忘了加。损失函数用对了吗。多分类用CrossEntropyLoss二分类用BCEWithLogitsLoss回归用MSELoss。用错了损失函数模型优化目标就错了自然不收敛。模型结构有没有问题。检查最后一层的输出维度是不是等于类别数中间层的激活函数有没有加全连接层的输入维度对不对。速查表我把自己遇到过的报错和解决方法整理成了一个表格贴在显示器旁边现在分享出来报错关键词可能原因优先排查shapes cannot be multiplied全连接层维度不匹配打印展平后的shapeExpected 4-dimensional input卷积层输入少了channel维检查DataLoader输出形状grad can be implicitly created对非标量调用了backward损失是否取mean/sumbackward through graph a second time重复backward加retain_graph或改逻辑CUDA out of memory显存不够降batch_size或加no_gradloss is NaN学习率太大或数据未归一化降学习率检查数据范围6. 学习路径建议与后续延伸6.1 不同基础的人该怎么用这套笔记零基础没学过Python和微积分先花一周时间补Python基础重点掌握列表、循环、函数、类的概念。然后过一遍吴恩达的机器学习前三周课程理解什么是梯度下降。再回来看刘二大人的课从线性模型开始按顺序推进。每看完一个视频把代码默写一遍再运行报错就根据第5章的排查表来定位问题。有Python基础但没接触过深度学习跳过Python语法部分直接从线性模型开始。重点是理解计算图和反向传播这两块花多少时间都值得。我建议手写三遍反向传播的推导过程直到能不看笔记写出两层网络的梯度公式。用过其他框架TensorFlow/Keras你的优势是理解训练流程重点看Pytorch和它们的不同之处。Pytorch的“动态图”机制比TensorFlow 1.x的静态图灵活很多——你可以随时print()中间张量的值不用先搭图再运行。另外nn.Module的写法也需要适应一下。6.2 从这套课程出发还能往哪些方向深入刘二大人的课程止步于CNN的基础实现但深度学习的版图远不止这些。学完这套课后我建议按下面的方向继续扩展方向一现代CNN架构。课程里手写的CNN是最朴素的版本实际应用中用的都是ResNet、EfficientNet这些带有残差连接、注意力机制的现代架构。可以在torchvision.models里直接调用预训练模型理解它们的结构设计思路。方向二目标检测与语义分割。这是CNN在计算机视觉中的两大主流应用。目标检测解决“图里有什么在哪里”的问题经典算法有YOLO系列和Faster R-CNN。语义分割解决“每个像素属于什么类别”的问题代表算法是U-Net和DeepLab。方向三序列模型与Transformer。CNN擅长处理图像这种空间结构的数据而文本、语音、时间序列这些有顺序关系的数据需要RNN或Transformer。Transformer现在是NLP领域的绝对主流理解了它的自注意力机制再看BERT、GPT这些模型就不会觉得陌生。方向四迁移学习。实际项目中很少有人从零训练一个大模型更常见的做法是拿在ImageNet上预训练好的模型在自己的数据集上微调几层。Pytorch里实现迁移学习非常方便只需要冻结前面的层替换最后一层的输出维度即可。6.3 我个人的三条学习建议第一不要追求一次看懂。我第一遍看反向传播的时候完全懵第二遍有点感觉第三遍才真正理解。深度学习的内容密度就是这样第一遍能建立整体印象就够了细节在重复中自然就通了。第二代码必须自己写不能复制粘贴。看视频时觉得“这代码很简单”自己写时才发现各种细节记不住——是optim.zero_grad()还是optimizer.zero_grad()是loss.backward()还是loss.backward没有括号。这些细节只有亲手打过一遍才会形成肌肉记忆。第三尽早做一个小项目。课程里的MNIST和CIFAR-10都是玩具数据集真正做项目时会遇到更多问题数据不平衡、过拟合、超参数调优、模型部署。我学完课程后花了三天做了一个猫狗分类的小项目踩的坑比看一个月视频还多但收获也大得多。最后分享一个我调参的小技巧先用一个非常小的子集比如100条数据跑通整个训练流程确认代码没有bug、损失能下降再换全量数据训练。这样可以快速验证代码正确性避免在完整数据集上浪费几十分钟才发现写错了。这个习惯帮我省下了大量等待时间。