MLP 与完整训练验证
前置知识:Dataset、DataLoader 与批次 · 自动求导与计算图 · shift、rolling、resample 与时间对齐
本章目录
本章学习目标
- 实现训练、验证、选模和最终评价
- 区分 train/eval 与关闭梯度
- 检查预测形状、损失和参数变化
用简单任务串起完整流程
多层感知机由线性变换和非线性激活交替组成。对于两个输入特征,可以先用 Linear(2,8) 映射到八个隐藏单元,通过 ReLU 引入非线性,再用 Linear(8,1) 得到连续输出。最后一层不加分类 softmax,因为这里学习的是回归目标。网络结构必须与任务和损失匹配,不能只照搬另一个分类教程。
本章生成固定种子的双特征样本,目标是第一列的两倍减第二列的一半。设置训练、验证和测试三组独立样本;示例数据只是验证流程,不代表真实市场或业务效果。先使用线性模型理解目标,再用小 MLP 看训练接口如何组合。
训练集拟合参数,验证集决定保存哪一轮模型,测试集在选模结束后评价一次。测试集不能参与每轮早停决策,也不能看到测试结果后继续调整结构和超参数。小型教学数据同样要体现这条信息边界,否则实现虽然可运行,却教错了研究流程。
一次训练更新的四步
每个批次先清理旧梯度,随后前向预测并计算标量损失,再反向计算梯度,最后由优化器更新参数。optimizer 持有需要更新的参数和自身状态;创建网络之后,把 model.parameters 交给优化器。只调用 loss.backward 不会训练成功,只调用 step 而没有有效梯度也不会形成正确更新。
回归示例要求预测和目标形状均为样本数乘一,用 assert 在损失前检查。损失是 MSE,即每项平方残差的平均。训练前后保存某层权重的副本,比较是否发生变化,再检查损失是否为有限值。这些结构检查比仅看最后一个小数更直接。
学习率过大可能使损失震荡或发散,过小可能在短期内变化不明显。问题也可能来自错误输入尺度、目标对齐、忘记 step 或 detach。先查流程和数据,再调整学习率;不要把所有异常都归因于网络不够复杂。
训练模式与梯度开关分别控制什么
model.train 和 model.eval 控制具有训练/推理差异的层,例如 Dropout 和 BatchNorm。它们不等于开启或关闭梯度。验证时一般既调用 eval,又在 no_grad 或 inference_mode 中执行前向,前者规定层行为,后者避免记录用于求导的图。
当前 MLP 没有 Dropout 和 BatchNorm,所以 train/eval 不会改变数值路径,但示例仍显式写出模式切换。这使读者以后加入这类层时保留正确流程。验证结束后,下一轮训练前再切回 train,不能因为网络暂时没有模式差异就省略整个约定。
验证过程中不调用优化器更新,不计算训练用的 backward。记录损失用 item,记录模型快照时复制 state_dict 中的张量。直接保存指向当前参数的引用,然后继续训练,所谓“最佳模型”可能被后续更新覆盖。
选择模型后再检查可解释的证据
每轮在固定验证集计算损失,若更好则保存权重副本。结束后加载该副本,再运行测试集。报告至少列出训练设置、选择标准、验证损失、最终测试损失、样本数量与设备;只报告最好一轮训练损失不能说明模型对未见样本的表现。
固定种子有利于同环境复现,但数值结果可能随版本、线程和设备产生小差异。验收不要求某个精确准确率,而要求形状正确、梯度有限、参数变化、损失有限、验证未更新参数及保存加载一致。学习效果判断还需与简单基线在同一集合比较。
真实业务中的缺失填充和标准化放在划分之后,仅由训练数据拟合。若数据存在同一用户重复记录或重叠时间标签,还需按任务设计组划分与时间边界。本章建立最小训练循环,综合项目再把 CSV 清洗、预处理、模型和结果导出串成完整交付。
交互演示:观察机制
完整示例与实际输出
import copy
import torch
torch.manual_seed(42); torch.set_num_threads(1)
X=torch.randn(120,2)
y=2*X[:,0:1]-.5*X[:,1:2]
train_X,train_y=X[:80],y[:80]
val_X,val_y=X[80:100],y[80:100]
test_X,test_y=X[100:],y[100:]
model=torch.nn.Sequential(torch.nn.Linear(2,8),torch.nn.ReLU(),torch.nn.Linear(8,1))
optimizer=torch.optim.Adam(model.parameters(),lr=.03)
loss_fn=torch.nn.MSELoss()
initial=model[0].weight.detach().clone()
best=float('inf'); best_state=None
for epoch in range(160):
model.train(); optimizer.zero_grad(set_to_none=True)
pred=model(train_X)
assert pred.shape==train_y.shape
loss=loss_fn(pred,train_y)
assert torch.isfinite(loss)
loss.backward()
assert all(p.grad is not None and torch.isfinite(p.grad).all() for p in model.parameters())
optimizer.step()
model.eval()
with torch.no_grad(): val=loss_fn(model(val_X),val_y).item()
if val<best: best=val; best_state=copy.deepcopy(model.state_dict())
assert not torch.equal(initial,model[0].weight)
model.load_state_dict(best_state); model.eval()
with torch.no_grad(): test=loss_fn(model(test_X),test_y).item()
assert best_state is not None and test>=0
print('device=cpu','epochs=160','validation_mse=',round(best,6),'test_mse=',round(test,6))
实际运行通过CPU Python execution.
device=cpu epochs=160 validation_mse= 0.002294 test_mse= 0.001088
五道自测:先回答,再核对
01 · 回归输出为什么不用 softmax?
查看提示
先定义目标数值的含义。
查看参考答案
连续目标不要求类别概率和为一,输出层与 MSE 回归目标匹配。
02 · eval 会自动禁止梯度计算吗?
查看提示
分开模式与图记录。
查看参考答案
不会,它改变特定层的行为;另用 no_grad/inference_mode 禁止记录梯度图。
03 · 保存最佳 state_dict 为什么要复制?
查看提示
快照应冻结当时数值。
查看参考答案
若仅持有当前张量引用,后续训练更新可能覆盖所谓最佳快照。
04 · 测试集每轮计算并选择最低点合适吗?
查看提示
测试表现是否影响了选择?
查看参考答案
不合适,选模使用验证集;测试集在决定模型后评价一次。
05 · 预测(N,1)、目标(N,)时仅损失可运行够吗?
查看提示
检查残差矩阵的实际shape。
查看参考答案
不够,可能发生错误广播。损失前应 assert 两者形状相同。
两项动手任务
任务 1 · 把示例改为无隐藏层基线,并保留相同划分和选模标准。
提示
先写出输入、输出、形状和判断条件,再运行小数据。
完整参考解答与检查方式
以下为独立可运行答案。数据、划分、轮数、MSE 和验证选模与正文保持一致,只改变模型结构。
import copy
import torch
torch.manual_seed(42); torch.set_num_threads(1)
X=torch.randn(120,2); y=2*X[:,0:1]-.5*X[:,1:2]
train_X,train_y=X[:80],y[:80]
val_X,val_y=X[80:100],y[80:100]
test_X,test_y=X[100:],y[100:]
model=torch.nn.Linear(2,1)
initial=model.weight.detach().clone()
optimizer=torch.optim.Adam(model.parameters(),lr=.03)
loss_fn=torch.nn.MSELoss(); best=float('inf'); best_state=None
for epoch in range(160):
model.train(); optimizer.zero_grad(set_to_none=True)
pred=model(train_X); assert pred.shape==train_y.shape
loss=loss_fn(pred,train_y); assert torch.isfinite(loss)
loss.backward(); optimizer.step()
model.eval()
with torch.no_grad(): val=loss_fn(model(val_X),val_y).item()
if val<best: best=val; best_state=copy.deepcopy(model.state_dict())
assert not torch.equal(initial,model.weight)
model.load_state_dict(best_state); model.eval()
with torch.no_grad(): test=loss_fn(model(test_X),test_y).item()
print('validation_mse=',best,'test_mse=',test)任务 2 · 证明一次验证没有改变参数或创建新梯度。
提示
先写出输入、输出、形状和判断条件,再运行小数据。
完整参考解答与检查方式
before={k:v.detach().clone() for k,v in model.state_dict().items()}
optimizer.zero_grad(set_to_none=True)
model.eval()
with torch.no_grad(): score=loss_fn(model(val_X),val_y).item()
assert all(torch.equal(before[k],v) for k,v in model.state_dict().items())
assert all(p.grad is None for p in model.parameters())
print(score)此段接在正文训练脚本后,复用其中的模型和验证数据。
官方资料与掌握状态
完成两项任务后,按独立完成程度记录。状态会尽量保存在当前浏览器,可导出备份。
动手任务记录
掌握状态与复习
关掉资料独立完成后记 A;提示后完成记 B;需要重学记 C。
记录尽量保存在当前浏览器,建议定期导出备份。