【Scala PyTorch深度学习】PyTorch On Scala 系列课程 第六章 13 :模型训练高阶【AI Infra 3.0】[PyTorch Scala 硕士研一课程]

PyTorch Scala 高校计算机 硕士研一课程
使用优化器更新权重
模型的参数(具有requires_grad=True的权重和偏差)在其.grad属性中包含已计算的梯度。这些梯度是在损失计算并使用loss.backward()执行反向传播时生成的。它们,例如∇θL∇θ**L,表示在参数空间中会使损失增长最快的方向。为了使损失最小化,我们需要朝着相反的方向调整参数。这正是优化器的作用。
在第4章中,您学习了如何实例化优化器,例如torch.optim.SGD或torch.optim.Adam,并传入模型的参数(model.parameters())以及学习率等配置信息。现在,在训练循环中,您使用优化器的step()方法来执行参数更新。
// 假设模型、损失函数和优化器已定义
// optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
// ... 在一个批次的训练循环内部 ...
// 前向传播
val outputs = model(inputs)
// 计算损失
val loss = criterion(outputs, labels)
// 反向传播 - 计算梯度
loss.backward()
// 使用优化器更新权重
optimizer.step()
调用optimizer.step()会遍历在优化器初始化时注册的所有参数。对于每个参数p,它会使用存储在p.grad中的梯度来更新参数值p.data。
随机梯度下降(SGD)使用的最基本的更新规则是:
参数=参数−学习率×梯度参数=参数−学习率×梯度
或者更正式地,对于参数θθ:
θnew=θold−η∇θLθne**w=θol**d−η∇θ**L
其中ηη是学习率(类似于创建优化器时传入的lr参数),而∇θL∇θ**L是通过loss.backward()计算的梯度,优化器通过parameter.grad在内部访问它。
不同的优化器实现了更复杂的更新规则。例如,像Adam这样的优化器对每个参数使用自适应学习率并融入动量思想,但核心思想保持不变:使用计算出的梯度调整参数以最小化损失。optimizer.step()调用将所选优化算法定义的具体更新逻辑进行了封装。
在调用optimizer.step()之前,必须先调用loss.backward()。backward()调用计算梯度,而step()调用则使用这些梯度来更新权重。如果不先调用backward(),.grad属性将不会被填充(或者会包含来自上一次迭代的旧值),优化器也就无法知道如何有效地调整参数。
更新权重后的下一个重要步骤是在处理下一个批次之前清除梯度。我们将在下一节“梯度清零”中介绍这一点。
梯度清零
调用 $loss.backward() 计算损失相对于模型参数(那些 requires_grad=True 的张量)的梯度。这些梯度表示为减少损失所需的改变方向和大小,它们存储在每个参数张量的 .grad 属性中。随后,$optimizer.step() 会根据所选的优化算法(如 SGD 或 Adam)使用这些存储的梯度来更新参数值。
然而,PyTorch 在反向传播过程中处理梯度的方式有一个不明显但非常重要的细节:PyTorch 会累积梯度。当您调用 $loss.backward()$ 时,为每个参数新计算的梯度会 添加 到该参数 .grad 属性中已有的值上。
如果您不处理这个问题,请考虑训练循环在多次迭代中会发生什么:
- 第一次迭代: 计算损失 L1L1,调用
$loss_1.backward()。梯度 ∇θL1∇θ**L1 被计算并存储在param.grad中。调用$optimizer.step()$。 - 第二次迭代: 计算损失 L2L2,调用
$loss_2.backward()。新的梯度 ∇θL2∇θ**L2 被计算。PyTorch 将这些梯度 添加 到现有梯度中,因此param.grad现在持有 ∇θL1+∇θL2∇θ**L1+∇θ**L2。调用$optimizer.step()$。
第二次迭代中的优化器步骤使用了不正确的梯度信息。它使用了当前批次和上一批次的混合梯度。这会阻止模型良好地学习,因为权重更新是基于来自不同数据点的陈旧且混合的梯度信号。
optimizer.zero_grad() 的用途
为了防止这种累积并确保优化器 仅 基于当前批次的梯度来更新权重,您必须在为下一次迭代计算梯度之前手动重置梯度。这正是 optimizer.zero_grad() 方法所做的。
调用 optimizer.zero_grad() 会遍历优化器被配置来管理的所有参数 (θθ),并将其 .grad 属性重置为零(或 None)。
何时调用 optimizer.zero_grad()
您需要在每次训练迭代中调用 optimizer.zero_grad() 一次。最常见且推荐的做法是在循环的 开始 处调用它,即在处理下一个批次之前:
// 训练循环示例代码片段
// optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
for epoch <-(1 to num_epochs):
for inputs, labels <- dataloader:
// 1. 清零上一迭代的梯度
optimizer.zero_grad()
// 2. 前向传播:计算预测输出
val outputs = model(inputs)
// 3. 计算损失
val loss = criterion(outputs, labels)
// 4. 反向传播:计算损失相对于参数的梯度
loss.backward()
// 5. 执行一次优化步骤(参数更新)
optimizer.step()
//... (评估、日志记录等) ...
或者,您可以在 optimizer.step() 之后 立即调用 optimizer.zero_grad()。在标准循环中,其功能结果是相同的。将其放在开始处可以清楚地划分新批次处理的开始。核心是,它必须在下一次 loss.backward() 调用 之前 被调用,以避免梯度在迭代之间累积。
忘记清零梯度是 PyTorch 训练循环中常见的错误源,这通常会导致模型无法收敛或表现出异常的学习情况。请务必确保 optimizer.zero_grad() 在您的训练迭代中位置正确。
虽然梯度累积通常是不希望发生的,但当 GPU 内存受限时,它可以被有意地用作一种方法来模拟更大的批次大小。在这种情况下,您可以对多个小批次执行前向和反向传播,通过在每次 loss.backward() 后 不 调用 optimizer.zero_grad() 来累积梯度,然后只在处理完所需数量的小批次后才调用 optimizer.step() 和 optimizer.zero_grad()。但是,对于常规训练,每次迭代清零梯度是常规步骤。
实现评估循环
为了客观评估模型在训练阶段后的表现,一种可靠的方法必不可少。仅凭训练损失可能产生误导,因为模型可能在训练数据上表现出色,但无法泛化到新的、未见过的数据。评估循环可以解决这一挑战。它的作用是衡量模型在独立数据集(如验证集或测试集)上的表现,这些数据未在权重更新过程中使用。
为何需要独立的评估循环?
训练涉及根据训练数据调整模型参数。然而,评估纯粹是为了评测。我们想回答:“给定此输入,模型的预测与实际目标有多接近?”同时不改变模型本身。执行评估需要一个独特的流程,原因有以下几点:
- 泛化能力评估: 它衡量模型处理训练中未曾见过数据的能力,这是大多数机器学习任务的最终目标。
- 防止数据泄露: 使用独立数据集可确保评估数据中的信息不会无意中影响训练过程(例如,被用于梯度更新)。
- 模型选择与调优: 验证集上的表现常用于选择最佳模型架构、决定何时停止训练(早期停止)或调整超参数。
- 检测过拟合: 比较训练集与验证集上的表现有助于识别过拟合。当模型对训练数据(包括其噪声)学习得过于透彻,从而失去泛化能力时,就会发生过拟合。此时训练数据上的表现可能持续提升,而验证数据上的表现却停滞不前或下降。
与训练循环的区别
评估循环与训练循环有相似之处(例如,遍历数据,执行前向传播),但存在重要区别:
- 不计算梯度: 由于我们只进行评估而不更新权重,因此无需计算或存储梯度。这节省了内存和计算资源。
- 不反向传播: 因此,不调用
$loss.backward()。 - 不执行优化器步骤: 模型的权重保持不变,因此不调用
$optimizer.step()和$optimizer.zero_grad()。 - 模型模式: 模型应切换到评估模式。
将模型设置为评估模式:model.eval()
PyTorch 模型(nn.Module)有不同的训练和评估模式。你可以使用model.train()和model.eval()在它们之间切换。在开始评估循环之前调用model.eval()非常重要。此调用会通知Dropout和Batch Normalization等层,模型正处于评估阶段。
- Dropout 层: 在评估期间被停用。我们希望模型展现其完整的预测能力,而不是随机丢弃神经元。
- Batch Normalization 层: 使用训练期间计算的运行统计数据(均值和方差),而不是当前批次的统计数据。这确保了输出的一致性,不受评估批次统计数据的影响。
评估结束后,如果你计划恢复训练(例如,在每个周期后进行评估),请记得使用model.train()将模型切换回训练模式。
禁用梯度计算:torch.no_grad()
为防止PyTorch在评估期间跟踪操作并构建用于梯度计算的计算图,你应该将评估循环代码封装在torch.no_grad()上下文管理器中。
with torch.no_grad():
// 这里是评估代码...
// 此块中的操作将不会跟踪梯度。
使用torch.no_grad()主要有两个优点:
- 效率: 它减少了内存消耗,因为反向传播所需的中间激活不会被存储。操作也可能运行得更快。
- 正确性: 它确保你不会在不需要时意外地计算梯度或尝试执行反向传播。
评估循环的结构
以下是评估函数的一个典型结构:
import torch
def evaluate_model(model, dataloader, criterion, device):
"""在提供的datasets上评估模型。"""
model.eval() // 将模型设置为评估模式
val total_loss = 0.0
val correct_predictions = 0
val total_samples = 0
with torch.no_grad(): // 禁用梯度计算
for (inputs, targets) <- dataloader:
// 将数据移动到与模型相同的设备上
val inputs = inputs.to(device)
val targets = targets.to(device)
// 前向传播
val outputs = model(inputs)
// 计算损失(可选,但对监控有用)
val loss = criterion(outputs, targets)
total_loss += loss.item() * inputs.size(0) // 累加批次损失
// 计算准确率(分类示例)
val _, predicted_labels = torch.max(outputs, dim=1)
correct_predictions += (predicted_labels == targets).sum().item()
total_samples += targets.size(0)
// 计算整个数据集的平均损失和准确率
val average_loss = total_loss / total_samples
val accuracy = correct_predictions / total_samples
model.train() // 如果之后需要,切换回训练模式
return average_loss, accuracy
// --- 用法示例 ---
// 假设你已经有:
// model: 你的 nn.Module 模型
// validation_loader: 你的验证集 DataLoader
// criterion: 你的损失函数(例如,nn.CrossEntropyLoss)
// device: torch.device('cuda' if torch.cuda_is_available() else 'cpu')
// 评估模型
val (val_loss, val_accuracy) = evaluate_model(model, validation_loader, criterion, device)
// println(f'Validation Loss: {val_loss:.4f}, Validation Accuracy: {val_accuracy:.4f}')
分步解析:
model.eval(): 将模型切换到评估模式。- 初始化指标: 设置变量以累加总损失和正确预测的数量(或其他相关指标)。同时,跟踪评估的总样本数。
with torch.no_grad():: 进入不计算梯度的上下文。- 遍历DataLoader: 循环遍历评估
DataLoader提供的批次。 - 设备放置: 确保输入数据和目标与模型位于同一设备上。
- 前向传播: 将输入数据通过模型(
outputs = model(inputs))。 - 计算损失: 使用准则计算损失。使用
loss.item()获取当前批次损失的Python标量值,并在累加前乘以批次大小(inputs.size(0)),以处理最后一个批次大小可能存在的差异。 - 计算指标: 从模型输出中确定预测(例如,对于分类任务,使用
torch.max获取最高概率的索引)。将预测与真实目标进行比较,并累加正确预测的数量和总样本数。 - 汇总结果: 遍历所有批次后,将累加的总量除以处理的总样本数,计算平均损失和总体准确率(或其他指标)。
model.train()(可选): 如果此评估发生在训练周期之间,将模型切换回训练模式。
此评估循环为模型的泛化表现提供了必要的反馈,指导训练过程并帮助你构建更高效的深度学习模型。同时监控这些评估指标和训练指标对理解模型行为非常重要。
此图显示了一个常见情况:在若干个周期后,验证损失开始增加,这表明过拟合已经出现,即使训练损失持续下降。评估循环对于检测这种情况非常重要。
运行训练周期(前向传播, 损失, 反向传播, 优化)运行评估循环(model.eval(), no_grad, 前向传播, 指标)周期结束记录训练和验证指标检查停止条件(例如:最大周期数, 早期停止)继续训练训练完成停止训练
典型的深度学习训练流程,在每个训练周期后加入评估,以监控表现并决定是否继续或停止训练。
保存和加载模型检查点
深度学习模型的训练通常耗时,根据模型复杂度和数据集大小,可能需要数小时甚至数天。每次因系统中断、后续微调或仅为预测而停止时都从头开始训练是不现实的。因此,保存和加载模型检查点变得非常重要。
检查点记录了训练过程在特定时刻的状态,方便您日后恢复。有效保存和加载 PyTorch 模型和训练状态是必要的组成部分。
应该保存什么?
保存检查点时,您需要根据目的决定需要哪些信息。通常,您至少会保存模型的参数。如果您打算恢复训练,还应保存优化器的状态,以及当前周期数和最新验证损失等其他元数据。
PyTorch 模型有一个内部状态字典,通过 model.state_dict() 访问,其中包含模型各层的所有学习参数(权重和偏置)。这是保存模型学习信息建议的方式。
为什么保存 state_dict 而不是整个模型对象(例如 torch.save(model, PATH))?保存 state_dict 更具弹性,也更不容易出问题。对整个模型对象进行序列化保存会存储保存时使用的特定代码结构。如果您之后重构或更改模型类定义,加载序列化对象可能会失败或导致意外行为。仅保存状态字典将学习参数与代码结构分离,使加载更稳定。
同样,Adam 或 SGD 等优化器也有内部状态(例如,动量缓冲区、自适应学习率),这些状态在训练过程中会变化。为了精确地恢复训练,您应该使用 optimizer.state_dict() 保存优化器的状态。
使用 torch.save 保存检查点
PyTorch 使用 torch.save() 来序列化和保存对象。要保存检查点,您通常会创建一个字典,其中包含模型的状态字典、优化器的状态字典以及任何其他相关信息,然后保存这个字典。
以下是在训练循环中保存检查点的常见模式:
// 假设 model, optimizer, epoch, loss 已定义
// val PATH = "path/to/your/checkpoint.pth" // 定义您的保存路径
// 创建检查点字典
val checkpoint = Map(
"epoch" -> (epoch + 1), // 保存下一个要开始的周期数
"model_state_dict" -> model.state_dict(),
"optimizer_state_dict" -> optimizer.state_dict(),
"loss" -> loss, // 或者可以是验证损失
// 添加任何其他您想保存的指标或信息
// "validation_accuracy" -> val_acc,
)
torch.save(checkpoint, PATH)
println(s"检查点已在周期 $epoch 保存到 $PATH")
您可以定期(例如,每 10 个周期)保存检查点,或在模型在验证集上取得新最佳表现时保存。
使用 torch.load 和 load_state_dict 加载检查点
要加载检查点,您首先使用 torch.load() 从文件中反序列化保存的字典。然后,您需要将状态字典加载回您的模型和优化器实例中。
注意: 在加载模型和优化器状态之前,您必须先创建它们的实例。load_state_dict() 方法将参数加载到一个现有对象中;它不会重新创建对象本身。
加载用于推理
如果您只需要模型进行预测(推理),并且不打算恢复训练,通常只需加载 model_state_dict。
// 首先,实例化您的模型结构
val model = YourModelClass(*args, **kwargs)
// 定义您保存的检查点路径
val PATH = "path/to/your/checkpoint.pth"
// 加载检查点字典
val checkpoint = torch.load(PATH)
// 从检查点加载模型状态字典
model.load_state_dict(checkpoint("model_state_dict"))
// 将模型设置为评估模式
model.eval()
// 现在模型已准备好进行推理
with torch.no_grad():
outputs = model(inputs)
设置 model.eval() 很关键,因为它会禁用 Dropout 等层,并使用运行统计数据对批量归一化层进行归一化,这是推理时的正确操作。
加载以恢复训练
如果您想从上次中断的地方继续训练,您需要加载模型和优化器的状态,并获取其他已保存的元数据,例如周期数。
// 首先实例化模型和优化器
val model = YourModelClass(*args, **kwargs)
val optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate) // 或您选择的优化器
// 定义路径
val PATH = "path/to/your/checkpoint.pth"
var start_epoch = 0
var best_loss = Float.PositiveInfinity // 示例:初始化最佳损失
// 检查检查点是否存在以进行加载
if os.path.exists(PATH) then
val checkpoint = torch.load(PATH)
model.load_state_dict(checkpoint("model_state_dict"))
optimizer.load_state_dict(checkpoint("optimizer_state_dict"))
start_epoch = checkpoint("epoch")
best_loss = checkpoint("loss") // 加载之前的损失
println(s"检查点已加载。从周期 $start_epoch 继续训练")
// 将模型设置为训练模式
model.train()
// 现在您可以继续训练循环,从 start_epoch 开始
// for epoch in range(start_epoch, num_epochs):
// # ... 训练步骤 ...
设置 model.train() 可确保 Dropout 和批量归一化等层在训练期间表现正常。
处理 CPU/GPU 设备映射
有时,您可能会保存一个在 GPU 上训练的模型,然后需要在只有 CPU 的机器上加载它,反之亦然。默认情况下,torch.load() 会尝试将张量加载到它们保存时所在的设备上。如果该设备不可用,这可能会导致错误。
为了处理这种情况,您可以使用 torch.load() 中的 map_location 参数。
// 将在 GPU 上训练的模型加载到 CPU
val checkpoint = torch.load(PATH, map_location=torch.device('cpu'))
model.load_state_dict(checkpoint("model_state_dict"))
// 将任何模型加载到当前可用设备(如果 GPU 可用则使用 GPU,否则使用 CPU)
val device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
val checkpoint = torch.load(PATH, map_location=device)
model.load_state_dict(checkpoint("model_state_dict"))
// 记住也要将您的模型移到设备上
model.to(device)
保存和加载检查点是深度学习工作流程中很普通的一部分。通过熟练掌握使用 torch.save、torch.load 和 load_state_dict 的这些方法,您可以保证训练进程安全,模型可以重复使用,并且训练过程面对中断时表现稳定。请记住,为了获得最大的适应性和稳定性,要保存模型和优化器的 state_dict,以及相关的元数据。
动手实践:完整训练流程
既然您已经了解了训练的各个组成部分,我们将其整合到一个完整的、可运行的例子中。本实践练习将引导您设置模型、准备数据,并实现训练和评估循环,从而巩固本章讨论的内容。我们还会提到保存训练好的模型状态。
一个使用合成数据的简单线性回归问题将被解决。训练模型的目标是学习 y≈2x+1y≈2x+1 的关系。
1. 设置:导入与超参数
首先,我们导入必要的PyTorch模块,并为训练过程定义一些基本超参数。
import torch
import torch.nn as nn
import torch.optim as optim
import torch.utils.data as data
// 超参数
val learning_rate = 0.01
val num_epochs = 100
val batch_size = 16
// 设备配置(如果可用则使用GPU)
val device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
println(s"正在使用设备: $device")
2. 数据准备
我们将生成用于线性关系的合成数据,并使用 TensorDataset 和 DataLoader 对其进行包装。
// 生成合成数据: y = 2x + 1 + 噪声
val true_weight = torch.tensor([[2.0]])
val true_bias = torch.tensor([1.0])
// 生成训练数据
val X_train_tensor = torch.randn(100, 1) * 5 // 100 个样本, 1 个特征
val y_train_tensor = true_weight * X_train_tensor + true_bias + torch.randn(100, 1) * 0.5 // 添加一些噪声
// 生成验证数据(独立数据集)
val X_val_tensor = torch.randn(20, 1) * 5 // 20 个样本, 1 个特征
val y_val_tensor = true_weight * X_val_tensor + true_bias + torch.randn(20, 1) * 0.5 // 添加一些噪声
// 创建数据集
val train_dataset = TensorDataset(X_train_tensor, y_train_tensor)
val val_dataset = TensorDataset(X_val_tensor, y_val_tensor)
// 创建数据加载器
val train_loader = DataLoader(dataset=train_dataset, batch_size=batch_size, shuffle=True)
val val_loader = DataLoader(dataset=val_dataset, batch_size=batch_size, shuffle=False) // 验证数据无需打乱
这里,TensorDataset 方便地将输入特征 (X) 和目标标签 (y) 张量包装起来。DataLoader 接着接收此数据集,并提供可迭代的批次,自动处理打乱和批次化。
3. 模型、损失与优化器
现在,定义模型架构、损失函数和优化器。由于我们正在模拟线性关系 y=wx+by=w**x+b,因此一个简单的线性层就足够了。
// 定义模型(一个简单的线性层)
// 输入特征尺寸 = 1, 输出特征尺寸 = 1
val model = nn.Linear(1, 1).to(device) // 将模型移动到选定设备
// 定义损失函数(用于回归的均方误差)
val loss_fn = nn.MSELoss() // 将损失函数移动到选定设备
// 定义优化器(随机梯度下降)
val optimizer = optim.SGD(model.parameters(), lr=learning_rate)
println("模型定义:")
println(model)
println("\n初始参数:")
for (name, param) <- model.named_parameters():
if param.requires_grad then
println(s"$name: ${param.data.squeeze()}")
我们实例化 nn.Linear,它表示操作 y=Wx+by=W**x+b。PyTorch 会自动初始化权重 (WW) 和偏置 (bb) 参数。我们使用均方误差 (nn.MSELoss),因为它是回归任务的标准方法,用于衡量预测值与真实值之间的平均平方差。选择随机梯度下降 (optim.SGD) 来根据计算出的梯度更新模型的参数。请注意,我们将 model.parameters() 传递给优化器,以便它知道要更新哪些张量。最后,我们将模型移动到配置好的设备(CPU 或 GPU)上。
4. 训练循环
这是模型从数据中迭代学习过程的核心。
println("\n开始训练...")
for (epoch) <- (0 until num_epochs):
model.train() // 将模型设置为训练模式
var running_loss = 0.0
var num_batches = 0
// 遍历DataLoader中的批次数据
for (i, (features, labels)) <- train_loader:
// 将批次数据移动到与模型相同的设备上
features = features.to(device)
labels = labels.to(device)
// 1. 前向传播:计算模型的预测
val outputs = model(features)
// 2. 计算损失
val loss = loss_fn(outputs, labels)
// 2. 计算损失
loss = loss_fn(outputs, labels)
// 3. 反向传播:计算梯度
// 首先,清除上一步的梯度
optimizer.zero_grad()
// 然后,执行反向传播
loss.backward()
// 4. 优化器步骤:更新模型权重
optimizer.step()
// 累加损失以便报告
running_loss += loss.item()
num_batches += 1
// 打印本轮的平均损失
val avg_epoch_loss = running_loss / num_batches
if (epoch + 1) % 10 == 0 then// 每10轮打印一次
println(s"Epoch [${epoch+1}/${num_epochs}], Training Loss: ${avg_epoch_loss:.4f}")
println("训练完成!")
我们来逐一分析训练轮次循环中的步骤:
model.train():将模型设置为训练模式。这对于像 Dropout 或 BatchNorm 这样的层很重要,因为它们在训练和评估期间行为不同。- 我们遍历
train_loader以获取features和labels的批次。 - 数据被移动到模型所在的
device上。这可以防止运行时错误。 - 前向传播:
outputs = model(features)计算模型对输入批次的预测。 - 损失计算:
loss = loss_fn(outputs, labels)使用 MSE 准则计算预测与实际标签之间的差异。 - 反向传播:
optimizer.zero_grad():清除旧梯度。如果忘记此步骤,梯度将从之前的迭代中累积,导致不正确的更新。loss.backward():计算损失相对于所有requires_grad=True的模型参数的梯度。
- 优化器步骤:
optimizer.step()使用反向传播中计算的梯度和优化算法(本例中为 SGD)更新模型的参数 (model.parameters())。 - 我们跟踪
running_loss来报告本轮的平均损失。
5. 评估循环
训练之后(或在训练期间定期,例如每轮结束后),我们需要在不更新模型权重的情况下,评估模型在未见数据(验证集)上的表现。
println("\n开始评估...")
model.eval() // 将模型设置为评估模式
var total_val_loss = 0.0
var num_val_batches = 0
// 评估时禁用梯度计算
with torch.no_grad():
for (i, (features, labels)) <- val_loader:
// 将批次数据移动到设备上
val features = features.to(device)
val labels = labels.to(device)
// 前向传播
val outputs = model(features)
// 计算损失
val loss = loss_fn(outputs, labels)
total_val_loss += loss.item()
num_val_batches += 1
val avg_val_loss = total_val_loss / num_val_batches
println(f"验证损失: {avg_val_loss:.4f}")
// 检查学习到的参数
println("\n学习到的参数:")
for (name, param) <- model.named_parameters():
if param.requires_grad:
println(f"$name: ${param.data.squeeze()}")
println(f"(真实权重: {true_weight.item():.4f}, 真实偏置: {true_bias.item():.4f})")
评估循环中的主要区别:
model.eval():将模型设置为评估模式。with torch.no_grad()::此上下文管理器在该代码块内禁用梯度计算。这很重要,因为我们在评估时不需要梯度,并且它能减少内存消耗并加快计算速度。- 我们不调用
loss.backward()或optimizer.step(),因为我们只测量性能,而不进行训练。 - 我们累加所有验证批次的损失,以获得平均验证损失。
评估后,我们打印学习到的参数。将它们与我们用于生成数据的 true_weight (2.0) 和 true_bias (1.0) 进行比较。在 100 轮训练后,它们应该相当接近。
6. 保存与加载模型状态
保存您训练好的模型非常重要。标准做法是保存模型的 state_dict,其中包含所有学习到的参数(权重和偏置)。
// 保存模型学习到的参数
val model_save_path = "linear_regression_model.pth"
torch.save(model.state_dict(), model_save_path)
println(f"\n模型 state_dict 已保存到 {model_save_path}")
// 加载模型状态的例子
// 首先,再次实例化模型架构
val loaded_model = nn.Linear(1, 1).to(device)
// 然后,加载保存的状态字典
loaded_model.load_state_dict(torch.load(model_save_path))
println("模型 state_dict 加载成功。")
// 请记住,如果用于推断,请将加载的模型设置为评估模式
loaded_model.eval()
// 现在您可以使用 loaded_model 进行预测了
// 使用已加载模型进行预测的例子:
with torch.no_grad():
val sample_input = torch.tensor(10.0).to(device) // 示例输入
val prediction = loaded_model(sample_input)
println(f"输入 10.0 的预测值: {prediction.item():.4f}")
// 预期输出应接近 2*10 + 1 = 21
通常,保存 state_dict 比保存整个模型对象更好,因为它更灵活,且在底层代码更改时更不容易出错。要加载状态,您需要首先创建相同模型架构的实例,然后将字典加载到其中。
完整可运行示例
以下是结合所有部分的完整脚本:
import torch
import torch.nn as nn
import torch.optim as optim
import torch.utils.data.{TensorDataset, DataLoader}
// 1. 设置:超参数和设备
val learning_rate = 0.01
val num_epochs = 100
val batch_size = 16
val device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
println(f"正在使用设备: {device}")
// 2. 数据准备
val true_weight = torch.tensor([[2.0]])
val true_bias = torch.tensor([1.0])
val X_train_tensor = torch.randn(100, 1, device=device) * 5 // 直接在设备上生成数据
val y_train_tensor = true_weight.to(device) * X_train_tensor + true_bias.to(device) + torch.randn(100, 1, device=device) * 0.5
val X_val_tensor = torch.randn(20, 1, device=device) * 5
val y_val_tensor = true_weight.to(device) * X_val_tensor + true_bias.to(device) + torch.randn(20, 1, device=device) * 0.5
val train_dataset = TensorDataset(X_train_tensor, y_train_tensor)
val val_dataset = TensorDataset(X_val_tensor, y_val_tensor)
val train_loader = DataLoader(dataset=train_dataset, batch_size=batch_size, shuffle=true)
val val_loader = DataLoader(dataset=val_dataset, batch_size=batch_size, shuffle=false)
// 3. 模型、损失和优化器
val model = nn.Linear(1, 1).to(device)
val loss_fn = nn.MSELoss()
val optimizer = optim.SGD(model.parameters(), lr=learning_rate)
println("模型定义:")
println(model)
println("\n初始参数:")
for (name, param) <- model.named_parameters():
if param.requires_grad:
println(f"{name}: {param.data.squeeze()}")
// 4. 训练循环
println("\n开始训练...")
for epoch <- 0 until num_epochs:
model.train()
var running_loss = 0.0
var num_batches = 0
for (i, (features, labels)) <- train_loader.zipWithIndex():
// 数据已在正确设备上
val outputs = model(features)
val loss = loss_fn(outputs, labels)
optimizer.zero_grad()
loss.backward()
optimizer.step()
running_loss += loss.item()
num_batches += 1
val avg_epoch_loss = running_loss / num_batches
if (epoch + 1) % 10 == 0:
println(f"Epoch [{epoch+1}/{num_epochs}], Training Loss: {avg_epoch_loss:.4f}")
println("训练完成!")
// 5. 评估循环
println("\n开始评估...")
model.eval()
var total_val_loss = 0.0
var num_val_batches = 0
with torch.no_grad():
for (i, (features, labels)) <- val_loader.withIndex():
// 数据已在正确设备上
val outputs = model(features)
val loss = loss_fn(outputs, labels)
total_val_loss += loss.item()
num_val_batches += 1
val avg_val_loss = total_val_loss / num_val_batches
println(f"验证损失: {avg_val_loss:.4f}")
println("\n学习到的参数:")
for (name, param) <- model.named_parameters():
if param.requires_grad:
println(f"{name}: {param.data.squeeze().item():.4f}") // 单个值使用 .item()
println(f"(真实权重: {true_weight.item():.4f}, 真实偏置: {true_bias.item():.4f})")
// 6. 保存和加载模型状态
val model_save_path = "linear_regression_model.pth"
torch.save(model.state_dict(), model_save_path)
println(f"\n模型 state_dict 已保存到 {model_save_path}")
// 加载模型状态的例子
// 首先,再次实例化模型架构
val loaded_model = nn.Linear(1, 1).to(device)
// 然后,加载保存的状态字典
loaded_model.load_state_dict(torch.load(model_save_path))
println("模型 state_dict 加载成功。")
// 请记住,如果用于推断,请将加载的模型设置为评估模式
loaded_model.eval()
println("模型 state_dict 加载成功。")
with torch.no_grad():
val sample_input = torch.tensor(10.0).to(device)
val prediction = loaded_model(sample_input)
println(f"输入 10.0 的预测值: {prediction.item():.4f}")
(注:在合并脚本中,数据生成略有修改,直接在目标 device 上创建张量以提高效率,从而无需在循环内部对批次数据使用 .to(device)。)
这个动手示例呈现了在 PyTorch 中训练几乎任何模型的基本结构。您现在有了一个结合数据加载、模型定义、训练迭代、评估和持久化的模板。您可以通过更改步骤3中的模型架构和步骤2中的数据准备来调整此结构以适应更复杂的模型和数据集。训练和评估循环的核心逻辑保持了显著的一致性。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)