1. 为什么我最终选择了这套入门路径1.1 从“看了就忘”到“抄完能跑”的转折我在学PyTorch之前已经被“看教程”这件事坑过很多次。今天收藏一份深度学习入门清单明天刷到一段讲解反向传播的短视频收藏夹装得满满当当真正打开终端敲代码的次数却屈指可数。直到我跟着“小土堆pytorch学习笔记”重新过了一遍基础知识才意识到问题不是出在教程数量上而是出在学习节奏和练习密度上。这套笔记给我最大的感受是它没有假设你会Python也没有默认你懂深度学习它假设的只是“你愿意打开终端跟着敲”。所以这篇笔记并不是课程目录的复述而是我学完上半部分之后把那些真正卡住过我的点、网上很少讲清楚的细节、以及我自己调试过程中攒下来的经验做一个整理。如果你正准备入门PyTorch或者学了一段时间但总觉得代码是在“抄别人的”这篇笔记应该能帮你把前面那段最陡的坡走得稍微平坦一些。1.2 这套笔记的覆盖面与我的筛选逻辑上半部分的核心内容从学习路径上看其实是围绕这几根主线展开的张量的创建与操作、数据加载管线、自动求导机制、神经网络模块的基本写法、训练循环的完整搭建。这几块正好构成一个最简模型的完整闭环——从原始数据到最终输出的梯度更新每一步都有对应的代码组件。我自己的习惯是学任何一个框架先抓住它的“最小可用闭环”也就是“数据进来、模型算完、梯度回来、参数更新”这一圈。只要这个圈子能跑通后续再往里面加卷积、加注意力机制、加分布式训练都只是往这个闭环里塞组件而已。所以我学下半部分时会明显感觉真正的分水岭并不在模型结构有多复杂而是在于对这套闭环里每一个环节的边界条件是否足够熟悉。这篇笔记记录的正是我在这段闭环里反复碰壁后留下的东西。2. 环境搭建里那些没人提前告诉你的暗坑2.1 Python版本与PyTorch版本之间的微妙关系环境搭建是我认为这套笔记最容易被低估的部分。很多人觉得装环境嘛跟着命令粘贴复制就行。但实际情况是PyTorch的安装失败案例里有相当一部分不是因为网络而是因为Python版本和PyTorch版本之间的匹配关系没搞清楚。我在配置环境时踩的第一个坑就是conda默认创建的新环境Python版本往往是当时的最新版而这个最新版不一定有对应的PyTorch预编译包。比如某些PyTorch版本在发布时官方支持列表里写的还是Python 3.8到3.11如果你用的是Python 3.12pip在解析依赖时可能找不到匹配的wheel包最后直接给你抛一个“No matching distribution found”的报错。我当时在这个错误上卡了快半小时一开始还以为是网络问题换了镜像源还是不行最后检查Python版本才发现匹配关系出了问题。所以我的建议是先确定PyTorch版本再反推Python版本。不要先建环境再装包而是先上官方网站查清楚你需要的版本支持哪些Python版本然后用conda创建指定Python版本的环境。比如我需要稳定版PyTorch 2.x搭配CUDA 11.8那创建环境时就直接写conda create -n torch_env python3.10这样后面安装基本顺风顺水。另外还有一个环境细节容易被忽略CUDA的版本对应关系。我一开始看到电脑上装有CUDA 12.x就以为万事大吉结果发现PyTorch官方提供的预编译包可能只覆盖部分CUDA版本。这里的关键是先确认显卡驱动支持的CUDA版本再选择对应的PyTorch安装命令。只要记住这个顺序就不会出现“装上了但GPU用不了跑起来还是CPU”的情况。2.2 下载速度和本地验证的两个实用技巧下载速度慢是另一个被反复提起的问题。官方源下载PyTorch这种大小以GB为单位的包在国内网络环境下经常让人等到怀疑人生。我的做法是直接用镜像源比如在安装命令后面加上镜像地址速度提升非常明显。这里顺便提一个小经验不要在基础环境里直接装PyTorch以后项目多了互相踩依赖会很难受。用一个独立的环境来学习和实验出了问题直接删掉重建几分钟就能恢复到干净状态。装完之后第一步验证除了那句常规的torch.__version__我强烈建议再做两个检查一是跑一下torch.cuda.is_available()确认GPU是否真的可用二是随便生成两个张量做一次矩阵乘法并把结果打印出来确认计算过程没有报错。我遇到过一种情况torch.cuda.is_available()返回True但真正在GPU上执行运算时却直接崩了。那其实是依赖库的版本冲突只有实际跑计算才能暴露出来。所以环境装完不要急着看版本号跑通一个最小计算才算真正装好了。3. 张量与数据操作前期最多的时间花在这里3.1 用一套“形状检查”的思路替代死记API张量是PyTorch里最基础的数据结构可以说后面所有操作都是在和张量打交道。我学这一部分时最大的体会是不要试图一开始就背下所有张量操作API而是先建立一套“形状变化”的直觉。这套笔记里反复强调的torch.zeros、torch.ones、torch.randn这些创建函数其实真正需要记住的并不多。更关键的是理解每个操作传入参数和你手头数据形状之间的对应关系。举个例子在图像处理的任务里一个批次的数据通常被组织成四维张量(batch_size, channels, height, width)简称BCHW。我第一次处理图像数据时完全不理解为什么要在这几个维度之间来回折腾。后来发现所有卷积、池化操作的参数设计都基于这个四维结构。你只要在每次写代码之前先打印一遍数据的shape把维度顺序写下来再去看你调用的API期望的形状基本不会犯方向性的错误。这个方法听起来简单但真的能省掉大量调试时间。另外一个值得养成的习惯是操作完张量之后立刻检查形状。我不止一次遇到过代码直接报错的情况错误信息是维度不匹配结果回溯一下发现是某个中间步骤的squeeze或reshape改变了维度数量。所以我在自己的笔记里反复提醒每次做完一个操作加一行print(x.shape)确认形状符合预期再往后写。这个习惯在前期可能显得有点笨拙但等模型结构复杂起来之后它能帮你迅速定位问题出在哪一层。3.2 Dataset与DataLoader里最容易忽略的细节数据加载这一部分我觉得是上半场学习的“隐形分水岭”。很多教程会带你快速过一遍Dataset和DataLoader然后直接进入模型搭建。但实际写代码时这个环节才是最容易埋坑的地方。我在实现自定义Dataset时犯的第一个错误就是没有正确实现__getitem__的返回类型。当时我以为返回一个PIL图像和一个整数标签就够了结果在后续做数据增强时发现某些操作要求输入张量而不是PIL图像不得不回头改数据结构。这里想分享一个小经验自定义Dataset时尽量让__getitem__直接返回张量类型的数据。虽然可以在训练循环里再做转换但在Dataset内部处理好类型会让后面的流程干净很多。DataLoader里另一个容易被忽略的参数是shuffle。训练集一般设置成True测试集和验证集设置成False。这个道理很简单训练时需要打乱数据顺序避免模型学到样本顺序带来的虚假规律而验证时要保证每次评估的数据顺序一致结果才可比较。我刚开始学的时候没太在意这个参数结果训练过程天真地以为模型学得不错换到测试集上一看数字掉得厉害排查了半天才发现是数据顺序的问题。这类小细节文档里都会写但写代码时脑子一热就容易忽略。还有一个关于DataLoader的经验batch_size不是越大越好。我刚开始训练时为了图省事直接把整个数据集作为一个batch塞进去结果内存直接扛不住。后来才明白batch_size的选择要同时考虑显存容量模型的参数量也直接决定了单batch能放多大。实用做法是先从一个比较保守的值开始比如32或64然后观察显存占用再逐步往上调。3.3 TensorBoard训练过程的可视化比你想象的更重要在数据操作阶段这套笔记已经把TensorBoard的基础用法讲了一遍。我当时觉得这只是一个锦上添花的工具直到自己开始训练模型才发现没有可视化几乎等于在盲飞。SummaryWriter可以记录损失曲线、验证精度甚至直接查看输入模型的数据图片。我在训练一个简单的图像分类模型时曾经通过TensorBoard发现某个类别的样本在训练集里数量极少导致这个类别的损失一直降不下去。如果没有可视化这个问题可能要等到最后查看混淆矩阵才能暴露出来而看到时往往已经浪费了大把训练时间。在TensorBoard的使用上一个值得注意的细节是确保不同的实验使用不同的日志目录。因为TensorBoard默认会把同一个目录下的所有日志聚合显示如果你跑了两组对比实验都写在同一个目录里你会看到一条诡异的混合曲线完全分不清是哪次实验的结果。我当时就犯了这个错误后来才养成每次训练前手动指定一个带时间戳的日志目录。4. 自动求导与反向传播理解PyTorch的发动机4.1 requires_grad 到底做了什么自动求导是PyTorch和其他传统数值计算库最本质的区别。我学到这里时最大的认知转变是理解了requires_grad参数不是“开关”那么简单而是一个影响整个计算图构建过程的标志。当你把一个张量的requires_grad设为TruePyTorch就会在后续所有涉及该张量的操作中自动记录运算路径以便之后反向传播时计算梯度。这里有一个非常容易踩的坑原地操作和requires_grad之间的冲突。比如x.requires_grad_(True)这种原地修改方式在某些情况下会导致计算图记录出错。另外如果你对某个叶子张量做了inplace操作后面反向传播时经常会抛出RuntimeError提示变量被修改过了。老实说我第一次遇到这个报错完全摸不着头脑后来查资料才明白PyTorch的记录机制不允许在计算图构建之后修改输入值。解决办法很简单非必要不使用原地操作如果确实需要修改数据就用torch.no_grad()包住或者重新创建张量。再来说说grad_fn这个属性。很多入门教程不会花篇幅讲它但它在调试时非常有用。每一个经过张量运算得到的新张量都会有一个grad_fn属性指向生成它的那个运算。输出一个张量的grad_fn你就可以看到它是由AddBackward0还是MulBackward0之类的操作产生的。这在验证自己构建的计算图结构时是极好的工具。我在学这块的时候习惯打印每一层输出的grad_fn确保计算图按照预期方向延伸。这种方式虽然朴素但比单纯看文档更容易建立起“计算图”的直觉。4.2 loss、backward() 与 optimizer.step() 的衔接关系很多人第一次写训练循环时会把loss.backward()和optimizer.step()当成两个并列的步骤背下来就完事了。但真正理解它们之间的关系调试时才能少走弯路。简单来说loss.backward()做的事是根据计算图反向传播计算出每个叶子张量对应的梯度并把梯度保存在张量的.grad属性里optimizer.step()做的事则是根据优化器策略比如SGD、Adam和这些保存好的梯度去更新模型参数。所以顺序一定不能反先算梯度再更新参数。我把这个流程类比成“先看导航地图再踩油门”backward()是查看当前位置和方向的偏差step()才是真的踩油门往前修正。如果没看地图就踩油门车就不知道往哪个方向开。早年我常犯的一个错误是在同一个batch上多次调用backward()导致梯度累加参数更新量被放大。这是因为PyTorch就算梯度是累加的。后来我养成了每次更新参数后都手动调用optimizer.zero_grad()的习惯把梯度清零避免上一次的梯度影响到这一轮更新。关于zero_grad()的位置还有一个细节值得提它应该在backward()之前调用还是调用之后从执行效果来说只要出现在下一轮backward()之前都行。但我个人习惯把它放在loss.backward()之前也就是每个batch最开始就清空梯度这样即使中途报错退出也不会把上次遗留的梯度带到下一次调试里。这个习惯帮我避免过好几次“莫名其妙的损失爆炸”。4.3 一个小实验帮助理解计算图如果你觉得以上概念还是抽象我推荐亲自动手做一个小实验创建一个简单的张量a设置requires_gradTrue然后定义b a * 2c b 1再调用c.backward()最后打印a.grad。这个结果应该是2因为c对a的梯度是b对a的梯度乘以c对b的梯度也就是2 × 1。这个小实验虽然简单但它是理解链式法则和计算图关系最好的起点。我学到这里的时候专门在笔记本上手写了推导过程把代码输出和手算结果对照发现一致的那一刻确实有“原来如此”的感觉。5. 用 nn.Module 搭出第一个像样的模型5.1 模型结构里的几个小习惯从零开始搭建模型时nn.Module无疑是核心基类。继承它并实现__init__和forward两个方法是任何PyTorch模型的基本骨架。我刚学的时候总觉得forward就是按顺序把各层调用一遍没什么特别之处。后来才逐渐意识到forward里写的是每个batch数据真正经过的计算路径它的灵活程度远超我的想象。你可以在这里加入条件判断、循环甚至多个分支的合并只要输入输出形状匹配逻辑就成立。我在写第一个自定义模型时犯过一个很低级的错误在__init__里定义了一个nn.Linear(28*28, 64)但在forward里直接把二维输入传了进去结果因为输入形状里包含了batch维度矩阵乘法时维度对不上。后来才发现Linear层的输入形状默认不包括batch维度你要么传入一个二维张量(batch_size, features)要么在forward里手动把输入reshape成正确形状。这类问题在文档里其实写得很明确但实际操作时很容易因为“惯性思维”犯错。还有一个习惯想分享把层定义和forward里的计算分开思考。__init__中主要是创建参数比如各层权重和偏置forward中才是数据在这些参数之间的流动过程。把这两件事在脑海里分开设计复杂模型时思路会清晰很多。而且PyTorch的nn.Module会自动管理__init__中注册的子模块的参数确保它们被optimizer找到。这个机制的好处是你不需要手动维护参数列表但前提是所有需要训练的参数都必须作为nn.Module的属性挂载而不是放在普通的Python列表里。我之前试过用列表存了几个nn.Linear层结果优化器一个参数都没更新训练了半天loss纹丝不动。后来才知道需要改用nn.ModuleList而不是list。这个坑非常隐蔽真的希望后来者少踩。5.2 损失函数与优化器的搭配思路损失函数与优化器是训练闭环里最重要的两个选择。对于分类问题最常见的损失函数是交叉熵损失nn.CrossEntropyLoss。这个函数有一点需要注意它在PyTorch里的输入是未经过softmax的原始logits而不是概率输出。我第一次用时习惯性地先加了一层softmax再把softmax的结果传给损失函数结果训练出来的模型效果总是差一口气。后来查文档才知道nn.CrossEntropyLoss内部已经包含了softmax操作外部再加softmax相当于做了两次反而破坏了数值稳定性。这个错误很隐蔽因为程序不会报错但训练效果就是不对。优化器方面SGD和Adam是两大主流。我的经验是入门阶段用Adam最友好它自带学习率自适应对初始学习率不太敏感。但如果你希望模型收敛到更好的结果或者想深入理解优化过程SGD加动量momentum是值得研究的方案。学习率的选择也很关键我习惯从一个相对大的值开始比如0.01或0.001然后观察损失曲线的变化趋势来调整。如果损失震荡剧烈就调低学习率如果损失下降过慢就适当调高。这套“观察再调整”的方法比盲目设置一个固定值要可靠得多。6. 训练循环里的意外与解决6.1 model.train() 和 model.eval() 的差别和意义训练循环里最容易被忽略的两行代码我认为是model.train()和model.eval()。在入门阶段很多教程甚至不会明确解释它们导致很多人直接不写。但实际上这两行代码控制着模型在训练和推理两种状态下的行为。对于只包含Linear层的最简模型这两行确实看不出差别但一旦模型里加入了BatchNorm或者Dropout忘记调用model.eval()就可能导致验证结果严重失真。我在实验里栽过一个跟头给模型加了Dropout层后每次验证时的准确率都不稳定来回波动很大。我一度以为是数据加载或者随机种子的原因排查了很久才发现是因为验证阶段没有调用model.eval()Dropout层在推理时仍然随机丢弃了一部分神经元导致每次预测结果都不一样。这是一个典型的“API不起眼但作用巨大”的例子。所以我的习惯是在训练循环开始前调用model.train()在进入验证阶段前调用model.eval()并配合torch.no_grad()上下文管理器这样既能保证推理结果的确定性也能节省内存占用。你不需要往深处理解它但一定要养成这个习惯。6.2 验证精度还是训练精度评估该盯谁训练循环里的另一个问题是我们到底应该关注训练集上的损失和精度还是验证集上的从实际经验来看验证集上的指标才是衡量模型泛化能力最接近真实的尺度。我做过一个实验训练集上的准确率一路飙升到接近99%但验证集准确率在某个epoch之后反而开始下降这就是典型的过拟合信号。如果没有关注验证集我可能还会沾沾自喜地继续训练直到最后提交测试才发现问题。所以手动写训练循环时我建议每个epoch结束后都跑一遍验证集并把验证损失、验证精度记下来加入TensorBoard。如果发现验证损失连续多个epoch不降反升就考虑提前停止或者调整正则化策略。这个过程听起来繁琐但对模型调优至关重要。PyTorch本身没有内置验证流程这块需要自己写代码——而这恰恰是学习框架的好机会。6.3 一个从失败到跑通的完整小Demo为了让你对整套流程有完整画面我分享一个我实际写过的最小图像分类Demo。数据集是一组模拟的灰度图像尺寸统一为28×28共10个类别。我用Dataset封装数据在__getitem__里把图像转换为张量并归一化到0到1之间。模型只用了一个包含两个全连接层的简单结构第一层从784维映射到128维激活函数用ReLU第二层从128维映射到10维。优化器选择Adam学习率设为0.001损失函数用交叉熵。训练时我以64为一个batch划分数据。每个epoch开始时把数据加载器里的shuffle设为True。前向传播后计算损失调用backward()更新梯度再用优化器更新参数最后清零梯度。头几个epoch损失下降得很慢我一度怀疑是学习率设置错了。后来查看TensorBoard发现损失虽然下降慢但曲线稳定原来是数据归一化没做好像素值范围不一致导致梯度量级偏小。修正归一化之后模型大概在第10个epoch左右在验证集上达到了接近95%的准确率。整个过程跑下来没有任何魔法就是踩坑、看曲线、找规律。谁都能做到只是需要耐心。6.4 保存与加载模型类型匹配问题训练结束之后保存和加载模型也是一个细节较多的环节。PyTorch官方推荐的保存方式是保存state_dict而不是直接保存整个模型对象。state_dict本质上是一个Python字典包含每一层参数的张量值。加载时需要先重新实例化一个相同结构的模型再调用load_state_dict方法。这个流程很多人觉得多此一举但它的好处是可移植性和灵活性都更好。直接把整个模型对象保存下来虽然方便但如果你改了模型类的嵌套结构或者代码文件路径变了很可能导致加载失败。我遇到过的一个具体问题是在CPU上训练的模型换到GPU上加载时直接报错提示张量设备不匹配。解决办法是在加载时给load_state_dict传入map_location参数指定设备为CPU或GPU。还有一个常见问题是保存模型时的类别映射和加载后实际使用的类别映射不一致。如果推理时标签顺序换了模型输出看起来正确但实际意义完全变了。这个问题在数据预处理后期做类别筛选时特别容易发生需要特别留意。写在最后的一个实操建议学到这里整理一下个人体会。如果你也在跟着这套笔记学习我强烈建议不要一口气把视频全刷完而是每学完一小节就合上视频凭自己记忆把代码重写一遍。写不出来就回去再看直到能独立写出来为止。这个过程虽然慢但比连续刷十集视频要有效得多。我自己的经验是我真正把张量操作和训练循环记住不是在看视频的时候而是在关掉视频之后自己从头敲代码、反复报错排错的那些夜晚。正是这些过程中的碰撞才让我对PyTorch不再感到陌生。