问题与目标
网络结构能运行,不代表它与任务匹配。输出层宽度、标签类型和损失函数形成一组契约;契约错了,模型可能立即报错,也可能更危险地持续训练却学不到正确目标。
本篇使用 nn.Module 定义一个多分类网络,完成一次前向、损失、反向和参数更新。输入是形状 (batch, 4) 的特征,输出是形状 (batch, 3) 的 logits。

任务类型同时决定模型输出形状、标签类型和损失函数,三者不能分开配置。
核心概念
nn.Module 管理参数与子模块
from torch import nn
class Classifier(nn.Module):
def __init__(self, input_size: int, hidden_size: int, classes: int):
super().__init__()
self.network = nn.Sequential(
nn.Linear(input_size, hidden_size),
nn.ReLU(),
nn.Linear(hidden_size, classes),
)
def forward(self, features):
return self.network(features)
在 __init__() 中将网络层赋给对象属性,PyTorch 才能注册其参数、迁移设备、切换训练模式并写入 state_dict。forward() 描述数据流,平时通过 model(x) 调用,不直接调 forward()。
nn.Sequential 适合单向串联的结构。多输入、跳连接或分支网络则应在自定义 forward() 中明确组织。
输出 logits,再由损失处理数值稳定性
| 任务 | 模型输出 | 标签 | PyTorch 损失 |
|---|---|---|---|
| 回归 | (N, 1) 或 (N,) 浮点数 | 同形状浮点数 | MSELoss / SmoothL1Loss |
| 二分类 | (N,) 原始 logits | 同形状 0/1 浮点数 | BCEWithLogitsLoss |
| 多分类 | (N, C) 原始 logits | (N,) 类别索引 long | CrossEntropyLoss |
CrossEntropyLoss 接收未归一化 logits,不需要在模型末尾先加 Softmax。BCEWithLogitsLoss 将 Sigmoid 与二元交叉熵合并,比手动 Sigmoid 后再计算损失更稳定。概率只在评估和推理解释时转换。
参数数量可以手算
Linear(4, 12) 包含 4×12 个权重和 12 个偏置;Linear(12, 3) 包含 12×3 + 3 个参数。参数量与样本量、输入信息和正则化共同影响过拟合风险。
model = Classifier(4, 12, 3)
trainable = sum(parameter.numel() for parameter in model.parameters() if parameter.requires_grad)
print(model)
print("trainable parameters:", trainable)
一次训练步的固定顺序
optimizer.zero_grad()
logits = model(features)
loss = loss_fn(logits, labels)
loss.backward()
optimizer.step()
zero_grad() 放在反向前或上一次更新后都可以,关键是每次常规更新只使用当前批次的梯度。
可运行实现
import torch
from torch import nn
torch.manual_seed(42)
features = torch.tensor([
[0.2, 0.8, 0.1, 0.4],
[0.9, 0.1, 0.6, 0.2],
[0.4, 0.7, 0.8, 0.5],
[0.1, 0.2, 0.2, 0.9],
], dtype=torch.float32)
labels = torch.tensor([0, 1, 2, 0], dtype=torch.long)
class Classifier(nn.Module):
def __init__(self) -> None:
super().__init__()
self.network = nn.Sequential(
nn.Linear(4, 12),
nn.ReLU(),
nn.Linear(12, 3),
)
def forward(self, inputs: torch.Tensor) -> torch.Tensor:
return self.network(inputs)
model = Classifier()
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.02)
model.train()
optimizer.zero_grad()
logits = model(features)
loss = loss_fn(logits, labels)
loss.backward()
optimizer.step()
print("logits shape:", logits.shape)
print("label shape:", labels.shape)
print("loss:", round(loss.item(), 4))
model.eval()
with torch.inference_mode():
probabilities = model(features).softmax(dim=1)
predictions = probabilities.argmax(dim=1)
print("probability sums:", probabilities.sum(dim=1))
print("predictions:", predictions.tolist())
示例只执行一次更新,用于验证网络契约和训练步骤。四条样本不足以证明分类能力。完整训练需要批次数据、验证集、多个 epoch 和独立评估。
检查参数是否真的更新
first_weight = next(model.parameters())
print(first_weight.shape)
print("gradient exists:", first_weight.grad is not None)
print("gradient norm:", round(first_weight.grad.norm().item(), 6))
梯度为 None 通常意味着参数没有参与当前损失的计算,或中间张量被 detach()、NumPy 转换等操作切断了计算图。
常见问题与排查
CrossEntropyLoss前手动 Softmax:删除 Softmax,将 logits 直接交给损失。- 多分类标签使用 one-hot 却形状不匹配:基础用法中使用
(N,)的long类别索引。 - 二分类 logits 与标签一个是
(N, 1)、一个是(N,):在模型边界明确squeeze(1)或统一保留二维。 - 网络层放在普通 Python 列表里:参数可能不会被注册,可使用
nn.ModuleList。 - 直接使用
forward():应调用model(inputs),保留 Module 钩子和统一调用逻辑。 - 输出形状正确却损失不降:检查标签含义、梯度、优化器是否持有当前模型参数。
小结
nn.Module 将网络结构、参数、设备和状态组织在一起。模型定义时最重要的不是层数,而是输入形状、输出形状、标签格式与损失函数形成一致契约。这份契约明确后,网络才能进入批量数据的完整训练流程。
License: CC BY-NC 4.0
Updated 2 hours ago
Was this article helpful? Give it a like.
0 comments


