跳转到内容

6.2.5 nn.Module

  • 使用 nn.Linear 并读懂它的参数 shape。
  • nn.Sequential 搭建简单模型。
  • __init__()forward() 写自定义 nn.Module
  • 检查 named_parameters()state_dict()
  • 理解 model.train()model.eval() 真正切换的是什么。

nn.Module 参数组织流程图

可以把 nn.Module 理解成模型容器:

层 + 参数 + 前向逻辑 + 模式状态 -> 一个模型对象

之后 optimizer 只需要拿到 model.parameters(),不用知道你写了多少层。

前面几节你见过这个运算:

logits = X @ W + b

nn.Linear(in_features, out_features) 会把同样的想法包装成一个可训练层。

import torch
from torch import nn
layer = nn.Linear(3, 2)
with torch.no_grad():
layer.weight.copy_(
torch.tensor(
[
[0.1, 0.2, 0.3],
[-0.1, 0.4, 0.2],
]
)
)
layer.bias.copy_(torch.tensor([0.01, -0.02]))
x = torch.tensor([[1.0, 2.0, 3.0]])
y = layer(x)
print("linear_lab")
print("input shape:", tuple(x.shape))
print("weight shape:", tuple(layer.weight.shape))
print("bias shape:", tuple(layer.bias.shape))
print("output:", torch.round(y * 100) / 100)

预期输出:

Terminal window
linear_lab
input shape: (1, 3)
weight shape: (2, 3)
bias shape: (2,)
output: tensor([[1.4100, 1.2800]], grad_fn=<DivBackward0>)

重要 shape 规则:

  • 输入:[batch, in_features]
  • 权重:[out_features, in_features]
  • 输出:[batch, out_features]

输出里的 grad_fn 表示它连在 autograd 计算图上。

当数据只是按顺序经过一串层时,可以用 nn.Sequential

import torch
from torch import nn
torch.manual_seed(11)
model = nn.Sequential(
nn.Linear(3, 4),
nn.ReLU(),
nn.Linear(4, 2),
)
batch = torch.randn(5, 3)
logits = model(batch)
print("logits shape:", tuple(logits.shape))

预期输出:

Terminal window
logits shape: (5, 2)

读模型结构:

[batch, 3]Linear(3, 4)ReLULinear(4, 2)[batch, 2]

这已经是一个小型多层感知机。

真实项目通常会写自定义模块,因为它能容纳具名子模块、分支逻辑、可复用辅助方法和更清楚的调试入口。

import torch
from torch import nn
class TinyClassifier(nn.Module):
def __init__(self, in_features=3, hidden=4, classes=2):
super().__init__()
self.net = nn.Sequential(
nn.Linear(in_features, hidden),
nn.ReLU(),
nn.Linear(hidden, classes),
)
def forward(self, x):
return self.net(x)
torch.manual_seed(11)
model = TinyClassifier()
batch = torch.randn(5, 3)
logits = model(batch)
print("module_lab")
print("logits shape:", tuple(logits.shape))
for name, param in model.named_parameters():
print(name, tuple(param.shape))
print("state keys:", list(model.state_dict().keys()))

预期输出:

Terminal window
module_lab
logits shape: (5, 2)
net.0.weight (4, 3)
net.0.bias (4,)
net.2.weight (2, 4)
net.2.bias (2,)
state keys: ['net.0.weight', 'net.0.bias', 'net.2.weight', 'net.2.bias']

职责分工:

方法或 API职责
__init__()创建层和子模块
forward()描述输入如何变成输出
parameters()把可学习参数交给 optimizer
named_parameters()暴露参数名和 shape,方便调试
state_dict()暴露可保存和加载的张量

不要把训练逻辑写进 forward()。Loss、backward()optimizer.step() 属于训练循环,不属于模型定义。

检查一个 nn.Module 时,分三层读:

层级问题证据
structure有哪些层,顺序是什么?print(model)
parameters哪些 tensor 会被训练?named_parameters()
behaviorforward() 对一个 batch 返回什么?一次输入/输出 shape 检查

这三层都清楚后,模型就不再是黑箱。它只是一个带可训练 tensor 和明确 forward 路径的 Python 对象。

读完一个 nn.Module,保留这三条检查:

结构
print(model)
参数
named_parameters() 及其形状
行为
一个输入批次 输出形状
边界
forward() 只把输入映射到输出

model.train() 不会自动跑训练循环,model.eval() 也不会自动跑验证。它们切换的是 Dropout、BatchNorm 等层的行为。

运行这个例子:

import torch
from torch import nn
class DropoutProbe(nn.Module):
def __init__(self):
super().__init__()
self.dropout = nn.Dropout(p=0.5)
def forward(self, x):
return self.dropout(x)
probe = DropoutProbe()
sample = torch.ones(6)
torch.manual_seed(3)
probe.train()
train_a = probe(sample)
train_b = probe(sample)
probe.eval()
eval_a = probe(sample)
eval_b = probe(sample)
print("mode_lab")
print("train outputs equal:", torch.equal(train_a, train_b))
print("eval outputs equal:", torch.equal(eval_a, eval_b))
print("eval output:", eval_a)

预期输出:

Terminal window
mode_lab
train outputs equal: False
eval outputs equal: True
eval output: tensor([1., 1., 1., 1., 1., 1.])

实用习惯:

model.train() # 训练 batch 前
model.eval() # 验证或预测前

验证时和 torch.no_grad() 搭配:

model.eval()
with torch.no_grad():
logits = model(batch)

这个例子使用两个特征和一个回归目标:

  • 每周学习小时;
  • 每周完成的练习题数量;
  • 预测分数。

目标值除以 100,这样这个小数据集训练会更稳定。

import torch
from torch import nn
class ScorePredictor(nn.Module):
def __init__(self):
super().__init__()
self.net = nn.Sequential(
nn.Linear(2, 16),
nn.ReLU(),
nn.Linear(16, 1),
)
def forward(self, x):
return self.net(x)
torch.manual_seed(42)
X = torch.tensor(
[
[2.0, 1.0],
[3.0, 2.0],
[4.0, 3.0],
[5.0, 5.0],
[6.0, 6.0],
[7.0, 8.0],
]
)
y = torch.tensor(
[
[55.0],
[60.0],
[68.0],
[78.0],
[85.0],
[92.0],
]
) / 100.0
model = ScorePredictor()
loss_fn = nn.MSELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.03)
print("training_lab")
for epoch in range(401):
pred = model(X)
loss = loss_fn(pred, y)
optimizer.zero_grad()
loss.backward()
optimizer.step()
if epoch % 100 == 0:
print(f"epoch={epoch:3d} loss={loss.item():.4f}")
model.eval()
with torch.no_grad():
test = torch.tensor([[6.5, 7.0]])
pred_score = model(test).item() * 100
print("predicted score:", round(pred_score, 2))

预期输出:

Terminal window
training_lab
epoch= 0 loss=0.4672
epoch=100 loss=0.0003
epoch=200 loss=0.0001
epoch=300 loss=0.0001
epoch=400 loss=0.0001
predicted score: 89.31

nn.Module ScorePredictor 结果图

现在它已经是完整的微型 PyTorch 模型:

datamodellosszero_gradbackwardoptimizer.stepeval prediction

什么时候用 Sequential,什么时候用自定义 Module?

Section titled “什么时候用 Sequential,什么时候用自定义 Module?”
场景推荐选择
简单直线堆叠nn.Sequential
多输入或多输出自定义 nn.Module
跳连或分支结构自定义 nn.Module
可复用组件自定义 nn.Module
需要更清楚的参数名自定义 nn.Module

真实深度学习项目里,自定义模块更常见,因为架构很快就会超过“直线堆叠”。

错误为什么有问题修复
forward() 里临时创建层每次调用都会创建新参数,可能无法被正确优化__init__() 里定义层
把 loss 和 optimizer 逻辑写进 forward()混淆模型定义和训练控制forward() 只负责输入到输出
忘记 super().__init__()子模块可能无法正确注册__init__() 开头调用
不检查参数名很难排查冻结层或缺失层打印 named_parameters()
验证前忘记 eval()Dropout/BatchNorm 还像训练时一样工作验证前调用 model.eval()
  1. ScorePredictor 的隐藏层大小从 16 改成 432。loss 有什么变化?
  2. 删除 ReLU()。这个小回归任务还能不能学?为什么更深的非线性任务可能需要它?
  3. 打印 model.state_dict() 的 key 和 shape。checkpoint 会保存哪些张量?
  4. 在 ReLU 后加入 nn.Dropout(p=0.2),比较 train()eval() 模式下的预测。
参考实现与讲解
  1. 4 可能表达能力不足,出现 underfit;32 更容易降低训练 loss,但也要看 validation loss,因为更大的模型也可能过拟合。
  2. 这个小回归任务如果接近线性,删除 ReLU() 仍可能学会。但没有非线性激活时,多层线性层本质上会合并成一层线性变换,无法表达更复杂的非线性模式。
  3. state_dict() 会保存可学习 tensor,例如 Linear 的 weight 和 bias。Dropout 有训练行为,但没有要保存的可学习参数。
  4. train() 模式下 dropout 会随机遮住一部分激活,所以同一输入的预测可能变化;eval() 模式下 dropout 关闭,预测应当稳定。
  • nn.Module 把层、参数、前向逻辑和模式状态统一管理。
  • forward() 应该描述数据流,不应该写训练循环。
  • model.parameters() 把模型和 optimizer 连接起来。
  • state_dict() 是标准 checkpoint 接口。
  • train()eval() 切换层行为,它们本身不运行训练或验证循环。