第6章:损失函数与优化器

🎉摘要:本文详细介绍深度学习中的损失函数(如交叉熵损失)和优化器(如Adam)的工作原理,带你理解模型训练核心五步:前向传播、计算损失、清零梯度、反向传播、更新参数。通过手动训练示例,直观感受损失下降的过程,是深度学习入门的必修内容。

这一章的目标

  • 理解损失函数(Loss Function) 是什么 —— 怎么给模型的预测 "打分"

  • 理解优化器(Optimizer) 是什么 —— 模型怎么根据 "分数" 修正自己

  • 跑一个最简单的 "手动训练",体会损失下降的过程

损失函数 —— 衡量"错得有多离谱"

第 5 章我们搭好了网络,给它一张图,它输出 10 个数字(叫做 logits,即未归一化的得分)。

但问题来了:这 10 个数字和正确答案(label)有什么关系?怎么告诉模型 "你猜错了,要改"?

损失函数就是干这件事的:它接收模型的预测和正确答案,算出一个数值表示"差了多少"。这个值叫损失(Loss)

  • Loss 很大 → 模型错得很离谱 → 需要大幅修正

  • Loss 很小 → 模型差不多对了 → 微调即可

  • Loss ≈ 0 → 模型预测完美(实际几乎不会发生)

📌 术语:交叉熵损失(CrossEntropyLoss)

分类问题最常用的损失函数。这个名字听起来很数学,但你不用懂公式。
它的行为很简单:模型对正确类别的预测概率越高,Loss 越低;预测概率越低,Loss 越高。

直观示例

import torch
import torch.nn as nn

# 定义交叉熵损失函数,用于多分类任务(MNIST数字识别)
loss_fn = nn.CrossEntropyLoss()

# 模拟输入:模型输出原始得分logits,shape[batch_size, 类别数]
# 代表一张图片,对应 0~9 一共10个数字的预测分值
logits = torch.tensor([[0.1, 0.2, 0.3, 5.0, 0.1, 0.2, 0.1, 0.1, 0.1, 0.1]])
label = torch.tensor([3])  # 图片真实标签:数字3

# 计算损失
loss = loss_fn(logits, label)
# 预测正确:目标类别分值远高于其他类别,损失数值很小
print(f"预测得好时的 Loss: {loss.item():.4f}")

# 预测错误案例
bad_logits = torch.tensor([[0.1, 0.2, 0.3, 0.1, 0.1, 5.0, 0.1, 0.1, 0.1, 0.1]])
# 真实类别3得分很低,错误类别5得分最高
loss2 = loss_fn(bad_logits, label)
# 预测偏差大,损失数值显著变大
print(f"预测得差时的 Loss: {loss2.item():.4f}")

运行代码,输出如下:

预测得好时的 Loss: 0.0679
预测得差时的 Loss: 4.9671

看到了吗?Loss 自动反映了预测的好坏。这就是为什么我们可以在训练中用它作为"优化信号"。

不同任务用什么损失函数?

任务类型损失函数PyTorch 类
多分类(如 MNIST 10类)交叉熵nn.CrossEntropyLoss()
二分类二元交叉熵nn.BCELoss() / nn.BCEWithLogitsLoss()
回归(预测数值)均方误差nn.MSELoss()

对 MNIST,用 nn.CrossEntropyLoss()。注意:它内部已经包含了 Softmax,所以不用在模型最后一层加 Softmax(加了反而会出错)。

优化器 —— 怎么修正模型

有了 Loss,我们就知道模型错得多离谱。接下来需要根据 Loss 调整模型参数(W 和 b),让下一次预测更好一点。

但问题来了:模型可能有几万甚至上百万个参数,每个参数该往哪个方向调?调多少?

这就是优化器的工作。它的核心思想是梯度下降(Gradient Descent)

📌 术语:梯度下降(Gradient Descent)

想象你在浓雾中站在一座山的半山腰,目标是走到山谷最低点。你看不到全貌,但能感受到脚下的坡度——坡往哪边斜,你就往哪边走,每走一步都是下坡,最终总能到达谷底。

"坡度"就是梯度。优化器做的事就是:

  1. 算出每个参数对 Loss 的影响有多大(梯度)

  2. 把参数往 Loss 减小的方向挪一点

  3. 重复

📌 术语:学习率(Learning Rate,简称 lr)

"每步走多大"。lr 太大 → 可能跨过最低点,来回震荡;lr 太小 → 走得太慢,训练很久。

学习率是最重要的超参数之一。对于初学者,lr=0.001(即 1e-3) 是一个安全的起点。

PyTorch 中常见的优化器

常见优化器如下:

  • SGD(带动量):最简单的优化器,带着惯性下山。收敛慢,但泛化效果经常很好,比赛常用。

  • AdaGrad:每个参数单独调步伐。适合稀疏数据;缺点训练后期步子越来越小,走不动。

  • RMSprop:AdaGrad 改良版,限制梯度无限累积,梯度震荡时走得更平稳,适合循环网络。

  • Adam(最常用):融合动量 + 自适应步长,收敛快,上手简单。

  • AdamW:修正 Adam 的权重衰减漏洞。当下通用首选,Transformer、图像任务主流。

  • AdaDelta / Adamax:Adam 衍生变体,日常很少使用。

  • LBFGS:二阶优化方法,需要全部数据一起计算,只适合小型网络,大模型不用。

上代码:

import torch.optim as optim

# 见第5章定义的 MNIST_CNN 网络
model = MNIST_CNN()

# Adam:目前最常用的优化器,自带"自适应学习率",初学者首选
optimizer = optim.Adam(model.parameters(), lr=0.001)

# SGD:最经典的随机梯度下降,简单但可能需要手动调学习率
# optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9)

对于本教程的 MNIST 项目,用 Adam、lr=0.001 就行了

手动跑一次"微型训练"

在写正式的完整训练循环之前,我们先手动做一次前向+反向+参数更新,体会这个过程:

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

# 第5章定义的网络
class MNIST_CNN(nn.Module):
    """MNIST 手写数字识别 CNN
    输入:batch张 1通道 28×28灰度手写数字图片
    输出:batch×10 的logits得分,对应数字0~9,未做softmax
    """
    def __init__(self):
        # 调用父类nn.Module构造函数,注册网络层,是pytorch模型必须写的
        super().__init__()

        # ── 第1组:检测低级特征(边缘、角落、线条) ──
        # Conv2d 参数:(输入通道, 输出通道, 卷积核大小, padding填充)
        # in_channels=1:MNIST灰度图,单通道
        # out_channels=32:使用32个3×3卷积核,输出32张特征图
        # kernel_size=3:3×3卷积核,捕捉局部相邻像素
        # padding=1:图片四周补1圈0,卷积前后宽高保持不变 28→28
        self.conv1 = nn.Conv2d(1, 32, kernel_size=3, padding=1)

        # MaxPool2d 最大池化,kernel_size=2:取2×2窗口内最大值作为输出
        # 步长默认等于kernel_size,宽高减半:28×28 →14×14
        # 作用:压缩特征图尺寸、降低计算量,让特征对微小位移更鲁棒
        self.pool1 = nn.MaxPool2d(kernel_size=2)

        # ── 第2组:检测高级特征(曲线、数字部件、局部形状) ──
        # 输入通道32:接收上一层32张特征图;输出通道提升到64,提取更多高阶组合特征
        # padding=1保证卷积后尺寸不变 14→14(因为前面已经池化过)
        self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1)

        # 第二次池化,尺寸再次减半:14×14 →7×7
        self.pool2 = nn.MaxPool2d(kernel_size=2)

        # ── 全连接层:把卷积提取的特征映射为类别得分,完成最终分类 ──
        # 64*7*7:conv2+pool2之后特征图维度(64,7,7),展平后向量长度
        # 3136维特征压缩映射到128维中间特征向量
        self.fc1 = nn.Linear(64 * 7 * 7, 128)
        # 最后一层:128维特征映射到10个输出,对应0‑9十个数字的原始得分logits
        self.fc2 = nn.Linear(128, 10)

    def forward(self, x):
        """
        前向传播逻辑:数据流过网络的计算流程
        :param x: 输入张量 x 形状: (batch_size, 1, 28, 28)
        :return: logits原始分类得分,shape=(batch_size,10)
        """
        # 第一层卷积+ReLU激活;ReLU引入非线性,网络才能学习复杂特征
        # 输出shape:(batch, 32, 28, 28)
        x = torch.relu(self.conv1(x))
        # 最大池化下采样,宽高减半;输出shape:(batch, 32, 14, 14)
        x = self.pool1(x)

        # 第二层卷积提取高级特征 + ReLU非线性激活
        # 输出shape:(batch, 64, 14, 14)
        x = torch.relu(self.conv2(x))
        # 第二次池化下采样;输出shape:(batch, 64, 7, 7)
        x = self.pool2(x)

        # 展平操作view:保留第0维batch,后面所有维度合并成一维
        # x.size(0) 获取batch大小;-1自动计算剩余维度总大小64*7*7=3136
        # 输出shape:(batch, 3136)
        x = x.view(x.size(0), -1)

        # 第一层全连接+ReLU,做特征融合变换;输出shape:(batch, 128)
        x = torch.relu(self.fc1(x))
        # 最后全连接输出10类原始得分logits,CrossEntropyLoss会内部做softmax,这里不用手动softmax
        # 输出shape:(batch, 10)
        x = self.fc2(x)
        return x


if __name__ == "__main__":
    # 准备数据:取一个很小的 batch(4张图,模拟)
    images = torch.randn(4, 1, 28, 28)        # 4张"假图片"
    labels = torch.tensor([3, 7, 1, 5])        # 正确答案

    # 创建模型、损失函数、优化器
    model = MNIST_CNN()
    loss_fn = nn.CrossEntropyLoss()
    optimizer = optim.Adam(model.parameters(), lr=0.001)

    # 一次训练迭代的 5 个步骤
    # 1. 前向传播:模型根据当前参数做出预测
    outputs = model(images)                     # → (4, 10)

    # 2. 计算损失:对比预测和正确答案,算出差了多少
    loss = loss_fn(outputs, labels)
    print(f"训练前 Loss: {loss.item():.4f}")

    # 3. 清零梯度:PyTorch 默认累积梯度,每次更新前必须清零
    optimizer.zero_grad()

    # 4. 反向传播:自动算出每个参数对 Loss 的梯度
    loss.backward()

    # 5. 更新参数:优化器根据梯度调整参数
    optimizer.step()

    # 再看一次 Loss(参数已经更新了,Loss 应该变小)
    outputs2 = model(images)
    loss2 = loss_fn(outputs2, labels)
    print(f"训练后 Loss: {loss2.item():.4f}")

运行代码,输出如下:

训练前 Loss: 2.2931
训练后 Loss: 1.8582

⚠️ 五个步骤缺一不可

这是深度学习最核心的代码模式。我当初学的时候就死记硬背这五步,背熟了后面什么都好理解:

① outputs = model(inputs) 前向传播

② loss = loss_fn(outputs, target) 计算损失

③ optimizer.zero_grad() 梯度清零

④ loss.backward() 反向传播(算梯度)

⑤ optimizer.step() 更新参数

关于 loss.backward() 的直觉理解

你可能好奇 loss.backward() 这行代码到底干了什么。

简单说:PyTorch 在每次前向传播时会偷偷记录一个 计算图(computation graph),记下数据从哪里来、经过了哪些运算。当你调用 loss.backward() 时,PyTorch 沿着这个计算图从后往前,用链式法则算出每个参数对 Loss 的偏导数(也就是梯度)。这个过程叫自动微分(Autograd)

你不需要手动算导数 —— 这就是 PyTorch 最大的便利之一。

本章小结

  • 损失函数衡量模型预测和正确答案之间的差距

  • MNIST 分类用 nn.CrossEntropyLoss()(自带 Softmax,模型输出不要加 Softmax)

  • 优化器根据梯度调整模型参数,让 Loss 逐步下降

  • Adam 优化器 + lr=0.001 是初学者最安全的选择

  • 训练核心五步:前向 → 算损失 → 清零梯度 → 反向传播 → 更新参数

点击查看完整的 手动跑一次训练迭代 示例代码。

  

说说我的看法
全部评论(
没有评论
关于
本网站专注于 Java、数据库(MySQL、Oracle)、Linux、软件架构及大数据等多领域技术知识分享。涵盖丰富的原创与精选技术文章,助力技术传播与交流。无论是技术新手渴望入门,还是资深开发者寻求进阶,这里都能为您提供深度见解与实用经验,让复杂编码变得轻松易懂,携手共赴技术提升新高度。如有侵权,请来信告知:hxstrive@outlook.com
其他应用
公众号