Skip to content

用 nn.Module 搭模型:像搭积木一样定义网络

torch.nn 子库提供了现成的网络层,nn.Module 是所有网络层与模型的基类。本篇学会两件事:怎么组合层,怎么把组合写成一个规范的模型类。

全连接层 nn.Linear:一切的基础

最常用的一层。nn.Linear(in_features, out_features) 计算 y = x @ W.T + b,权重 W 形状 [out, in],偏置 b 形状 [out],创建时随机初始化且自带 requires_grad=True

python
import torch
import torch.nn as nn

layer = nn.Linear(784, 10)        # 输入 784 维,输出 10 维
print(layer.weight.shape)          # torch.Size([10, 784])
print(layer.bias.shape)            # torch.Size([10])

x = torch.rand(32, 784)            # 32 个样本
y = layer(x)                       # 调用层就像调用函数!输出 [32, 10]

注意 layer(x) 用的是 Python 的 __call__ 语法糖,看起来像函数调用,实际触发了模块的内部机制(钩子、train/eval 模式等),所以永远写 layer(x) 而不是 layer.forward(x)

常用层速查

python
nn.Flatten()                  # 展平(默认保留 batch 维),图像进全连接前常用
nn.ReLU()                     # 激活函数:max(0, x),最常用的非线性
nn.Sigmoid()                  # 压到 (0,1),二分类输出层用
nn.Tanh()                     # 压到 (-1,1)
nn.Softmax(dim=-1)            # 把一组数变成概率分布(多分类"概率"用)
nn.Dropout(p=0.2)             # 训练时随机 20% 置零,防过拟合;eval 时自动失效
nn.Conv2d(3, 16, kernel_size=3)   # 卷积层,图像处理核心
nn.MaxPool2d(2)               # 最大池化,降分辨率
nn.BatchNorm1d(64)            # 批归一化,加速收敛
nn.Embedding(10000, 128)      # 词嵌入查表,NLP 核心
nn.LSTM(128, 64)              # 循环神经网络层

激活函数的选择没有太多悬念:隐藏层默认 ReLU(或其变体 GELU),输出层按任务定——回归不加、二分类 1 个神经元 + Sigmoid(或直接输出 logit 配 BCEWithLogitsLoss)、多分类直接输出 logits 配 CrossEntropyLoss

损失函数与优化器:模型搭完配什么

损失函数(Loss Function)量化"预测与正确答案差多远",优化器(Optimizer)根据梯度更新参数。与任务对应的常用组合:

任务输出层损失函数标签 dtype
回归1 个神经元,不加激活nn.MSELossfloat32
二分类1 个神经元,不加激活nn.BCEWithLogitsLossfloat32
多分类类别数 n 个神经元nn.CrossEntropyLossint64

两个高频疑惑:

  • 为什么多分类不要自己套 Softmax? 因为 CrossEntropyLoss 内部已经集成了 LogSoftmax + NLLLoss,且直接吃 logits 在数值上更稳定。输出层再套 Softmax 等于算了两次,梯度还不对。
  • 为什么 CrossEntropyLoss 的标签是 int64 而不是 one-hot? 它期望的标签形状是 [batch] 的类别下标(如 tensor([3, 1, 7])),不是 [batch, 10] 的 one-hot。形状/类型错了直接报错。

优化器日常二选一:AdamWoptim.AdamW(model.parameters(), lr=1e-3),默认首选,收敛快、对学习率不那么敏感)或 SGD+动量optim.SGD(..., lr=0.01, momentum=0.9),调好了泛化常更好)。学习率调度可以用 optim.lr_scheduler.StepLR / CosineAnnealingLR 包一层,后面实战与进阶章节有完整用法。

组合层:nn.Sequential

如果数据只是"一层层顺序流过",没有分支,用 nn.Sequential 最省事:

python
model = nn.Sequential(
    nn.Flatten(),
    nn.Linear(784, 128),
    nn.ReLU(),
    nn.Dropout(0.2),
    nn.Linear(128, 10),
)
y = model(torch.rand(32, 784))   # [32, 10]
print(model)                     # 打印出网络结构,debug 友好

标准写法:自定义 Module 类

一旦模型有分支、需要重写前向逻辑,或者你希望代码可读(带命名的 forward 步骤),就应该继承 nn.Module这是官方推荐、也是所有开源项目里的标准姿势

python
class SimpleNet(nn.Module):
    def __init__(self):
        super().__init__()                      # 必须!注册模块机制依赖它
        self.flatten = nn.Flatten()
        self.fc1 = nn.Linear(784, 128)
        self.relu = nn.ReLU()
        self.drop = nn.Dropout(0.2)
        self.fc2 = nn.Linear(128, 10)

    def forward(self, x):
        x = self.flatten(x)      # [32,28,28] → [32,784]
        x = self.relu(self.fc1(x))
        x = self.drop(x)
        return self.fc2(x)       # [32, 10],原始打分(logits)

model = SimpleNet()
print(sum(p.numel() for p in model.parameters()))  # 101770 —— 参数量

三条铁律:

  1. __init__ 里第一行必须 super().__init__(),否则属性注册机制失效,model.parameters() 返回空列表,优化器收不到任何参数,训练一动不动——报错都不报,属于"静默失败",非常阴间。
  2. 层定义在 __init__,连接逻辑写在 forwardforward 的输入就是 batch 张量,返回值就是模型输出。
  3. 外部永远调用 model(x),不要手动调 model.forward(x)

parameters() 与命名

python
for name, p in model.named_parameters():
    print(name, tuple(p.shape))
# fc1.weight (128, 784)
# fc1.bias (128,)
# fc2.weight (10, 128)
# fc2.bias (10,)

model.parameters() 就是优化器的输入:optim.SGD(model.parameters(), lr=0.01)。子模块自动递归注册——这也是为什么所有层必须在 __init__ 里作为属性赋值。

train() 与 eval():容易被忽略的开关

python
model.train()   # 训练模式:Dropout 生效,BatchNorm 更新运行统计量
model.eval()    # 评估/推理模式:Dropout 关闭,BatchNorm 使用已学统计量

每个 epoch 开始训练前 model.train(),每次评估/推理前 model.eval(),要养成肌肉记忆。eval() 这个名字极具欺骗性——它不评估任何东西,只切模式;评估还要自己写循环算指标(实战章节有模板)。

小结

  • 层是乐高积木(nn.Linear 等),nn.Module 子类是拼好的成品模型。
  • 无分支用 nn.Sequential,有分支/追求可读性用自定义类。
  • 牢记:super().__init__()model(x) 调用、train()/eval() 切换。

下一篇:数据管道 Dataset 与 DataLoader——模型搭好了,数据怎么喂?