1. 张量是训练系统中的共同数据结构
torch.Tensor 同时承载数值、形状、数据类型、设备和梯度信息。排查训练错误时,不要只看数值,至少同时确认:
print(x.shape, x.dtype, x.device, x.requires_grad)
常见创建方式:
import torch
a = torch.tensor([1, 2, 3], dtype=torch.float32)
b = torch.zeros((2, 3))
c = torch.ones_like(b)
d = torch.rand((2, 3)) # [0, 1) 均匀分布
e = torch.randn((2, 3)) # 标准正态分布
f = torch.arange(0, 10, 2)
g = torch.linspace(0, 1, steps=5)
数据类型要与任务匹配
- 模型输入通常是浮点张量,如
float32。 - 多分类标签通常是
long,与CrossEntropyLoss匹配。 - 掩码通常是
bool。 - 不要依赖隐式类型转换;出现损失函数或索引错误时先检查
dtype。
2. 形状操作
x = torch.randn(3, 4, 5)
x.shape # torch.Size([3, 4, 5])
x.reshape(12, 5) # 允许在必要时复制
x.view(12, 5) # 要求底层存储布局兼容
x.unsqueeze(0) # (1, 3, 4, 5)
x.squeeze(0) # 只删除指定的大小为 1 的维度
x.flatten(start_dim=1)
x.permute(0, 2, 1)
实践中优先显式指定 squeeze(dim),避免把 batch 维一并删掉。view() 报连续性错误时,可改用 reshape(),或者先调用 contiguous(),但要理解这可能产生复制。
广播
从尾部维度向前比较,两个维度相等,或其中一个为 1,才能广播:
a = torch.tensor([1, 2, 3]) # (3,)
b = torch.tensor([[4], [5], [6]]) # (3, 1)
c = a + b # (3, 3)
广播不会真的把输入复制成完整矩阵,但可能让错误形状“看起来能运行”。损失异常时,要确认广播是否符合语义。
3. 设备迁移
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = model.to(device)
x = x.to(device)
y = y.to(device)
模型参数和参与同一次运算的张量必须位于同一设备。启用 DataLoader(pin_memory=True) 后,可配合:
x = x.to(device, non_blocking=True)
non_blocking=True 是否真正异步,取决于源内存、设备和后续同步点,不能仅凭参数名认定已经提速。
4. 自动微分的核心链条
flowchart LR
X[叶子张量<br/>requires_grad=True] --> O[前向运算]
O --> L[标量 loss]
L --> B[loss.backward]
B --> G[梯度累积到 parameter.grad]
G --> S[optimizer.step]
一个最小例子:
x = torch.tensor([2.0, 3.0], requires_grad=True)
y = x**2 + 3 * x
loss = y.sum()
loss.backward()
print(x.grad) # 2*x + 3 -> tensor([7., 9.])
backward() 为什么常从标量开始
标量损失对参数求导会自然得到一组梯度。非标量张量也能调用 backward(),但需要传入同形状的上游梯度,本质是计算向量-雅可比积:
y.backward(torch.ones_like(y))
梯度默认累积
每次反向传播都会把新梯度加到 .grad:
optimizer.zero_grad(set_to_none=True)
loss.backward()
optimizer.step()
set_to_none=True 通常更省内存,但要注意:没有收到梯度的参数保持 grad is None,它与全零梯度的优化器行为并不完全相同。
5. detach()、no_grad()、inference_mode() 与 eval()
| 工具 | 影响范围 | 典型用途 |
|---|---|---|
tensor.detach() |
返回与原张量共享存储、但脱离当前计算图的张量 | 截断某一条梯度路径 |
torch.no_grad() |
上下文内不记录反向图 | 验证、推理或局部无梯度计算 |
torch.inference_mode() |
比 no_grad() 更严格,还关闭部分视图追踪和版本计数 |
纯评估和部署推理 |
model.eval() |
切换 Dropout、BatchNorm 等模块的运行行为 | 验证与推理前切换模型模式 |
它们不是替代关系。评估通常同时需要:
model.eval()
with torch.inference_mode():
logits = model(x)
detach() 的正确认识
x = torch.tensor(2.0, requires_grad=True)
y = x**2
z = y.detach()
print(z.requires_grad) # False
此时直接 z.backward() 会失败,因为 z 已经没有原计算图的 grad_fn。如果希望“从这里开始建立一张新图”,可以显式使用:
z = y.detach().clone().requires_grad_(True)
但新图对 z 求出的梯度不会回传给原来的 x。
冻结参数不等于分离某个输出
迁移学习中的冻结应直接设置参数:
for parameter in backbone.parameters():
parameter.requires_grad_(False)
冻结后再创建优化器更清晰:
optimizer = torch.optim.AdamW(
(p for p in model.parameters() if p.requires_grad),
lr=1e-3,
)
6. item() 的边界
loss.item() 把单元素张量转成 Python 数字,适合日志记录:
running_loss += loss.item() * batch_size
它会脱离计算图,并且 GPU 张量取值通常需要主机与设备同步。因此不要在高频内部循环中无目的地反复调用,也不要把它描述成一种通用的“释放显存”手段。
7. 梯度裁剪
RNN 等模型出现梯度爆炸时,可以在 backward() 之后、step() 之前裁剪:
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()
梯度裁剪是稳定训练的保护措施,不会替代合适的初始化、学习率和模型结构。
8. 快速排错清单
loss是否为有限值:torch.isfinite(loss)。- 输入、标签、模型是否在同一设备。
- 标签类型和范围是否匹配损失函数。
- 输出是否是 logits;使用
CrossEntropyLoss时不要先手动softmax()。 model.train()/model.eval()是否在正确阶段调用。- 是否忘记清梯度,或错误地在清梯度前读取梯度。
- 是否误用
detach()截断了本应保留的梯度路径。 - 广播是否掩盖了错误形状。
来源映射
本笔记主要归纳自:
第二周.md标准训练过程.mdd2l_Study.ipynbLeNet_MNIST.ipynb
API 行为参考: