69·深度学习入门进阶

损失函数与优化器

deep-learningoptimization

第 69 课:损失函数与优化器

学习目标

完成本课学习后,你将能够:

  1. 理解损失函数在机器学习中的核心作用。
  2. 区分并解释均方误差和交叉熵损失函数的适用场景。
  3. 理解梯度下降优化算法的基本原理及其变体。
  4. 使用PyTorch框架定义损失函数并调用优化器。

核心概念

想象你在训练一个模型,目标是让它变得“聪明”。那么,我们如何量化模型当前的“聪明程度”呢?损失函数就是这把尺子。

1. 损失函数:模型的“错题本”与“判分员”

损失函数(Loss Function)也叫目标函数或代价函数(Cost Function)。它的任务很简单:计算模型预测结果与真实答案之间的“差距”或“错误”有多大。这个差距值就是一个数字(标量)。我们的最终目标,就是通过调整模型的参数,让这个“差距值”变得尽可能小。

不同的任务,需要不同的“尺子”来衡量错误。

  • 均方误差 (MSE) - 回归任务的标尺 对于预测一个连续数值的任务(如房价、温度),最常用的损失函数是均方误差。它计算的是所有样本的预测值与真实值之差的平方的平均值。 Loss = (1/N) * Σ(预测值 - 真实值)²

    • 通俗理解:误差会被“放大”(因为平方),对大的错误惩罚更重。好比考试中,错一道大题扣的分比错一道选择题多得多。
  • 交叉熵损失 (Cross-Entropy Loss) - 分类任务的标尺 对于分类任务(如判断图片是猫还是狗),我们通常使用交叉熵损失。它衡量的是预测的概率分布与真实标签的分布之间的差异。

    • 通俗理解:模型输出的是“这是猫的概率是0.9,是狗的概率是0.1”。真实答案是“100%是猫”(即概率分布为[1, 0])。交叉熵损失会计算这两个分布有多“不同”。当预测概率越接近真实标签,损失就越小。

2. 优化器:寻找“最优解”的登山向导

知道了“错题本”上的错误有多大(损失值),下一步就是如何改正错误,也就是如何调整模型参数(权重和偏置)来减小这个损失值。这就是优化器(Optimizer)的工作。

最基础的方法是梯度下降

  • 梯度:简单理解为“在当前参数点,往哪个方向走,损失函数下降得最快”。数学上就是损失函数关于参数的偏导数。
  • 下降:沿着梯度的反方向(即损失下降最快的方向)更新参数。 新参数 = 旧参数 - 学习率 * 梯度
    • 学习率:一个超参数,控制每一步“迈多大”。太大会错过最低点,太小则收敛很慢。

单纯的梯度下降可能会有收敛慢或陷入局部最小值的问题。因此,诞生了许多改进的优化器:

  • 动量法:给梯度下降加上“惯性”,有助于在相关方向上加速并抑制振荡。
  • Adam:目前最常用、鲁棒性很强的优化器。它结合了动量法和另一种自适应学习率的思想(对每个参数维护独立的学习率),通常能快速收敛且效果良好。

代码示例

我们将通过两个例子,演示在PyTorch中如何使用损失函数和优化器。

示例1:线性回归(使用均方误差损失)

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

# 1. 准备数据 (假设 y = 2x + 1 + 噪声)
X = torch.unsqueeze(torch.linspace(-1, 1, 100), dim=1)  # 100个数据点
y = 2 * X + 1 + torch.randn(X.size()) * 0.2  # 加入一些噪声

# 2. 定义一个简单的线性模型
model = nn.Linear(1, 1)  # 输入维度1,输出维度1

# 3. 定义损失函数:均方误差
criterion = nn.MSELoss()

# 4. 定义优化器:随机梯度下降 (SGD)
optimizer = optim.SGD(model.parameters(), lr=0.01)  # lr是学习率

# 5. 训练循环
for epoch in range(100):
    # 前向传播:计算预测值
    y_pred = model(X)
    
    # 计算损失
    loss = criterion(y_pred, y)
    
    # 反向传播与优化
    optimizer.zero_grad()   # 清零梯度!(重要步骤)
    loss.backward()         # 计算梯度
    optimizer.step()        # 更新参数

    if (epoch+1) % 20 == 0:
        print(f'Epoch [{epoch+1}/100], Loss: {loss.item():.4f}')

# 查看学习到的参数 (应接近 w=2, b=1)
print(f"学习到的权重 w: {model.weight.item():.4f}")
print(f"学习到的偏置 b: {model.bias.item():.4f}")

示例2:二分类(使用二元交叉熵损失)

import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset

# 1. 生成一些简单的二分类数据
X = torch.randn(200, 2)  # 200个样本,2个特征
# 根据简单的规则生成标签 (例如,当x1 + x2 > 0时为1类)
y = (X[:, 0] + X[:, 1] > 0).float().unsqueeze(1)

# 创建DataLoader以便批量训练
dataset = TensorDataset(X, y)
dataloader = DataLoader(dataset, batch_size=32, shuffle=True)

# 2. 定义一个简单的神经网络 (一个隐藏层)
class SimpleNet(nn.Module):
    def __init__(self):
        super(SimpleNet, self).__init__()
        self.layer1 = nn.Linear(2, 10)
        self.relu = nn.ReLU()
        self.layer2 = nn.Linear(10, 1)
        self.sigmoid = nn.Sigmoid()  # 输出概率值

    def forward(self, x):
        x = self.relu(self.layer1(x))
        x = self.sigmoid(self.layer2(x))
        return x

model = SimpleNet()

# 3. 定义损失函数:二元交叉熵损失
criterion = nn.BCELoss()  # 输入是概率值

# 4. 定义优化器:Adam (通常比SGD更高效)
optimizer = optim.Adam(model.parameters(), lr=0.001)

# 5. 训练循环
for epoch in range(50):
    for batch_X, batch_y in dataloader:
        # 前向传播
        outputs = model(batch_X)
        loss = criterion(outputs, batch_y)

        # 反向传播与优化
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

    if (epoch+1) % 10 == 0:
        print(f'Epoch [{epoch+1}/50], Loss: {loss.item():.4f}')

实践练习

练习1:调整学习率

使用示例1(线性回归)的代码,将学习率lr分别改为0.10.001,并运行训练。观察最终损失值和收敛速度有何不同?将结果写在注释中。

练习2:选择正确的损失函数

你有一个模型,用于将图片分为“猫”、“狗”、“鸟”三类(多分类)。你应该使用nn.MSELossnn.BCELoss还是nn.CrossEntropyLoss?为什么?请在代码注释中回答。

练习3:搭建与训练一个小型网络

参考示例2的结构,完成以下任务:

  1. 创建一个新的数据集,其中特征X有3个维度,标签y为二分类。
  2. 定义一个包含两个隐藏层的神经网络(例如,输入层3 -> 隐藏层1: 16 -> 隐藏层2: 8 -> 输出层1)。
  3. 使用合适的损失函数和Adam优化器进行训练。
# 练习3 代码框架
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset

# 1. 生成数据
X = torch.randn(300, 3)
y = ((X[:, 0] * 0.5 + X[:, 1] * -1 + X[:, 2] * 2) > 0).float().unsqueeze(1)

# 2. 创建数据集和DataLoader
dataset = TensorDataset(X, y)
dataloader = DataLoader(dataset, batch_size=32, shuffle=True)

# 3. 定义网络模型 (请补全)
class MyNet(nn.Module):
    def __init__(self):
        super(MyNet, self).__init__()
        # 请定义网络层
        self.fc1 = nn.Linear(3, 16)
        self.fc2 = nn.Linear(16, 8)
        self.fc3 = nn.Linear(8, 1)
        self.relu = nn.ReLU()
        self.sigmoid = nn.Sigmoid() # 二分类输出概率

    def forward(self, x):
        # 请定义前向传播
        x = self.relu(self.fc1(x))
        x = self.relu(self.fc2(x))
        x = self.sigmoid(self.fc3(x))
        return x

model = MyNet()

# 4. 定义损失函数和优化器 (请补全)
criterion = nn.BCELoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

# 5. 训练循环 (请补全)
for epoch in range(30):
    for batch_X, batch_y in dataloader:
        outputs = model(batch_X)
        loss = criterion(outputs, batch_y)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
    if (epoch+1) % 10 == 0:
        print(f'Epoch [{epoch+1}/30], Loss: {loss.item():.4f}')

常见错误

  1. 损失函数选择错误:对回归任务使用分类损失函数(如BCELoss),或对分类任务使用MSE。这会导致模型无法正确学习。
  2. 忘记清零梯度 (optimizer.zero_grad()):这是PyTorch中的一个关键步骤。梯度默认是累加的,如果不清零,梯度会不断叠加,导致参数更新错误。
  3. 学习率设置不当
    • 学习率太大:训练损失会剧烈震荡甚至发散(损失变成NaN)。
    • 学习率太小:训练过程极其缓慢,可能需要很多轮迭代才能看到下降。
  4. 混淆损失函数输入:例如,nn.BCELoss要求输入已经是概率值(经过Sigmoid后),而nn.CrossEntropyLoss要求输入是原始logits(未经Sigmoid或Softmax)。

小结

本课我们学习了深度学习的两个核心组件:

  • 损失函数:是模型性能的“裁判”,为优化提供明确的目标。对于回归问题常用均方误差(MSE),对于分类问题常用交叉熵损失(Cross-Entropy)
  • 优化器:是模型训练的“引擎”,负责根据损失函数计算的梯度来更新模型参数,最小化损失。梯度下降是基本原理,而Adam是目前实践中最常用且表现优异的优化器。

在PyTorch中,我们通过nn.模块定义损失函数(如nn.MSELoss()),通过torch.optim模块调用优化器(如optim.Adam()),并在训练循环中严格按照“前向传播 -> 计算损失 -> 清零梯度(optimizer.zero_grad()) -> 反向传播(loss.backward()) -> 参数更新(optimizer.step())”的顺序进行操作。掌握这两者,就握住了模型训练的“方向盘”和“油门”。

练习编辑器

rust
Loading...

继续学习

完成本课后,建议继续学习下一课「PyTorch 基础:Tensor 与自动求导」 以巩固所学知识。