深度学习系列教程(第五章)
第五章 神经网络的学习(训练)(小白保姆级教程)
笔记提示:这一章是深度学习最核心的内容!前面学的张量、神经网络都是 “工具”,这一章教你怎么让神经网络 “真的学会东西”。我会用最通俗的话讲,每个知识点都标好重点,方便你直接抄笔记。
5.1 什么是 “训练” 神经网络?
大白话解释:
神经网络就像一个刚生下来的婴儿,什么都不会。我们给它看很多很多手写数字的图片,告诉它 “这个是 0,那个是 1”,它慢慢就学会了自己识别数字。这个 “学习” 的过程,就叫训练。
核心逻辑(必须记下来):
- 神经网络一开始的参数(权重和偏置)都是随机的,所以它的预测结果全是错的
- 我们用损失函数来衡量 “它错得有多离谱”
- 用梯度下降法告诉它 “往哪个方向改参数能少犯错误”
- 用反向传播算法快速计算出每个参数应该改多少
- 重复上面的步骤几百万次,直到它犯的错误足够小
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 三个核心概念(必考!)
我把这三个概念用最通俗的话解释清楚,你一定要背下来:
- 学习率(lr)
- 大白话:你每一步迈多大
- 太大:步子迈太大,会直接跨过山谷,永远到不了最低点
- 太小:步子迈太小,走得太慢,要花很久才能到最低点
- 经验值:一般从 0.1、0.01、0.001 开始试
- Batch Size(批量大小)
- 大白话:你每次看多少个样本再走一步
- 太小:每次只看 1 个样本,方向容易跑偏,走得摇摇晃晃
- 太大:每次看所有样本,方向很准,但走得太慢,而且显卡内存不够
- 经验值:32、64、128、256,根据你的显卡内存选
- 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 本章核心知识点总结(直接抄笔记)
- 训练的本质:通过不断调整参数,让损失函数的值最小
- 损失函数:衡量模型预测的好坏,不同任务用不同的损失函数
- 梯度下降:沿着负梯度方向调整参数,让损失变小
- 三个核心概念:学习率(步长)、Batch Size(每批样本数)、Epoch(轮次)
- DataLoader:自动分 batch、打乱数据,高效喂数据给模型
- 反向传播:PyTorch 自动帮我们计算所有参数的梯度,只需调用
loss.backward() - 标准训练流程:准备数据→构建模型→定义损失和优化器→训练循环
- 训练循环四步:前向传播→计算损失→反向传播→更新参数→清零梯度
5.9 小白常见问题解答
-
Q:为什么每次运行结果都不一样?
A:因为参数初始化是随机的,数据打乱也是随机的。设置随机种子可以让结果可复现:
torch.manual_seed(42) -
Q:损失一直不下降怎么办?
A:先检查学习率是不是太大或太小,再检查损失函数是不是选对了,最后检查数据预处理有没有问题。
-
Q:训练集准确率很高,但验证集准确率很低怎么办?
A:这是过拟合了。可以减少模型复杂度、增加训练数据、使用正则化(后面会讲)。
-
Q:为什么要把模型和数据移到 GPU?
A:GPU 做矩阵运算比 CPU 快几百倍,训练速度会大大提升。
更多推荐

所有评论(0)