pytorch|【pytorch笔记】(五)自定义损失函数、学习率衰减、模型微调


本文目录:

  • 1. 自定义损失函数
  • 2. 动态调整学习率
  • 3. 模型微调-torchvision
    • 3.1 使用已有模型
    • 3.2 训练特定层

1. 自定义损失函数 虽然pytorch提供了许多常用的损失函数,但很多时候我们需要自定义一些新的损失函数来满足某些特定任务的需求,这时就需要我们自己写损失函数了。
pytorch 自定义损失函数主要有两种方式:
  • 自定义函数:这种方法比较简单,写个函数即可。但需要注意的是输入输出的变量都得是torch.tensor类型的数据,且尽量采用torch自带的函数计算loss,否则可能会无法求导。
import torchdef my_loss(x, y): loss = torch.mean((x - y)**2) return loss

  • 自定义类:这种方式虽然需要提前实例化类,但更为常用,显得专业。自定义类继承于nn.Module,相当于定义了一个网络层,可以维护状态和存储参数信息。自定义类有两个关键要点,一是定义初始化方法(init),二是定义前向计算方式(forward)。
    以下给出一个我自己写的分位数回归损失函数——pinball loss:
import torch.nn as nn import torchclass PinballLoss(nn.Module): """ Pinball loss at all confidence levels. """ def __init__(self, quantiles): """ InitializeParameters ---------- quantiles : the list of quantiels, [1, n], list or ndarray. For example, quantiles = [0.025 0.050.075 0.925 0.950.975].""" super(PinballLoss, self).__init__() self.quantiles = quantilesdef forward(self, preds, target): """ Compute the pinball lossParameters ---------- preds : the predictions at all quantiles, torch.tensor, [n, len(quantiles)]. target : the ground truth of y, torch.tensor, [n, 1].Returns ------- loss : the value of the pinball loss."""assert preds.shape[0] == target.shape[0] if target.ndim == 1: target = target.unsqueeze(dim=1)losses = [] for i, q in enumerate(self.quantiles): errors = target[:, 0] - preds[:, i] losses.append(torch.max((q - 1) * errors, q * errors).unsqueeze(1))loss = torch.mean(torch.sum(torch.cat(losses, dim=1), dim=1))return loss

2. 动态调整学习率 学习率(learning rate)本质上就是优化方法里的搜索步长,不同优化方法之间最大的差异在于他们的搜索方向和搜索步长不同。深度学习通过不断更新优化参数来减小loss,该过程的本质其实也是最优化里寻找全局最优解(最小的loss)的过程,只是实际情况中经常无法寻找到全局最优解或成本太高,往往采用表现还行的局部最优解。
那么,为什么要动态调整学习率呢?来看下图
pytorch|【pytorch笔记】(五)自定义损失函数、学习率衰减、模型微调
文章图片

(图源见水印)
我们的目标是到达这条曲线的“谷底”(局部最优解),如果学习率太大,会经常跨过这个谷底,难以收敛到局部最优解,导致模型表现一般;如果学习率太小,虽然最终也能到达谷底,但是这个过程耗时漫长,等得花都谢了。这说明太大或太小的学习率都不合适,但这个合适的学习率到底多大合适呢?我们无从得知,只能凭经验判断。神经网络参数的初始权重不同,且每一次优化过程的初始起点、搜索方向都可能不同,这就导致每一次跑出来的结果都有所不同,论文里的完美结果经常难以复现(指定随机数种子可以改善),这也是深度学习有时候像玄学、炼丹的原因。尽管如此,在实际工作中我们只要不断调参得到一个较为满意的结果,得益于神经网络的强大拟合能力,其结果往往就已经超越了许多传统方法。
咳咳,扯远了。在实际的模型训练中,我们一般采用动态调整学习率,也称学习率衰减的策略来加速算法收敛。其思想为:先取一个较大的学习率加速收敛,然后随着遍历次数(epoch)的增加逐步减小学习率,防止跳出局部最优解。
pytorch官方在torch.optim.lr_schduler里提供了许多学习率衰减的方法,这里列出常用的几种方式:
Scheduler 说明
StepLR 等间隔调整学习率,如每隔10个epoch衰减一次学习率
MultiStepLR 多间隔调整学习率,如epoch分别在10,30,70时衰减一次学习率
ExponentialLR 指数衰减学习率,每训练一个epoch调整一次学习率,即 l r ? = l r × g a m m a e p o c h lr^*=lr \times gamma^{epoch} lr?=lr×gammaepoch
CosineAnnealingLR 余弦退火函数调整学习率,随着epoch增加,学习率呈余弦函数状衰减
更多学习率衰减方法参考pytorch官网。
以下给出一个学习率衰减的模板:
import torchvision.models as models from torch.optim import SGD from torch.optim.lr_scheduler import ExponentialLRmodel = models.vgg16()#可更换为任意模型 optimizer = SGD(model, 0.1)#参数优化器 scheduler = ExponentialLR(optimizer, gamma=0.9)#学习率衰减控制器for epoch in range(20): for input, target in dataset: optimizer.zero_grad() output = model(input) loss = loss_fn(output, target) loss.backward() optimizer.step() scheduler.step()

以上是pytorch官网给出的方法。当然,如果官方给的方法无法满足我们的需求,那我们可以自己写一个函数来调整学习率。
假设我们现在需要学习率每隔30轮下降为原来的1/10,假设官网没有符合我们需求的schduler(实际上StepLR符合),那我们可以自己写一个函数:
def adjust_learning_rate(optimizer, epoch): lr = args.lr * (0.1 ** (epoch // 30)) for param_group in optimizer.param_groups: param_group['lr'] = lr

然后我们就可以在训练过程中调用我们自定义的函数来实现学习率的动态变化。
def adjust_learning_rate(optimizer,...): ...optimizer = torch.optim.SGD(model.parameters(),lr = args.lr,momentum = 0.9) for epoch in range(10): train(...) validate(...) adjust_learning_rate(optimizer,epoch)

3. 模型微调-torchvision 深度学习和计算机算力的不断发展催生出了许多包含巨量参数的模型,如GPT-3等,这些大模型花费了创造它们的公司大量的算力、时间成本,我们普通人想要训练出这样的大模型困难重重:
  • 缺少数据。这些大模型通常是在大量数据的基础上训练出来的,动不动就几百G几百T的,普通电脑难以存下这么多数据。
  • 缺少算力。大模型通常是用服务器或数据中心的几百张NVIDIA显卡上跑出来的,光这硬件成本就得至少几百上千万,电费也不是一笔小数目(怎么有点像挖矿呢)。
  • 缺少技术(大佬请无视)。综上,想要训练出大模型,需要大量数据和显卡,要驱动模型在大数据和大量显卡上正常训练需要一点技术,普通人如果对这方面的知识接触少的话一时半会儿很难跑得起来模型。
【pytorch|【pytorch笔记】(五)自定义损失函数、学习率衰减、模型微调】那我们的普通人就没法用大模型了吗?答案是否定的,有许多已经训练好的模型已经开源分享出来了,比如torchvision.models里就有许多预训练好的模型,我们只需要下载下来就能用了。但这存在一个问题,假设我们要做的识别猫的种类,这些预训练好的模型通常是针对特定任务的,比如图像识别,他们采用的数据集里面包含猫的照片可能就只有一小部分,无法满足我们的需求。要解决这个问题,我们可以采用迁移学习(transfer learning)。
迁移学习可以将源数据集上学到的知识迁移到目标数据集上。例如,虽然ImageNet数据集的图像里包含的猫的图片不是很多,但在该数据集上训练的模型可以抽取比较通用的图像特征,从而能够帮助识别边缘、纹理、形状和物体组成等。这些类似的特征对于识别猫也可能同样有效。
迁移学习的一大应用场景是模型微调(finetune)。简单来说,就是我们先找到一个同类的别人训练好的模型,把别人现成的训练好了的模型拿过来,换成自己的数据,通过训练调整一下参数。 在PyTorch中提供了许多预训练好的网络模型(VGG,ResNet系列,mobilenet系列…),这些模型都是PyTorch官方在相应的大型数据集训练好的。
模型微调的主要流程是:复制预训练好的模型,微调除输出层外的其它层参数,修改输出层参数并随机初始化,在目标数据集上上训练模型。
3.1 使用已有模型 以torchvision.models为例,里面列出了许多用于图像分类任务的模型,我们下载导入一下即可使用。
但要注意的是,Windows环境下 torchvision 下载的预训练模型保存在C:\Users\\.cache,这一点对于C盘空间较小的用户来说很不友好,也不符合使用习惯,如果.cahe里的文件不小心被清理掉了还得重新下载一遍。为此,我们需要修改一下预训练模型的保存路径,以下提供一种修改方式:
import torchvision.models as models import os os.environ['TORCH_HOME']='E:\pytorch\Data'#修改模型保存路径,下载好后重新加载模型就会在这个目录下加载了# pretrained = True表示下载使用预训练得到的权重,False表示不使用预训练得到的权重 resnet34 = models.resnet34(pretrained=True)

3.2 训练特定层 在默认情况下,参数的属性.requires_grad = True,如果我们从头开始训练或微调不需要注意这里。但如果我们正在提取特征并且只想为新初始化的层计算梯度,其他参数不进行改变。那我们就需要通过设置requires_grad = False来冻结部分层。在PyTorch官方中提供了这样一个例程。
def set_parameter_requires_grad(model, feature_extracting): if feature_extracting: for param in model.parameters(): param.requires_grad = False

在下面我们仍旧使用resnet18为例的将1000类改为4类,但是仅改变最后一层的模型参数,不改变特征提取的模型参数;注意我们先冻结模型参数的梯度,再对模型输出部分的全连接层进行修改,这样修改后的全连接层的参数就是可计算梯度的。
import torchvision.models as models# 冻结参数的梯度 feature_extract = True model = models.resnet18(pretrained=True) set_parameter_requires_grad(model, feature_extract)# 修改模型 num_ftrs = model.fc.in_features model.fc = nn.Linear(in_features=num_ftrs, out_features=4, bias=True)

之后在训练过程中,model仍会进行梯度回传,但是参数更新则只会发生在fc层。通过设定参数的requires_grad属性,我们完成了指定训练模型的特定层的目标,这对实现模型微调非常重要。
参考资料:
[1] Datawhale_深入浅出pytorch
[2] https://blog.csdn.net/qq_27825451/article/details/95165265
[3] pytorch官方文档
[4] https://www.jianshu.com/p/26a7dbc15246
[5] https://blog.csdn.net/yanxiangtianji/article/details/112256618

    推荐阅读