Appearance
用 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.MSELoss | float32 |
| 二分类 | 1 个神经元,不加激活 | nn.BCEWithLogitsLoss | float32 |
| 多分类 | 类别数 n 个神经元 | nn.CrossEntropyLoss | int64 |
两个高频疑惑:
- 为什么多分类不要自己套 Softmax? 因为
CrossEntropyLoss内部已经集成了 LogSoftmax + NLLLoss,且直接吃 logits 在数值上更稳定。输出层再套 Softmax 等于算了两次,梯度还不对。 - 为什么 CrossEntropyLoss 的标签是 int64 而不是 one-hot? 它期望的标签形状是
[batch]的类别下标(如tensor([3, 1, 7])),不是[batch, 10]的 one-hot。形状/类型错了直接报错。
优化器日常二选一:AdamW(optim.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 —— 参数量三条铁律:
__init__里第一行必须super().__init__(),否则属性注册机制失效,model.parameters()返回空列表,优化器收不到任何参数,训练一动不动——报错都不报,属于"静默失败",非常阴间。- 层定义在
__init__,连接逻辑写在forward。forward的输入就是 batch 张量,返回值就是模型输出。 - 外部永远调用
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——模型搭好了,数据怎么喂?