PyTorch:张量与自动求导 (PyTorch: Tensors & Autograd)


章节概述

PyTorch 是深度学习的事实标准框架。它的核心只有两个概念:张量(Tensor,NumPy 的 GPU 版本)和自动求导(Autograd,自动计算梯度)。本章从 C 程序员的视角出发,对比 torch.Tensornumpy.array 及 C 数组的异同,深入理解自动求导的计算图机制,最后构建一个完整的神经网络训练循环。

核心理念:深度学习训练的本质是”前向传播计算损失 → 反向传播计算梯度 → 梯度下降更新参数”的循环。PyTorch 帮你做了两件事:(1) Tensor 把矩阵运算搬到 GPU,比 C 手写循环快 100-1000 倍;(2) autograd 自动计算导数,让你不用手动推导反向传播公式。训练完成后,你导出模型给 C++ 程序做推理——推理不需要自动求导,只需要前向传播。


第一节:torch.Tensor 基础

1.1 张量 vs NumPy 数组 vs C 数组

import torch
import numpy as np
 
# C 数组: float arr[3][4]; // 栈上的连续内存
# NumPy: arr = np.zeros((3, 4), dtype=np.float32) # 堆上分配,C ordered
# PyTorch: t = torch.zeros(3, 4) # 可在 GPU,可跟踪梯度
 
t = torch.tensor([[1.0, 2.0, 3.0],
 [4.0, 5.0, 6.0]])
print(f"shape: {t.shape}") # (2, 3)
print(f"dtype: {t.dtype}") # torch.float32
print(f"device: {t.device}") # cpu
print(f"requires_grad: {t.requires_grad}") # False

核心差异:C 的 float[2][3] 在栈/堆上分配,形状是编译期常量。PyTorch Tensor 的形状是属性(可 reshape)、dtype 是属性、设备是属性(CPU vs CUDA:0)、梯度跟踪是属性。Tensor 是一个”带元数据的连续内存块指针”,类似 C++ STL 的 std::vector

1.2 张量创建

python -c "
import torch
print('zeros :', torch.zeros(2, 3))
print('ones :', torch.ones(2, 3))
print('randn :', torch.randn(2, 3)) # 标准正态分布
print('arange:', torch.arange(0, 10, 2)) # 像 C 的 for(i=0;i<10;i+=2)
print('linspace:', torch.linspace(0, 1, 5)) # 0 到 1 等分 5 个点
"
# 从 NumPy 转换(共享内存!)
python -c "
import torch, numpy as np
a = np.array([[1.,2.],[3.,4.]])
t = torch.from_numpy(a)
t[0,0] = 99
print('numpy array after torch change:', a) # [[99. 2.][3. 4.]] — 共享内存!
"

torch.from_numpy() 与源数组共享内存,修改 Tensor 会影响原 NumPy 数组。这与 C 中多个指针指向同一块内存类似。

1.3 张量运算与索引

python -c "
import torch
a = torch.tensor([[1.,2.],[3.,4.]])
b = torch.tensor([[5.,6.],[7.,8.]])
 
print('a + b:', a + b) # 逐元素加法
print('a @ b:', a @ b) # 矩阵乘法
print('a * b:', a * b) # 逐元素乘法(不是点积!)
print('a.sum():', a.sum()) # 所有元素求和
print('a.sum(dim=0):', a.sum(dim=0)) # 沿第0维(行)求和 → (2,)
print('a.sum(dim=1):', a.sum(dim=1)) # 沿第1维(列)求和 → (2,)
print('a.view(4):', a.view(4)) # reshape 为 1D(共享内存)
"

1.4 GPU 操作

# 检查 GPU 可用性(一行流)
python -c "
import torch
print('CUDA available:', torch.cuda.is_available())
if torch.cuda.is_available():
 print('GPU name:', torch.cuda.get_device_name(0))
 t = torch.randn(1000, 1000).cuda()
 print('GPU tensor:', t.device)
 print('Back to CPU:', t.cpu().device)
"
# Apple Silicon (M1/M2/M3)
python -c "
import torch
print('MPS available:', torch.backends.mps.is_available())
"

GPU 注意事项:CPU 和 GPU Tensor 不能直接运算,cuda_tensor + cpu_tensor 会报错。需要用 .cpu().cuda() 统一设备。在 C 中类比:不能把 GPU 显存地址和 CPU 内存地址传给同一个 memcpy


第二节:Autograd — 自动求导

2.1 为什么要自动求导

假设你有一个函数 loss = (wx - y)^2,参数为 w。在 C 中你需要手动推导并编码梯度:

// C: 手动求导
float loss = (w * x - y) * (w * x - y); // loss = (wx - y)²
float dloss_dw = 2 * (w * x - y) * x; // 手动推导 ∂loss/∂w
w -= lr * dloss_dw; // 梯度下降

对于有上百万参数的深度神经网络,手动推导梯度是不可能的。PyTorch 的 autograd 自动完成此工作:

# PyTorch: 自动求导
import torch
w = torch.tensor([2.0], requires_grad=True)
x = torch.tensor([3.0])
y_true = torch.tensor([10.0])
 
loss = (w * x - y_true).pow(2).mean() # 前向传播
loss.backward() # 自动计算所有 requires_grad=True 的梯度
print(f"loss: {loss.item():.4f}") # 16.0000
print(f"∂loss/∂w: {w.grad.item():.4f}") # -24.0000 → 2*(6-10)*3 = -24

2.2 计算图(Computational Graph)

每次 Tensor 运算,PyTorch 都在后台构建一个有向无环图(DAG):

graph LR
 W["w<br/>(requires_grad=True)"] --> MUL["mul(w, x)"]
 X["x"] --> MUL
 MUL --> SUB["sub(mul, y)"]
 Y["y"] --> SUB
 SUB --> POW["pow(sub, 2)"]
 POW --> MEAN["mean() = loss"]

 style W fill:#f9f,stroke:#333
 style X fill:#fff,stroke:#333
 style Y fill:#fff,stroke:#333
 style MEAN fill:#f96,stroke:#333

backward() 沿着这条图反向传播:
loss.grad = 1.0
→ mean 反向: 1.0 / N
→ pow 反向: 2 * (wx - y)
→ sub 反向: 1 (对 wx), -1 (对 y)
→ mul 反向: x (对 w), w (对 x)
→ w.grad = x * 2 * (wx - y) * 1.0 / N


每次 `.backward()` 后,计算图被释放(PyTorch 默认行为)。这节省了内存,但意味着不能 `backward()` 两次。如果需要保留,使用 `loss.backward(retain_graph=True)`。

2.3 梯度下降完整循环
--------------------

```python
import torch

# 数据
X = torch.tensor([[1.0], [2.0], [3.0], [4.0]])
Y = torch.tensor([[3.0], [5.0], [7.0], [9.0]]) # Y = 2X + 1

# 参数
w = torch.randn(1, requires_grad=True)
b = torch.randn(1, requires_grad=True)

lr = 0.01
for epoch in range(500):
 # 前向传播
 y_pred = X * w + b # 计算图开始构建
 loss = ((y_pred - Y) ** 2).mean()

 # 反向传播
 loss.backward()

 # 梯度下降(with torch.no_grad() 防止这些操作进入计算图)
 with torch.no_grad():
 w -= lr * w.grad
 b -= lr * b.grad
 w.grad.zero_() # 清零梯度,否则会累加
 b.grad.zero_()

 if epoch % 100 == 0:
 print(f"Epoch {epoch:3d}: loss={loss.item():.6f}, w={w.item():.3f}, b={b.item():.3f}")

C 程序员的关键理解with torch.no_grad(): 关闭自动求导,相当于告诉 PyTorch”下面这段代码只是推理/参数更新,不需要记录到计算图中”。梯度累积(w.grad += new_grad)是 PyTorch 的默认行为,所以每次更新后必须 .zero_()。这对于习惯手动管理内存的 C 程序员是常见的错误点。


第三节:nn.Module — 构建神经网络

3.1 把线性回归包装为 Module

import torch
import torch.nn as nn
 
class LinearRegressor(nn.Module):
 def __init__(self):
 super().__init__()
 self.linear = nn.Linear(1, 1) # 输入 1 维,输出 1 维 → y = wx + b
 
 def forward(self, x):
 return self.linear(x)
 
model = LinearRegressor()
print(model)
# LinearRegressor(
# (linear): Linear(in_features=1, out_features=1, bias=True)
# )

nn.Linear 内部就是 W @ x + bWb 自动注册为模型参数(requires_grad=True)。你不需要手动创建和追踪它们。

3.2 用 Optimizer 替代手动梯度更新

import torch
import torch.nn as nn
import torch.optim as optim
 
X = torch.tensor([[1.0], [2.0], [3.0], [4.0]])
Y = torch.tensor([[3.0], [5.0], [7.0], [9.0]])
 
model = nn.Linear(1, 1) # 等价于 y = wx + b
criterion = nn.MSELoss() # 均方误差损失
optimizer = optim.SGD(model.parameters(), lr=0.01) # 随机梯度下降
 
for epoch in range(500):
 optimizer.zero_grad() # 清零梯度(替代 w.grad.zero_())
 y_pred = model(X) # 前向传播
 loss = criterion(y_pred, Y) # 计算损失
 loss.backward() # 反向传播
 optimizer.step() # 更新所有参数(替代手动 w -= lr * w.grad)
 
print(f"w={model.weight.item():.3f}, b={model.bias.item():.3f}")

3.3 多层神经网络

import torch
import torch.nn as nn
 
class MLP(nn.Module):
 def __init__(self, in_features, hidden, out_features):
 super().__init__()
 self.net = nn.Sequential(
 nn.Linear(in_features, hidden),
 nn.ReLU(), # ReLU(x) = max(0, x)
 nn.Linear(hidden, hidden),
 nn.ReLU(),
 nn.Linear(hidden, out_features),
 )
 
 def forward(self, x):
 return self.net(x)
 
model = MLP(in_features=4, hidden=64, out_features=3)
print(f"Parameters: {sum(p.numel() for p in model.parameters())}")
 
# 模拟输入
x = torch.randn(8, 4) # batch_size=8, features=4
out = model(x)
print(f"Input: {x.shape} → Output: {out.shape}") # (8, 4) → (8, 3)

3.4 激活函数速查

激活函数公式用途
ReLUmax(0, x)隐藏层默认选择
Sigmoid1/(1+e^(-x))二分类输出层
Softmaxe^x_i / Σe^x_j多分类输出层
Tanh(e^x - e^(-x))/(e^x + e^(-x))RNN / LSTM
GELUx * Φ(x)Transformer(BERT, GPT)

第四节:完整训练循环

4.1 分类任务完整示例

import torch
import torch.nn as nn
import torch.optim as optim
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
 
# 1. 数据准备
X, y = load_iris(return_X_y=True)
X = StandardScaler().fit_transform(X)
X_train, X_test, y_train, y_test = train_test_split(
 X, y, test_size=0.3, random_state=42
)
X_train = torch.tensor(X_train, dtype=torch.float32)
X_test = torch.tensor(X_test, dtype=torch.float32)
y_train = torch.tensor(y_train, dtype=torch.long)
y_test = torch.tensor(y_test, dtype=torch.long)
 
# 2. 模型定义
class IrisClassifier(nn.Module):
 def __init__(self):
 super().__init__()
 self.net = nn.Sequential(
 nn.Linear(4, 16),
 nn.ReLU(),
 nn.Linear(16, 3),
 )
 
 def forward(self, x):
 return self.net(x)
 
model = IrisClassifier()
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.01)
 
# 3. 训练循环
for epoch in range(200):
 model.train()
 optimizer.zero_grad()
 logits = model(X_train)
 loss = criterion(logits, y_train)
 loss.backward()
 optimizer.step()
 
 if epoch % 50 == 0:
 model.eval()
 with torch.no_grad():
 preds = model(X_test).argmax(dim=1)
 acc = (preds == y_test).float().mean()
 print(f"Epoch {epoch:3d}: loss={loss.item():.4f}, test_acc={acc:.3f}")
 
# 4. 最终评估
model.eval()
with torch.no_grad():
 preds = model(X_test).argmax(dim=1)
 acc = (preds == y_test).float().mean()
 print(f"\nFinal test accuracy: {acc:.3f}")

4.2 训练循环的标准模板

# PyTorch 训练循环固定模板(伪代码)
for epoch in range(epochs):
 # === 训练阶段 ===
 model.train()
 for X_batch, y_batch in train_loader:
 optimizer.zero_grad() # 清零梯度
 output = model(X_batch) # 前向传播
 loss = criterion(output, y_batch) # 计算损失
 loss.backward() # 反向传播
 optimizer.step() # 更新参数
 
 # === 验证阶段 ===
 model.eval()
 with torch.no_grad(): # 验证不需要梯度
 for X_batch, y_batch in val_loader:
 output = model(X_batch)
 # 计算验证指标(accuracy 等)
 
# === 保存模型 ===
torch.save(model.state_dict(), "model.pth")

4.3 模型保存与加载

# 保存(推荐:只保存参数,不保存结构)
torch.save(model.state_dict(), "iris_model.pth")
 
# 加载
model = IrisClassifier()
model.load_state_dict(torch.load("iris_model.pth"))
model.eval()
 
# 保存整个模型(不推荐:序列化不稳定)
torch.save(model, "iris_full.pth")

对比 C 操作torch.save(model.state_dict(), "iris_model.pth") 等价于把每个参数的数组值写入二进制文件。C++ 侧用 libtorch 同样可以加载 .pth 文件。



练习

以下题目用于验证本章所学内容:

题号题目链接涉及知识点
本章无对应力扣题请用动手练习题自检