在这里插入图片描述

PyTorch Scala 高校计算机 硕士研一课程

使用优化器更新权重

模型的参数(具有requires_grad=True的权重和偏差)在其.grad属性中包含已计算的梯度。这些梯度是在损失计算并使用loss.backward()执行反向传播时生成的。它们,例如∇θL∇θ**L,表示在参数空间中会使损失增长最快的方向。为了使损失最小化,我们需要朝着相反的方向调整参数。这正是优化器的作用。

在第4章中,您学习了如何实例化优化器,例如torch.optim.SGDtorch.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 属性中已有的值上。

如果您不处理这个问题,请考虑训练循环在多次迭代中会发生什么:

  1. 第一次迭代: 计算损失 L1L1,调用 $loss_1.backward()。梯度 ∇θL1∇θ**L1 被计算并存储在 param.grad 中。调用 $optimizer.step()$
  2. 第二次迭代: 计算损失 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()。但是,对于常规训练,每次迭代清零梯度是常规步骤。

实现评估循环

为了客观评估模型在训练阶段后的表现,一种可靠的方法必不可少。仅凭训练损失可能产生误导,因为模型可能在训练数据上表现出色,但无法泛化到新的、未见过的数据。评估循环可以解决这一挑战。它的作用是衡量模型在独立数据集(如验证集或测试集)上的表现,这些数据未在权重更新过程中使用。

为何需要独立的评估循环?

训练涉及根据训练数据调整模型参数。然而,评估纯粹是为了评测。我们想回答:“给定此输入,模型的预测与实际目标有多接近?”同时不改变模型本身。执行评估需要一个独特的流程,原因有以下几点:

  1. 泛化能力评估: 它衡量模型处理训练中未曾见过数据的能力,这是大多数机器学习任务的最终目标。
  2. 防止数据泄露: 使用独立数据集可确保评估数据中的信息不会无意中影响训练过程(例如,被用于梯度更新)。
  3. 模型选择与调优: 验证集上的表现常用于选择最佳模型架构、决定何时停止训练(早期停止)或调整超参数。
  4. 检测过拟合: 比较训练集与验证集上的表现有助于识别过拟合。当模型对训练数据(包括其噪声)学习得过于透彻,从而失去泛化能力时,就会发生过拟合。此时训练数据上的表现可能持续提升,而验证数据上的表现却停滞不前或下降。

与训练循环的区别

评估循环与训练循环有相似之处(例如,遍历数据,执行前向传播),但存在重要区别:

  1. 不计算梯度: 由于我们只进行评估而不更新权重,因此无需计算或存储梯度。这节省了内存和计算资源。
  2. 不反向传播: 因此,调用$loss.backward()
  3. 不执行优化器步骤: 模型的权重保持不变,因此调用$optimizer.step()$optimizer.zero_grad()
  4. 模型模式: 模型应切换到评估模式。

将模型设置为评估模式: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()主要有两个优点:

  1. 效率: 它减少了内存消耗,因为反向传播所需的中间激活不会被存储。操作也可能运行得更快。
  2. 正确性: 它确保你不会在不需要时意外地计算梯度或尝试执行反向传播。

评估循环的结构

以下是评估函数的一个典型结构:

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}')

分步解析:

  1. model.eval() 将模型切换到评估模式。
  2. 初始化指标: 设置变量以累加总损失和正确预测的数量(或其他相关指标)。同时,跟踪评估的总样本数。
  3. with torch.no_grad(): 进入不计算梯度的上下文。
  4. 遍历DataLoader: 循环遍历评估DataLoader提供的批次。
  5. 设备放置: 确保输入数据和目标与模型位于同一设备上。
  6. 前向传播: 将输入数据通过模型(outputs = model(inputs))。
  7. 计算损失: 使用准则计算损失。使用loss.item()获取当前批次损失的Python标量值,并在累加前乘以批次大小(inputs.size(0)),以处理最后一个批次大小可能存在的差异。
  8. 计算指标: 从模型输出中确定预测(例如,对于分类任务,使用torch.max获取最高概率的索引)。将预测与真实目标进行比较,并累加正确预测的数量和总样本数。
  9. 汇总结果: 遍历所有批次后,将累加的总量除以处理的总样本数,计算平均损失和总体准确率(或其他指标)。
  10. 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.loadload_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.savetorch.loadload_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. 数据准备

我们将生成用于线性关系的合成数据,并使用 TensorDatasetDataLoader 对其进行包装。

// 生成合成数据: 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("训练完成!")

我们来逐一分析训练轮次循环中的步骤:

  1. model.train():将模型设置为训练模式。这对于像 Dropout 或 BatchNorm 这样的层很重要,因为它们在训练和评估期间行为不同。
  2. 我们遍历 train_loader 以获取 featureslabels 的批次。
  3. 数据被移动到模型所在的 device 上。这可以防止运行时错误。
  4. 前向传播outputs = model(features) 计算模型对输入批次的预测。
  5. 损失计算loss = loss_fn(outputs, labels) 使用 MSE 准则计算预测与实际标签之间的差异。
  6. 反向传播
    • optimizer.zero_grad():清除旧梯度。如果忘记此步骤,梯度将从之前的迭代中累积,导致不正确的更新。
    • loss.backward():计算损失相对于所有 requires_grad=True 的模型参数的梯度。
  7. 优化器步骤optimizer.step() 使用反向传播中计算的梯度和优化算法(本例中为 SGD)更新模型的参数 (model.parameters())。
  8. 我们跟踪 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})")  

评估循环中的主要区别:

  1. model.eval():将模型设置为评估模式。
  2. with torch.no_grad()::此上下文管理器在该代码块内禁用梯度计算。这很重要,因为我们在评估时不需要梯度,并且它能减少内存消耗并加快计算速度。
  3. 我们不调用 loss.backward()optimizer.step(),因为我们只测量性能,而不进行训练。
  4. 我们累加所有验证批次的损失,以获得平均验证损失。

评估后,我们打印学习到的参数。将它们与我们用于生成数据的 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中的数据准备来调整此结构以适应更复杂的模型和数据集。训练和评估循环的核心逻辑保持了显著的一致性。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐