目录
- 1.选取训练模型中的损失函数
- 1.1L1损失函数
- 1.2均值平方差(MSE)损失函数
- 1.3交叉熵损失(CrossEntropyLoss)函数
- 1.4加权交叉熵
- 1.5分类模型中常用的3种损失函数
- 2.Softmax接口的使用
- 3.优化器的使用与优化参数的查看
- 3.1优化器的使用
- 3.2优化参数的查看
- 4.用退化学习率训练模型
- 4.1手动实现退化学习率
- 4.2PyTorch中的退化学习率接口----lr_scheduler
- 4.3用lr_scheduler接口实现退化学习率
- 4.4用lr_scheduler接口实现MultiStepLR学习率
- 4.5用lr_scheduler接口实现ReduceLROnPlateau退化学习率
- 5.为模型添加钩子函数
- 5.1模型正向数据流处理结构中的钩子函数
- 5.2模型反向结构中的钩子函数
- 6.梯度累加的训练方法
1.选取训练模型中的损失函数
1.1L1损失函数
L1损失函数是以类的形式封装的。需要先对其进行实例化,再进行使用。
loss = torch.nn.L1Loss()(pre, label)
pre代表模型输出的预测值,label代表输入样本对应的标签
1.2均值平方差(MSE)损失函数
loss = torch.nn.MSELoss()(pre, label)
1.3交叉熵损失(CrossEntropyLoss)函数
loss = torch.nn.CrossEntropyLoss()(pre, label)
1.4加权交叉熵
加权交叉熵是在交叉熵的基础上乘以系数(加权)。该系数用于增加或减少正样本在计算交叉熵时的损失值。
在训练一个多分类器时,如果训练样本很不均衡,则可以通过加权交叉熵有效地控制训练模型分类的平衡性。
loss = torch.nn.CrossEntropyLoss(weight)(pre, label)
其中,参数weight是一个1维张量,该张量中含有n(n代表分类个数)个匀速,即为每个分类分配不同的权重。
1.5分类模型中常用的3种损失函数
- BCELoss:用于单标签二分类或者多标签二分类,即一个样本可以有多个分类,彼此不互斥。输出和目标的维度是(batch,C),batch是样本数量,C是类别数量。每个C值代表属于一类标签的概率
- BCEWithLogitsLoss:也用于单标签二分类或者多标签二分类,它相当于Sigmoid与BCELoss的结合,即对网络输出的结果先做一次Sigmoid将其值域变为[0, 1],再对其与标签之间做BCELoss。当网络最后一层使用nn.Sigmoid时,用BCELoss;当网络最后一层不使用nn.Sigmoid时,用BCEWithLogitsLoss。
- CrossEntropyLoss:用于多类别分类,输出和目标的维度是(batch,C),batch是样本数量,C是类别数量。分别对批次种每个C维数据计算Softmax,并从这C维数据中找到最大值所对应的索引,将该索引当作最终的分类结果。
2.Softmax接口的使用
Softmax是分类任务中的常用算法,需要用softmax将目标分为几类,在搭建模型时就在最后一层使用几个神经元节点。
在模型的训练过程中,Softmax算法常和损失函数放在一起配套使用,在PyTorch中,可以通过两种接口的调用方式进行交叉熵的计算:1.使用LogSofrmax()和NLLLoss()方法计算交叉熵。2.使用CrossEntropyLoss()方法计算交叉熵。
import torch
# 定义模拟数据
logits = torch.autograd.Variable(torch.tensor([[2, 0.5, 6], [0.1, 0, 3]]))
labels = torch.autograd.Variable(torch.LongTensor([2, 1]))
# 计算softmax
print(torch.nn.Softmax(dim=1)(logits)) # 输出:tensor([[0.0179, 0.0040, 0.9781], [0.0498, 0.0451, 0.9051]])
# 计算LogSoftmax
logsoftmax = torch.nn.LogSoftmax(dim=1)(logits)
print(logsoftmax) # 输出:tensor([[-4.0222, -5.5222, -0.0222], [-2.9997, -3.0997, -0.0997]])
# 计算NLLLoss
output = torch.nn.NLLLoss()(logsoftmax, labels)
print(output) # 输出:tensor(1.5609)
# 计算CrossEntropyLoss
print(torch.nn.CrossEntropyLoss()(logits, labels))
- logits:神经网络的计算结果。一共有两个数据,每个数据的结果包含三个数值,代表三种分类的结果
- labels:神经网络计算结果对应的标签。一共有两个数值,每个数值代表对应数据的所属分类
3.优化器的使用与优化参数的查看
在PyTorch中可以用torch.optim构建一个optimizer对象。该对象能够保持当前参数状态,并基于计算得到的梯度进行参数更新。
3.1优化器的使用
优化器中封装了神经网络在反向传播中的一系列优化策略。这些优化策略可以使模型在训练过程中更快、更好地进行收敛。构建一个Adam优化器对象的具体代码如下:
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
其中,Adam是优化器方法。该方法的参数较多,其中最常用的参数是以下两个。
- 待优化权重参数:一般是固定写法,调用模型的parameters()方法,将返回值传入即可
- 优化时的学习率:用来控制优化器在工作时对参数的调节幅度。优化器在工作时,会先算出梯度(根据损失值对某个参数求偏导),再沿着该梯度(这里可以把梯度当作斜率)的方向算出一段距离(该距离由学习率控制)之后的差值,然后将该差值作为变化值更新到原有参数上。学习率越大,则模型的收敛速度越快,但是模型的训练效果容易出现较大的震荡;学习率越小,则模型的震荡幅度越小,但是收敛越慢。
3.2优化参数的查看
PyTorch中的每个优化器类都有param_groups属性。该属性记录着每个待优化权重Parameter的配置参数。属性param_groups是一个列表对象,该列表对象中的元素与待优化权重Parameter一一对应,以字典对象的形式存放着待优化权重Parameter的配置参数。
可以用如下语句查看字典中的配置参数名称:
list(optimizer.param_groups[0].keys())
该代码取出了属性param_groups中的第一个待优化权重的配置参数。运行后,系统会输出该配置参数中的参数名称,例如:
[‘params’, ‘lr’, ‘betas’, ‘eps’, ‘weight_decay’, ‘amsgrad’]
- params:优化器要优化的权重参数
- lr:学习率
- weight_decay:权重参数的衰减率
- amsgrad:是否使用二阶冲量的方式
4.用退化学习率训练模型
在训练过程中,退化学习率能够将大学习率的速度优势和小学习率的精度优势都发挥出来,即在训练的速度与精度之间找到平衡。其原理是,训练刚开始时使用大的学习率快速训练,训练到一定程度后使用小的学习率来提高精度。
4.1手动实现退化学习率
losses = [] # 定义列表,用于接收每一步的损失值
lr_list = [] # 定义列表,用于接收每一步的学习率
for i in range(epochs):
loss = model.getloss(xt, yt)
losses.append(loss.item()) # 保存中间状态的损失值
optimizer,zero_grad() # 清空之前的梯度
loss.backward() # 反向传播损失值
optimizer.step() # 更新参数
if i %50 == 0:
for p in optimizer.param_groups:
p['lr'] *= 0.99
lr_list.append(optimizer.state_dict()['param_groups'][0]['lr'])
这段代码实现了每训练50步就将学习率乘以0.99(将学习率变小),从而实现退化学习率的效果。
4.2PyTorch中的退化学习率接口----lr_scheduler
该接口支持多种退化学习率的实现,每种退化学习率都是通过一个类来实现的。具体如下:
- 等间隔调整学习率StepLR:每训练一定步数就调整一次学习率。调整的方式为lr=lr*gamma(gamma为手动设置的退化率)。
- 多间隔调整学习率MultiStepLR:按照指定的步数来调整学习率。调整的方式也是lr=lr*gamma。
- 指数衰减调整学习率ExponentialLR:每训练一步,学习率呈指数型衰减,即将学习率调整为lr=lr*gamma^strp(step为训练步数)。
- 余弦退火函数调整学习率CosineAnnealingLR:每训练一步,学习率呈余弦函数型衰减。(余弦退火是指按照余弦函数的曲线进行衰减的)
- 根据指标调整学习率ReduceLROnPlateau:当某指标(loss或accuracy)在最近几次训练中都没有变化(下降或升高超过给定阈值)时,调整学习率。
- 自定义调整学习率LambdaLR:为不同参数组设定不同学习率调整策略。
其中,LambdaLR退化学习率最灵活,可以根据需求指定任何策略的学习率变化。这在fine-tune方法(微调模型的一种方法)中特别有用,不仅可以为不同层设置不同的学习率,还可以为不同层设置不同的学习率调整策略。
4.3用lr_scheduler接口实现退化学习率
losses = []
lr_list = []
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=50, gamma=0.99) # 设置退化学习率,每50步乘以0.99
for i in range(epochs):
loss = model.getloss(xt, yt)
losses.append(loss.item()) # 保存中间状态的损失值
optimizer.zero_grad() # 清空之前的梯度
loss.backward() # 反向传播损失值
optimizer.step() # 更新参数
scheduler.step() # 调用退化学习率对象
4.4用lr_scheduler接口实现MultiStepLR学习率
losses = []
lr_list = []
scheduler = torch.optim.lr_scheduler.MultiStepLR(optimizer, milestones=[200, 700, 800], gamma=0.99) # 设置退化学习率,训练到200, 700, 800步时对学习率进行退化操作
for i in range(epochs):
loss = model.getloss(xt, yt)
losses.append(loss.item()) # 保存中间状态的损失值
optimizer.zero_grad() # 清空之前的梯度
loss.backward() # 反向传播损失值
optimizer.step() # 更新参数
scheduler.step() # 调用退化学习率对象
4.5用lr_scheduler接口实现ReduceLROnPlateau退化学习率
losses = []
lr_list = []
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer,
mode='min', # 要监控模型的最大值(max)还是最小值(min)
factor=0.5, # 学习率退化率参数gamma
patience=5, # 不再减小(或增大)的累计次数
verbose=True, # 再触发规则时是否打印信息
threshold=0.0001, # 监控值触发规则的阈值
threshold_mode='abs', # 计算触发规则的方法
cooldown=0, # 触发规则后的停止监控步数,避免lr下降过速
min_lr=0, # 设置训练过程中系统所允许的最小学习率
eps=1e-08) # 当学习率的调整幅度小于该值时,则停止调整
for i in range(epochs):
loss = model.getloss(xt, yt)
losses.append(loss.item()) # 保存中间状态的损失值
scheduler.step(loss.item()) # 调用退化学习率对象
optimizer.zero_grad() # 清空之前的梯度
loss.backward() # 反向传播损失值
optimizer.step() # 更新参数
threshold_mode有两种取值
- rel:在参数mode为max时,如果监控值超过best*(1+threshold),则触发规则;在参数mode为min时,如果监控值低于best*(1-threshold),则触发规则。(best为训练过程中的历史最好值。)
- abs:在参数mode为max时,如果监控值超过best+threshold,则触发规则;在参数mode为min时,如果监控值低于best-threshold,则触发规则。
5.为模型添加钩子函数
通过向模型中添加钩子函数,可以实现对模型的细粒度控制。
5.1模型正向数据流处理结构中的钩子函数
register_forward_hook(hook)
在module中注册一个正向数据流处理hook。每次调用forward()方法计算输出时,这个hook就会被调用。hook()函数的定义如下:
hook(module, input, output)
hook函数不能修改input和output的值,它返回一个句柄(handle)。调用handle的remove()方法,可以将hook从module中移除。
具体的使用实例如下:
import torch
from torch import nn
import torch.autograd import Variable
def for_hook(module, input, output): # 定义钩子函数
print("模型:", module)
for val in input:
print("输入:", val)
for out_val in output:
print("输出:", out_val)
class Model(nn.Module):
def __init__(self):
super(Model, self).__init__()
def forward(self, x):
return x+1
model = Model() # 实例化模型
x = Variable(torch.FloatTensor([1]), requires_grad=True)
handle = model.register_forward_hook(for_hook) # 注册钩子
print("模型结果:", model(x)) # 运行模型
输出如下结果
模型:Model()
输入:tensor([1.], requires_grad=True)
输出:tensor(2., grad_fn=<SelectBackward>)
模型结果:tensor([2.], grad_fn=<AddBackward0>)
输出结果的前三行时钩子函数中的内容。
5.2模型反向结构中的钩子函数
register_backward_hook(hook)
在module中注册一个反向hook,每次计算module的inputs的梯度时,这个hook会被调用。hook函数的定义如下:
hook(module, grad_input, grad_output)
如果module有多个输入/输出,则参数grad_input和grad_output会是一个tuple类型。函数hook()没有权限对输入和输出参数进行修改,但是它可以选择性地返回部分的输出梯度,这个返回的梯度在下一层网络的计算中被当作输入参数(grad_input)传入。
与正向数据流处理的钩子函数一样,反向的钩子函数也会返回一个句柄(handle)。调用handle的remove()方法,可以将hook从module中移除。
6.梯度累加的训练方法
在训练模型过程中,增大模型的批次规模可以提升模型的泛化能力。但由于BERT模型的参数过多,本身就占用显存过大,如果再使用大批次数据进行训练,则必须有超大显存的GPU才可以胜任。
在没有合适的硬件资源条件下,可以使用梯度累加的方法来完成大批次数据的训练过程。梯度累加是指:在训练过程中,每次的迭代过程只计算梯度,并将其进行累加,当累加到一定次数后,再用累加的梯度统一更新网络参数。这种方式把多次训练的梯度放在一起进行更新,变相实现了大批次数据的训练效果。(使用时需要注意,学习率也要适当放大)
PyTorch内部的梯度机制本身就是自动累加的。在实现时,可以按照设定的累计次数进行参数更新,并调用优化器的zero_grad()方法对累加的梯度进行清空,代码如下:
for loop in 循环次数:
loss = loss_fun(pred, y) # 计算损失值
loss = loss / accumulation_steps # 计算损失的平均值
loss.backward()
if((loop+1)%accumulatiion_steps)==0: # 根据累计次数进行参数更新
optimizer.step() # 参数更新
optimizer.zero_grad() # 清空梯度
另外,在多任务训练的场景下,一个模型的损失值往往有2个及以上个数子任务的损失值合并而成,例如
loss = loss1 + loss2
loss.backward()
这种方式在显存紧张的设备上并不可取。因为在计算损失值时,系统都会为其生成一个用于梯度回传的计算图。在合并损失后再统一计算梯度时,系统必须同时在两个计算图中进行反向求导。如果先分别对每个损失进行梯度计算,则系统每次只加载一个计算图,会节省显存的开销。
loss1.backward()
loss2.backward()
loss = loss1 + loss2
梯度累加只是让训练结果更大程度地与大批次数据的训练结果接近,但其实际效果与大批次数据的训练结果还是有一定差距的。这部分差距主要是由模型中的批量归一化所引起的。在梯度累加方法中,批量归一化所计算的均值与方差所表现的数据比较片面,这种方法步入大批次数据训练时的均值与方差表现的数据分布更准确。可以将BN中的momentum参数适当调低,使其能够对更长的序列信息进行统计。