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

PyTorch Scala 高校计算机 硕士研一课程
章节 6: 实现训练循环
你已经学习了如何使用 torch.nn 定义模型,以及如何高效地使用 Dataset 和 DataLoader 准备数据。本章将侧重于整合这些组成部分,以实际训练你的神经网络。我们将构建被称为训练循环的标准迭代过程。
你将学习涉及的重要步骤:
- 设置模型、损失函数和优化器。
- 迭代
DataLoader提供的数据批次。 - 执行前向传播:将输入数据送入模型以获得预测结果。
- 计算损失:使用所选标准来衡量模型预测与真实标签之间的差异,通常表示为 LL。
- 执行反向传播:通过调用
$loss.backward()计算损失相对于模型参数 (∇θL∇θ**L) 的梯度。 - 更新模型权重:使用优化器根据计算出的梯度调整模型参数 (θθ),通常通过
$optimizer.step()。 - 在下一次迭代前,使用
$optimizer.zero_grad()将梯度归零。
此外,我们将介绍如何实现一个独立的循环来评估模型在验证集或测试集上的表现,以及在训练期间或之后保存和加载模型状态(检查点)的方法。到本章结束时,你将能够为你的 PyTorch 模型实现一个完整的训练和评估流程。
训练循环的构成
训练神经网络是一个迭代优化过程。你为模型提供数据,衡量其预测的准确度,然后微调其内部参数(权重和偏置),以减少这种不准确性。这个循环会重复多次。管理这种重复过程的代码结构通常被称为训练循环。
整体结构:周期与批次
从宏观上看,训练通常包含两个嵌套循环:
- 外层循环(周期): 一个周期表示对整个训练数据集的一次完整遍历。训练通常跨越多个周期,让模型能够多次查看并从每个数据样本中学习。周期的数量是你根据模型表现何时稳定或停止提升来选择的一个超参数。
- 内层循环(批次): 由于内存限制,一次性处理整个数据集在计算上通常是不可行的。因此,在每个周期内,我们以称为批次的更小片段遍历数据集。你之前学过的
DataLoader负责提供这些批次。与逐个处理样本或一次性使用整个数据集相比,以批次进行训练内存高效,还能带来更稳定的收敛和更好的泛化能力。
每次批次迭代中的核心步骤
对于在一个周期内处理的每个批次,训练循环执行一系列明确定义的步骤。让我们分解一下典型迭代中发生的情况:
-
获取数据: 从
DataLoader获取下一批输入数据(特征)及其对应的目标标签。在此阶段,确保数据被传输到模型参数所在的正确计算设备(CPU或GPU)也很重要。 -
梯度清零: 在计算当前批次的梯度之前,你必须明确重置从上一次迭代积累的梯度。如果忘记这一步,梯度将在批次间累加,导致不正确的更新,并可能在训练期间发散。通过在优化器对象上调用
zero_grad()方法来完成。// 代码:在新批次处理前重置梯度 optimizer.zero_grad() -
前向传播: 将输入特征批次送入你的模型。模型通过其层处理数据,应用学习到的权重和激活函数,最终生成一批预测或输出。
// 代码:获取模型预测 val predictions = model(input_batch) -
计算损失: 使用你选择的损失函数(准则)将模型的
predictions与真实的target_batch进行比较,例如用于分类的nn.CrossEntropyLoss或用于回归的nn.MSELoss。损失函数返回一个单一的标量值,表示当前批次的平均误差或差异。这个值显示了模型在这个特定批次上的表现好坏。// 代码:计算损失 val loss = criterion(predictions, target_batch) -
反向传播: 这是PyTorch的自动微分引擎Autograd计算梯度的地方。调用
$loss.backward()计算损失标量相对于每个requires_grad=True的模型参数的梯度(nn.Module中参数的默认设置)。这些梯度表示损失对每个参数变化的敏感度;本质上,它们告诉优化器如何调整每个权重以降低损失。// 代码:通过反向传播计算梯度 loss.backward() -
更新权重(优化器步进): 计算出梯度后,优化器现在可以调整模型的参数了。调用
$optimizer.step()根据计算出的梯度和优化器的特定算法(如带动量的SGD、Adam等)更新每个参数。目标是朝着最小化损失的方向迈出一小步。// 代码:更新模型参数 optimizer.step()
训练循环中的一次迭代是模型处理数据并更新参数的完整单元。这个循环为DataLoader提供的每个批次重复进行。一旦所有批次处理完毕,一个周期就完成了,外层循环开始下一个周期,重复整个批次迭代过程。
训练周期开始批次迭代1. 获取批次(数据 + 标签)2. 梯度清零optimizer.zero_grad()3. 前向传播predictions = model(inputs)4. 计算损失loss = criterion(preds, labels)5. 反向传播loss.backward()6. 更新权重optimizer.step()结束批次迭代(为下一个批次重复)
流程图显示了PyTorch训练循环单次批次迭代中的操作顺序。
设置模型、损失函数和优化器
在进入训练的迭代过程之前,我们需要准备好核心部件:模型本身、衡量其误差的方法(损失函数)以及根据误差更新模型的机制(优化器)。这个准备阶段确保所有必需的组件都已初始化并为训练循环做好了准备。
实例化模型
首先,你需要一个神经网络模型的实例。定义自定义网络结构通常涉及继承torch.nn.Module。创建一个模型类的对象即可:
// 假设 'SimpleNet' 是你之前定义的自定义 nn.Module 类
val model = SimpleNet(input_size=784, hidden_size=128, output_size=10)
println(model)
这会创建网络结构,包括其所有层和参数(权重和偏置)。最初,这些参数具有随机值(或者如果你实现了特定的初始化方案,则由这些方案确定的值)。
将模型移动到正确设备
深度学习计算,特别是训练,在GPU上速度要快得多。PyTorch使得将模型移动到合适的设备(CPU或GPU)变得简单。一种好的做法是尽早定义目标设备,然后始终将模型和数据都移动到该设备上。
import torch
// 确认可用设备
val device = if (torch.cuda.is_available()) "cuda" else "cpu"
println(f"Using {device} device")
// 将模型移动到所选设备
model.to(device)
执行model.to(device)会原地修改模型,如果CUDA可用,则将其所有参数和缓冲区移动到GPU内存中,否则保留在CPU上。请记住,任何参与模型计算的张量(如输入数据)也必须位于相同的设备上。我们将在训练循环内部处理数据张量的移动。
定义损失函数
损失函数,常被称为准则函数,衡量模型预测与实际目标值之间的距离。PyTorch在torch.nn模块中提供了许多标准损失函数。选择哪种损失函数很大程度上取决于你正在解决的问题类型(例如,回归、分类)。
对于多分类问题,nn.CrossEntropyLoss很常用。它在一个高效的类中结合了nn.LogSoftmax和nn.NLLLoss(负对数似然损失)。
// 用于多分类
val loss_fn = torch.nn.CrossEntropyLoss()
// 用于回归问题(预测连续值)
loss_fn = torch.nn.MSELoss() // 均方误差损失
你像实例化模型一样实例化所选的损失函数。这个loss_fn对象稍后将在训练循环中被调用,通常接收模型的输出和真实标签作为输入,以计算一个标量损失值。
配置优化器
优化器实现了一种算法(如随机梯度下降或Adam),用于根据反向传播期间计算的梯度调整模型的参数。其作用是使损失函数最小化。优化器位于torch.optim包中。
初始化优化器时,你必须提供两个重要的参数:
- 模型的参数: 你告诉优化器它应该更新哪些张量。这可以通过使用
model.parameters()轻松完成,该方法返回模型中所有可训练参数的迭代器。 - 学习率(
lr): 这个超参数控制参数更新的步长。找到一个合适的学习率对有效的训练很有帮助。这通常需要尝试。
import torch.optim as optim
// 使用随机梯度下降 (SGD)
val learning_rate = 0.01
val optimizer = optim.SGD(model.parameters(), lr=learning_rate)
// 或者,使用Adam优化器
optimizer = optim.Adam(model.parameters(), lr=0.001)
在这里,我们创建了一个SGD优化器实例。它现在持有model所有参数的引用,并且知道当其step()方法稍后被调用时要使用的学习率。不同的优化器可能还有额外的超参数(如SGD的momentum或Adam的betas),你可以在初始化时进行配置。
随着模型被实例化并移动到正确设备,损失函数被定义,以及优化器被配置来更新模型的参数,我们已经设置好了所有必需的组件。我们现在准备继续进行训练过程的核心:在训练循环中迭代数据并执行前向传播、损失计算、反向传播和参数更新。
使用 DataLoader 遍历数据
DataLoader 对象设计用于管理训练数据,处理批处理、数据混洗以及可能的并行加载。它有助于在训练循环中高效地迭代数据。作为 Python 可迭代对象,DataLoader 能够简单地在每个训练周期中系统地为模型提供数据批次。
标准做法是使用 Python 的 for 循环。在每次迭代中,DataLoader 会生成一个数据批次,通常包含输入特征及其对应的目标标签。
// 假设这些已定义并配置好:
// train_dataloader = DataLoader(your_dataset, batch_size=64, shuffle=True)
// model = YourNeuralNetwork()
// loss_fn = torch.nn.CrossEntropyLoss() // 损失函数示例
// optimizer = torch.optim.SGD(model.parameters(), lr=0.01) // 优化器示例
// device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
// model.to(device) // 确保模型在正确的设备上
val num_epochs = 10 // 遍历数据集的次数示例
// 外部循环,用于处理训练周期
for (epoch <- 0 until num_epochs) {
println(f"Epoch ${epoch+1}\n-------------------------------")
// 将模型设置为训练模式。
// 这会启用诸如 dropout 和批归一化更新之类的功能。
model.train()
// 内部循环,用于处理一个训练周期内的批次
// 遍历 DataLoader 提供的批次
for (batch_idx <- 0 until train_dataloader.size) {
// 1. 解包批次
// 结构取决于您的 Dataset 的 __getitem__ 方法。
// 对于监督学习,通常是 (inputs, labels)。
val data_batch = train_dataloader(batch_idx)
val inputs = data_batch._1
val labels = data_batch._2
// 2. 将数据移动到目标设备(GPU 或 CPU)
// 这必须与模型所在的设备匹配。
inputs = inputs.to(device)
labels = labels.to(device)
// 3. 前向传播:计算模型输出
// 使用“inputs”和“labels”在此处进行。 <---
// (这些将在后续部分详细说明)
// 后续逻辑放置的示例占位符:
val predictions = model(inputs)
val loss = loss_fn(predictions, labels)
// 4. 计算损失
// 5. 反向传播:计算梯度
// 这会填充模型参数的 `.grad` 属性。
loss.backward()
// 6. 更新模型参数
// 优化器根据梯度执行参数更新。
optimizer.step()
// 7. 清空梯度缓冲区
// 这是必要的,因为 PyTorch 会累积梯度。
optimizer.zero_grad()
// 可选:定期打印进度
if batch_idx % 100 == 0:
val current_batch_size = inputs.size(0) // 获取当前批次的大小
// 将 0.0 替换为实际计算出的损失值,用于记录
val current_loss = 0.0
print(f" Batch {batch_idx}: [{current_batch_size} samples] Current Loss: {current_loss:.4f}") # 示例日志
// ---> 通常在此之后进行验证数据的评估循环 <---
// (我们将在本章后面介绍评估循环)
println("训练完成!")
让我们来分析一下这个内部循环的主要部分:
- 训练周期循环:外部的
for epoch in range(num_epochs):循环控制着我们遍历整个数据集的次数。 - 设置训练模式:
model.train()在每个训练周期开始时被调用。这很要紧,因为像torch.nn.Dropout或torch.nn.BatchNorm2d这样的层在训练期间(例如,应用 dropout、更新运行统计数据)与评估时有不同的行为。这个调用能确保它们处于正确的模式。 - 遍历 DataLoader:
for batch_idx, data_batch in enumerate(train_dataloader):是核心的迭代操作。enumerate提供了一个批次计数器 (batch_idx),而train_dataloader一次生成一个data_batch。 - 解包数据:我们将
data_batch解包成inputs和labels。DataLoader返回的结构与它所包装的Dataset的__getitem__方法返回的结构直接对应。对于典型的监督任务,这通常是包含特征和目标的元组或列表。 - 将数据移动到设备:
inputs.to(device)和labels.to(device)是不可或缺的步骤。神经网络计算,特别是模型的前向传播,要求模型的参数和输入数据位于相同的计算设备上(例如,都位于 CPU,或都位于特定的 GPU)。这一步将DataLoader获取的批次数据(通常在 CPU 内存中)移动到放置模型的设备上。未能进行此同步是运行时错误的常见原因。这还能确保,如果device设置为 CUDA 设备,计算可以从 GPU 加速中受益。
值得一提的是,如果你的 DataLoader 在初始化时设置了 drop_last=False(这是默认设置),一个训练周期中生成的最后一个批次可能包含比指定 batch_size 更少的样本。如果数据集中的样本总数不能被批次大小完全整除,就会发生这种情况。PyTorch 操作通常能很好地处理可变批次大小,但如果你进行任何假定固定批次大小的计算(例如对固定数量的损失求平均),请注意这一点。
当数据批次 (inputs, labels) 成功加载到目标设备后,你现在已为训练迭代循环中的主要计算步骤做好了充分准备:
- 将
inputs送入模型以获得预测(前向传播)。 - 使用损失函数将预测与
labels进行比较(计算损失)。 - 根据损失计算梯度(反向传播)。
- 使用优化器更新模型的权重(更新权重)。
这些步骤针对每个批次重复执行,构成了模型训练过程的核心,也是后续章节的重点。
前向传播:获取预测结果
设置好模型、损失函数、优化器和数据加载器后,训练循环的核心部分便开始了。每次迭代中的第一个操作步骤是前向传播。在此步骤中,模型接收当前批次的输入数据并生成预测结果。
可以将前向传播看作信息流经神经网络的过程,从输入层,经过所有隐藏层,最终到达输出层。每个层都会对其从前一个层接收到的数据执行其定义的计算。
PyTorch中前向传播的工作原理
前向传播在PyTorch中是一个直接的过程。通过继承torch.nn.Module,用户可以在模型的forward方法中明确定义其操作的结构和顺序。
PyTorch的nn.Module基类提供了一个__call__方法。这使您能够将模型实例当作函数一样使用。当您调用model(inputs)时,PyTorch会隐式地执行您定义的forward方法,将inputs传递通过网络的各层。
实现前向传播
在您的训练循环内部,从DataLoader获取一个批次的数据(特征和标签)后,您将特征(输入)送入您的模型:
// 假设 'model' 是您的 nn.Module 子类的一个实例
// 假设 'data_batch' 是从 DataLoader 加载的
// 假设 'device' 已定义(例如,'cuda' 或 'cpu')
// 解包批次数据(根据您的 DataLoader 结构进行调整)
val inputs = data_batch._1
val labels = data_batch._2
inputs = inputs.to(device) // 将输入数据移动到正确的设备上
labels = labels.to(device) // 将标签移动到正确的设备上(损失计算需要)
// --- 前向传播 ---
// 将输入通过模型
val outputs = model(inputs)
// -----------------------
// 'outputs' 现在包含模型对 'inputs' 批次的预测结果。
// 下一步将使用这些 'outputs' 和 'labels' 计算损失。
以下是此步骤的可视化表示:
DataLoader 批次(输入, 标签)输入(在设备上)提取并移至设备model(inputs)前向送入输出(预测/Logits)
在前向传播过程中数据流向:加载一个批次,输入数据被准备并发送到合适的设备,然后通过模型生成输出。
理解输出
前向传播生成的outputs张量包含了模型对给定输入批次的预测结果。这些预测结果的具体性质取决于模型的最后一层和任务:
- 分类: 通常,输出代表每个类别的原始分数,称为 logits。这些logits通常会传递给像
nn.CrossEntropyLoss这样的损失函数,该函数会在内部应用Softmax函数。 - 回归: 输出可能直接代表预测的连续值。
- 其他任务: 对于像分割或目标检测这样的任务,输出结构会更复杂,以反映任务的具体要求。
务必确保您的输入数据(inputs)和模型(model)位于相同的计算设备上(CPU或特定的GPU)。如果它们在不同的设备上,PyTorch会抛出运行时错误。如代码片段所示,使用.to(device)将数据批次移动到指定的device是防止这种情况发生的标准做法。
前向传播根据模型的当前参数(权重和偏差)计算模型的预测结果。这些预测结果随后将在下一步:计算损失中与真实标签进行比较。
计算损失
模型根据输入数据批次生成预测(通常称为 outputs 或 logits)。为了评估这些预测与实际真实标签的匹配程度,需要一个评估机制。这就是损失函数的作用。
量化模型误差
一个损失函数,也称为标准或目标函数,它以数学方式衡量模型预测(y*y*)与真实目标值(yy)之间的差异。训练的目的通常是使这个损失值最小化。损失越小表示模型的预测越接近给定数据批次的实际目标。
在PyTorch中,损失函数在 torch.nn 模块中随即可用,就像模型层和激活函数一样。您通常在训练循环外部只实例化一次损失函数。常见选择包括:
nn.MSELoss(均方误差):常用于回归任务,目标是预测连续值。它计算预测和目标之间的平均平方差。LMSE=1N∑i=1N(yi−yi)2*L**MSE*=*N*1*i*=1∑*N*(*y**i*−*y*i)2其中 NN 是批次中的样本数量。nn.CrossEntropyLoss:是多类别分类问题的标准选择。此标准在一个类中方便地结合了nn.LogSoftmax和nn.NLLLoss(负对数似然损失)。它需要来自模型最后一层的原始、未归一化的分数(logits)作为输入,以及目标类别索引(整数)作为标签。nn.BCEWithLogitsLoss:用于二元分类或多标签分类任务。与CrossEntropyLoss类似,它在一个步骤中结合了 Sigmoid 层和二元交叉熵损失,以获得更好的数值稳定性。它也需要原始 logits 作为输入。
在PyTorch中计算损失
一旦您实例化了所选的标准(例如,criterion = nn.CrossEntropyLoss()),在循环中计算损失就很直接。您只需将模型的输出张量和包含真实标签的张量传递给标准对象即可:
// --- 训练循环内部 ---
// 假设:
// model: 您的神经网络模型(例如,nn.Module 实例)
// criterion: 您选择的损失函数(例如,nn.CrossEntropyLoss())
// inputs: 来自 DataLoader 的输入数据批次
// labels: 来自 DataLoader 的相应真实标签批次
// 1. 正向传播(已完成)
val outputs = model(inputs)
// 2. 计算损失
val loss = criterion(outputs, labels)
// 'loss' 现在包含当前批次的计算损失值。
// 它是一个标量张量(只有一个元素的张量)。
// 3. 后续步骤:反向传播 (loss.backward()),优化器步进...
// --- 循环迭代片段结束 ---
理解生成的 loss 变量代表什么很重要:
- 标量值: 它通常是一个数值(一个零维张量),表示整个批次的平均损失。
- 计算图连接: 重要的是,这个
loss张量仍然连接着 PyTorch 在正向传播期间构建的计算图。它知道哪些操作和哪些模型参数促成了它的最终值。 - 梯度计算已启用: 因为它连接着计算图并依赖于
requires_grad=True的参数,所以loss张量本身隐式地具有requires_grad=True。这使得我们可以在下一步调用loss.backward(),自动计算损失相对于模型所有可学习参数(∇θL∇θ**L)的梯度。
这个计算出的 loss 值作为反向传播过程的起点,它调整模型的权重,以期在后续迭代中产生更低的损失。
反向传播:计算梯度
您已经成功计算出损失,它衡量了模型预测与实际目标值之间的偏离程度。接下来一个重要步骤是了解 如何 调整模型参数(权重和偏置)以减小此损失。这就是反向传播发挥作用的地方。
PyTorch 使用 .backward() 方法执行反向传播。当此方法在损失张量上调用时,模型参数的梯度便被计算出来。
// 假设 'loss' 是您的损失函数产生的标量输出
loss.backward()
调用 loss.backward() 会触发 PyTorch 的自动微分引擎 Autograd。回忆一下第 3 章,PyTorch 如何在正向传播过程中执行运算时动态构建计算图?此图记录了从输入数据和模型参数到最终损失值的运算序列。
backward() 调用会启动对此图的 逆向 遍历,从损失张量本身开始。利用微积分链式法则,Autograd 高效地计算图中每个 requires_grad 属性设为 True 的张量相对于损失的梯度。
具体来说,对于模型中参与了损失计算的每个参数 θθ(例如 nn.Linear 或 nn.Conv2d 层中的权重和偏置),loss.backward() 会计算偏导数:
∂L∂θ∂θ∂L
这个梯度 ∂L∂θ∂θ∂L 代表了损失 LL 对参数 θθ 微小变化的敏感度。它说明了 θθ 为减小损失所需变化的方向和大小。
这些计算出的梯度会存放在哪里?PyTorch 会直接将其存储在对应参数张量的 .grad 属性中。
// 示例:反向传播后访问线性层权重的梯度
// model = nn.Linear(10, 1)
// ... (正向传播和损失计算) ...
// loss.backward()
// 现在您可以检查梯度
println(model.weight.grad)
println(model.bias.grad)
在调用 loss.backward() 后,您通常会在模型参数的 .grad 属性中发现非 None 值。计算图中的中间张量的梯度通常不保留以节省内存,尽管如果调试或高级技术需要,可以修改此行为。通过 nn.Module 定义的模型参数被认为是图中的“叶”节点,它们的梯度会被保留。
正向传播反向传播 (loss.backward())输入 (X)预测值 (Y_pred)* W + B权重 (W)requires_grad=TruedL/dW存储于 W.grad偏置 (B)requires_grad=TruedL/dB存储于 B.grad损失 (L)损失函数目标值 (Y_true)dL/dL = 1dL/dY_pred
正向传播构建计算图(实线)。调用
loss.backward()启动反向传播(虚线),从损失开始计算梯度,并将它们存储在W和B等张量的.grad属性中。
梯度累加
一个需要理解的重要行为是 PyTorch 会 累加 梯度。当您调用 loss.backward() 时,新计算出的梯度会 添加 到参数的 .grad 属性中已有的值上。它们不会覆盖之前的值。
这种累加是设计意图,且在特定情况下有用,例如模拟更大的批量大小或训练循环神经网络 (RNN)。然而,在每次处理一个批量的标准训练循环中,您通常只想根据当前批量计算梯度。如果您不清除上次迭代的梯度,就会累积多个批量的梯度,从而导致参数更新不正确。
正因如此,如本章引言中所述并将在后面详细说明,您必须在每个训练迭代开始时,通常在正向传播 之前 或恰好在调用 loss.backward() 之前,显式地将梯度归零。执行此操作的标准方法涉及优化器,即使用 optimizer.zero_grad()。
在梯度计算完成并存储在参数的 .grad 属性中之后,下一步便是使用这些梯度更新模型的参数,这由优化器的 step() 方法处理。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)