提高工作效率的PyTorch技巧
PyTorch技巧提高效率
介绍
你是否曾经花费数小时来调试一个机器学习模型,却找不到准确率不提高的原因?你是否曾经觉得一切都应该完美运行,但由于某种神秘的原因你却没有得到出色的结果?
不再有这种情况了。作为初学者,探索PyTorch可能令人望而生畏。在本文中,您将探索经过验证的工作流程,这些工作流程肯定会改善您的结果并提高模型的性能。
1. 过度拟合单批次数据
是否曾经在大型数据集上训练了数小时的模型,却发现损失不再下降,准确率也趋于平稳?那么,请先进行一次合理性检查。
在大型数据集上进行训练和评估可能很耗时,而且在数据的一个小子集上首先调试模型更容易。一旦我们确定模型可以工作,我们就可以轻松地将训练扩展到完整的数据集。
不要在整个数据集上进行训练,始终在单个批次上进行训练以进行合理性检查。
batch = next(iter(train_dataloader)) # 获取单个批次
# 对于所有的epochs,持续在单个批次上训练。
for epoch in range(num_epochs):
inputs, targets = batch
predictions = model.train(inputs)
考虑上面的代码片段。假设我们已经有了一个训练数据加载器和一个模型。我们可以轻松地获取数据集的第一个批次,而不是遍历整个数据集。然后,我们可以在单个批次上进行训练,以检查模型是否能够学习数据的模式和变化。
如果损失减小到一个非常小的值,我们就知道模型可以过度拟合这些数据,并且可以确信它可以在短时间内进行学习。然后,我们可以通过仅更改一行代码来将其训练到完整的数据集上:
# 对于所有的epochs,遍历所有的数据批次。
for epoch in range(num_epochs):
for batch in iter(dataloader):
inputs, targets = batch
predictions = model.train(inputs)
如果模型可以过度拟合单个批次数据,它应该能够学习完整数据集中的模式。这种过度拟合批次的方法可以更容易地进行调试。如果模型甚至不能过度拟合一个单个批次,我们可以确定问题出在模型实现上,而不是数据集上。
2. 数据归一化和洗牌
对于数据序列不重要的数据集,洗牌数据是很有帮助的。例如,对于图像分类任务,如果将不同类别的图像放入单个批次中,模型会更好地适应数据。 如果传递的数据按照相同的顺序,我们将面临模型基于数据传递的顺序学习模式的风险,而不是学习数据内在的变化。因此,最好传递洗牌后的数据。为此,我们可以简单地使用PyTorch提供的DataLoader对象,并将shuffle设置为True。
from torch.utils.data import DataLoader
dataset = # 加载数据
dataloder = DataLoader(dataset, shuffle=True)
此外,当使用机器学习模型时,归一化数据是很重要的。当我们的数据存在较大的方差,并且某个特定参数的值高于数据集中所有其他属性时,归一化就显得尤为重要。这可能导致其中一个参数主导其他所有参数,从而降低准确性。我们希望所有输入参数都处于相同的范围内,最好是具有0的均值和1.0的方差。为此,我们必须对数据集进行转换。通过了解数据集的均值和方差,我们可以简单地使用torchvision.transforms.Normalize函数。
import torchvision.transforms as transforms
image_transforms = transforms.Compose([
transforms.ToTensor(),
# 对我们的数据中的值进行归一化
transforms.Normalize(mean=(0.5,), std=(0.5))
])
我们可以将每个通道的均值和标准差传递给transforms.Normalize函数,它将自动将数据转换为具有0均值和标准差为1的数据。
3. 梯度裁剪
梯度爆炸是RNN和LSTM中已知的问题。然而,它不仅限于这些架构。任何具有深层的模型都可能遭受梯度爆炸的问题。在高梯度上的反向传播可能导致发散而不是逐渐减少的损失。
考虑下面的代码片段。
for epoch in range(num_epochs):
for batch in iter(train_dataloader):
inputs, targets = batch
predictions = model(inputs)
optimizer.zero_grad() # 移除所有先前的梯度
loss = criterion(targets, predictions)
loss.backward() # 计算模型权重的梯度
# 将模型权重的梯度裁剪到指定的max_norm值
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1)
# 裁剪后优化模型权重
optimizer.step()
为了解决梯度爆炸问题,我们使用梯度裁剪技术,将梯度值裁剪在指定的范围内。例如,如果我们将1作为裁剪或范数值,所有梯度将被裁剪在[-1, 1]范围内。如果我们有一个梯度值为50的梯度爆炸,它将被裁剪为1。因此,梯度裁剪解决了梯度爆炸问题,使得模型能够缓慢地优化朝着收敛方向。
4. 切换训练/评估模式
这一行代码肯定会提高模型的测试准确性。几乎所有深度学习模型都会使用dropout和归一化层。这些只在稳定的训练中才需要,以确保模型不会因数据的变化而过拟合或发散。诸如BatchNorm和Dropout的层在训练期间为模型参数提供了正则化。然而,一旦训练完毕,它们就不再需要了。 将模型切换到评估模式会禁用仅在训练中需要的层,并使用完整的模型参数进行预测。
为了更好地理解,考虑下面的代码片段。
for epoch in range(num_epochs):
# 在训练数据集上进行迭代时使用训练模式
model.train()
for batch in iter(train_dataloader):
# 训练代码和损失优化
# 在验证数据集上进行准确性检查时使用评估模式
model.eval()
for batch in iter(val_dataloader):
# 仅进行预测和损失计算,没有反向传播
# 没有优化器步骤,因此我们可以省略不需要的层。
在评估过程中,我们不需要对模型参数进行任何优化。我们在验证步骤中不计算任何梯度。为了更好地进行评估,我们可以省略Dropout和其他归一化层。例如,它将启用所有模型参数,而不仅仅是像Dropout层中的一部分权重。这将大大提高模型的准确性,因为您将能够使用完整的模型。
5. 使用Module和ModuleList
PyTorch模型通常继承自torch.nn.Module基类。根据文档:
以这种方式分配的子模块将被注册,并且在调用to()等函数时会转换其参数。
模块基类允许注册模型中的每个层。然后我们可以使用model.to()和类似的函数,如model.train()和model.eval(),它们将应用于模型中的每个层。如果不这样做,将不会更改模型中包含的每个层的设备或训练模式。您将不得不手动执行此操作。 一旦您简单地在模型对象上使用一个函数,模块基类将自动为您进行转换。
此外,一些模型包含类似的连续层,可以使用for循环轻松初始化,并包含在列表中。这简化了代码。然而,它引起了与上述相同的问题,因为简单的Python列表中的模块不会自动注册到模型中。 我们应该使用ModuleList来包含模型中的类似连续层。
import torch
import torch.nn as nn
# 从Module基类继承
class Model(nn.Module):
def __init__(self, input_size, output_size):
# 初始化Module的父类
super().__init__()
self.dense_layers = nn.ModuleList()
# 添加5个线性层,并将它们包含在ModuleList中
for i in range(5):
self.dense_layers.append(
nn.Linear(input_size, 512)
)
self.output_layer = nn.Linear(512, output_size)
def forward(self, x):
# 简化前向传播。
# 使用循环而不是为每个层重复一行代码
for layer in range(len(self.dense_layers)):
x = layer(x)
return self.output_layer(x)
上面的代码片段展示了使用模型和子层创建模型的正确方式。在训练和评估模型时,使用Module和ModuleList可以避免意外错误。
结论
上述方法是PyTorch机器学习框架的最佳实践。它们被广泛使用,并且得到了PyTorch文档的推荐。使用这些方法应该是机器学习代码流程的主要方式,肯定会提高结果。
穆罕默德·阿尔哈姆(Muhammad Arham)是一名深度学习工程师,专注于计算机视觉和自然语言处理。他曾在Vyro.AI部署和优化多个生成式人工智能应用,这些应用在全球排行榜上名列前茅。他热衷于构建和优化智能系统的机器学习模型,并坚信不断改进。