课程0基础Agent开发课 / 数学基础 / 优化理论入门-损失函数与梯度下降实战
— 11 min read

优化理论入门-损失函数与梯度下降实战

> **本文的定位**:这是第3章数学基础的最后一篇,属于实战篇。把前面三篇学过的数学工具(线性代数的矩阵运算、微积分的梯度、概率论的损失函数)组合起来,看一个完整的训练过程是什么样的。同时深入理解过拟合问题——它是机器学习中最常见、最重要的挑战之一。

优化理论入门:损失函数与梯度下降实战

本文的定位:这是第3章数学基础的最后一篇,属于实战篇。把前面三篇学过的数学工具(线性代数的矩阵运算、微积分的梯度、概率论的损失函数)组合起来,看一个完整的训练过程是什么样的。同时深入理解过拟合问题——它是机器学习中最常见、最重要的挑战之一。


一、优化是什么:找最优解的艺术

"优化"是一个听起来高大上但其实非常直觉的概念。

你每天都在做优化:用最少的钱买到质量最好的商品;找一条从家到公司距离最短的路;在有限时间内完成最重要的任务。这些都是优化问题——在某些约束条件下,找到让某个目标最大化或最小化的最优方案

机器学习里的优化,目标很明确:找到一组模型参数,使得损失函数(预测误差)最小化

这个问题之所以难,是因为:

  • 参数空间极其巨大(大模型有几十亿个参数)
  • 损失函数的"地形"非常复杂(到处都是山峰和山谷)
  • 不能穷举(随机尝试需要天文数字的时间)

前面几篇学的梯度和梯度下降,就是解决这个问题的核心工具。本篇把这些工具组合起来,看一个完整的训练循环。


二、一个完整的训练循环

下面这段代码是深度学习训练的标准模板。把它理解透,你就能看懂几乎所有深度学习的训练代码。

这段代码在做什么:创建一个简单的两层神经网络,在随机生成的二分类数据上训练100轮,每隔20轮打印一次损失值,观察损失是否在下降。

python
import torch
import torch.nn as nn
import torch.optim as optim

# 1. 准备数据
X = torch.randn(100, 10)       # 100个样本,10个特征
y = torch.randint(0, 2, (100,))  # 二分类标签(0或1)

# 2. 定义模型
# nn.Sequential:把多个层按顺序叠在一起,数据从上到下流过每一层
# nn.Linear(10, 64):全连接层,把10维输入变换成64维
# nn.ReLU():激活函数,给网络加入非线性能力(把负数变成0)
model = nn.Sequential(
    nn.Linear(10, 64),
    nn.ReLU(),
    nn.Linear(64, 2)
)

# 3. 定义损失函数和优化器
criterion = nn.CrossEntropyLoss()  # 分类任务用交叉熵损失
optimizer = optim.Adam(model.parameters(), lr=1e-3)  # Adam优化器,学习率0.001

# 4. 训练循环
for epoch in range(100):
    # 前向传播:数据从输入层流向输出层,得到预测结果
    outputs = model(X)
    loss = criterion(outputs, y)  # 计算预测结果和真实标签的差距

    # 反向传播:从输出向输入方向计算每个参数的梯度
    optimizer.zero_grad()  # 清空上一步的梯度(PyTorch会累加梯度,必须先清空)
    loss.backward()        # 自动计算所有参数的梯度(用链式法则)
    optimizer.step()       # 按梯度更新参数(沿梯度反方向走一步)

    if epoch % 20 == 0:
        print(f"Epoch {epoch}, Loss: {loss.item():.4f}")

运行这段代码,你应该看到类似这样的输出:

code
Epoch 0,  Loss: 0.7021
Epoch 20, Loss: 0.6183
Epoch 40, Loss: 0.5492
Epoch 60, Loss: 0.4830
Epoch 80, Loss: 0.4215

损失值从0.7021逐渐下降到0.4215,这说明模型在不断学习,预测越来越准。

关键的三行代码

python
optimizer.zero_grad()  # ① 清空梯度(必须在backward之前)
loss.backward()        # ② 计算梯度(反向传播)
optimizer.step()       # ③ 更新参数(梯度下降一步)

这三行代码的顺序是固定的,不能颠倒。它们构成了所有深度学习训练的基本节奏。


三、过拟合:死记硬背 vs 真正理解

欠拟合、恰好拟合、过拟合对比
三种拟合状态——欠拟合误差大、恰好拟合泛化好、过拟合记住了噪声

用一个学生类比建立直觉

考虑两个学生:

学生A(死记硬背):把100道练习题的答案全部背下来,考试时只要题目和练习题一模一样,能满分;但题目稍有变化,就完全不会。

学生B(真正理解):理解了解题方法,对练习题和新题目都能举一反三,成绩稳定。

过拟合就是学生A的情况:模型在训练集上表现完美(背答案),但在没见过的新数据上表现很差(不会举一反三)。

欠拟合则是另一个极端:连练习题都做不好,说明模型根本没有学到任何有用的规律。

过拟合在数据上的表现

python
# 过拟合的典型症状:
# 训练集准确率: 99%
# 测试集准确率: 65%  ← 差距巨大,过拟合了

# 欠拟合的典型症状:
# 训练集准确率: 65%
# 测试集准确率: 63%  ← 差距很小,但两个都很低,欠拟合了

# 理想状态:
# 训练集准确率: 92%
# 测试集准确率: 89%  ← 差距小,且两个都高,恰当拟合

为什么过拟合是大问题?

模型最终是要用在新数据上的,不是用来"背"训练数据的。如果模型只在训练集上好,对新数据一塌糊涂,那这个模型就没有实用价值。

怎么看出过拟合

最直观的方法是学习曲线:同时画出训练集损失和验证集损失,随训练轮数的变化趋势。

  • 过拟合:训练集损失持续下降,但验证集损失先下降后上升——两条线分叉了
  • 欠拟合:两条线都高,说明模型太简单
  • 正常:两条线都下降,且保持接近

四、常见损失函数

不同任务需要不同的损失函数:

python
# 回归任务(预测连续数值):均方误差
mse = nn.MSELoss()
# predictions:模型预测的房价
# targets:真实房价
# 损失 = 平均((预测值 - 真实值)²)
loss = mse(predictions, targets)

# 二分类(只有两种结果:是/否):二元交叉熵
bce = nn.BCEWithLogitsLoss()
# logits是未经sigmoid(把任意数值压缩到0-1区间的函数)的原始输出
loss = bce(logits, labels)

# 多分类(有多种结果:猫/狗/鸟...):交叉熵
ce = nn.CrossEntropyLoss()
# labels是类别索引(猫=0, 狗=1, 鸟=2...)
loss = ce(logits, labels)

# 语言模型(LLM训练):也是交叉熵,但针对每个位置的词
# LLM训练的目标:预测下一个词,用交叉熵衡量预测准不准

选哪个损失函数?

一个简单的判断方法:

  • 输出是一个连续的数字(房价、温度)→ MSE
  • 输出是"是/否"两种可能 → BCE
  • 输出是多个类别中的一个 → CrossEntropy
  • 输出是一段文字 → CrossEntropy(每个位置单独算)

五、解决过拟合的三种武器

武器1:Dropout(随机关闭神经元)

训练时随机把一部分神经元(通常20%~50%)"关掉",强迫网络不能过度依赖任何单个神经元,必须学到更分散、更鲁棒的特征。

类比:让学生随机忘掉一部分知识,强迫他必须真正理解,而不能依赖死记硬背的特定答案。

python
model = nn.Sequential(
    nn.Linear(10, 64),
    nn.ReLU(),
    nn.Dropout(0.5),  # 训练时随机关闭50%的神经元
    nn.Linear(64, 2)
)

# 重要:Dropout在训练和推理时行为不同
model.train()   # 训练模式:Dropout生效,随机关闭神经元
model.eval()    # 推理模式:Dropout关闭,所有神经元都开着

武器2:权重衰减(L2正则化)

在损失函数中加入对参数"过大"的惩罚。参数越大,惩罚越重。这迫使模型学到更简单、更泛化的规律,而不是去"记住"每个训练样本的细节。

类比:限制学生备忘录的长度,迫使他写最重要的规律,而不是把所有答案都抄上去。

python
# AdamW:Adam优化器 + 权重衰减
# weight_decay=1e-4 表示对参数大小施加惩罚
optimizer = optim.AdamW(model.parameters(), lr=1e-3, weight_decay=1e-4)

武器3:早停(Early Stopping)

监控验证集损失:如果验证集损失连续N轮没有改善,就停止训练。

原理:训练越久,模型越可能把训练数据的噪声也"学"进去,导致过拟合。在过拟合开始恶化之前及时停下来,保存最好的那个版本。

python
best_val_loss = float('inf')
patience = 10  # 连续10轮不改善就停止
counter = 0

for epoch in range(1000):
    train_loss = train_one_epoch(model, train_loader)
    val_loss = evaluate(model, val_loader)

    if val_loss < best_val_loss:
        best_val_loss = val_loss
        counter = 0
        torch.save(model.state_dict(), 'best_model.pt')  # 保存最好的模型
    else:
        counter += 1
        if counter >= patience:
            print(f"早停:{epoch}轮后验证集损失不再改善")
            break

# 训练结束后,加载最好的模型版本
model.load_state_dict(torch.load('best_model.pt'))

六、过拟合与欠拟合的诊断速查

症状 原因 解决方案
训练集高、验证集低 过拟合(记答案了) 加Dropout、权重衰减、早停、增加数据量
训练集和验证集都低 欠拟合(模型太简单) 增加模型层数/宽度、训练更长时间
两者都高且接近 正常 继续调优或部署

七、学习率调度:让训练更稳定

固定学习率往往不是最优的。

直觉:训练初期可以大步走(快速逼近最优点),训练后期要小步走(精细调整,避免跨过最优点)。

python
import torch.optim as optim

# 假设 model 和 optimizer 已定义
# optimizer = optim.AdamW(model.parameters(), lr=1e-3)

# 余弦退火:学习率按余弦曲线从初始值降到接近0(先快后慢地降低)
# scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=100)

# Warmup + 余弦退火(Transformer训练常用)
# 先从极小值线性增大到目标值,再余弦衰减
# from transformers import get_cosine_schedule_with_warmup
# scheduler = get_cosine_schedule_with_warmup(
#     optimizer,
#     num_warmup_steps=100,
#     num_training_steps=1000
# )

# 在训练循环中使用:
# for epoch in range(100):
#     train(model, train_loader)
#     scheduler.step()

八、批归一化(Batch Normalization)

批归一化是让深层网络训练更稳定的关键技术。

为什么需要它?

想象神经网络的每一层都在"适应"来自上一层的数据分布。但上一层的参数在不断更新,每次更新后分布都会变化,这一层就需要重新适应——像在追一个移动的靶子。这个现象叫"内部协变量偏移",会让训练变慢、不稳定。

批归一化(Batch Normalization)的解决方案:在每层之后,把这批数据的输出归一化到均值为0、方差为1的标准分布,然后再乘以可学习的参数做缩放和平移。这样每层的输入分布保持稳定,训练更快更稳。

python
# 让每一层的输入分布更稳定,加速训练
model = nn.Sequential(
    nn.Linear(10, 64),
    nn.BatchNorm1d(64),  # 批归一化:稳定这一层的输入分布
    nn.ReLU(),
    nn.Linear(64, 2)
)

九、实际调参建议

  1. 先跑通,再调优:先让代码跑起来不报错,再开始调参数
  2. 学习率最重要:先试 1e-3,不好再试 1e-41e-2
  3. 批大小影响稳定性:批大小(每次送入模型的样本数)大,训练更稳定但显存占用多
  4. 用验证集选模型:永远不要用测试集来调参,那是最终评估用的
  5. 记录实验:用MLflow(开源机器学习实验跟踪工具)或wandb(可视化训练过程的工具)记录每次实验的参数和结果

小结

优化理论的核心流程:

code
1. 定义损失函数(量化"模型有多差")
2. 计算梯度(找"哪个方向能让损失下降")
3. 更新参数(往梯度反方向走一步)
4. 重复,直到损失不再明显下降(收敛)

这个循环贯穿所有深度学习训练,从简单的线性回归到大语言模型的预训练,本质都是一样的。

过拟合是最常见的敌人:模型把训练数据的噪声也学进去了,在新数据上表现差。Dropout、权重衰减、早停是对抗过拟合的三大武器。

后续应用:本文的训练循环模板将在以下章节直接用到:

本页目录