01-玩转LangChain:从模型调用到Prompt模板与输出解析的完整指南
02-玩转 LangChain Memory 模块:四种记忆类型详解及应用场景全覆盖
03-全面掌握 LangChain:从核心链条构建到动态任务分配的实战指南
04-玩转 LangChain:从文档加载到高效问答系统构建的全程实战
05-玩转 LangChain:深度评估问答系统的三种高效方法(示例生成、手动评估与LLM辅助评估)
06-从 0 到 1 掌握 LangChain Agents:自定义工具 + LLM 打造智能工作流!
07-【深度解析】从GPT-1到GPT-4:ChatGPT背后的核心原理全揭秘
08-【万字长文】MCP深度解析:打通AI与世界的“USB-C”,模型上下文协议原理、实践与未来
01-【深度学习-Day 1】为什么深度学习是未来?一探究竟AI、ML、DL关系与应用
02-【深度学习-Day 2】图解线性代数:从标量到张量,理解深度学习的数据表示与运算
03-【深度学习-Day 3】搞懂微积分关键:导数、偏导数、链式法则与梯度详解
04-【深度学习-Day 4】掌握深度学习的“概率”视角:基础概念与应用解析
05-【深度学习-Day 5】Python 快速入门:深度学习的“瑞士军刀”实战指南
06-【深度学习-Day 6】掌握 NumPy:ndarray 创建、索引、运算与性能优化指南
07-【深度学习-Day 7】精通Pandas:从Series、DataFrame入门到数据清洗实战
08-【深度学习-Day 8】让数据说话:Python 可视化双雄 Matplotlib 与 Seaborn 教程
09-【深度学习-Day 9】机器学习核心概念入门:监督、无监督与强化学习全解析
10-【深度学习-Day 10】机器学习基石:从零入门线性回归与逻辑回归
11-【深度学习-Day 11】Scikit-learn实战:手把手教你完成鸢尾花分类项目
12-【深度学习-Day 12】从零认识神经网络:感知器原理、实现与局限性深度剖析
13-【深度学习-Day 13】激活函数选型指南:一文搞懂Sigmoid、Tanh、ReLU、Softmax的核心原理与应用场景
14-【深度学习-Day 14】从零搭建你的第一个神经网络:多层感知器(MLP)详解
15-【深度学习-Day 15】告别“盲猜”:一文读懂深度学习损失函数
16-【深度学习-Day 16】梯度下降法 - 如何让模型自动变聪明?
17-【深度学习-Day 17】神经网络的心脏:反向传播算法全解析
18-【深度学习-Day 18】从SGD到Adam:深度学习优化器进阶指南与实战选择
19-【深度学习-Day 19】入门必读:全面解析 TensorFlow 与 PyTorch 的核心差异与选择指南
20-【深度学习-Day 20】PyTorch入门:核心数据结构张量(Tensor)详解与操作
21-【深度学习-Day 21】框架入门:神经网络模型构建核心指南 (Keras & PyTorch)
22-【深度学习-Day 22】框架入门:告别数据瓶颈 - 掌握PyTorch Dataset、DataLoader与TensorFlow tf.data实战
23-【深度学习-Day 23】框架实战:模型训练与评估核心环节详解 (MNIST实战)
在前面的系列文章中,我们已经逐步学习了深度学习框架的基础知识,包括张量的操作(Day 20)、如何使用框架构建神经网络模型(Day 21),以及如何高效地加载和处理数据(Day 22)。这些知识点如同建造房屋的砖瓦和工具,现在,是时候将它们整合起来,搭建并运行我们深度学习项目的“引擎”——模型训练与评估循环。
本篇文章将聚焦于使用深度学习框架(以 PyTorch 为例,但核心思想同样适用于 TensorFlow/Keras 等)进行模型训练和评估的完整流程。我们将深入探讨训练循环的每一个关键步骤,理解评估模式的重要性,并学习如何计算和解读常用的评估指标。最终,我们将通过一个经典的 MNIST 手写数字识别实战案例,将所有理论知识付诸实践。学完本篇,您将能够独立编写并执行一个完整的模型训练与评估流程,为后续更复杂的项目打下坚实的基础。
在正式开始训练循环之前,我们需要明确两件重要的事情:如何衡量模型的预测与真实标签之间的差异(损失函数),以及如何根据这个差异来调整模型的参数以使其表现更好(优化器)。
选择合适的损失函数和优化器对于模型的训练效果至关重要。
深度学习框架通常内置了多种常用的损失函数,我们可以根据任务类型进行选择。
对于预测连续值的回归任务,常用的损失函数是:
torch.nn.MSELoss()
tf.keras.losses.MeanSquaredError()
对于预测离散类别标签的分类任务,常用的损失函数是:
torch.nn.BCELoss()
(通常需要配合 Sigmoid 输出) 或 torch.nn.BCEWithLogitsLoss()
(内部集成了 Sigmoid,更稳定)tf.keras.losses.BinaryCrossentropy()
torch.nn.CrossEntropyLoss()
(内部集成了 LogSoftmax 和 NLLLoss,期望输入为原始 logits)tf.keras.losses.CategoricalCrossentropy()
(期望one-hot编码的标签和softmax输出) 或 tf.keras.losses.SparseCategoricalCrossentropy()
(期望整数编码的标签和softmax输出)在我们的 MNIST 示例中,由于是多分类问题,我们将使用 torch.nn.CrossEntropyLoss()
。
# PyTorch 示例
import torch
import torch.nn as nn
# 假设是多分类任务,模型输出10个类别的logits
criterion = nn.CrossEntropyLoss()
# 示例:
# outputs = model(inputs) # 模型的原始输出 (logits)
# loss = criterion(outputs, labels) # labels 是真实的类别索引
优化器的选择同样重要,它关系到模型收敛的速度和效果。
常见的优化器有:
在 PyTorch 中,优化器通常在 torch.optim
模块下。在 TensorFlow/Keras 中,它们在 tf.keras.optimizers
模块下。
# PyTorch 示例
import torch.optim as optim
# model 是我们定义的神经网络模型实例
# learning_rate 是学习率,一个重要的超参数
# optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9)
optimizer = optim.Adam(model.parameters(), lr=0.001)
这里的 model.parameters()
会告诉优化器需要更新哪些参数。
万事俱备,现在我们可以构建模型训练的核心循环了。训练循环本质上是一个迭代过程,在每个迭代(通常称为一个 epoch)中,模型会遍历整个训练数据集(或分批次遍历),并根据损失进行参数更新。
一个典型的训练迭代(针对一个 batch 的数据)通常包含以下五个步骤:
假设我们已经有了模型 model
,损失函数 criterion
,优化器 optimizer
,以及一批输入数据 inputs
和对应的标签 labels
。
将输入数据传递给模型,模型会执行其内部定义的一系列运算(例如线性变换、激活函数等),最终输出预测结果。
# PyTorch 示例
# inputs 是当前批次的数据
outputs = model(inputs) # outputs 是模型对这批输入的预测结果
使用之前定义的损失函数,比较模型的预测 outputs
和真实的 labels
。
# PyTorch 示例
loss = criterion(outputs, labels) # loss 是一个标量,表示当前批次的平均损失
在 PyTorch 中,梯度是会累积的(这在某些高级应用如 RNN 中有用)。但在大多数情况下,每次进行反向传播计算梯度之前,我们需要将模型参数的梯度清零,以避免受到之前批次梯度的影响。
# PyTorch 示例
optimizer.zero_grad()
关键点:这一步非常重要,如果忘记清零梯度,梯度会不断累积,导致训练过程出错。
这是深度学习框架的核心魔法之一。调用损失张量的 .backward()
方法,框架会自动利用链式法则计算损失函数关于网络中所有可训练参数(requires_grad=True
的张量)的梯度。这些梯度会存储在对应参数的 .grad
属性中。
# PyTorch 示例
loss.backward() # 自动计算梯度
一旦梯度计算完毕,优化器就可以使用这些梯度来更新模型的参数了。更新的规则由所选的优化器算法(如 SGD、Adam)决定。
# PyTorch 示例
optimizer.step() # 根据梯度更新模型参数
通常,我们不会一次性将整个数据集都喂给模型进行训练,原因有二:
一个典型的训练流程会包含多个 epoch,每个 epoch 包含多个 iteration。
# PyTorch 伪代码结构
num_epochs = 10
for epoch in range(num_epochs):
# 在每个 epoch 开始时,通常会将模型设置为训练模式
model.train() # 作用是启用 Dropout 和 Batch Normalization 等层的训练行为
for inputs, labels in train_loader: # train_loader 是一个数据加载器,按批次提供数据
# 1. 梯度清零
optimizer.zero_grad()
# 2. 前向传播
outputs = model(inputs)
# 3. 计算损失
loss = criterion(outputs, labels)
# 4. 反向传播
loss.backward()
# 5. 参数更新
optimizer.step()
# 通常在一个 epoch结束后,会进行模型评估 (详见下一节)
# print(f"Epoch [{epoch+1}/{num_epochs}], Loss: {loss.item():.4f}")
仅仅训练模型是不够的,我们还需要知道模型在未见过的数据上的表现如何,即模型的泛化能力。这就需要进行模型评估。
在评估模型时,我们通常需要将模型切换到评估模式。这主要是因为某些层(如 Dropout 层和 Batch Normalization 层)在训练和评估时的行为是不同的:
在 PyTorch 中,通过 model.eval()
来将模型切换到评估模式。相应地,model.train()
用于切换回训练模式。
此外,在评估阶段,我们不需要计算梯度,因为我们不进行参数更新。关闭梯度计算可以减少内存消耗并加速计算。在 PyTorch 中,这可以通过 torch.no_grad()
上下文管理器来实现。
# PyTorch 示例
model.eval() # 将模型设置为评估模式
with torch.no_grad(): # 在这个块内部,所有计算都不会追踪梯度
# 进行评估...
# test_outputs = model(test_inputs)
# ...
model.train() # 如果后续还需要训练,记得切换回训练模式
评估流程通常在验证集 (validation set) 或测试集 (test set) 上进行:
model.eval()
)。torch.no_grad()
包裹评估代码。选择什么评估指标取决于具体的任务。
对于分类任务,计算准确率是一个常见的评估步骤。
# PyTorch 示例 (假设在评估循环内部)
# correct_predictions = 0
# total_samples = 0
#
# model.eval()
# with torch.no_grad():
# for inputs, labels in test_loader: # test_loader 是测试数据加载器
# outputs = model(inputs)
# _, predicted_classes = torch.max(outputs.data, 1) # 获取概率最高的类别索引
# total_samples += labels.size(0)
# correct_predictions += (predicted_classes == labels).sum().item()
#
# accuracy = 100 * correct_predictions / total_samples
# print(f'Accuracy on the test set: {accuracy:.2f}%')
现在,我们将把前面讨论的所有概念整合起来,用 PyTorch 框架从头开始训练一个简单的多层感知器 (MLP) 来对 MNIST 手写数字数据集进行分类。
首先,导入所有必要的库,并定义一些超参数。
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
from torch.utils.data import DataLoader
# 定义超参数
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') # 优先使用GPU
input_size = 28 * 28 # MNIST 图像是 28x28 像素
hidden_size = 128 # 隐藏层大小
num_classes = 10 # 数字 0-9,共10类
num_epochs = 5 # 训练轮次
batch_size = 64 # 批大小
learning_rate = 0.001 # 学习率
PyTorch 的 torchvision
模块提供了直接加载 MNIST 数据集的功能,并可以方便地进行数据转换。
# 数据预处理:转换为张量,并进行归一化
transform = transforms.Compose([
transforms.ToTensor(), # 将 PIL Image 或 numpy.ndarray 转换为 torch.Tensor,并将像素值从 [0, 255] 缩放到 [0.0, 1.0]
transforms.Normalize((0.1307,), (0.3081,)) # MNIST 数据集的均值和标准差 (经验值)
])
# 下载并加载训练数据集
train_dataset = torchvision.datasets.MNIST(root='./data',
train=True,
transform=transform,
download=True)
# 下载并加载测试数据集
test_dataset = torchvision.datasets.MNIST(root='./data',
train=False,
transform=transform)
# 创建数据加载器
train_loader = DataLoader(dataset=train_dataset,
batch_size=batch_size,
shuffle=True) # 训练时打乱数据
test_loader = DataLoader(dataset=test_dataset,
batch_size=batch_size,
shuffle=False) # 测试时不需要打乱
我们构建一个包含一个隐藏层的简单 MLP。
class MLP(nn.Module):
def __init__(self, input_size, hidden_size, num_classes):
super(MLP, self).__init__()
self.fc1 = nn.Linear(input_size, hidden_size) # 输入层到隐藏层
self.relu = nn.ReLU() # ReLU 激活函数
self.fc2 = nn.Linear(hidden_size, num_classes) # 隐藏层到输出层
def forward(self, x):
# 将输入的 28x28 图像展平为 784 维向量
x = x.reshape(-1, 28 * 28)
out = self.fc1(x)
out = self.relu(out)
out = self.fc2(out)
# 注意:对于 nn.CrossEntropyLoss(),不需要在模型末尾加 Softmax 层,它内部会处理
return out
model = MLP(input_size, hidden_size, num_classes).to(device) # 将模型移动到 GPU (如果可用)
回顾Day 21:[框架]入门(二):构建模型,我们学习了如何使用框架定义模型。
# 损失函数:交叉熵损失,适用于多分类任务
criterion = nn.CrossEntropyLoss()
# 优化器:Adam
optimizer = optim.Adam(model.parameters(), lr=learning_rate)
现在,我们整合第二节中学习的训练循环五个步骤。
print("开始训练...")
total_steps = len(train_loader)
for epoch in range(num_epochs):
model.train() # 设置为训练模式
for i, (images, labels) in enumerate(train_loader):
# 将数据移动到 GPU (如果可用)
images = images.to(device)
labels = labels.to(device)
# 1. 前向传播
outputs = model(images)
# 2. 计算损失
loss = criterion(outputs, labels)
# 3. 梯度清零
optimizer.zero_grad()
# 4. 反向传播
loss.backward()
# 5. 参数更新
optimizer.step()
if (i + 1) % 100 == 0: # 每 100 个 batch 打印一次信息
print(f'Epoch [{epoch+1}/{num_epochs}], Step [{i+1}/{total_steps}], Loss: {loss.item():.4f}')
print("训练完成!")
为了在训练后或训练过程中监控模型性能,我们编写一个评估函数。
def evaluate_model(model_to_eval, data_loader):
model_to_eval.eval() # 设置为评估模式
correct_predictions = 0
total_samples = 0
with torch.no_grad(): # 在评估阶段不计算梯度
for images, labels in data_loader:
images = images.to(device)
labels = labels.to(device)
outputs = model_to_eval(images.reshape(-1, 28 * 28)) # 确保输入形状正确
_, predicted_classes = torch.max(outputs.data, 1) # 获取预测类别
total_samples += labels.size(0)
correct_predictions += (predicted_classes == labels).sum().item()
accuracy = 100 * correct_predictions / total_samples
return accuracy
在训练循环结束后,调用评估函数。
# 评估模型在测试集上的表现
test_accuracy = evaluate_model(model, test_loader)
print(f'模型在测试集上的准确率: {test_accuracy:.2f}%')
# 如果需要,也可以在每个 epoch 结束后评估在验证集上的表现 (此处我们直接用测试集)
# for epoch in range(num_epochs):
# # ... 训练代码 ...
# val_accuracy = evaluate_model(model, test_loader) # 或 validation_loader
# print(f'Epoch [{epoch+1}/{num_epochs}], Validation Accuracy: {val_accuracy:.2f}%')
通过以上步骤,我们就完成了一个使用 PyTorch 框架训练和评估 MNIST 分类模型的完整流程。您可以尝试调整超参数(如学习率、批大小、隐藏层大小、epoch 数)或者模型结构,观察它们对最终结果的影响。
在实际的模型训练与评估过程中,可能会遇到各种问题,同时也存在许多可以进一步优化和探索的方向。
这是初学者常遇到的问题,可能的原因有:
optimizer.zero_grad()
,或者损失函数选择错误等。仔细检查代码逻辑。当基础训练流程跑通后,可以从以下方面尝试提升模型性能:
仅仅打印损失值和最终的准确率可能不够直观。使用 TensorBoard (TensorFlow生态) 或类似的工具 (如 matplotlib
配合 PyTorch) 来可视化训练过程中的损失曲线、准确率曲线等指标,可以帮助我们更好地理解模型的学习动态,并进行诊断。
例如,可以绘制每个 epoch 的训练损失和验证准确率:
# 伪代码 - 使用 matplotlib 绘制曲线
# train_losses = []
# val_accuracies = []
# for epoch in range(num_epochs):
# # ... 训练 ...
# # current_train_loss = ...
# # train_losses.append(current_train_loss)
#
# # ... 评估 ...
# # current_val_accuracy = ...
# # val_accuracies.append(current_val_accuracy)
# import matplotlib.pyplot as plt
# plt.figure(figsize=(10, 4))
# plt.subplot(1, 2, 1)
# plt.plot(train_losses, label='Training Loss')
# plt.xlabel('Epoch')
# plt.ylabel('Loss')
# plt.legend()
#
# plt.subplot(1, 2, 2)
# plt.plot(val_accuracies, label='Validation Accuracy')
# plt.xlabel('Epoch')
# plt.ylabel('Accuracy')
# plt.legend()
# plt.show()
本篇文章详细介绍了使用深度学习框架进行模型训练与评估的完整流程,这是深度学习实践中至关重要的一环。核心要点回顾:
optimizer.zero_grad()
):清除旧梯度。outputs = model(inputs)
):获取模型预测。loss = criterion(outputs, labels)
):评估预测与真实值的差异。loss.backward()
):计算损失对各参数的梯度。optimizer.step()
):根据梯度调整模型参数。model.eval()
切换模型到评估状态,关闭 Dropout 和 Batch Normalization 的训练行为,并使用 torch.no_grad()
节约计算资源。掌握了模型训练与评估的流程,就如同掌握了驾驶汽车的核心技能。接下来,我们将面临更多样化的道路(更复杂的模型和任务),但基本的驾驶原理是相通的。希望本篇内容能为您在深度学习的道路上提供坚实的支撑。