☰
深度学习预备知识精讲:张量、自动微分与数据预处理实战
2026/10/20 19:07:11 网站建设 项目流程

说实话,我最早翻到《动手学深度学习》第二章(预备知识)的时候,第一反应是:这章不就是复习数学和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的每个小节都是一个坑位,你不填,后面就会反复掉进去。宁可在这里多花一周,也不要在后面用一个月来还债。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询