课程0基础Agent开发课 / 深度学习基础 / PyTorch入门-张量操作与自动求导
— 19 min read

PyTorch入门-张量操作与自动求导

PyTorch是深度学习领域的标准工具。理解它的设计哲学,比会用它的API更重要。

PyTorch入门:张量操作与自动求导

PyTorch是深度学习领域的标准工具。理解它的设计哲学,比会用它的API更重要。

前置知识提示:本文理论内容较多,建议先完成以下内容再阅读:

如果你是Java工程师nn.Module 类似Java的抽象类,forward() 是必须实现的抽象方法;DataLoader 类似Java的迭代器模式;PyTorch的动态计算图类似Java中的链式方法调用,每次调用都会"记录"操作历史。

本文目标:重点理解「为什么需要PyTorch」、「张量与神经网络的关系」和「计算图是什么」。代码是辅助理解的工具,不要求跑通,先读懂逻辑。


1.1 一、为什么需要 PyTorch?

在回答"PyTorch 是什么"之前,先回答一个更根本的问题:PyTorch 之前,深度学习是怎么做的?

1.1.1 手工推导梯度:一场噩梦

第3章讲过,训练神经网络的核心是梯度下降——反复计算损失函数对每个参数的梯度,然后沿着梯度的反方向调整参数。

问题在于:梯度怎么算?

对于一个只有两层、每层 3 个神经元的小网络,参数已经有几十个。损失函数是这些参数的复合函数(复合又复合又复合),要手动对每个参数求偏导数,需要反复应用链式法则,计算过程复杂且极易出错。

早期研究者真的是这样做的。2000 年代以前,实现一个神经网络意味着:

  1. 在纸上推导出该网络的反向传播公式
  2. 把公式逐行翻译成 C 或 FORTRAN 代码
  3. 调试——因为数学推导和代码实现都可能有错

换一个网络结构?重头推导一遍。增加一层?重新算一遍链式法则。

这不是工程问题,而是认知负担问题:大量精力花在"正确地算梯度"上,而不是"设计更好的网络结构"上。这严重限制了研究者的实验速度。

1.1.2 早期框架的困境:静态计算图

2010 年代初,Theano 和早期 TensorFlow(1.x)出现了。它们提出了"计算图"的概念(后面会详细解释),能自动计算梯度——这是巨大的进步。

但它们有一个共同的设计缺陷:静态计算图

使用这些框架时,你需要先"描述"整个计算过程,构建一张完整的图,然后把数据"喂进"这张图执行。图一旦建好就不能动了。

这带来了两个痛点:

  • 调试困难:你无法在计算过程中打印中间值或设置断点,因为执行是在图构建完之后统一发生的,就像在编译后才能运行
  • 灵活性差:网络结构必须在运行前完全确定。但很多自然语言处理任务的输入序列长度是动态的,RNN 的循环次数随输入变化——静态图很难优雅地处理这类情况

1.1.3 PyTorch 的回答:动态计算图

2016 年,Facebook AI Research 发布了 PyTorch。它的核心创新是动态计算图(Dynamic Computation Graph,也叫 Define-by-Run):

计算图不是预先构建的,而是在你执行代码时实时生成的。

每一行 Python 代码运行时,PyTorch 都在背后悄悄记录"刚才做了什么操作"。等你需要反向传播时,PyTorch 就沿着这些记录,自动算出所有梯度。

这意味着:

  • 你可以像写普通 Python 代码一样写深度学习代码
  • 可以随时打印中间结果调试
  • 网络结构可以根据输入动态变化(比如 RNN 的循环次数随句子长度变化)
  • if-else、for 循环、函数调用——任何 Python 控制流都可以自然地用在网络里
code
PyTorch 的本质:
  你写 Python → PyTorch 悄悄记录操作 → 你调用 .backward() → PyTorch 自动求所有梯度

加上对 GPU 的无缝支持(把数据和模型一起搬到 GPU 上,用几乎一样的代码获得数十倍加速),PyTorch 迅速成为学术研究的首选工具,并在 2020 年代初超越 TensorFlow 成为业界主流。

截至 2026 年 3 月:PyTorch 2.x 已经是稳定版本,新增了 torch.compile() 等性能优化特性。本文聚焦核心概念,版本细节以官方文档为准。


1.2 二、张量:多维Excel表格

1.2.1 张量就是多维的Excel表格

很多教程把张量(Tensor)介绍为"多维数组",这在技术上没错,但缺少直觉感。更好的类比是:张量就是多维的Excel表格

  • 一个数字就像一个单元格:标量(0维张量),比如损失值 0.35
  • 一行数字就像一行数据:向量(1维张量),比如一个神经元的所有输入 [0.2, 0.8, 0.5, 0.1]
  • 一张Excel表格(行×列):矩阵(2维张量),比如一层神经网络的权重
  • 多张Excel表格叠在一起(批次×行×列):3维张量,比如一批文本的词向量
  • 再加一个维度(批次×通道×高×宽):4维张量,比如一批彩色图片

张量是所有这些结构的统一抽象。深度学习之所以用张量,是因为神经网络的每一个核心操作,都天然地是张量运算

1.2.2 矩阵乘法如何对应前向传播

回忆神经网络的前向传播:输入经过一层神经元,每个神经元计算"加权求和 + 激活函数"。

以最简单的全连接层为例:输入是 4 个特征,这一层有 3 个神经元。

每个神经元的计算是:

code
神经元1的输出 = w11*x1 + w12*x2 + w13*x3 + w14*x4 + b1
神经元2的输出 = w21*x1 + w22*x2 + w23*x3 + w24*x4 + b2
神经元3的输出 = w31*x1 + w32*x2 + w33*x3 + w34*x4 + b3

如果你把所有权重排成一个 3×4 的矩阵 W,把输入排成一个长度为 4 的向量 x,把偏置排成长度为 3 的向量 b,那么上面三个公式可以统一写成:

code
输出向量 = W × x + b

这就是矩阵乘法。一层神经网络的前向传播,本质上就是一次矩阵乘法

这个认识非常重要:

  • 整个神经网络的前向传播,就是一系列矩阵乘法串联在一起(加上激活函数)
  • GPU 最擅长的事恰好是大规模矩阵运算(这就是为什么深度学习需要 GPU)
  • 张量运算库(比如 PyTorch)的核心,就是高效执行这些矩阵运算

1.2.3 批处理:为什么要一次处理多条数据

实际训练时,我们不会一次只处理一条数据,而是一批(batch)一起处理。原因有两个:

  1. 效率:GPU 的并行性在处理大量数据时才能充分发挥。一次处理 64 条数据,比循环 64 次处理单条数据快得多
  2. 梯度稳定:单条数据的梯度噪声很大,多条数据的平均梯度更稳定

批处理让张量维度增加了一维。输入从"4 个特征的向量"变成"64 × 4 的矩阵"(64 条数据,每条 4 个特征)。矩阵乘法天然支持批处理,维度自动扩展,这是张量运算的另一个优势。

1.2.4 代码验证:创建张量和矩阵乘法

python
import torch

# 创建一个全连接层的输入(模拟64条数据,每条4个特征)
x = torch.randn(64, 4)       # 形状: (64, 4)

# 权重矩阵:4个输入特征 → 3个神经元
W = torch.randn(3, 4)        # 形状: (3, 4)
b = torch.randn(3)            # 偏置: 3个神经元各一个

# 前向传播就是矩阵乘法:(64,4) × (4,3) + (3,) = (64,3)
output = x @ W.T + b          # @是矩阵乘法运算符,.T是转置
print(output.shape)           # torch.Size([64, 3])
# 64条数据,每条得到3个神经元的输出值

# 张量的关键属性
print(x.dtype)    # torch.float32(默认浮点精度)
print(x.device)   # cpu(还未移动到GPU)
print(x.shape)    # torch.Size([64, 4])

这段代码做的事情,和一个有 3 个神经元的全连接层的前向传播,在数学上完全等价。

1.2.5 GPU:为什么深度学习需要它

CPU 的设计目标是通用计算,擅长处理复杂的控制逻辑,有少量但强大的计算核心(通常 8-32 个)。

GPU 的设计目标是图形渲染,需要同时处理屏幕上数百万个像素点的颜色计算——这是典型的"大量简单运算并行执行"。GPU 有数千个计算核心,非常适合矩阵乘法这类操作。

深度学习的核心运算(矩阵乘法)和图形渲染(像素计算)在计算模式上高度相似:都是对大量数据做相同的简单运算。这就是为什么 GPU 能让深度学习训练加速 10-100 倍。

在 PyTorch 里,把计算从 CPU 移到 GPU 只需要一行:

python
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# 数据和模型都要移到同一个设备,运算才能发生
x = x.to(device)

代码几乎不变,但背后的执行从 CPU 切换到了 GPU。这是 PyTorch 抽象做得好的体现——硬件细节被隐藏起来,你专注于网络结构的设计。


1.3 三、自动求导:自动追踪计算过程

PyTorch自动求导流程图
PyTorch 自动求导完整流程——前向传播构建计算图,反向传播自动计算梯度

这一节是PyTorch最核心的概念。理解了计算图,就理解了PyTorch的本质。

1.3.1 自动求导是什么:自动追踪你的每一步计算

想象一个"记账本"。你做的每一步计算,PyTorch都悄悄地在记账本上记下来:"刚才做了平方运算"、"刚才做了乘法"、"刚才做了加法"……

当你需要反向传播时,PyTorch就翻出这本账,从最后一页往前翻,用链式法则一步一步算出每个参数的梯度。

这就是**自动求导(Automatic Differentiation)**的核心——你只需要写前向计算代码,PyTorch会自动帮你记录过程、计算梯度。

1.3.2 什么是计算图?

计算图(Computation Graph)是这个"记账本"的数学表达——一种记录"计算过程"的数据结构。

用一个简单例子来说明。假设我们要计算 y = x² + 2x + 1,当 x = 3 时:

code
第一步:a = x²      (x=3,a=9)
第二步:b = 2x      (x=3,b=6)
第三步:c = a + b   (c=15)
第四步:y = c + 1   (y=16)

这四步计算可以用一张图来表示:节点是中间变量,边是操作(平方、乘法、加法)。这张图就叫计算图。

code
x(3) ──[平方]──→ a(9) ──→
                            [加法]──→ c(15) ──[+1]──→ y(16)
x(3) ──[×2]───→ b(6) ──→

计算图有两个关键性质:

  1. 它记录的不只是结果,还记录了"如何得到这个结果"
  2. 有了计算图,反向传播就是在这张图上倒着走

1.3.3 为什么要记录计算图?

答案是:为了反向传播

反向传播的核心是链式法则:要算损失函数对某个参数的梯度,需要把从该参数到损失函数路径上所有操作的导数乘起来。

手动算链式法则很繁琐,但如果有计算图,这件事可以自动化:

  • 沿着计算图正向走一遍,记录所有中间值(前向传播)
  • 从输出端反向沿着图走回去,用链式法则逐步计算每个节点的梯度(反向传播)

PyTorch 在你做前向计算时,悄悄构建这张计算图。等你调用 .backward(),它就沿着图倒着走,自动算出所有梯度。

1.3.4 计算图如何处理复杂网络?

一个真实的神经网络可能有数百层、数十亿参数。它的计算图也会非常庞大——但原理完全一样:

  1. 数据从输入层经过每一层,每次运算都在图上增加节点
  2. 最终得到损失值(标量)
  3. 对损失值调用 .backward()
  4. PyTorch 从损失值出发,沿着图反向走,计算每个参数的梯度
  5. 梯度被存储在每个参数的 .grad 属性里,等待优化器使用

这个过程中,程序员完全不需要手工推导任何公式。PyTorch 把"正确地计算梯度"这件苦差事彻底自动化了。

1.3.5 计算图的生命周期

计算图在调用 .backward() 后默认会被销毁。这是为了节省内存——图本身占用大量内存,计算完梯度就不再需要了。

这也解释了一个常见现象:如果你对同一个计算调用两次 .backward(),第二次会报错,因为图已经被销毁。若需要多次反向传播(某些特殊场景),可以使用 retain_graph=True 保留计算图。

在推理(Inference,用训练好的模型做预测,不需要训练)时,我们不需要计算梯度,也就不需要构建计算图。使用 torch.no_grad() 上下文管理器可以关闭图的构建,节省内存和计算:

python
# requires_grad=True 告诉 PyTorch:追踪这个张量的计算历史
x = torch.tensor(3.0, requires_grad=True)

# 前向计算:PyTorch 悄悄记录每一步操作
y = x ** 2 + 2 * x + 1   # y = x^2 + 2x + 1

# 反向传播:沿着计算图反向走,自动算梯度
y.backward()

# dy/dx = 2x + 2,在 x=3 处 = 8
print(x.grad)   # tensor(8.)  ← 和手算结果完全一致

# 推理时关闭计算图构建,节省内存
with torch.no_grad():
    z = x ** 2 + 2 * x + 1   # 这次不构建图,节省内存

1.3.6 梯度累加陷阱

PyTorch 有一个重要的设计决定:梯度是累加的,不是覆盖的。

每次调用 .backward(),梯度会加到 .grad 上,而不是替换它。这个设计在某些场景(如梯度累积模拟大 batch)很有用,但在普通训练循环中是一个陷阱:

如果你忘记在每步训练前清空梯度,第 2 步的梯度 = 第 1 步梯度 + 第 2 步梯度,参数更新就完全乱了,损失不会正常下降。

这就是为什么训练循环里的第一行永远是 optimizer.zero_grad()——清空上一步留下的梯度。


1.4 四、训练循环的核心节奏

1.4.1 三步节奏的逻辑

整个神经网络的训练,无论多复杂,在结构上都是以下三步的循环:

python
optimizer.zero_grad()   # ① 清空上一步的梯度(必须,原因见上一节)
loss.backward()         # ② 反向传播:沿计算图算出所有参数的梯度
optimizer.step()        # ③ 优化器按梯度更新参数(走一步)

顺序不能乱,逻辑是:

  • 必须先清空,否则梯度污染
  • 必须先算梯度(backward),才能用梯度更新参数(step
  • step 完成后,参数已经更新,这一步训练结束

Java 类比:这三步类似于 Java 事务处理的模式——先重置状态(zero_grad),再计算操作(backward),最后提交(step)。如果忘记"重置",下一次操作会在污染的状态上进行。

1.4.2 训练模式和评估模式

model.train()model.eval() 是两个经常被忽视但很重要的切换。

网络里有些组件在训练时和推理时行为不同:

  • Dropout(随机丢弃层):训练时随机关闭一些神经元(防止过拟合,强迫网络不依赖单一路径);推理时必须全部打开,否则输出不稳定
  • BatchNorm(批归一化):训练时用当前 batch 的统计数据做归一化;推理时用训练期间积累的全局统计数据

model.train() 开启这些组件的训练行为;model.eval() 切换到推理行为。每次切换场景都必须显式调用。

1.4.3 损失函数:衡量"差了多少"

损失函数(Loss Function)是训练的"罗盘",衡量模型预测与真实标签之间的差距。损失越小,模型越好。

不同任务用不同的损失函数:

任务类型 常用损失函数 直觉理解
分类(判断类别) 交叉熵损失(CrossEntropyLoss) 预测概率分布与真实分布的"距离"
回归(预测数值) 均方误差(MSELoss) 预测值与真实值差的平方的平均
二分类 二元交叉熵(BCELoss) 交叉熵的二分类特例

损失函数的输出是一个标量(单个数字)。反向传播必须从标量出发,这就是为什么最终的 loss 一定是一个数字。

1.4.4 优化器:怎么"走一步"

梯度告诉我们"应该朝哪个方向调整参数",但没说"应该调多大"。优化器决定这个"步长"的策略。

最基础的是 SGD(随机梯度下降):每个参数直接减去 学习率 × 梯度。简单粗暴,但在学习率选择不当时容易在最优解附近震荡。

Adam(Adaptive Moment Estimation)是目前最常用的优化器。它的核心改进是:为每个参数自适应地调整学习率——历史上梯度大的参数,步长自动变小;历史上梯度小的参数,步长自动变大。实际效果是对学习率不那么敏感,收敛更稳定。

Adam 不是炼丹魔法,它的本质仍然是梯度下降,只是用了更聪明的步长策略。第3章的梯度下降原理仍然完全适用。


1.5 五、MNIST:把所有概念串起来

MNIST(Modified National Institute of Standards and Technology database)是深度学习的"Hello World"——6 万张手写数字图片(0-9),每张 28×28 像素的灰度图。

用它来验证前面所有概念,重点不是代码本身,而是每一步背后的"为什么"。

python
import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms
from torch.utils.data import DataLoader

# ===== 第一步:数据准备 =====
# ToTensor():把像素值从 [0,255] 转换为 [0,1] 的浮点张量
# Normalize():再做标准化,让数据分布以0为中心(加速收敛)
transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.1307,), (0.3081,))  # MNIST 数据集的均值和标准差
])
train_data = datasets.MNIST('./data', train=True, download=True, transform=transform)
train_loader = DataLoader(train_data, batch_size=64, shuffle=True)
# DataLoader 的作用:把 6 万张图片分成批次,每批 64 张
# shuffle=True:每个 epoch 打乱顺序,防止模型记住数据排列规律

# ===== 第二步:定义网络结构 =====
class MNISTNet(nn.Module):  # 继承 nn.Module,就像 Java 继承抽象类
    def __init__(self):
        super().__init__()
        self.net = nn.Sequential(
            nn.Flatten(),           # 把 28×28 的图片展平成 784 维向量
            nn.Linear(784, 128),    # 全连接层:784 维 → 128 维(矩阵乘法)
            nn.ReLU(),              # 激活函数:让网络能学习非线性关系
            nn.Linear(128, 10),     # 128 维 → 10 维(对应 0-9 十个类别)
        )
    def forward(self, x):           # forward() 就是前向传播,必须实现
        return self.net(x)

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = MNISTNet().to(device)       # 模型移到 GPU(如果有的话)

# ===== 第三步:定义损失函数和优化器 =====
criterion = nn.CrossEntropyLoss()   # 分类任务用交叉熵损失
optimizer = optim.Adam(model.parameters(), lr=1e-3)  # Adam 优化器

# ===== 第四步:训练循环 =====
model.train()                        # 开启训练模式(Dropout 等组件生效)
for epoch in range(3):
    for X, y in train_loader:        # X: (64,1,28,28) 的批次,y: (64,) 的标签
        X, y = X.to(device), y.to(device)

        optimizer.zero_grad()        # ① 清空梯度(避免累加污染)
        output = model(X)            # ② 前向传播:调用 forward(),PyTorch 同时构建计算图
        loss = criterion(output, y)  # ③ 计算损失:预测 vs 真实标签
        loss.backward()              # ④ 反向传播:沿计算图自动求梯度
        optimizer.step()             # ⑤ 更新参数:按梯度走一步

    print(f"Epoch {epoch+1}: loss = {loss.item():.4f}")

这段代码约 30 行,但包含了深度学习训练的全部核心要素:张量运算、前向传播、计算图、反向传播、梯度下降。更复杂的网络(包括大语言模型的微调)在结构上与此完全一致,只是网络更深、数据更多、优化策略更复杂。


小结

概念 本质 关键理解
张量 统一的多维数组结构 神经网络的所有运算(前向传播)都是张量运算,本质是矩阵乘法
计算图 记录计算过程的有向图 前向传播时自动构建,.backward() 时沿图反向传播梯度
自动求导 基于计算图的梯度自动计算 彻底解放了手工推导梯度的工作量
GPU 加速 矩阵运算的硬件并行化 .to(device) 一行切换,代码几乎不变
训练循环 zero_grad → forward → loss → backward → step 顺序固定,逻辑严格,是所有深度学习代码的基础框架
损失函数 衡量预测与真实值的差距 分类用交叉熵,回归用均方误差
优化器 决定参数更新策略 Adam 是目前最常用的,对学习率不敏感

掌握这七个概念,你就能读懂绝大多数深度学习代码,以及理解第16章模型微调的技术原理。


后续应用:本文知识将在以下章节直接用到:

  • 第5章第5篇 Transformer架构详解:注意力机制的计算就是一系列张量运算,理解了矩阵乘法才能看懂 Attention
  • 第16章 模型微调:微调本质上是对预训练模型继续执行训练循环,用的是完全相同的 zero_grad → backward → step 框架;LoRA 等参数高效微调方法,更需要理解计算图和梯度的概念
本页目录