【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()前向传播将输入特征批次送入你的模型。模型通过其层处理数据应用学习到的权重和激活函数最终生成一批预测或输出。// 代码获取模型预测valpredictionsmodel(input_batch)计算损失使用你选择的损失函数准则将模型的predictions与真实的target_batch进行比较例如用于分类的nn.CrossEntropyLoss或用于回归的nn.MSELoss。损失函数返回一个单一的标量值表示当前批次的平均误差或差异。这个值显示了模型在这个特定批次上的表现好坏。// 代码计算损失vallosscriterion(predictions,target_batch)反向传播这是PyTorch的自动微分引擎Autograd计算梯度的地方。调用$loss.backward()计算损失标量相对于每个requires_gradTrue的模型参数的梯度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 类valmodelSimpleNet(input_size784,hidden_size128,output_size10)println(model)这会创建网络结构包括其所有层和参数权重和偏置。最初这些参数具有随机值或者如果你实现了特定的初始化方案则由这些方案确定的值。将模型移动到正确设备深度学习计算特别是训练在GPU上速度要快得多。PyTorch使得将模型移动到合适的设备CPU或GPU变得简单。一种好的做法是尽早定义目标设备然后始终将模型和数据都移动到该设备上。importtorch// 确认可用设备valdeviceif(torch.cuda.is_available())cudaelsecpuprintln(fUsing {device} device)// 将模型移动到所选设备model.to(device)执行model.to(device)会原地修改模型如果CUDA可用则将其所有参数和缓冲区移动到GPU内存中否则保留在CPU上。请记住任何参与模型计算的张量如输入数据也必须位于相同的设备上。我们将在训练循环内部处理数据张量的移动。定义损失函数损失函数常被称为准则函数衡量模型预测与实际目标值之间的距离。PyTorch在torch.nn模块中提供了许多标准损失函数。选择哪种损失函数很大程度上取决于你正在解决的问题类型例如回归、分类。对于多分类问题nn.CrossEntropyLoss很常用。它在一个高效的类中结合了nn.LogSoftmax和nn.NLLLoss负对数似然损失。// 用于多分类valloss_fntorch.nn.CrossEntropyLoss()// 用于回归问题预测连续值loss_fntorch.nn.MSELoss()// 均方误差损失你像实例化模型一样实例化所选的损失函数。这个loss_fn对象稍后将在训练循环中被调用通常接收模型的输出和真实标签作为输入以计算一个标量损失值。配置优化器优化器实现了一种算法如随机梯度下降或Adam用于根据反向传播期间计算的梯度调整模型的参数。其作用是使损失函数最小化。优化器位于torch.optim包中。初始化优化器时你必须提供两个重要的参数模型的参数你告诉优化器它应该更新哪些张量。这可以通过使用model.parameters()轻松完成该方法返回模型中所有可训练参数的迭代器。学习率lr这个超参数控制参数更新的步长。找到一个合适的学习率对有效的训练很有帮助。这通常需要尝试。importtorch.optimas optim// 使用随机梯度下降 (SGD)vallearning_rate0.01valoptimizeroptim.SGD(model.parameters(),lrlearning_rate)// 或者使用Adam优化器optimizeroptim.Adam(model.parameters(),lr0.001)在这里我们创建了一个SGD优化器实例。它现在持有model所有参数的引用并且知道当其step()方法稍后被调用时要使用的学习率。不同的优化器可能还有额外的超参数如SGD的momentum或Adam的betas你可以在初始化时进行配置。随着模型被实例化并移动到正确设备损失函数被定义以及优化器被配置来更新模型的参数我们已经设置好了所有必需的组件。我们现在准备继续进行训练过程的核心在训练循环中迭代数据并执行前向传播、损失计算、反向传播和参数更新。使用 DataLoader 遍历数据DataLoader对象设计用于管理训练数据处理批处理、数据混洗以及可能的并行加载。它有助于在训练循环中高效地迭代数据。作为 Python 可迭代对象DataLoader能够简单地在每个训练周期中系统地为模型提供数据批次。标准做法是使用 Python 的for循环。在每次迭代中DataLoader会生成一个数据批次通常包含输入特征及其对应的目标标签。// 假设这些已定义并配置好// train_dataloader DataLoader(your_dataset, batch_size64, shuffleTrue)// model YourNeuralNetwork()// loss_fn torch.nn.CrossEntropyLoss() // 损失函数示例// optimizer torch.optim.SGD(model.parameters(), lr0.01) // 优化器示例// device torch.device(cuda if torch.cuda.is_available() else cpu)// model.to(device) // 确保模型在正确的设备上valnum_epochs10// 遍历数据集的次数示例// 外部循环用于处理训练周期for(epoch-0until num_epochs){println(fEpoch${epoch1}\n-------------------------------)// 将模型设置为训练模式。// 这会启用诸如 dropout 和批归一化更新之类的功能。model.train()// 内部循环用于处理一个训练周期内的批次// 遍历 DataLoader 提供的批次for(batch_idx-0until train_dataloader.size){// 1. 解包批次// 结构取决于您的 Dataset 的 __getitem__ 方法。// 对于监督学习通常是 (inputs, labels)。valdata_batchtrain_dataloader(batch_idx)valinputsdata_batch._1vallabelsdata_batch._2// 2. 将数据移动到目标设备GPU 或 CPU// 这必须与模型所在的设备匹配。inputsinputs.to(device)labelslabels.to(device)// 3. 前向传播计算模型输出// 使用“inputs”和“labels”在此处进行。 ---// 这些将在后续部分详细说明// 后续逻辑放置的示例占位符valpredictionsmodel(inputs)vallossloss_fn(predictions,labels)// 4. 计算损失// 5. 反向传播计算梯度// 这会填充模型参数的 .grad 属性。loss.backward()// 6. 更新模型参数// 优化器根据梯度执行参数更新。optimizer.step()// 7. 清空梯度缓冲区// 这是必要的因为 PyTorch 会累积梯度。optimizer.zero_grad()// 可选定期打印进度ifbatch_idx%1000:valcurrent_batch_sizeinputs.size(0)// 获取当前批次的大小// 将 0.0 替换为实际计算出的损失值用于记录valcurrent_loss0.0print(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、更新运行统计数据与评估时有不同的行为。这个调用能确保它们处于正确的模式。遍历 DataLoaderfor 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_lastFalse这是默认设置一个训练周期中生成的最后一个批次可能包含比指定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 结构进行调整valinputsdata_batch._1vallabelsdata_batch._2 inputsinputs.to(device)// 将输入数据移动到正确的设备上labelslabels.to(device)// 将标签移动到正确的设备上损失计算需要// --- 前向传播 ---// 将输入通过模型valoutputsmodel(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均方误差常用于回归任务目标是预测连续值。它计算预测和目标之间的平均平方差。LMSE1N∑i1N(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. 正向传播已完成valoutputsmodel(inputs)// 2. 计算损失vallosscriterion(outputs,labels)// loss 现在包含当前批次的计算损失值。// 它是一个标量张量只有一个元素的张量。// 3. 后续步骤反向传播 (loss.backward())优化器步进...// --- 循环迭代片段结束 ---理解生成的loss变量代表什么很重要标量值它通常是一个数值一个零维张量表示整个批次的平均损失。计算图连接重要的是这个loss张量仍然连接着 PyTorch 在正向传播期间构建的计算图。它知道哪些操作和哪些模型参数促成了它的最终值。梯度计算已启用因为它连接着计算图并依赖于requires_gradTrue的参数所以loss张量本身隐式地具有requires_gradTrue。这使得我们可以在下一步调用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_gradTruedL/dW存储于 W.grad偏置 (B)requires_gradTruedL/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()方法处理。