YAOTU INSIGHTS

《动手学深度学习》PyTorch版:从环境搭建到模型训练入门

《动手学深度学习》PyTorch版:从环境搭建到模型训练入门
我前前后后放弃过三次深度学习。第一次是买了一本厚得能当砖头的大部头看到第三章的梯度推导就撑不住睡着了第二次是跟着网上的视频课敲代码光配环境就报错报了整整两天最后连张量长什么样都没弄明白第三次是直接拿别人打包好的模型跑通了一个演示结果换了个数据集我完全不知道从哪下手。真正把这件事重新捡回来的是《动手学深度学习》的 PyTorch 版本。这套教材最反直觉的一点在于它几乎不跟你做长篇大论的理论铺垫而是先让你把一段能跑的代码敲出来看到输出之后再回头解释为什么这么写。对刚入门的人、或者像我这种被劝退过好几次的人来说这种先动手、再理解的节奏比任何一本讲得滴水不漏的教科书都管用。下面我就把这套书啃下来的整个过程拆开讲包括环境怎么搭、张量为什么重要、第一次训练为什么跑不出结果以及后面进阶阶段该怎么安排节奏。1. 这套教材最值钱的地方是逼你亲手写出每一行代码我一开始也走过弯路觉得看视频就够了老师讲得清清楚楚我点头如捣蒜关掉视频脑子一片空白。后来才明白深度学习这个东西看会了和写出来完全是两码事。这套书的核心价值恰恰不在于它讲了多少高级理论而在于它设计了一条你必须动手才能往下走的路径。每一章后面都有配套的练习代码留了空让你填你不填下一章的内容就没法验证自己到底懂没懂。1.1 大部头教材和这套课程的思路差异传统的大部头教材逻辑是自底向上的先把数学基础铺满概率论、线性代数、微积分、凸优化一路讲到你怀疑人生。好处是体系完整坏处是等你终于熬到神经网络那一章前面的热情早就耗光了而且你依然不知道一个真实的模型长什么样。这套课程反过来是自顶向下、需求驱动的。它默认你线性代数和高数有一点点底子但不要求你精通直接带你从线性回归、softmax 回归这种最朴素的模型开始每一步都对应一段可运行的代码。这种差异带来的直接影响是你在第一周就能看到数据进去、结果出来的完整闭环。哪怕你一开始并不完全理解损失函数是怎么推导出来的你也能看到它的数值在训练过程中一点点下降。这种正反馈对坚持下来极其重要。我自己就是靠这种能跑通的成就感才没有在第三章再次放弃。等你把代码跑顺了再回头补数学你会发现那些公式突然有了具体的画面感不再是一堆抽象的符号。1.2 从零实现和简洁实现这两条线别只挑一条走这套书有个很聪明的设计几乎每个核心内容都给了两套代码一套是从零实现用最基础的张量操作把模型的手动前向、手动反向写出来另一套是简洁实现直接调用框架的层和优化器。很多人的误区是觉得从零实现太啰嗦我直接用简洁版不就行了这其实浪费了这本书最大的价值。从零实现的意义在于让你知道框架帮你在背后做了哪些事。比如你自己手动算过一遍交叉熵的梯度之后再用现成的损失函数你心里是有底的出了怪问题也知道该往哪个方向查。而简洁实现的意义在于让你熟悉工程上真实的写法毕竟没人会在生产代码里手写反向传播。我的建议是两条线都要走而且顺序不能反先啃从零实现卡住了也别急着看答案熬一熬再对照简洁实现那个原来框架就是这么干的的瞬间就是真正内化的时刻。1.3 啃完之后你大概会处在什么水平我不想给你灌鸡汤说读完就能进大厂做算法。这套书的目标不是把你培养成调参高手而是让你具备看懂一篇论文的模型结构、并把它的核心部分用代码复现出来的能力。学完之后你大概率能独立完成这几件事用 PyTorch 搭一个卷积网络在常见数据集上跑出合理精度、理解张量运算和自动求导的机制、看得懂主流模型的代码结构、知道遇到训练不收敛该往哪个方向排查。但有些东西它覆盖得并不深比如分布式训练、大规模工程部署、最新的各类注意力结构细节这些需要你后续再补。所以我的定位是把它当成入门到能上手的桥梁而不是终点。摆正这个预期很重要否则你学到后半段会焦虑觉得怎么还有这么多不会的。会的东西永远是有限的关键是拿到了一张能继续往前走的通行证。2. 环境这一步别怕花时间它是唯一一次性的成本我得先给你打预防针环境配置是这门课劝退率最高的环节没有之一。但好消息是它是一次性成本配好之后你后面几个月都不用再碰它。我见过太多人卡在这里报了几个错就以为自己不适合学这个。其实跟你资质没关系纯粹是环境这件事本身就有很多隐形的坑踩过一次就记住了。下面把我认为最稳妥的一条路径讲清楚。2.1 先把一个关键选择定下来本地还是云端动手之前先做决策你到底是在自己电脑上装还是用云端的现成环境。判断标准其实很简单看两件事一是有没有一块像样的独立显卡二是你愿不愿意花时间折腾驱动。如果你只是跟着前面几章学张量和基础模型CPU 版本完全够用本地装个 CPU 版 PyTorch 几分钟的事。等你学到卷积、视觉那块没有显卡会慢到让你失去耐心这时候要么本地有卡要么转云端。云端的好处是环境都给你配好了开箱即用主要是按量计费坏处是每次重启可能要重新拉环境而且你得习惯远程开发的方式。我的实际做法是本地用 CPU 版过基础章节等到需要跑卷积的时候再上云端。别一上来就追求一步到位先把最难的能跑起来这件事解决后面缺什么补什么。2.2 用 conda 建一个干干净净的独立环境千万不要在系统自带的 Python 里直接装库这是我踩过最疼的坑。不同项目对库的版本要求不一样混在一起迟早出问题最惨的情况是把系统的 Python 搞崩了。正确做法是给这门课单独建一个虚拟环境。整体流程就是装一个 Anaconda或更轻量的 Miniconda用它创建一个指定 Python 版本的独立环境激活这个环境后再装 PyTorch之后所有操作都在这个环境里进行。关键点在于装 PyTorch 的时候别凭记忆手敲版本号直接去官网的安装页面根据你的系统、装包工具、Python 版本和是否用显卡让它生成一行命令给你复制。因为 PyTorch、CUDA 版本、Python 版本之间的对应关系是硬约束配错了就是一连串看不懂的报错。另外要提醒一句conda 和 pip 这两个装包工具尽量别在同一个环境里反复横跳装同一个库容易把依赖搞乱整个过程统一用一个就好。2.3 装完之后必须做的三行验证装完不代表成功一定要验证。我习惯跑三件事第一导入 PyTorch 并打印它的版本号确认装上了第二检查显卡是否可用如果返回 True 说明显卡版装对了返回 False 就说明你装的是 CPU 版或者显卡环境没配好第三随手建两个张量做个加法能正常输出结果就说明运行没问题。这三步看着简单但能帮你把装是装了、但根本跑不了这种最坑的情况提前筛出来。我见过有人装完兴冲冲地往下学学到第三天才发现程序一直默默在 CPU 上跑白白浪费了显卡。验证的时候顺便把这几行代码存成一个文件留着以后换机器、换环境拿出来跑一遍就能快速确认状态。2.4 新手最容易卡住的几个报错我把遇到的高频问题归成三类。第一类是找不到模块通常是没激活虚拟环境或者在一个环境里装、在另一个环境里跑解决方式是先确认当前激活的是哪个环境。第二类是显卡相关的报错多半是显卡驱动版本和 PyTorch 自带的运行库版本对不上这种最省事的办法是重新去官网核对版本重新装别硬修。第三类是路径和编码问题尤其在国内环境里文件名带中文或者路径有空格都可能报奇怪的错养成全英文路径的习惯能省掉很多麻烦。提示遇到报错先别急着搜先看报错信息的最后几行绝大多数问题原因都写在那里比任何教程都准。3. 张量这关过不去后面全是空中楼阁很多人学深度学习急着往卷积、注意力那些看起来高级的地方冲结果基础没打牢一遇到维度不匹配就懵。我想说的是张量这一章值得你反复看、反复敲它是整本书真正的地基。地基没打稳后面每往前走一步都是摇摇晃晃的。3.1 张量到底是什么为什么不是普通数组你可以把张量先粗略理解成带类型、能在显卡上运算、还能自动求导的多维数组。它跟普通的数组有很多相似的操作比如取元素、切片、形状变换但多了三个关键能力第一它能放在显卡上做并行加速第二它记录了梯度信息能参与自动求导第三它有明确的形状和数据类型运算时会被严格检查。正是因为多了这些能力张量的一些行为跟普通数组不太一样比如形状必须匹配才能运算类型不匹配会直接报错。刚开始你会觉得这些限制很烦但正是这些限制保证了后面的模型不会因为一个隐蔽的形状错误而算出莫名其妙的结果。我理解张量的转折点是在真正做过一次广播和一次自动求导之后那种哦原来它记得我每一步操作的感觉比看十遍定义都来得实在。3.2 广播机制最安静的 bug 制造机广播机制是张量运算里最容易被忽视、也最容易埋雷的地方。简单说当两个形状不同的张量做运算时框架会自动把某些维度撑开来对齐让运算能够进行下去。好处是写起来很简洁坏处是它太安静了一旦你的形状本来就有问题它不会报错而是给你算出一个看起来正常、实际上错误的结果。我吃过一次亏本来想把一个向量加到一批样本上结果因为形状没对齐广播出来的结果维度是对的数值全错了我调了大半天才发现问题出在这。从那以后我养成了一个习惯凡是涉及形状变换或者跨维度运算的地方随手打印一下张量的形状确认符合预期再往下走。多花几秒钟打印形状能省下几个小时排查。这个习惯我从学张量一直保留到现在受益无穷。3.3 视图、副本和就地操作的区别这三个词看着抽象但它们的区别会直接影响你的程序是否正确。视图是指新张量和原张量共享同一块内存你改其中一个另一个也跟着变副本是复制出一份独立的数据改一个不影响另一个就地操作是直接在原数据上修改不产生新张量通常带一个下划线后缀。为什么要在意这个因为在梯度计算和参数更新的时候误用了共享内存或者就地操作会悄无声息地破坏计算结果而且不报错。我刚开始学的时候看到某个操作既能用视图写法又能用副本写法觉得随便选一个都行结果在一次手动实现反向传播时因为共享内存导致梯度算错了。搞清楚这层关系之后你写代码会谨慎很多知道哪些地方必须用副本隔离开。3.4 自动求导计算图是怎么悄悄被记下来的自动求导是 PyTorch 的核心能力理解它能解释很多为什么这么写的问题。机制其实不算复杂当你对一个需要求导的张量做运算时框架会顺手把每一步操作记录下来形成一张计算图。等到你调用反向传播时它就沿着这张图从后往前用链式法则一层层把梯度算出来。有两个细节必须记住。第一梯度默认是累加的不是覆盖的所以每一轮训练前都要手动把上一轮的梯度清零忘了清零是新手最常见的错误之一后果是梯度越滚越大模型直接训飞。第二默认情况下框架只保留最后一步的中间结果如果你需要反复求导得显式声明要保留中间值。我自己第一次手动实现一个两层网络时就是因为没清梯度看着损失一路上蹿还以为是学习率设大了折腾了很久才发现是这么个低级问题。4. 从线性回归到 softmax第一次把模型从数据到预测串通基础打完之后会进入第一个完整模型的阶段。线性回归看着简单但它把数据加载、前向计算、损失计算、反向传播、参数更新这一整套流程包圆了后面再复杂的模型骨架也是这一套。所以这一章不是让你学怎么拟合一条直线而是让你第一次把整条链路亲手串起来。4.1 数据加载这一步其实藏着很多默认行为很多人对数据加载不上心觉得就是把数据喂进去而已其实这里有几个默认行为值得你停下来看清楚。首先是批次数据不是一条条送进模型的而是打包成一批一批送批次大小会直接影响训练速度和最终效果。其次是打乱每轮训练前把数据顺序打乱能避免模型记住样本的顺序这一点在分类任务里尤其关键。还有一个容易被忽略的是数据预处理和迭代的配合。比如很多实现里数据会被归一化处理这个步骤必须在划分训练集和验证集之后、并且用同一套参数来做否则会出现信息泄漏让你的验证结果虚高。我早期做实验时就犯过这个错测试准确率高得离谱后来才发现是预处理用错了把测试集的信息漏进了训练过程。数据这块看着枯燥却是最值得较真的地方。4.2 把训练循环的每一行拆开看训练循环基本就是固定的几件事但每一件都有它的道理。前一步是清空上一轮的梯度接着是前向计算得到模型对当前批次的预测然后拿预测和真实标签比算出损失再调用反向传播让框架把每个参数的梯度算出来最后让优化器根据梯度更新参数。我强烈建议你第一次学的时候别复制粘贴就完事而是一行一行问自己这行在干什么、为什么不能省。比如为什么梯度要清零、为什么更新要在反向传播之后、为什么损失要算平均而不是求和。这些问题想清楚了后面你自己改模型、加技巧的时候才知道每一步该怎么动。我见过有人能跑通代码但完全不知道每行在干嘛换一个数据集立刻瘫痪问题就出在这。4.3 损失函数和优化器不是随便配的组合损失函数衡量的是模型预测和真实答案的差距优化器则决定了怎么根据这个差距去调整参数。选择的时候不是随便抓一个就行得跟任务匹配。回归类问题常用平方误差分类问题常用交叉熵这个对应关系要记牢。优化器这边早期实践里最常用的是一类带动量的随机梯度下降它比朴素版本收敛更稳、更快。我想强调一个容易误解的点损失函数和优化器之间不是随便组合都行的。有些损失函数隐含了对输出形式的假设比如交叉熵一般配合经过 softmax 处理后的输出如果你直接把它用在没有归一化的原始输出上数值会很难看梯度也会不稳定。这些约束书本上未必写得那么直白但你在实际写代码的时候会撞上。我的建议是每换一个组合先在一个很小的数据上跑几轮看损失是不是正常下降确认配置没问题再上全量数据。4.4 损失不下降的完整排查链路这是我被问得最多的问题也是我自己撞过最多次的墙。遇到损失不下降别慌按顺序排查。第一步先确认数据本身没问题标签有没有对应错乱输入有没有归一化得离谱。第二步检查学习率太大导致震荡、太小导致几乎不动都是典型症状可以试着调一个数量级看看变化。第三步确认梯度清零做了没有这个低级错误造成的后果非常迷惑。如果以上都排除了再往深里看模型结构是不是太浅或者太深、初始化是不是不合理、损失函数的用法对不对。我自己的经验是八成的问题出在前三步尤其是学习率和梯度清零这两个。剩下两成往往是你对某个操作的理解有偏差这时候打印中间张量的形状和数值比空想有用得多。5. 卷积网络和训练技巧决定你能不能跑出结果的阶段走到这一块你会从能跑通进入能跑好的阶段。卷积网络是处理图像类任务的主力结构而各种训练技巧决定了你能不能把结果真正做出来。这一章的门槛比前面高因为它涉及的调参经验更多光靠看书不够必须自己动手试。5.1 卷积到底在算什么抛开术语卷积干的事情其实很朴素拿一个小窗口在输入图像上从左到右、从上到下滑动每滑到一个位置就把窗口里的数值和图像对应位置的数值逐项相乘再相加得到输出图上的一个数。这个窗口就是卷积核它记录的是某种局部特征的模式比如边缘、纹理。卷积核里的数值不是手写的而是通过训练学出来的。为什么要用卷积而不是直接用全连接因为它有两个天然优势。第一它只关注局部参数比全连接少得多不容易过拟合也好训练。第二它对位置不敏感同一个特征出现在图像的不同位置卷积都能识别到这叫平移不变性。理解了这两点你就明白为什么图像任务几乎都用卷积起步。我当初看卷积的公式觉得绕后来把它想成一个小模板在图上找相似的局部图案一下子就通了。5.2 过拟合和对策别一股脑全上过拟合的典型表现是训练集上的表现越来越好验证集上的表现却开始变差。原因是模型把训练数据的噪声也记住了而不是学到通用的规律。常见的对策有几种增加数据量、做数据增强、加正则化、在合适的地方加丢弃、以及早停。每个手段都有它的适用场景不是堆得越多越好。我的实际经验是先判断是不是真的过拟合了再动手。很多时候你以为是过拟合其实是训练集和验证集划分有问题或者两者分布差异太大。确认是过拟合之后优先级一般是先加数据增强这个手段便宜又有效然后再考虑正则化和丢弃。丢弃用得过猛会让模型欠拟合训练变得很慢我吃过一次亏丢弃比例设高了模型怎么都不收敛调回来立刻就正常了。5.3 学习率、批次大小和迭代轮数的联动关系这三个参数不是独立的它们之间存在相互影响很多人调参调不出效果就是没有意识到这点。学习率决定每一步走多大批次大小决定一次看多少样本迭代轮数决定总共看几遍数据。批次变大之后梯度估计更稳往往可以配合稍大一点的学习率批次很小的时候梯度噪声大学习率就得相应调小。我建议的调参策略是分步来别一起动。先把批次大小和迭代轮数固定住单独调学习率找到一个能让损失稳定下降的值然后固定学习率再调批次大小看它对稳定性和速度的影响。整个过程一定要有记录哪个配置对应哪个结果都记下来否则你会陷入改了不知道有没有变好的混乱里。这种实验管理的习惯比任何单一技巧都值钱。5.4 上显卡之后才会遇到的显存问题一旦开始用显卡跑卷积显存就会成为新的约束。最常见的报错是显存不够通常是批次大小太