1. 为什么一本364页的深度学习手册值得你花时间啃
第一次看到这个标题的时候,我脑子里冒出来的第一个念头是:又是一本“速成”书吧。市面上打着“动手学”旗号的资料太多了,翻开来要么是公式堆砌,要么是代码跑不通,最后变成“动手抄”。但真正翻完这本364页的PyTorch版手册之后,我的判断变了——它确实配得上“熬夜也要啃完”这句话,原因不在于它讲了多少高深的理论,而在于它把“从零到能跑通一个模型”这条路径铺得足够平。
先说清楚这本书到底解决什么问题。深度学习的入门门槛从来不是数学,而是“环境配好了、代码跑起来了、结果看懂了”这三件事之间的断层。很多人卡在第一步:装完框架,打开编辑器,面对一个空白的.py文件不知道从哪下手。这本手册的价值就在于,它用PyTorch作为主线,把每一个概念都落到了可执行的代码上。你不需要先啃完三百页的数学推导才能动手,而是可以边跑代码边理解背后的原理。
适合谁来读?我的判断是三类人。第一类是有一定Python基础、想转方向做深度学习但一直没找到入口的开发者;第二类是在校学生,课上讲了一堆反向传播、卷积核,但从来没自己搭过一个完整的训练循环;第三类是已经用过其他框架、想切到PyTorch的工程师,需要一本能快速对照API和设计思路的参考。如果你完全没写过Python,那建议先补一下基础语法,否则读起来会比较吃力。
关键词里提到的“PyTorch”“动手学深度学习”“PDF开源”,这三个词其实指向了同一件事:降低入门门槛。PyTorch本身的设计哲学就是“像写NumPy一样写神经网络”,动态图机制让调试变得直观;而“动手学”这三个字意味着这本书不是用来读的,是用来敲的。364页的体量不算厚,但每一页的信息密度都不低,尤其是代码部分,基本没有废话。
我自己的习惯是,拿到这类手册先不急着从头读,而是翻到目录看结构。这本书的组织方式很清晰:从张量操作开始,到线性回归、多层感知机,再到卷积网络、循环网络,最后落到注意力机制和优化算法。每一章都是“概念解释→数学表达→代码实现→运行结果”这个循环。这个结构的好处是,你可以在任何一个环节停下来,把代码复制到本地跑一遍,看看输出长什么样,再回去读文字解释,理解会深很多。
提示:不要试图一次性读完。我的建议是每次只啃一章,把这一章的代码全部手敲一遍(不要复制粘贴),跑通之后再进入下一章。手敲的过程会强迫你注意每一个参数的名字和形状,这是复制粘贴永远给不了的手感。
还有一个细节值得说:这本书的代码是基于PyTorch的,但它的重点不是教你调包,而是让你理解每一个层、每一个损失函数、每一个优化器在做什么。比如讲卷积的时候,它会先让你用最朴素的方式实现一个卷积运算,再对比nn.Conv2d的输出,这种“先手写再调包”的思路,对于建立直觉非常有用。很多人用框架用久了,遇到问题只会查文档,就是因为缺少这一层“手写”的经历。
2. 拿到PDF之后的第一件事:把环境跑通再谈其他
2.1 环境配置的坑比想象中多
很多人拿到PDF之后的第一反应是打开就读,读到第三章发现代码跑不起来,然后回头折腾环境,折腾两天之后热情就没了。我的建议是反过来的:先花半天时间把环境配好,确保一个最小的训练脚本能跑通,再开始读书。这一步看起来浪费时间,实际上是在给后面省时间。
PyTorch的安装本身不复杂,但版本匹配是个容易忽略的问题。你需要关注三个东西的版本:Python、PyTorch、CUDA(如果你有NVIDIA显卡)。这三者之间有一个兼容性矩阵,版本对不上就会出现各种奇怪的报错。比如Python 3.11刚出来的时候,某些PyTorch版本还不支持,装上去之后import torch直接报错。我的经验是,不要追求最新的版本,选一个稳定组合:Python 3.9或3.10,PyTorch 2.0以上,CUDA 11.8或12.1。这个组合在大多数场景下都能跑通。
如果你没有独立显卡,用CPU版本也完全可以。这本书里的示例模型都不大,CPU跑起来虽然慢一点,但验证代码逻辑足够了。安装的时候用官方推荐的命令,不要自己去pip install一堆乱七八糟的包,很容易把依赖关系搞乱。我习惯用conda创建一个独立环境,这样即使装崩了,删掉重建就行,不会影响系统里的其他项目。
conda create -n d2l python=3.10 conda activate d2l pip install torch torchvision torchaudio pip install d2l最后那个d2l包是这本书配套的工具库,里面封装了一些绘图和数据处理函数。装完之后,你可以跑一个简单的测试:
import torch x = torch.randn(3, 4) print(x.shape) print(x.device)如果输出是torch.Size([3, 4])和cpu(或者cuda:0),说明环境没问题。这一步看起来简单,但我见过太多人卡在这里,原因是pip源的问题或者网络问题导致包没下全。如果你在国内,建议配置一下镜像源,速度会快很多。
2.2 数据集的下载与缓存策略
这本书里用到的数据集不少,从最基础的Fashion-MNIST到后面的文本数据集。这些数据集在第一次运行的时候会自动下载,但下载速度取决于网络状况。我的做法是提前把所有数据集下载好,放到统一的缓存目录里,然后在代码里指定路径。这样即使换环境,也不用重新下载。
PyTorch默认的缓存路径是~/.cache/torch(Linux/Mac)或者C:\Users\你的用户名\.cache\torch(Windows)。你可以通过设置环境变量TORCH_HOME来改变这个路径。我一般会把它设到一个空间比较大的盘里,因为后面跑图像分类的时候,数据集可能会占几个G。
还有一个细节:有些数据集在下载之后需要解压,解压过程可能会因为磁盘空间不足而失败。建议提前检查一下磁盘剩余空间,至少留出10G以上的余量。另外,如果你在多个项目里都用同一个数据集,共享缓存目录可以省下不少空间。
注意:不要手动去改数据集的文件名或者目录结构,框架对目录结构有固定的要求。如果你下载的是压缩包,让它自动解压,不要自己解压之后挪来挪去,很容易导致路径对不上。
2.3 编辑器与调试工具的搭配
读这本书的时候,我强烈建议用Jupyter Notebook或者VS Code的Notebook模式。原因很简单:这本书的代码是分块讲解的,每一块都有对应的输出。用Notebook可以一块一块地跑,看到中间结果,理解每一步在做什么。如果你用普通的.py文件,每次都要从头跑到尾,调试效率会低很多。
VS Code的好处是可以直接连远程服务器,如果你本地机器性能不够,可以把代码放到服务器上跑,本地只做编辑。配置好Remote-SSH之后,体验和本地几乎一样。Jupyter Lab则更适合快速实验,尤其是需要画图的时候,%matplotlib inline一开,图表直接嵌在页面里,比弹窗方便。
调试工具方面,print大法在入门阶段其实够用了,但如果你想深入一点,可以学一下pdb或者VS Code的断点调试。尤其是在排查形状不匹配的问题时,断点调试能让你看到每一步的张量形状变化,比盯着报错信息猜要快得多。
3. 从张量到训练循环:这本书的骨架怎么搭
3.1 张量操作是地基,但不用死记硬背
这本书的前几章都在讲张量,这是PyTorch里最基础的数据结构。很多人看到reshape、squeeze、unsqueeze、permute这些操作就头大,觉得要记的东西太多。我的经验是,不用死记,理解几个核心概念就行。
张量的本质就是一个多维数组,和NumPy的ndarray几乎一样,区别在于PyTorch的张量可以放到GPU上,并且支持自动求导。形状变换的操作看起来多,但归纳起来就两类:改变维度数量和改变维度顺序。reshape和view是改变维度数量,permute和transpose是改变维度顺序,squeeze和unsqueeze是增删大小为1的维度。理解了这个分类,用的时候查一下文档就行,不需要背。
真正需要熟练掌握的是广播机制。这是很多形状报错的根源。简单说,当两个形状不同的张量做运算时,PyTorch会尝试把它们“扩展”到相同的形状。规则是从最后一个维度开始往前对齐,要么维度相等,要么其中一个维度是1。比如形状(3, 1)和(1, 4)相加,结果形状是(3, 4)。这个机制在写损失函数和自定义层的时候经常用到,不理解的话会经常遇到“shape mismatch”的报错。
我建议在读完张量这一章之后,自己出几道形状变换的题目,手动算一下结果形状,然后用代码验证。比如:一个形状为(2, 3, 4)的张量,经过permute(2, 0, 1)之后形状是什么?经过unsqueeze(1)之后呢?这种练习做上十道,基本就能建立直觉了。
3.2 线性回归:第一个完整的训练循环
线性回归是这本书里第一个完整的模型,虽然简单,但包含了训练循环的所有要素:数据加载、模型定义、损失函数、优化器、前向传播、反向传播、参数更新。把这部分吃透,后面所有的模型都是在这个骨架上做替换。
书里的代码大致是这样的结构:
# 定义模型 net = nn.Linear(2, 1) # 初始化参数 net.weight.data.normal_(0, 0.01) # 定义损失函数 loss = nn.MSELoss() # 定义优化器 trainer = torch.optim.SGD(net.parameters(), lr=0.03) # 训练循环 for epoch in range(num_epochs): for X, y in data_iter: l = loss(net(X), y) trainer.zero_grad() l.backward() trainer.step()这段代码看起来简单,但有几个地方容易出错。第一,trainer.zero_grad()的位置。PyTorch默认会累积梯度,如果你不在每次迭代开始时清零,梯度会一直累加,导致参数更新错误。第二,l.backward()之前要确保l是一个标量,如果是向量,需要先求和或者求平均。第三,trainer.step()之后不需要手动清零梯度,下一次迭代开始时zero_grad()会处理。
我见过有人把zero_grad()放在backward()之后、step()之前,这样也能跑,但逻辑上是错的,因为梯度在step()之后就没用了,清零的时机不影响结果,但放在前面更符合直觉。还有一个常见的错误是忘记调用trainer.step(),结果损失一直不下降,排查半天才发现参数根本没更新。
提示:在训练循环里加一行打印损失的代码,每10个batch打印一次。这样你能直观地看到损失在下降,如果一直不降或者震荡,说明学习率或者数据有问题。这个习惯在调试复杂模型的时候尤其有用。
3.3 从线性模型到多层感知机:激活函数的作用
线性回归之后,书里引入了多层感知机(MLP)。这里的关键变化是加入了激活函数。如果没有激活函数,多层线性层的叠加等价于一层线性层,模型的表达能力没有提升。激活函数的作用是引入非线性,让网络能够拟合复杂的函数。
书里重点讲了ReLU、Sigmoid和Tanh。ReLU是现在最常用的,因为它的梯度在正区间恒为1,不会出现梯度消失的问题,而且计算简单。Sigmoid和Tanh在早期用的多,但现在主要用在特定的场景,比如二分类的输出层或者门控机制里。
我自己的经验是,入门阶段先用ReLU,遇到问题再考虑换。有一个细节是ReLU的负数区间梯度为0,如果某个神经元的输入一直是负数,这个神经元就“死”了,梯度永远为0,参数不再更新。这种情况在训练初期比较常见,可以通过减小学习率或者用LeakyReLU来缓解。
书里在讲MLP的时候,还提到了权重初始化的重要性。如果权重初始化得太小,信号在层间传递时会逐渐衰减;如果太大,会逐渐放大,导致梯度爆炸。常用的初始化方法有Xavier初始化和He初始化,分别适用于Sigmoid/Tanh和ReLU。PyTorch的nn.Linear默认用的是均匀分布初始化,范围是[-1/sqrt(fan_in), 1/sqrt(fan_in)],对于浅层网络够用了,但深层网络最好手动指定初始化方法。
3.4 训练循环的通用模板
把线性回归和MLP的代码对比一下,你会发现训练循环的结构几乎一样,区别只在于模型定义和数据处理。这意味着你可以把训练循环写成一个通用的函数,后面所有的模型都复用这个函数。书里虽然没有明确给出这个模板,但你在读的时候可以自己提炼。
一个通用的训练函数大概长这样:
def train(net, train_iter, loss, num_epochs, trainer): for epoch in range(num_epochs): for X, y in train_iter: l = loss(net(X), y) trainer.zero_grad() l.backward() trainer.step() print(f'epoch {epoch + 1}, loss {l.item():.4f}')这个模板可以应对大部分监督学习任务。后面遇到卷积网络、循环网络,只需要把net换成对应的模型,数据加载部分换成对应的DataLoader,训练循环本身不用改。这种“模板化”的思路能帮你把精力集中在模型结构上,而不是每次都在重复写训练代码。
4. 卷积与循环:两个最容易劝退的章节怎么啃
4.1 卷积网络:从手写卷积到调包
卷积网络是这本书里第一个“看起来复杂”的章节。很多人看到卷积核、步幅、填充、通道这些概念就懵了。我的建议是,先不要管nn.Conv2d,自己用最朴素的方式实现一个卷积运算,理解它在做什么,再去用框架的API。
书里有一个很好的例子:用corr2d函数实现二维互相关运算。这个函数只有几行代码,但把卷积的核心逻辑讲清楚了:一个卷积核在输入上滑动,每个位置做逐元素乘法再求和。理解了这一步,再去看nn.Conv2d的参数就很容易了。in_channels和out_channels决定卷积核的数量和形状,kernel_size决定滑动窗口的大小,stride决定滑动的步长,padding决定在输入边缘补多少圈零。
填充的作用是控制输出尺寸。如果不填充,每次卷积之后特征图都会变小,几层之后就没法再卷了。填充的大小通常取kernel_size // 2,这样输出尺寸和输入尺寸保持一致。步幅的作用是降采样,步幅为2时输出尺寸减半,这在后面讲ResNet的时候会经常用到。
池化层是另一个容易混淆的概念。最大池化和平均池化都是对局部区域做聚合,区别在于取最大值还是平均值。池化的作用主要是降维和增加平移不变性。书里讲池化的时候,会对比它和卷积的区别:卷积是有参数的,池化没有参数;卷积改变通道数,池化不改变通道数。
我自己的经验是,卷积网络这部分一定要动手画图。拿一张纸,画一个5x5的输入,一个3x3的卷积核,手动算一下输出是什么。然后再加一个padding=1,再算一遍。这个过程花不了多少时间,但能帮你建立空间直觉。后面遇到nn.Conv2d的参数时,脑子里能浮现出对应的图像,而不是一堆抽象的数字。
4.2 循环网络:时间维度的展开
循环网络(RNN)是另一个劝退点,因为它的输入多了一个时间维度。很多人不理解hidden state是怎么传递的,也不理解为什么需要detach。我的理解方式是:把RNN想象成一个被反复调用的函数,每次调用时传入当前输入和上一次的输出,然后返回新的输出。
书里讲RNN的时候,会先实现一个最朴素的版本,用循环手动展开时间步。这个版本虽然慢,但逻辑清晰:初始化隐藏状态,然后对每个时间步,计算新的隐藏状态和输出。理解了这一步,再去看nn.RNN的API就很容易了。nn.RNN的返回值有两个:输出序列和最终隐藏状态。输出序列包含了每个时间步的输出,最终隐藏状态可以用来初始化下一个batch的隐藏状态。
这里有一个容易踩的坑:隐藏状态在batch之间传递时,需要先detach,否则计算图会一直保留,导致内存爆炸。书里在讲RNN的时候会提到这一点,但很多人第一次读的时候会忽略。我的做法是,在每次forward之前,把隐藏状态从计算图中分离出来:
hidden = hidden.detach()还有一个细节是梯度裁剪。RNN在反向传播时,梯度会沿着时间步连乘,很容易出现梯度爆炸。梯度裁剪的做法是,如果梯度的范数超过某个阈值,就按比例缩放。PyTorch提供了torch.nn.utils.clip_grad_norm_函数,用起来很方便。书里在讲RNN的训练时会提到这个技巧,但不会展开讲,你可以自己查一下文档。
4.3 注意力机制:从Seq2Seq到Transformer
注意力机制是这本书里比较靠后的内容,也是现在最热门的方向。书里的讲法是先从Seq2Seq的问题入手:编码器把输入序列压缩成一个固定长度的向量,解码器从这个向量生成输出序列。当输入序列很长时,固定长度的向量会成为瓶颈,注意力机制就是为了解决这个问题。
注意力的核心思想是:解码器在生成每个词的时候,不是只看编码器的最终状态,而是看编码器所有时间步的状态,并根据相关性加权求和。这个“相关性”就是注意力权重,通过一个小的神经网络计算得到。书里会给出注意力机制的数学表达和代码实现,代码量不大,但理解起来需要一点时间。
我建议在读这一章之前,先把前面的Seq2Seq部分跑通,理解编码器和解码器各自在做什么。然后在这个基础上加注意力,对比加之前和加之后的输出差异。这种对比式的学习方式比单独看注意力机制的代码要有效得多。
Transformer是注意力的进阶版本,书里会讲多头注意力和位置编码。多头注意力就是把注意力机制并行地做多次,每次用不同的投影矩阵,最后把结果拼接起来。位置编码是为了让模型知道序列中每个元素的位置,因为注意力机制本身是不区分顺序的。这部分内容比较抽象,建议结合代码一起看,尤其是nn.MultiheadAttention的输入输出形状,自己手动算一遍。
5. 那些手册不会告诉你但实际会遇到的坑
5.1 显存不够用怎么办
跑这本书的代码时,最容易遇到的问题就是显存不够。尤其是后面讲卷积网络和Transformer的时候,模型参数量上去了,batch size稍微大一点就OOM(Out of Memory)。我的经验是,先不要急着换显卡,试试这几个方法。
第一,减小batch size。这是最直接的方法,但要注意batch size太小会导致训练不稳定,梯度噪声大。通常从32或者64开始试,如果还是OOM,再往下调。第二,用梯度累积。如果batch size降到1还是不够,可以把多个小batch的梯度累加起来,再更新一次参数。这样等效于大batch,但显存占用小。第三,用混合精度训练。PyTorch的torch.cuda.amp可以自动把部分计算转成半精度,显存占用能减少一半左右,速度也有提升。第四,及时释放不用的变量。Python的垃圾回收机制有时候不会立即释放显存,可以手动调用torch.cuda.empty_cache()。
还有一个容易被忽略的点是,Jupyter Notebook里如果反复运行同一个cell,之前的计算图可能还占着显存。这时候重启kernel是最彻底的办法。我习惯在跑大模型之前先重启一次kernel,确保显存是干净的。
5.2 损失不下降的排查思路
训练过程中损失不下降,原因可能有很多。我的排查顺序是这样的:先看数据,再看模型,最后看超参数。
数据方面,检查输入和标签是否对应。有时候数据加载器打乱了顺序,但标签没有跟着打乱,导致模型学不到东西。检查方法是手动取一个batch,打印输入和标签,看看是否匹配。模型方面,检查输出层的维度是否和类别数一致,损失函数是否适合当前任务。比如多分类用交叉熵,回归用均方误差,用错了损失函数损失当然不降。超参数方面,学习率是最关键的。学习率太大会震荡,太小会下降很慢。可以试试用学习率扫描,从1e-1到1e-5,每个跑几个epoch,看哪个下降最快。
还有一个常见原因是初始化。如果权重初始化得太小,信号传不到输出层,损失会一直在一个很高的值附近徘徊。可以试试换一种初始化方法,或者手动把权重调大一点。
5.3 过拟合与欠拟合的判断
过拟合和欠拟合是训练中经常遇到的两个问题,判断方法很简单:看训练损失和验证损失的差距。如果训练损失一直在降,但验证损失先降后升,说明过拟合了。如果训练损失和验证损失都很高,说明欠拟合。
过拟合的解决方法有:增加数据量、加正则化(L2正则化或者Dropout)、减小模型复杂度、早停。书里在讲多层感知机的时候会提到Dropout,在讲卷积网络的时候会提到数据增强。这些方法的效果因任务而异,我的经验是先用数据增强和Dropout,如果还不够再考虑减小模型。
欠拟合的解决方法有:增加模型复杂度、减小正则化强度、增加训练轮数、调大学习率。欠拟合通常说明模型容量不够,或者训练不够充分。可以先试试把模型加深加宽,看看训练损失能不能降下去。
5.4 代码跑得慢的优化技巧
这本书里的代码在CPU上跑可能比较慢,尤其是卷积网络和循环网络。如果你有NVIDIA显卡,确保代码跑在GPU上。检查方法是打印torch.cuda.is_available(),如果是True,再把模型和数据移到GPU上:
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') net = net.to(device) X = X.to(device)还有一个提速技巧是用DataLoader的num_workers参数。默认是0,表示在主进程里加载数据,设为4或者8可以并行加载,速度会快很多。但要注意,在Windows上num_workers大于0可能会有问题,建议在Linux或者Mac上用。
另外,pin_memory=True可以加速CPU到GPU的数据传输,尤其是数据量大的时候效果明显。这个参数在DataLoader里设置,配合num_workers一起用。
6. 怎么把这本书的价值榨干
6.1 不要只读一遍
这本书的信息密度很高,第一遍读的时候很多细节会漏掉。我的做法是读三遍:第一遍快速过,了解整体结构,知道每一章讲什么;第二遍精读,把代码全部敲一遍,跑通;第三遍带着问题读,比如“为什么这里用这个损失函数”“为什么这个层的输出维度是这样”,把每个细节都搞清楚。
第二遍是最花时间的,但也是最关键的。手敲代码的过程中,你会遇到各种报错,解决报错的过程就是学习的过程。我建议把遇到的报错和解决方法记下来,形成一个自己的“踩坑笔记”。后面再遇到类似的问题,查笔记比查搜索引擎快得多。
6.2 用项目驱动学习
光读不练,很快就忘了。我的建议是,每读完一章,找一个相关的小项目练手。比如读完线性回归,可以找一个房价预测的数据集,自己从头搭一个模型;读完卷积网络,可以找一个图像分类的比赛数据集,试试能不能达到一个不错的准确率;读完循环网络,可以试试文本分类或者时间序列预测。
项目不需要很大,关键是完整。从数据加载、模型定义、训练循环到评估,全部自己写一遍。写完之后和书里的代码对比,看看哪些地方可以改进。这种对比能帮你发现自己的盲点。
6.3 参与开源社区的讨论
这本书是开源的,意味着你可以看到其他人的讨论和贡献。遇到不懂的地方,可以去项目的issue区搜一下,很可能已经有人问过类似的问题。你也可以自己提issue,通常会有维护者或者其他读者回复。
参与讨论的好处是,你能看到不同人的理解角度。有时候书里的一句话你没看懂,但别人用另一种方式解释之后,你就豁然开朗了。而且,回答别人的问题也是检验自己理解程度的好方法。如果你能把一个问题讲清楚,说明你真的懂了。
6.4 建立自己的代码库
读这本书的过程中,你会积累很多代码片段:数据加载的模板、训练循环的模板、常用的层和损失函数。建议把这些代码整理成一个自己的库,后面做新项目的时候直接复用。这样不仅能提高效率,还能帮你形成自己的代码风格。
我自己的做法是,按功能分目录:data/放数据加载相关的代码,models/放模型定义,train/放训练循环,utils/放绘图和日志工具。每个目录下再按具体任务分子目录。这样积累下来,后面遇到新任务时,大部分代码都能直接拿来用,只需要改模型部分。
6.5 关注PyTorch的版本更新
PyTorch的版本更新比较快,有些API会变。这本书的代码是基于某个特定版本写的,如果你用的版本更新,可能会遇到API不兼容的问题。我的建议是,先按照书里的版本跑通,然后再尝试用新版本跑,遇到不兼容的地方查一下迁移指南。
PyTorch的官方文档有版本迁移的说明,比如从1.x到2.x的变化。主要的变动包括torch.jit的改进、torch.compile的引入、以及一些API的废弃。这些变化不影响核心概念的理解,但会影响代码的运行。保持关注官方博客和GitHub的release notes,能帮你及时了解这些变化。
提示:如果你在跑书里的代码时遇到
AttributeError或者ImportError,先检查一下PyTorch的版本。很多时候是版本不匹配导致的,降级或者升级就能解决。
7. 一些个人体会
啃完这本书之后,我最大的感受是:深度学习的入门真的不需要先成为数学家。这本书用代码作为主线,把数学公式放在辅助的位置,让你先看到结果,再理解原理。这种“自底向上”的学习路径,对于工程师背景的人来说非常友好。
另一个感受是,PyTorch的设计确实比早期的框架更人性化。动态图机制让调试变得像写普通Python代码一样自然,不需要先定义计算图再运行。这种即时反馈的体验,能让你更快地建立直觉。
如果让我给一个具体的建议,那就是:不要跳过任何一章的代码。哪怕你觉得这个概念很简单,也把代码敲一遍。很多时候,你以为自己懂了,但一敲代码就发现细节对不上。这种“以为懂了但实际没懂”的地方,就是你需要重点攻克的地方。
最后,364页看起来不多,但每一页都值得花时间。不要追求速度,追求的是每一章都能自己复现出来。慢就是快,这句话在学深度学习的时候特别适用。