第五章 神经网络的学习(训练)(小白保姆级教程)

笔记提示:这一章是深度学习最核心的内容!前面学的张量、神经网络都是 “工具”,这一章教你怎么让神经网络 “真的学会东西”。我会用最通俗的话讲,每个知识点都标好重点,方便你直接抄笔记。

5.1 什么是 “训练” 神经网络?

大白话解释

神经网络就像一个刚生下来的婴儿,什么都不会。我们给它看很多很多手写数字的图片,告诉它 “这个是 0,那个是 1”,它慢慢就学会了自己识别数字。这个 “学习” 的过程,就叫训练

核心逻辑(必须记下来)

  1. 神经网络一开始的参数(权重和偏置)都是随机的,所以它的预测结果全是错的
  2. 我们用损失函数来衡量 “它错得有多离谱”
  3. 梯度下降法告诉它 “往哪个方向改参数能少犯错误”
  4. 反向传播算法快速计算出每个参数应该改多少
  5. 重复上面的步骤几百万次,直到它犯的错误足够小

5.2 损失函数:衡量模型错得有多离谱

5.2.1 损失函数是什么?

大白话解释

损失函数就是一个 “打分器”。模型预测完,损失函数给它打个分:分数越高,错得越离谱;分数越低,预测得越准。我们训练的目标,就是让这个分数尽可能低。

5.2.2 常用的损失函数(重点!)

我把最常用的 5 种损失函数整理成表格,你直接抄到笔记里:

损失函数名称 适用场景 大白话解释 PyTorch 代码
MAE(L1 损失) 回归任务(预测房价、温度等连续值) 计算预测值和真实值的 “平均绝对差”,对异常值不敏感 nn.L1Loss()
MSE(L2 损失) 回归任务 计算预测值和真实值的 “平均平方差”,对大错误惩罚更重 nn.MSELoss()
Smooth L1 回归任务(目标检测常用) MAE 和 MSE 的结合体,小误差用 MSE,大误差用 MAE,最稳定 nn.SmoothL1Loss()
二元交叉熵 二分类任务(判断是猫还是狗、是垃圾邮件还是正常邮件) 专门用来衡量 “概率预测” 的好坏 nn.BCEWithLogitsLoss()(推荐)
交叉熵 多分类任务(手写数字识别、图像分类) 多分类任务的标准损失函数,和 Softmax 天生一对 nn.CrossEntropyLoss()(推荐)

5.2.3 代码示例(直接运行)

import torch
import torch.nn as nn

# ------------------- 1. 回归任务损失函数 -------------------
print("=== 回归任务损失函数 ===")
# 模拟:模型预测了5个房子的价格,真实价格也有5个
pred_price = torch.tensor([100.0, 200.0, 300.0, 400.0, 500.0])
true_price = torch.tensor([110.0, 190.0, 320.0, 380.0, 510.0])

# MAE损失
mae_loss = nn.L1Loss()(pred_price, true_price)
print(f"MAE损失: {mae_loss.item():.2f}")  # 输出:14.00

# MSE损失
mse_loss = nn.MSELoss()(pred_price, true_price)
print(f"MSE损失: {mse_loss.item():.2f}")  # 输出:200.00

# ------------------- 2. 二分类任务损失函数 -------------------
print("\n=== 二分类任务损失函数 ===")
# 模拟:模型预测了5个邮件是不是垃圾邮件(输出是原始分数,不是概率)
pred_spam = torch.tensor([-2.0, -1.0, 0.0, 1.0, 2.0])
# 真实标签:0=正常邮件,1=垃圾邮件
true_spam = torch.tensor([0.0, 0.0, 1.0, 1.0, 0.0])

# 推荐用BCEWithLogitsLoss(自动帮你做Sigmoid转概率)
bce_loss = nn.BCEWithLogitsLoss()(pred_spam, true_spam)
print(f"二元交叉熵损失: {bce_loss.item():.4f}")  # 输出:0.7133

# ------------------- 3. 多分类任务损失函数 -------------------
print("\n=== 多分类任务损失函数 ===")
# 模拟:模型预测了8张图片属于6个类别的分数
pred_class = torch.randn(8, 6)  # 形状:[样本数, 类别数]
# 真实标签:每个样本属于哪个类别(0-5)
true_class = torch.tensor([1, 0, 5, 4, 2, 3, 0, 5])

# 推荐用CrossEntropyLoss(自动帮你做Softmax转概率)
ce_loss = nn.CrossEntropyLoss()(pred_class, true_class)
print(f"交叉熵损失: {ce_loss.item():.4f}")

笔记重点

  • ✅ 多分类任务用nn.CrossEntropyLoss()输出层不要加 Softmax!损失函数已经内置了
  • ✅ 二分类任务用nn.BCEWithLogitsLoss()输出层不要加 Sigmoid
  • ❌ 不要用nn.BCELoss(),数值不稳定,容易出问题

5.3 梯度下降法:告诉模型怎么改参数

5.3.1 梯度下降是什么?

大白话解释

想象你站在一座山上,蒙着眼睛,想要走到山脚下(损失最小的地方)。你会怎么做?

  • 你伸出脚,感受一下哪个方向是向下的(梯度方向)
  • 朝着向下的方向走一小步(学习率)
  • 重复上面的动作,直到你感觉脚下已经平了(损失不再下降)

这就是梯度下降法!梯度就是 “山的坡度”,负梯度方向就是 “下山最快的方向”。

5.3.2 三个核心概念(必考!)

我把这三个概念用最通俗的话解释清楚,你一定要背下来:

  1. 学习率(lr)
    • 大白话:你每一步迈多大
    • 太大:步子迈太大,会直接跨过山谷,永远到不了最低点
    • 太小:步子迈太小,走得太慢,要花很久才能到最低点
    • 经验值:一般从 0.1、0.01、0.001 开始试
  2. Batch Size(批量大小)
    • 大白话:你每次看多少个样本再走一步
    • 太小:每次只看 1 个样本,方向容易跑偏,走得摇摇晃晃
    • 太大:每次看所有样本,方向很准,但走得太慢,而且显卡内存不够
    • 经验值:32、64、128、256,根据你的显卡内存选
  3. Epoch(轮次)
    • 大白话:你把整个训练集完整看了多少遍
    • 太少:模型还没学会,损失还在下降
    • 太多:模型学过头了,开始 “背答案”(过拟合)
    • 经验值:一般从 10-100 开始试,看验证集损失不再下降就停止

举个例子

  • 训练集有 2000 个样本
  • Batch Size=64
  • 那么 1 个 Epoch 需要走:2000 ÷ 64 ≈ 32 步(Iteration)
  • 训练 10 个 Epoch,总共走:10 × 32 = 320 步

5.3.3 随机梯度下降(SGD)

大白话解释

我们不用每次都看所有样本再走一步,而是每次随机看一小批(Batch)样本,计算这一小批的梯度,然后走一步。这就叫随机梯度下降(SGD)

优点:

  • 速度快很多
  • 引入了一点随机性,反而能帮助模型跳出局部最小值,找到更好的解

5.4 数据集和 DataLoader:高效喂数据给模型

5.4.1 为什么需要 DataLoader?

大白话解释

如果我们手动把数据分成一批一批的,还要打乱顺序,还要多线程加载,会非常麻烦。PyTorch 给我们提供了两个工具:

  • Dataset:定义怎么读取一条数据
  • DataLoader:自动把 Dataset 里的数据分成一批一批的,还能打乱顺序、多线程加载

5.4.2 代码示例(直接运行)

import torch
from torch.utils.data import TensorDataset, DataLoader

# 1. 准备模拟数据:10个样本,每个样本3个特征,1个标签
x = torch.randn(10, 3)  # 特征:形状[10, 3]
y = torch.randn(10)     # 标签:形状[10]

# 2. 创建Dataset:把特征和标签打包在一起
dataset = TensorDataset(x, y)

# 3. 创建DataLoader:自动分batch、打乱数据
# batch_size=3:每个批次3个样本
# shuffle=True:每个Epoch开始前打乱数据
# drop_last=False:如果最后一个batch不够3个,保留它
dataloader = DataLoader(dataset, batch_size=3, shuffle=True, drop_last=False)

# 4. 遍历DataLoader,看看每个批次的数据
print("=== 遍历DataLoader ===")
for batch_idx, (x_batch, y_batch) in enumerate(dataloader):
    print(f"批次 {batch_idx+1}:")
    print(f"  特征形状: {x_batch.shape}")
    print(f"  标签形状: {y_batch.shape}")
    print(f"  特征值:\n{x_batch}")
    print(f"  标签值:\n{y_batch}\n")

运行结果解释

  • 10 个样本,batch_size=3,所以会分成 4 个批次
  • 前 3 个批次每个 3 个样本,最后 1 个批次 1 个样本
  • 因为 shuffle=True,所以每次运行的顺序都不一样

笔记重点

  • TensorDataset是最常用的 Dataset,直接把张量打包
  • ✅ 训练集一定要加shuffle=True,验证集和测试集不要加
  • num_workers参数可以设置多线程加载数据,Windows 下一般设为 0,Linux 下可以设为 CPU 核心数

5.5 反向传播算法:快速计算梯度

5.5.1 为什么需要反向传播?

大白话解释

神经网络有几百万个参数,如果我们一个一个手动计算每个参数的梯度,要算到天荒地老。反向传播算法就是一个 “快速计算梯度的魔法”,它利用链式法则,从输出层往回算,一次就能算出所有参数的梯度。

5.5.2 计算图:理解反向传播的关键

大白话解释

计算图就是把神经网络的计算过程画成一张图,每个节点是一个运算,边是数据流动的方向。

举个最简单的例子:计算 y = 2x + 3

  • 输入:x
  • 第一步:计算 a = 2 * x
  • 第二步:计算 y = a + 3
  • 输出:y

画成计算图就是:x → [×2] → a → [+3] → y

前向传播:从左到右计算,得到 y 的值

反向传播:从右到左计算,得到 x 的梯度(也就是 y 对 x 的导数)

5.5.3 PyTorch 的自动微分:不用手动算梯度!

好消息:PyTorch 已经帮我们实现了反向传播算法,我们完全不用手动计算梯度!只需要调用一个方法:loss.backward()

代码示例(神奇的自动微分)

import torch
import torch.nn as nn

# 1. 定义一个最简单的模型:y = w*x + b
# 我们要让模型学会:w=2.5,b=5.0
x = torch.tensor([10.0])
y_true = torch.tensor([30.0])  # 2.5*10 + 5.0 = 30.0

# 2. 初始化参数(随机值)
w = torch.randn(1, requires_grad=True)  # requires_grad=True:告诉PyTorch要跟踪这个参数的梯度
b = torch.randn(1, requires_grad=True)

# 3. 前向传播:计算预测值
y_pred = w * x + b
print(f"初始预测值: {y_pred.item():.2f}")  # 一开始肯定不是30.0

# 4. 计算损失
loss = nn.MSELoss()(y_pred, y_true)
print(f"初始损失: {loss.item():.2f}")

# 5. 反向传播:自动计算所有参数的梯度!
loss.backward()

# 6. 查看计算出来的梯度
print(f"\n自动计算的梯度:")
print(f"w的梯度: {w.grad.item():.2f}")  # 损失对w的导数
print(f"b的梯度: {b.grad.item():.2f}")  # 损失对b的导数

运行结果解释

  • PyTorch 自动帮我们算出了 w 和 b 的梯度
  • 梯度是负的,说明我们应该增大 w 和 b 的值,才能让损失变小
  • 这就是反向传播的魔力!

笔记重点

  • ✅ 只有设置了requires_grad=True的张量,PyTorch 才会计算它的梯度
  • loss.backward()会自动计算所有依赖于 loss 的参数的梯度
  • ✅ 梯度会累积,所以每次更新参数后,一定要调用optimizer.zero_grad()清零梯度!

5.6 完整的模型训练流程(四步走!)

好消息:所有 PyTorch 模型的训练流程都是一模一样的!只要你学会了这个流程,以后训练任何模型都不用怕。

我把训练流程总结成四步走,你直接背下来:

第一步:准备数据

  • 加载数据集
  • 划分训练集和验证集
  • 创建 Dataset 和 DataLoader

第二步:构建模型

  • 定义神经网络结构
  • 把模型移到 GPU(如果有的话)

第三步:定义损失函数和优化器

  • 损失函数:根据任务类型选
  • 优化器:一般用 Adam 或者 SGD

第四步:训练循环(最核心!)

for epoch in range(总轮次):
    # 训练阶段
    model.train()  # 告诉模型现在是训练模式
    for x_batch, y_batch in train_dataloader:
        # 1. 前向传播:计算预测值
        y_pred = model(x_batch)
        # 2. 计算损失
        loss = loss_fn(y_pred, y_batch)
        # 3. 反向传播:计算梯度
        loss.backward()
        # 4. 更新参数
        optimizer.step()
        # 5. 清零梯度!(非常重要!)
        optimizer.zero_grad()
    
    # 验证阶段(每个Epoch结束后做一次)
    model.eval()  # 告诉模型现在是验证模式
    with torch.no_grad():  # 验证阶段不需要计算梯度,节省内存
        for x_batch, y_batch in valid_dataloader:
            y_pred = model(x_batch)
            loss = loss_fn(y_pred, y_batch)
            # 计算验证集准确率等指标
    
    # 打印这一轮的训练损失和验证损失
    print(f"Epoch {epoch+1}, 训练损失: {train_loss:.4f}, 验证损失: {val_loss:.4f}")

5.6.1 完整代码示例(线性回归)

这是一个完整的、可以直接运行的训练代码,你可以逐行运行,看看每一步发生了什么:

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

# ------------------- 第一步:准备数据 -------------------
print("=== 第一步:准备数据 ===")
# 生成模拟数据:y = 2.5x + 5.0 + 噪声
x = torch.randn(100, 1)  # 100个样本,每个样本1个特征
y = x * 2.5 + 5.0 + torch.randn(100, 1) * 0.2  # 加一点噪声,让数据更真实

# 创建Dataset和DataLoader
dataset = TensorDataset(x, y)
dataloader = DataLoader(dataset, batch_size=6, shuffle=True)

# ------------------- 第二步:构建模型 -------------------
print("\n=== 第二步:构建模型 ===")
# 最简单的线性模型:y = w*x + b
model = nn.Linear(in_features=1, out_features=1)
print(f"初始参数:w={model.weight.item():.2f}, b={model.bias.item():.2f}")

# ------------------- 第三步:定义损失函数和优化器 -------------------
print("\n=== 第三步:定义损失函数和优化器 ===")
loss_fn = nn.MSELoss()  # 回归任务用MSE损失
optimizer = optim.SGD(model.parameters(), lr=0.01)  # SGD优化器,学习率0.01

# ------------------- 第四步:训练循环 -------------------
print("\n=== 第四步:开始训练 ===")
loss_list = []  # 存储每轮的平均损失,方便画图

for epoch in range(200):
    total_loss = 0
    iter_num = 0
    
    for x_batch, y_batch in dataloader:
        # 1. 前向传播
        y_pred = model(x_batch)
        # 2. 计算损失
        loss = loss_fn(y_pred, y_batch)
        # 3. 反向传播
        loss.backward()
        # 4. 更新参数
        optimizer.step()
        # 5. 清零梯度
        optimizer.zero_grad()
        
        total_loss += loss.item()
        iter_num += 1
    
    # 计算这一轮的平均损失
    avg_loss = total_loss / iter_num
    loss_list.append(avg_loss)
    
    # 每20轮打印一次
    if (epoch + 1) % 20 == 0:
        print(f"Epoch {epoch+1:3d}, 平均损失: {avg_loss:.4f}")

# ------------------- 训练结束,查看结果 -------------------
print("\n=== 训练结束 ===")
print(f"最终参数:w={model.weight.item():.2f}, b={model.bias.item():.2f}")
print(f"真实参数:w=2.50, b=5.00")

# 画图:损失下降曲线和拟合结果
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))

# 损失下降曲线
ax1.plot(loss_list)
ax1.set_title("损失下降曲线")
ax1.set_xlabel("Epoch")
ax1.set_ylabel("损失")

# 拟合结果
ax2.scatter(x, y, label="真实数据")
ax2.plot(x, model(x).detach().numpy(), c='r', label="拟合直线")
ax2.set_title("线性回归拟合结果")
ax2.legend()

plt.show()

运行结果解释

  • 你会看到损失一直在下降,最后稳定在一个很小的值
  • 最终学到的 w 和 b 会非常接近真实值 2.5 和 5.0
  • 拟合的红线会几乎穿过所有的点

5.7 实战:手写数字识别训练

现在我们把前面学的所有知识整合起来,训练一个真正的手写数字识别模型!

5.7.1 任务介绍

  • 数据集:MNIST 手写数字数据集(42000 张 28×28 的灰度图)
  • 任务:识别图片中的数字是 0-9 中的哪一个(10 分类任务)
  • 模型:3 层全连接神经网络

5.7.2 完整代码

import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import TensorDataset, DataLoader
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import MinMaxScaler

# ------------------- 第一步:准备数据 -------------------
def get_digit_data():
    # 1. 加载数据集(你需要先从Kaggle下载train.csv放到data文件夹里)
    dataset = pd.read_csv("./data/train.csv")
    
    # 2. 拆分特征和标签
    x = dataset.drop("label", axis=1)
    y = dataset["label"]
    
    # 3. 划分训练集和测试集(8:2)
    x_train, x_test, y_train, y_test = train_test_split(
        x, y, test_size=0.2, random_state=42
    )
    
    # 4. 归一化:把像素值从0-255缩放到0-1
    scaler = MinMaxScaler()
    x_train = scaler.fit_transform(x_train)
    x_test = scaler.transform(x_test)
    
    # 5. 转换为PyTorch张量
    x_train = torch.tensor(x_train).float()
    x_test = torch.tensor(x_test).float()
    y_train = torch.tensor(y_train.to_numpy()).long()
    y_test = torch.tensor(y_test.to_numpy()).long()
    
    return x_train, x_test, y_train, y_test

print("=== 加载数据 ===")
x_train, x_test, y_train, y_test = get_digit_data()
print(f"训练集大小: {x_train.shape}")
print(f"测试集大小: {x_test.shape}")

# 创建Dataset和DataLoader
train_dataset = TensorDataset(x_train, y_train)
train_dataloader = DataLoader(train_dataset, batch_size=256, shuffle=True)

valid_dataset = TensorDataset(x_test, y_test)
valid_dataloader = DataLoader(valid_dataset, batch_size=256)

# ------------------- 第二步:构建模型 -------------------
print("\n=== 构建模型 ===")
model = nn.Sequential(
    nn.Linear(784, 50),   # 输入层:784个像素 → 隐藏层1:50个神经元
    nn.ReLU(),            # 激活函数
    nn.Linear(50, 100),   # 隐藏层1:50 → 隐藏层2:100
    nn.ReLU(),            # 激活函数
    nn.Linear(100, 10)    # 隐藏层2:100 → 输出层:10个类别
    # 注意:输出层不要加Softmax!CrossEntropyLoss已经内置了
)

# 自动选择设备:有GPU用GPU,没有用CPU
device = torch.device(
    "cuda" if torch.cuda.is_available()
    else "mps" if torch.backends.mps.is_available()
    else "cpu"
)
model.to(device)
print(f"使用设备: {device}")

# ------------------- 第三步:定义损失函数和优化器 -------------------
loss_fn = nn.CrossEntropyLoss()  # 多分类任务用交叉熵损失
optimizer = optim.SGD(model.parameters(), lr=0.1)  # SGD优化器,学习率0.1

# ------------------- 第四步:训练循环 -------------------
print("\n=== 开始训练 ===")
epochs = 20

for epoch in range(epochs):
    # 训练阶段
    model.train()
    train_loss = 0
    train_acc_num = 0
    
    for inputs, targets in train_dataloader:
        # 把数据移到GPU
        inputs = inputs.to(device)
        targets = targets.to(device)
        
        # 1. 前向传播
        y_pred = model(inputs)
        # 2. 计算损失
        loss = loss_fn(y_pred, targets)
        # 3. 反向传播
        loss.backward()
        # 4. 更新参数
        optimizer.step()
        # 5. 清零梯度
        optimizer.zero_grad()
        
        # 统计损失和准确率
        train_loss += loss.item() * inputs.shape[0]
        y_pred_class = torch.argmax(y_pred, dim=1)
        train_acc_num += torch.sum(y_pred_class == targets).item()
    
    # 计算训练集平均损失和准确率
    train_loss_avg = train_loss / len(train_dataset)
    train_acc = train_acc_num / len(train_dataset)
    
    # 验证阶段
    model.eval()
    val_loss = 0
    val_acc_num = 0
    
    with torch.no_grad():  # 验证阶段不需要计算梯度
        for inputs, targets in valid_dataloader:
            inputs = inputs.to(device)
            targets = targets.to(device)
            
            y_pred = model(inputs)
            loss = loss_fn(y_pred, targets)
            
            val_loss += loss.item() * inputs.shape[0]
            y_pred_class = torch.argmax(y_pred, dim=1)
            val_acc_num += torch.sum(y_pred_class == targets).item()
    
    # 计算验证集平均损失和准确率
    val_loss_avg = val_loss / len(valid_dataset)
    val_acc = val_acc_num / len(valid_dataset)
    
    # 打印结果
    print(f"Epoch {epoch+1:2d} | 训练损失: {train_loss_avg:.4f} | 训练准确率: {train_acc:.4f} | 验证损失: {val_loss_avg:.4f} | 验证准确率: {val_acc:.4f}")

# 保存训练好的模型
torch.save(model.state_dict(), "digit_model.pth")
print("\n模型已保存为 digit_model.pth")

5.7.3 运行结果

  • 训练 20 个 Epoch 后,验证集准确率应该能达到 97% 以上
  • 这意味着模型在 100 张手写数字图片中,能正确识别 97 张以上!

5.8 本章核心知识点总结(直接抄笔记)

  1. 训练的本质:通过不断调整参数,让损失函数的值最小
  2. 损失函数:衡量模型预测的好坏,不同任务用不同的损失函数
  3. 梯度下降:沿着负梯度方向调整参数,让损失变小
  4. 三个核心概念:学习率(步长)、Batch Size(每批样本数)、Epoch(轮次)
  5. DataLoader:自动分 batch、打乱数据,高效喂数据给模型
  6. 反向传播:PyTorch 自动帮我们计算所有参数的梯度,只需调用loss.backward()
  7. 标准训练流程:准备数据→构建模型→定义损失和优化器→训练循环
  8. 训练循环四步:前向传播→计算损失→反向传播→更新参数→清零梯度

5.9 小白常见问题解答

  1. Q:为什么每次运行结果都不一样?

    A:因为参数初始化是随机的,数据打乱也是随机的。设置随机种子可以让结果可复现:torch.manual_seed(42)

  2. Q:损失一直不下降怎么办?

    A:先检查学习率是不是太大或太小,再检查损失函数是不是选对了,最后检查数据预处理有没有问题。

  3. Q:训练集准确率很高,但验证集准确率很低怎么办?

    A:这是过拟合了。可以减少模型复杂度、增加训练数据、使用正则化(后面会讲)。

  4. Q:为什么要把模型和数据移到 GPU?

    A:GPU 做矩阵运算比 CPU 快几百倍,训练速度会大大提升。

更多推荐