问题与目标
训练失败并不只有“模型不够大”一个原因。损失不降、剧烈震荡、训练表现越来越好但验证表现变差,对应的问题和处理方向都不同。
本篇用同一份自建非线性数据比较三个学习率,记录训练损失与验证损失。目标不是记住一个通用学习率,而是根据曲线现象缩小排查范围。

图中三条曲线使用相同数据和初始参数。学习率改变的不只是收敛快慢,也包括整条优化路径。
核心概念
训练与验证曲线要一起看
| 曲线现象 | 常见原因 | 优先检查 |
|---|---|---|
| 训练和验证损失都高 | 欠拟合、特征不足、优化未收敛 | 训练时长、学习率、模型容量 |
| 训练降、验证先降后升 | 过拟合 | 数据量、正则化、早停 |
| 两条曲线大幅震荡 | 学习率过大、批次太小或数据异常 | 降低学习率、查梯度与输入 |
| 损失几乎不动 | 学习率过小、梯度断开或模型没有参数 | 梯度、计算图、优化器参数列表 |
训练准确率很高不是完成信号。训练数据参与了参数更新,真正关心的是模型对未参与更新的数据能否保持表现。
学习率控制每次更新的步长
学习率大时不一定学得快。它可能不断跨过低损失区域,导致震荡或 nan。学习率小时更新稳定,却可能在有限 epoch 内还没有接近合适区域。
学习率调度器在训练过程中修改步长,但不能修复错误标签、数据泄漏或损失与输出不匹配。
SGD、Momentum 和 Adam
- SGD 直接按当前梯度更新,逻辑清晰,但在狭长损失地形中容易摇摆。
- Momentum 累积历史更新方向,帮助减少局部震荡并沿稳定方向加速。
- Adam 为不同参数维护自适应步长,常是快速建立基线的起点,但仍需选择学习率和权重衰减。
优化器没有脱离数据和任务的统一排名。比较时要使用相同初始参数、批次顺序、训练轮数和评估口径。
epoch、batch 和 iteration
- batch 是一次前向与反向传播使用的样本子集。
- iteration 是一次参数更新。
- epoch 表示训练集大致被完整使用一遍。
批次变大通常使梯度估计更稳定,但占用更多内存,也会改变更新次数和优化特性。
可运行实现
python
import copy
import numpy as np
import torch
from torch import nn
torch.manual_seed(42)
rng = np.random.default_rng(42)
X = rng.uniform(-2.5, 2.5, size=(500, 2)).astype("float32")
y = ((X[:, 0] ** 2 + 0.7 * X[:, 1]) > 1.4).astype("int64")
indices = rng.permutation(len(X))
train_index, valid_index = indices[:400], indices[400:]
X_train, y_train = torch.from_numpy(X[train_index]), torch.from_numpy(y[train_index])
X_valid, y_valid = torch.from_numpy(X[valid_index]), torch.from_numpy(y[valid_index])
template = nn.Sequential(nn.Linear(2, 16), nn.ReLU(), nn.Linear(16, 2))
initial_state = copy.deepcopy(template.state_dict())
def run(learning_rate: float) -> tuple[list[float], list[float]]:
model = nn.Sequential(nn.Linear(2, 16), nn.ReLU(), nn.Linear(16, 2))
model.load_state_dict(initial_state)
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate)
loss_fn = nn.CrossEntropyLoss()
train_history, valid_history = [], []
for _ in range(80):
model.train()
logits = model(X_train)
loss = loss_fn(logits, y_train)
optimizer.zero_grad()
loss.backward()
optimizer.step()
train_history.append(float(loss.item()))
model.eval()
with torch.inference_mode():
valid_loss = loss_fn(model(X_valid), y_valid)
valid_history.append(float(valid_loss.item()))
return train_history, valid_history
for rate in (0.001, 0.05, 1.0):
train_curve, valid_curve = run(rate)
print(
rate,
"train", round(train_curve[-1], 4),
"valid", round(valid_curve[-1], 4),
"best valid", round(min(valid_curve), 4),
)
三次实验从相同初始参数开始,主要变量只是学习率。0.001 通常下降较慢,0.05 可建立较稳定的基线,1.0 的表现则更依赖初始值和数据。不要将这个结果当成其他项目的参数答案。
保存曲线
python
import matplotlib.pyplot as plt
train_curve, valid_curve = run(0.05)
figure, axis = plt.subplots(figsize=(6, 4))
axis.plot(train_curve, label="train")
axis.plot(valid_curve, label="valid")
axis.set(xlabel="Epoch", ylabel="Cross-entropy", title="Training diagnosis")
axis.legend()
figure.tight_layout()
figure.savefig("training_curve.png", dpi=150)
plt.close(figure)
曲线文件应和数据版本、随机种子、优化器参数一起保存。否则图可以观察,却无法追溯对应的实验。
常见问题与排查
- 不同优化器直接比最终分数:先固定数据划分、初始参数和训练预算。
- 只记录最后一个 epoch:保留完整曲线和最佳验证位置。
- 损失下降但业务指标不升:损失和最终指标职责不同,同时记录两者。
- 验证时没有切换
eval():Dropout 和 BatchNorm 会保持训练行为,导致结果不稳定。 - 发现过拟合就立即换优化器:先检查数据划分、模型容量和正则化。
- 损失为
nan:检查学习率、非法数值、梯度范围和对数输入。
小结
训练曲线是诊断工具,不是美化结果的配图。训练与验证的相对趋势用于区分欠拟合、过拟合和优化不稳定;学习率和优化器改变参数更新路径,但无法替代数据与任务检查。
许可协议:CC BY-NC 4.0
更新于 1 小时前
觉得文章有帮助?点个赞吧!
0 条评论


