pytorch常用代码梯度篇(梯度裁剪、梯度累积、冻结预训练层等)

ptorch常用代码梯度篇(梯度裁剪、梯度累积、冻结预训练层等)

梯度裁剪(Gradient Clipping)#

在训练比较深或者循环神经网络模型的过程中,我们有可能发生梯度爆炸的情况,这样会导致我们模型训练无法收敛。 我们可以采取一个简单的策略来避免梯度的爆炸,那就是梯度截断 Clip, 将梯度约束在某一个区间之内,在训练的过程中,在优化器更新之前进行梯度截断操作!!!!! 注意这个方法只在训练的时候使用,在测试的时候验证和测试的时候不用。

整个流程简单总结如下:

  1. 加载训练数据和标签
  2. 模型输入输出
  3. 计算 loss 函数值
  4. loss 反向传播
  5. 梯度截断
  6. 优化器更新梯度参数
import torch.nn as nn
outputs = model(data)
loss= loss_fn(outputs, target)
loss.backward()
nn.utils.clip_grad_norm_(model.parameters(), max_norm=20, norm_type=2)
optimizer.step()
optimizer.zero_grad()

nn.utils.clip_grad_norm_ 输入是(NN 参数,最大梯度范数,范数类型 = 2) 一般默认为 L2 范数。

梯度累积#

常规网络如下:

# 正常网络
optimizer.zero_grad()
for idx, (x, y) in enumerate(train_loader):
    pred = model(x)
    loss = criterion(pred, y)
    
    loss.backward()
    optimizer.step()
    optimizer.zero_grad()
    
    if (idx+1) % eval_steps == 0:
        eval()

需要梯度累计时,每个 mini-batch 仍然正常前向传播以及反向传播,但是反向传播之后并不进行梯度清零,因为 PyTorch 中的 loss.backward() 执行的是梯度累加的操作,所以当我们调用 4 次 loss.backward() 后,这 4 个 mini-batch 的梯度都会累加起来。但是,我们需要的是一个平均的梯度,或者说平均的损失,所以我们应该将每次计算得到的 loss除以 accum_steps

# 梯度累积

accum_steps = 4
optimizer.zero_grad()
for idx, (x, y) in enumerate(train_loader):
    pred = model(x)
    loss = criterion(pred, y)
    
    # normlize loss to account for batch accumulation
    loss = loss / accum_steps
    
    loss.backward()
    
    if (idx+1) % accum_steps == 0 or (idx+1) == len(train_loader):
        optimizer.step()
        optimizer.zero_grad()
    if (idx+1) % eval_steps == 0:
            eval()

总的来说,梯度累加就是计算完每个 mini-batch 的梯度后不清零,而是做梯度的累加,当累加到一定的次数之后再更新网络参数,然后将梯度清零。通过这种延迟更新的手段,可以实现与采用大 batch_size 相近的效果

冻结某些层#

在加载预训练模型的时候,我们有时想冻结前面几层,使其参数在训练过程中不发生变化。

def freeze(module):
    """
    Freezes module's parameters.
    """
    
    for parameter in module.parameters():
        parameter.requires_grad = False
        
def get_freezed_parameters(module):
    """
    Returns names of freezed parameters of the given module.
    """
    
    freezed_parameters = []
    for name, parameter in module.named_parameters():
        if not parameter.requires_grad:
            freezed_parameters.append(name)
            
    return freezed_parameters
import torch
from transformers import AutoConfig, AutoModel


# initializing model
model_path = "microsoft/deberta-v3-base"
config = AutoConfig.from_pretrained(model_path)
model = AutoModel.from_pretrained(model_path, config=config)


# freezing embeddings and first 2 layers of encoder
freeze(model.embeddings)
freeze(model.encoder.layer[:2])

freezed_parameters = get_freezed_parameters(model)
print(f"Freezed parameters: {freezed_parameters}")

# selecting parameters, which requires gradients and initializing optimizer
model_parameters = filter(lambda parameter: parameter.requires_grad, model.parameters())
optimizer = torch.optim.AdamW(params=model_parameters, lr=2e-5, weight_decay=0.0)
Freezed parameters: ['embeddings.word_embeddings.weight', 'embeddings.LayerNorm.weight', 'embeddings.LayerNorm.bias', 'encoder.layer.0.attention.self.query_proj.weight', 'encoder.layer.0.attention.self.query_proj.bias', 'encoder.layer.0.attention.self.key_proj.weight', 'encoder.layer.0.attention.self.key_proj.bias', 'encoder.layer.0.attention.self.value_proj.weight', 'encoder.layer.0.attention.self.value_proj.bias', 'encoder.layer.0.attention.output.dense.weight', 'encoder.layer.0.attention.output.dense.bias', 'encoder.layer.0.attention.output.LayerNorm.weight', 'encoder.layer.0.attention.output.LayerNorm.bias', 'encoder.layer.0.intermediate.dense.weight', 'encoder.layer.0.intermediate.dense.bias', 'encoder.layer.0.output.dense.weight', 'encoder.layer.0.output.dense.bias', 'encoder.layer.0.output.LayerNorm.weight', 'encoder.layer.0.output.LayerNorm.bias', 'encoder.layer.1.attention.self.query_proj.weight', 'encoder.layer.1.attention.self.query_proj.bias', 'encoder.layer.1.attention.self.key_proj.weight', 'encoder.layer.1.attention.self.key_proj.bias', 'encoder.layer.1.attention.self.value_proj.weight', 'encoder.layer.1.attention.self.value_proj.bias', 'encoder.layer.1.attention.output.dense.weight', 'encoder.layer.1.attention.output.dense.bias', 'encoder.layer.1.attention.output.LayerNorm.weight', 'encoder.layer.1.attention.output.LayerNorm.bias', 'encoder.layer.1.intermediate.dense.weight', 'encoder.layer.1.intermediate.dense.bias', 'encoder.layer.1.output.dense.weight', 'encoder.layer.1.output.dense.bias', 'encoder.layer.1.output.LayerNorm.weight', 'encoder.layer.1.output.LayerNorm.bias']

可以看到前两层的 weight 和 bias 的 requires_grad 都为 False,表示它们不可训练。

最后在定义优化器时,只对 requires_grad 为 True 的层的参数进行更新。(这里用filter筛选只传入了requires_grad为True的参数,但如果直接传入全部参数应该也可以达到只训练未冻结层参数的效果)

optimizer = optim.Adam(filter(lambda p: p.requires_grad, net.parameters()), lr=0.01)

其他注意事项#

  1. with torch.no_grad()或者@torch.no_grad()中的数据不需要计算梯度,也不会进行反向传播。不需要计算梯度的代码块(如验证测试)用 with torch.no_grad() 包含起来,节省显存
model.eval()
with torch.no_grad():
   pass
@torch.no_grad()
def eval():
	...
  1. model.eval() 和 torch.no_grad() 的区别在于,model.eval() 是将网络切换为测试状态,例如 BN 和dropout在训练和测试阶段使用不同的计算方法。torch.no_grad() 是关闭 PyTorch 张量的自动求导机制,以减少存储使用和加速计算,得到的结果无法进行 loss.backward()。

  2. model.zero_grad()会把整个模型的参数的梯度都归零, 而optimizer.zero_grad()只会把传入其中的参数的梯度归零.

  3. loss.backward() 前用 optimizer.zero_grad() 清除累积梯度。如果在循环里需要把optimizer.zero_grad()写在后面,那应该在循环外需要先调用一次optimizer.zero_grad()

  4. 查看网络中的梯度

params = list(model.named_parameters())
(name, param) = params[28]
print(name)
print(param.grad)
print('-------------------------------------------------')
0
0
« 上一篇: BERT如何进化-预训练语言模型研究进展【1】
» 下一篇: 优雅地实现多头自注意力——使用einsum(爱因斯坦求和)进行矩阵运算
posted @ 2022-05-07 17:15  MapleTx  阅读( 1206)  评论( 0编辑  收藏  举报
好的,这是一个比较完整的任务,需要分几个步骤来完成。 首先是数据集的处理。我们需要使用Fruits 360数据集,这个数据集里面包含了多种水果的图像,适合用来进行分类。我们可以使用PyTorch内置的`torchvision.datasets.ImageFolder`类来读取数据集。在读取之前,我们需要对数据集进行数据增强,增加一些随机变换的操作,以增加训练集的多样性,防止过拟合。以下是一个简单的数据增强函数: ```python import torchvision.transforms as transforms # 数据增强 train_transforms = transforms.Compose([ transforms.RandomResizedCrop(size=256, scale=(0.8, 1.0)), transforms.RandomRotation(degrees=15), transforms.ColorJitter(), transforms.RandomHorizontalFlip(), transforms.CenterCrop(size=224), # ImageNet标准尺寸 transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) # ImageNet标准归一化 ]) # 验证集和测试集只需要进行标准化操作 val_transforms = transforms.Compose([ transforms.Resize(size=256), transforms.CenterCrop(size=224), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) ``` 这里使用了`transforms`模块来进行数据增强。具体的变换包括随机裁剪、随机旋转、颜色抖动、随机水平翻转等,这些变换可以根据实际情况进行选择和调整。 然后我们可以读取数据集并应用数据增强: ```python from torchvision.datasets import ImageFolder from torch.utils.data import DataLoader # 读取数据集并应用数据增强 train_dataset = ImageFolder(root='fruits-360/Training/', transform=train_transforms) val_dataset = ImageFolder(root='fruits-360/Validation/', transform=val_transforms) test_dataset = ImageFolder(root='fruits-360/Test/', transform=val_transforms) # 使用DataLoader进行batch处理 train_loader = DataLoader(dataset=train_dataset, batch_size=32, shuffle=True, num_workers=4) val_loader = DataLoader(dataset=val_dataset, batch_size=32, shuffle=False, num_workers=4) test_loader = DataLoader(dataset=test_dataset, batch_size=32, shuffle=False, num_workers=4) ``` 接下来是模型的设计。我们可以使用一个预训练的ResNet50模型作为基础模型,再在其基础上添加一些自定义的全连接来进行分类。这样可以充分利用预训练模型的特征提取能力,同时也可以进行一定程度的模型微调。 ```python import torch.nn as nn import torchvision.models as models # 加载预训练模型 resnet = models.resnet50(pretrained=True) # 冻结所有卷积的参数 for param in resnet.parameters(): param.requires_grad = False # 替换最后一全连接 num_ftrs = resnet.fc.in_features resnet.fc = nn.Linear(num_ftrs, len(train_dataset.classes)) # 定义模型 model = resnet ``` 注意到这里我们将模型的最后一全连接替换成了一个新的全连接,输出的类别数为数据集中的类别数。这里还需要注意到,我们将所有卷积的参数都设置为不需要梯度更新,这样可以避免在微调过程中过多地调整网络的权重,从而保留模型的特征提取能力。 接下来是模型的训练。我们需要使用标准量化和批量归一化来提高训练的稳定性,同时也需要使用权重衰减、梯度裁剪和Adam优化来进行模型优化。 ```python import torch.optim as optim # 定义优化器和损失函数 optimizer = optim.Adam(model.parameters(), lr=0.001, weight_decay=0.001) criterion = nn.CrossEntropyLoss() # 定义训练函数 def train(model, optimizer, criterion, train_loader, val_loader, num_epochs=10, device='cpu'): best_acc = 0.0 for epoch in range(num_epochs): model.train() running_loss = 0.0 running_corrects = 0 for inputs, labels in train_loader: inputs, labels = inputs.to(device), labels.to(device) optimizer.zero_grad() outputs = model(inputs) loss = criterion(outputs, labels) loss.backward() nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) # 梯度裁剪 optimizer.step() running_loss += loss.item() * inputs.size(0) _, preds = torch.max(outputs, 1) running_corrects += torch.sum(preds == labels.data) epoch_loss = running_loss / len(train_loader.dataset) epoch_acc = running_corrects.double() / len(train_loader.dataset) print('Epoch {}/{}, Loss: {:.4f}, Acc: {:.4f}'.format(epoch+1, num_epochs, epoch_loss, epoch_acc)) # 在验证集上测试模型性能 model.eval() val_running_loss = 0.0 val_running_corrects = 0 for inputs, labels in val_loader: inputs, labels = inputs.to(device), labels.to(device) with torch.no_grad(): outputs = model(inputs) loss = criterion(outputs, labels) val_running_loss += loss.item() * inputs.size(0) _, preds = torch.max(outputs, 1) val_running_corrects += torch.sum(preds == labels.data) val_loss = val_running_loss / len(val_loader.dataset) val_acc = val_running_corrects.double() / len(val_loader.dataset) print('Val Loss: {:.4f}, Val Acc: {:.4f}'.format(val_loss, val_acc)) # 保存最好的模型 if val_acc > best_acc: best_acc = val_acc torch.save(model.state_dict(), 'model.pt') print('Training finished. Best Val Acc: {:.4f}'.format(best_acc)) ``` 这里的训练函数使用交叉熵损失函数,同时也进行了梯度裁剪和权重衰减。在每个epoch之后,还需要在验证集上测试模型的性能,并保存最好的模型。 最后是模型的测试和应用。我们可以读取训练好的模型,并在测试集上测试模型的性能。同时,我们还可以使用该模型来实现一个简单的分类系统,用于对新的水果图像进行分类。 ```python # 读取模型 model.load_state_dict(torch.load('model.pt')) # 在测试集上测试模型性能 model.eval() test_running_loss = 0.0 test_running_corrects = 0 for inputs, labels in test_loader: inputs, labels = inputs.to(device), labels.to(device) with torch.no_grad(): outputs = model(inputs) loss = criterion(outputs, labels) test_running_loss += loss.item() * inputs.size(0) _, preds = torch.max(outputs, 1) test_running_corrects += torch.sum(preds == labels.data) test_loss = test_running_loss / len(test_loader.dataset) test_acc = test_running_corrects.double() / len(test_loader.dataset) print('Test Loss: {:.4f}, Test Acc: {:.4f}'.format(test_loss, test_acc)) # 实现分类系统 import matplotlib.pyplot as plt import numpy as np from PIL import Image def predict_image(image_path): image = Image.open(image_path) image_tensor = val_transforms(image).float() image_tensor = image_tensor.unsqueeze_(0) input = image_tensor.to(device) output = model(input) index = output.data.cpu().numpy().argmax() return train_dataset.classes[index] image_path = 'fruits-360/Test/Apple Braeburn/0_100.jpg' result = predict_image(image_path) print(result) ``` 这里的分类系统实现了一个`predict_image`函数,它可以接受一张水果图像的路径作为输入,返回该图像对应的水果类别。我们可以使用该函数来对新的水果图像进行分类,并输出预测结果。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值