明明写了多个 nn.Linear,前向也能得到结果,但创建 Adam 或 SGD 时却报参数列表为空:
ValueError: optimizer got an empty parameter list
这条消息说的是“传给优化器的可训练对象为空”,并不等于程序里完全没有权重。对应原始问题把各层放在 self.nn_layers = [] 中,恰好需要区分“层已经创建”和“层已被父模块注册”。
先观察:有 Linear,为什么 parameters() 还是空的
下面保留原帖动态建层的写法,缩成两层小网络。可以在已有 PyTorch 的环境直接运行,不需要数据集:
import torch
from torch import nn
class PlainListActor(nn.Module):
def __init__(self):
super().__init__()
self.layers = [nn.Linear(4, 8), nn.Linear(8, 2)]
def forward(self, x):
x = torch.relu(self.layers[0](x))
return self.layers[1](x).softmax(dim=-1)
bad_model = PlainListActor()
print("josh050608")
print("registered parameters:", [name for name, _ in bad_model.named_parameters()])
print("inner Linear parameters:", [name for name, _ in bad_model.layers[0].named_parameters()])
print("forward shape:", bad_model(torch.ones(3, 4)).shape)
try:
torch.optim.Adam(bad_model.parameters(), lr=0.001)
except ValueError as exc:
print("optimizer error:", exc)
关键检查会显示:父模型的注册参数列表为空,第一层 Linear 自己却有 weight、bias,前向仍可输出 [3,2]。所以“能调用 forward”不能证明 model.parameters() 已经找到了这些层。
父模块会管理作为子模块注册的层;普通 Python list 只是一个普通容器。放在里面的 Linear 不会因此自动出现在父模块的 parameters、state_dict 或递归设备迁移中。
修复:动态层列表使用 ModuleList
对原帖,最小修改是把 self.nn_layers = [] 改成 self.nn_layers = nn.ModuleList(),随后继续 append。无需把整个动态网络改写成另一套固定结构。
下面是保留原帖 4→128→2 配置的完整版本。构造时先复制调用方传入的列表,避免添加输出层时把原列表也改掉:
class NetActor(nn.Module):
def __init__(self, state_size, action_size, hidden_sizes):
super().__init__()
sizes = [state_size, *list(hidden_sizes), action_size]
self.nn_layers = nn.ModuleList([
nn.Linear(source, target)
for source, target in zip(sizes[:-1], sizes[1:])
])
def forward(self, x):
for layer in self.nn_layers[:-1]:
x = torch.relu(layer(x))
return self.nn_layers[-1](x).softmax(dim=-1)
model = NetActor(4, 2, [128])
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
print("registered shapes:", [(n, tuple(p.shape)) for n, p in model.named_parameters()])
print("parameter elements:", sum(p.numel() for p in model.parameters()))
这里应注册四个参数张量:第一层 weight、bias 和第二层 weight、bias,元素总数为 898。ModuleList 本身不定义 forward,上面的循环仍负责实际计算,这也是它与 Sequential 的区别。
如果层已存在于列表中,也可以用 nn.ModuleList(existing_layers) 保留这些层对象。对于刚开始构建的模型,直接在构造函数使用 ModuleList 更清楚。改完类后要重新创建实例和优化器;Notebook 里只重新运行类定义,不会自动改变旧对象。
不仅看参数非空,还要检查它真的更新了
继续运行下面代码。这里用三条合成输入和简单的概率损失检查梯度路径,不代表原 Actor 算法的完整训练目标:
inputs = torch.ones(3, 4)
before = model.nn_layers[0].weight.detach().clone()
optimizer.zero_grad()
probabilities = model(inputs)
assert probabilities.shape == (3, 2)
assert torch.allclose(probabilities.sum(dim=1), torch.ones(3))
loss = -probabilities[:, 0].log().mean()
loss.backward()
for name, parameter in model.named_parameters():
assert parameter.grad is not None, name
assert torch.isfinite(parameter.grad).all(), name
optimizer.step()
assert not torch.equal(before, model.nn_layers[0].weight)
assert "nn_layers.0.weight" in model.state_dict()
print("registered parameters:", len(list(model.parameters())))
print("first weight updated:", not torch.equal(before, model.nn_layers[0].weight))
这样能同时验证三件事:优化器拿到了参数、反向能到达各层、step 后权重发生变化。真实工程再把示例损失换回自己的策略目标。
如果改成 ModuleList 仍然为空
先确认创建优化器时传入的是新模型,而不是旧实例。然后直接打印 list(model.named_parameters()),区分下面几类情况:
| 观察 | 下一步检查 |
|---|---|
| 没有注册参数,层只在普通 list/dict 中 | 使用 ModuleList/ModuleDict,或把层直接挂到模型属性 |
| 构造函数里的定义没有执行 | 核对 __init__ 拼写、父类初始化与构造分支 |
| named_parameters 非空,传给优化器的对象却空 | 检查筛选条件、生成器是否已经被遍历耗尽 |
| 自定义“权重”只是普通 Tensor | 根据设计注册为 nn.Parameter,而不只是设置 requires_grad |
同名 CSDN 案例里确实有 __init__ 下划线写错的情况,但它不能解释原帖已经正确初始化父类、却使用普通列表的代码。相同报错要根据模型定义与注册参数列表判断,不能把某一篇帖子的根因当成通用答案。
参考:原始问题;同报错的 CSDN 拼写错误案例、ModuleList(2.5)、nn.Module(2.5)、优化器(2.5)。