说实话,我最早翻到《动手学深度学习》第二章(预备知识)的时候,第一反应是:这章不就是复习数学和Python基础吗?跳过应该也不影响吧。后来认真过了一遍才发现,这章是整个课程里“性价比”最高的一章,后面每一个模型、每一段训练代码,全都在反复使用这里的概念——张量操作、数据预处理、线性代数、微积分、自动微分。
如果你也是初学深度学习,或者之前草草跳过基础、现在越学越费力,那这篇笔记大概率能帮你把ch02真正吃透。我会按章内模块往下拆,结合我实际敲代码时踩过的坑和验证过的思路,把“为什么这么做”也讲清楚,而不是简单罗列API。
1. 张量操作:深度学习的核心数据结构
1.1 张量到底是什么:从数组到计算图节点
课程一上来就会介绍张量(Tensor)。说白了,它就是深度学习里的“数据容器”:一个数字是零维张量,一列数是向量(一维张量),一个表格是矩阵(二维张量),再往上的图像、批量样本就对应三维、四维甚至五维张量。
我第一次学的时候有个困惑:它和Python列表或NumPy数组有什么区别?课程里其实明确说了:张量除了能存数据,还能被框架“盯着”——它会自动记录这个数据参与了哪些运算,方便之后计算梯度。这一点才是它成为深度学习基石的原因。
举个具体例子:你用普通数组也能实现weight * x + bias,但后面算梯度就得自己写链式法则。而张量只需要标记requires_grad=True,反向传播时梯度自动就出来了。有了这个设计,研究模型的精力就能集中在结构设计上,而不是手工推导导数。
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和shuffle=True,训练集每个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_grad=True) b = torch.zeros(1, requires_grad=True) 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_grad=True必须设置在叶子张量上,如果从一个普通数组直接torch.tensor(data)再往后运算,它是不会有梯度的。
我调试时经常遇到一种情况:模型参数根本不更新,loss始终不降。排查后往往发现是梯度没传到参数上——要么忘了requires_grad=True,要么在某个中间环节用了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的每个小节都是一个坑位,你不填,后面就会反复掉进去。宁可在这里多花一周,也不要在后面用一个月来还债。