模型一深,几十个层要先装进容器才谈得上 forward。Sequential 用直线路径换「免写 forward」,ModuleList 用「只注册不执行」换控制流自由——选错容器的代价不是报错,而是参数静默消失。
Sequential:直线契约,免写 forward
Sequential 是约束最强的容器:执行顺序即注册顺序,上层输出直接喂下层,forward 因此可以自动生成。命名随之有两种——自动索引或 OrderedDict 显式命名:
import torch.nn as nn
from collections import OrderedDict
auto = nn.Sequential(nn.Linear(784, 256), nn.ReLU(), nn.Linear(256, 10))
# 子模块名是 "0"、"1"、"2",state_dict 键形如 2.weight
named = nn.Sequential(OrderedDict([
("encoder", nn.Linear(784, 256)),
("head", nn.Linear(256, 10)),
]))
# 键名变成 encoder.weight / head.weight,改结构时不易错位直线契约的适用面因此清晰:整段直线前馈,或大模型里的局部直线块——residual block 主干常是 Sequential,而 block 本身是手写 forward 的自定义类,因为 shortcut 支线装不进直线。一旦要分支、多输入或跳跃连接,出路只有自定义 Module 自己写 forward。
ModuleList:解开注册与执行
Sequential 把「注册进模块树」和「按序执行」绑死;当层数本身是超参——N 层 nn.Linear 与 nn.Dropout 交替的 MLP、深度可分离卷积堆叠——两者必须解开。ModuleList 正落在这里:只负责注册,执行路径完全交给你的 forward。
先看只装不注册会发生什么。普通 Python list 装层是经典事故:
import torch.nn as nn
class Bad(nn.Module):
def __init__(self):
super().__init__()
self.layers = [nn.Linear(64, 64) for _ in range(3)] # 普通 list
class Good(nn.Module):
def __init__(self):
super().__init__()
self.layers = nn.ModuleList(nn.Linear(64, 64) for _ in range(3))
print(list(Bad().parameters())) # [] —— 参数从优化器视野里消失
print(len(list(Good().parameters()))) # 6 —— 3 层 × (weight + bias)赋值时没有任何东西被拦截登记,Bad 的权重连同梯度一起消失:loss 不降、模型照跑、零报错。注册语义的底层机制——__setattr__ 拦截与 _modules 树——在 torch.nn 及其引用的 notes_nn_module_setattr 中展开,本篇不重述。没有 forward 既是 ModuleList 的代价也是它的自由:控制流必须手写,换来层数、分支、循环上零限制。
ModuleDict 与命名的下游影响
需求再进一步是「按名选路」——多任务头按任务名取、MoE 路由雏形按专家名取——此时把 ModuleList 换成 ModuleDict:
import torch
import torch.nn as nn
heads = nn.ModuleDict({"seg": nn.Linear(256, 21), "depth": nn.Linear(256, 1)})
feat = torch.randn(4, 256)
logit = heads["seg"](feat) # 运行时按任务名选头
print(logit.shape) # torch.Size([4, 21])同一注册语义还有两个装裸权重的亲戚:ParameterList / ParameterDict,装 nn.Parameter 而非 Module,服务逐层可学习缩放这类自定义权重集合。所有容器最终共用 add_module(name, module) 这一个底层入口,名字直接决定 state_dict 键名——layers.0.weight 与 layers.fc.weight 是两个 checkpoint 世界,改名即失配。
选型与坑位
选型收束全文:直线用 Sequential,迭代建层用 ModuleList,按名选路用 ModuleDict;容器如何嵌套进模块树、拼成完整模型见 PyTorch 架构总览。三个高频坑:
- Sequential 里放多输入层:自动 forward 只单参数传递,attention 这类 query/key/value 三输入层调用即 TypeError
- ModuleList 混入普通 list:部分层注册、部分消失,无报错、loss 不降,最难排查
- 命名重复:OrderedDict 重复键与
add_module重复名都不报错,后者悄悄覆盖前者——层直接没了