
说实话我最早翻到《动手学深度学习》第二章预备知识的时候第一反应是这章不就是复习数学和Python基础吗跳过应该也不影响吧。后来认真过了一遍才发现这章是整个课程里“性价比”最高的一章后面每一个模型、每一段训练代码全都在反复使用这里的概念——张量操作、数据预处理、线性代数、微积分、自动微分。如果你也是初学深度学习或者之前草草跳过基础、现在越学越费力那这篇笔记大概率能帮你把ch02真正吃透。我会按章内模块往下拆结合我实际敲代码时踩过的坑和验证过的思路把“为什么这么做”也讲清楚而不是简单罗列API。1. 张量操作深度学习的核心数据结构1.1 张量到底是什么从数组到计算图节点课程一上来就会介绍张量Tensor。说白了它就是深度学习里的“数据容器”一个数字是零维张量一列数是向量一维张量一个表格是矩阵二维张量再往上的图像、批量样本就对应三维、四维甚至五维张量。我第一次学的时候有个困惑它和Python列表或NumPy数组有什么区别课程里其实明确说了张量除了能存数据还能被框架“盯着”——它会自动记录这个数据参与了哪些运算方便之后计算梯度。这一点才是它成为深度学习基石的原因。举个具体例子你用普通数组也能实现weight * x bias但后面算梯度就得自己写链式法则。而张量只需要标记requires_gradTrue反向传播时梯度自动就出来了。有了这个设计研究模型的精力就能集中在结构设计上而不是手工推导导数。1.2 创建与形状操作shape是第一生产力ch02里大量操作围绕“形状”展开reshape、view、transpose、permute。初学者最容易翻车的一件事就是把形状搞错。我来用一个实际操作说明一下。比如你有一个形状为(12,)的一维张量12个元素想把它变成(3, 4)的二维矩阵直接x.reshape(3, 4)就行。但如果你先做了转置操作再view就可能报错——因为view要求底层数据在内存中是连续存储的而transpose会把数据变得“不连续”。这时候用reshape更安全因为它必要时会自动复制数据、重新排列。我实际调模型时养成了一个习惯每进行一次矩阵变换就打印一下x.shape。看着不起眼但这招至少帮我省下了几个小时调试维度错误的时间。还有一点特别想提醒reshape可以只指定一个维度另一维用-1自动推断。比如x.reshape(-1, 4)表示“行数自动算列数固定为4”这种方法在数据整理时非常常用。1.3 广播机制维度不匹配时的隐含规则很多新手看到a b两个张量形状不一样时第一反应是“这肯定报错吧”实际上主流深度学习框架支持一种叫“广播”的机制当两个张量的维度从右往左对齐时如果某一个维度缺失或为1它就会自动扩展到另一个张量的形状。我用一个生活化类比来记你有一排灯泡形状1, 10的向量和一个控制台形状10, 1的向量广播机制就像把两个不同方向的光同时铺满一块10x10的屏幕。写代码时这种机制非常好用——比如给一批样本的每个特征加同一个偏置直接X b就行不用手动复制b。不过广播也会埋雷。我有一次做矩阵运算本意是维度(64, 128)乘以(128, 64)结果手滑把第二个矩阵写成了(64, 128)框架没有直接报错——因为广播机制把它当成逐元素运算处理了最后结果是形状全乱了直到损失一路飞涨才发现问题。这提醒我维度检查一定要主动做不能依赖报错来提醒。2. 数据预处理从原始数据到模型输入2.1 数据读取与清洗最容易被低估的一环ch02里用了一个简单例子用Pandas读CSV处理缺失值。当时我心想不就是fillna嘛太简单了。后来自己做真实项目才发现这个环节才是决定模型上限的地方。课程里的做法很直接对数值特征缺失值用均值填充对类别特征把它转成索引或者独热编码。这个思路本身没错但我觉得有两点可以扩展。第一填充缺失值之前要想清楚“为什么缺失”。如果缺失是一种有含义的状态比如用户没填写收入那你可能更适合加一个“是否缺失”的辅助特征而不是简单填均值。第二验证集和测试集的填充参数必须用训练集计算出来不能各自算各自的。否则会引入数据泄漏——模型在评估时“偷看”了未来信息导致验证结果虚高。我自己的实操流程是这样的先用pandas.read_csv读进内存用df.info()和df.describe()看每一列的类型、缺失情况、分布区间数值型缺失列按特征业务含义决定填0、均值或中位数类别型列先value_counts()看类别数量再决定直接编码还是合并低频类别。2.2 标准化与归一化让梯度下降更顺畅ch02提到了将特征缩放的重要性。为什么要做我用一个实际观察说明假设有两个特征一个是身高数值在1.6到1.9之间一个是月收入数值在5000到20000之间。如果不做缩放损失函数的地形会像一个“狭长山谷”梯度下降在某个方向上反复震荡收敛非常慢缩放之后地形接近一个圆碗梯度下降路径短得多收敛也快得多。常见的两种做法是最小-最大归一化(x - min) / (max - min)把数据压到0到1区间标准化(x - mean) / std让数据均值为0、方差为1。我更推荐后者特别是后续要用激活函数如Sigmoid、Tanh时标准化能让数值落在激活函数敏感区间梯度更稳定。具体实现建议直接用框架的层或封装好的计算组件来处理但核心原理一定要懂否则调参时就只能瞎猜。2.3 数据划分与批次加载课程里手工实现了数据迭代器用next()每次取出一小批样本。这个设计看起来是“为了教学演示”但它背后是整个训练循环的雏形。我做项目时一般会这样组织数据原始数据先随机打乱注意要固定随机种子方便复现分出训练集、验证集、测试集比例常见的是7:2:1验证集用于调参和早停测试集只在最终评估时碰一次加载数据时设好batch_size和shuffleTrue训练集每个epoch都要重新打乱验证集和测试集一般不打乱。这里有一个我踩过的坑早期做实验时我把测试集当验证集天天用它调参最后模型“表现很好”一上真实环境就崩。后来我才明白——你在测试集上调过参数它就不再是“未知数据”了。正确做法是验证集管调参测试集管最终判分这个纪律一定要守住。3. 线性代数与微积分模型背后的数学引擎3.1 矩阵乘法前向传播的灵魂课程在ch02里快速过了一遍线性代数包括标量、向量、矩阵、点积、矩阵乘法、范数。表面上看是数学复习其实每一个概念都对应一个深度学习场景。比如矩阵乘法XW b就是全连接层的核心运算X是批量样本W是权重b是偏置。我当初学到这里最大的顿悟是为什么不用循环写每个神经元的计算因为矩阵乘法能把“对一批样本、对多个输出神经元”的计算一次性做完底层还有高度优化的矩阵运算库甚至GPU加速比Python循环快几个数量级。理解这一点对后续看代码特别重要。当你看到output x weight.t() bias这种写法时要能立刻反应过来这是在沿特征维度做变换把输入空间映射到输出空间。如果看到维度匹配不上最先要检查的就是weight是不是需要转置。3.2 导数与梯度下山找最低点微积分部分课程重点讲了导数和链式法则。对我这种非数学背景的人来说最有用的理解方式是“下山模型”想象你站在一座山上当前参数位置你要摸黑走到山谷损失最小处。你要判断下一步该往哪个方向走、走多远。梯度告诉你的正是“最陡上坡方向”取个负号就是最陡下坡方向。学习率则是“每步迈多大”。链式法则要解决的是更具体的问题一个深层网络里最终损失对某个浅层参数求导中间隔着很多层运算。链式法则就是把这些“局部导数”乘起来。课程里强调这个概念是因为自动微分本质上就是它在工程上的实现。3.3 范数给向量“称重”范数也是容易被忽略的点尤其是L2范数。它计算的是向量各元素平方和的平方根直观上就是向量的“长度”。后续学正则化时你会见到“权重衰减”往损失里加上权重的L2范数目的是防止某些参数过大、导致模型过拟合。ch02在这里埋了个伏笔但如果不认真理解范数的意义后面看正则化代码就会发蒙。4. 自动微分深度学习的工程灵魂4.1 隐式计算图框架在背后做了什么课程中后期花了不小篇幅讲自动微分。我认为这是ch02真正值得反复读的一节。它回答了一个核心问题神经网络动辄上百万参数梯度到底怎么高效算出来的框架的做法是在你执行y x * w b这类操作时它不只是算出一个结果还会在内存里建立一张“计算图”——记录谁是谁的输入、谁由谁生成。等到你调用.backward()时框架从最终结果通常是损失出发沿着这张图反向走一遍每经过一个节点就应用一次链式法则把局部梯度乘起来。这个过程对使用者几乎是透明的但它解释了训练循环里那几行固定代码的含义optimizer.zero_grad()把上一次梯度清掉loss.backward()从当前损失反向传播计算梯度optimizer.step()根据梯度更新参数。4.2 实操代码与避坑细节我用一小段伪代码来还原训练循环的基本轮廓# 模型参数标记需要梯度 w torch.randn(10, 1, requires_gradTrue) b torch.zeros(1, requires_gradTrue) for epoch in range(num_epochs): # 1. 前向计算 y_pred x w b loss ((y_pred - y) ** 2).mean() # 2. 梯度清零必须 if w.grad is not None: w.grad.zero_() b.grad.zero_() # 3. 反向传播 loss.backward() # 4. 更新参数 with torch.no_grad(): w - lr * w.grad b - lr * b.grad这段代码我手敲过很多次有几个细节必须提醒。第一zero_grad不能省。框架的梯度是累积的如果你不清零下一次backward()会把新梯度加到旧梯度上参数更新方向就乱了。第二更新参数时建议包在no_grad()里因为更新操作本身不是“训练目标”不需要加入计算图。第三requires_gradTrue必须设置在叶子张量上如果从一个普通数组直接torch.tensor(data)再往后运算它是不会有梯度的。我调试时经常遇到一种情况模型参数根本不更新loss始终不降。排查后往往发现是梯度没传到参数上——要么忘了requires_gradTrue要么在某个中间环节用了detach()截断梯度。遇到这类问题最直接的办法是把w.grad打印出来看看是不是None或全零。4.3 停止梯度传播你需要知道的两个工具除了训练我们经常还需要“不计算梯度”的操作。典型场景是验证和测试评估模型准确率时不需要反向传播却还要跑前向计算。这时候如果用no_grad()包裹能省不少内存和时间因为框架不会为这些操作保存计算图。另一个相关操作是detach()。它返回一个新的张量和原张量共享数据但不再参与梯度计算。有时候你想把某个中间结果拿出来做可视化、算额外指标又不想它影响梯度路径就可以用detach()。我自己的习惯是凡是在训练循环里只需要数值的地方能detach就detach避免无意中把不需要的节点链进计算图。5. 常见错误与排查实战5.1 维度不匹配与广播陷阱ch02之后做练习时几乎每个人都会遇到类似报错size mismatch, m1: [64 x 128], m2: [64 x 128]这个错误其实给了你很多信息两个矩阵里有一个形状不对。我排查维度问题有三板斧打印每一步张量的shape看报错里提到的维度确定是哪个操作出了问题确认是想做“矩阵乘法”还是“逐元素运算”两者对维度的要求完全不同。广播陷阱则更难察觉因为有些时候代码不报错但结果不符合预期。比如把一个(batch, 1)的向量和一个(batch, feature)的矩阵相加框架会自动把前者沿特征维复制结果虽然在数学上合法但你可能根本没意识到自己写错了一个维度位置。所以我现在写任何带广播的代码都会在关键操作后面加一句注释写明预期形状。5.2 梯度异常None、全零或NaN梯度相关问题是我在实际训练中遇到最多的。整理几个常见样本w.grad是None往往是没有调用backward()或者w的requires_grad没有开启w.grad全零可能是损失函数对w不敏感比如w没参与计算图的某些分支或者梯度在中间被detach截断了loss为NaN常见原因是学习率过大导致参数更新时直接飞出有效数值范围也可能是输入数据没有归一化、损失函数里有除零操作。遇到NaN我的第一反应是把学习率调到很小比如1e-4试跑再看是哪个环节引入的非数值。5.3 设备与数据类型不匹配很多新手会忽略的设备问题实际训练时也很常见模型在GPU上数据却还在CPU上框架会直接报设备不匹配的错误或者计算图里的张量一个float32、一个float64导致某些操作失败。我建议从一开始就养成统一管理的习惯模型和数据都用同一套设备配置数据类型统一设置成float32大多数框架默认单精度。一个小技巧是写一个设备初始化函数在脚本开头就设定好后面所有张量创建都通过它来生成这样能避免大量低级的设备错误。6. 学完这章之后你该掌握的自测清单课程笔记看到这里我认为可以给自己做个小测试确认真的吸收了ch02而不只是“看过了一遍”能亲手创建一个形状是(8, 3, 224, 224)的随机张量并解释每个维度分别代表什么知道view和reshape的区别以及什么时候会报错能用广播机制一次给一个(64, 100)矩阵的每一行加上同一个偏置向量能看懂一段“读CSV 填充缺失值 标准化 划分数据”的预处理代码能闭着眼写出训练循环的四步清梯度、前向计算损失、反向传播、更新参数能解释no_grad()和detach()各自的用途和区别遇到loss不下降时知道先去查哪个环节的梯度。如果这些都能做到那你已经有足够的地基往下走了。如果还有几项不太稳我建议不要急着开下一章——后面线性神经网络、多层感知机全都在这些机制上盖楼地基有裂缝楼越高越容易塌。另外我想说一点学习方式上的心得ch02里每一段代码都值得亲手敲一遍而不是直接复制。敲的过程中你才会注意到哪些参数要手动设置、哪些步骤有顺序依赖这些“肌肉记忆”在后续做实验时非常管用。我印象最深的是自动微分那一节当时我为了搞懂计算图把小例子里的每一步中间结果的grad_fn都打印出来看——这个做法让我在后来调复杂模型时能一眼看出梯度断在了哪个节点。根据我个人经验读这门课最怕的不是内容难而是“以为自己懂了”。ch02的每个小节都是一个坑位你不填后面就会反复掉进去。宁可在这里多花一周也不要在后面用一个月来还债。