一、为不同层设置不同学习率
1.1 前言
在深度学习模型的训练过程中,学习率作为一个关键的超参数,对模型的收敛速度和最终性能有着重大影响。传统方法通常采用统一的学习率,但随着研究的深入,我们发现为网络的不同层设置不同的学习率可能会带来显著的性能提升。本文将详细探讨这一策略的实施方法及其在PyTorch框架中的具体应用。
1.2 层级学习率的理论基础
深度神经网络的不同层次在特征提取和信息处理上扮演着不同的角色。基于这一认知,我们可以合理推断对不同层采用差异化的学习策略可能会更有效:
-
底层特征提取:网络的前几层通常负责捕获通用的低级特征,如边缘、纹理等。这些特征往往具有较强的通用性和可迁移性。
-
高层语义理解:网络的后几层则倾向于提取更为抽象和任务相关的高级特征。
-
任务特定层:如全连接分类层,直接与特定任务相关。
基于上述观察我们可以制定相应的学习率策略:
-
对于预训练的底层,使用较小的学习率以保持其已学到的通用特征。
-
对于中间层,可以采用适中的学习率。
-
对于任务特定的顶层,则可以使用较大的学习率以快速适应新任务。
1.3 PyTorch实现:以ResNet为例
下面我们将以ResNet18为例,演示如何在PyTorch中实现层级学习率设置。
1.3.1 模型定义
首先,我们加载预训练的ResNet18模型(可改为自己定义或修改的模型),并修改其最后一层以适应新的分类任务:
import torch
import torch.nn as nn
import torchvision.models as models
# 加载预训练的ResNet18模型
model = models.resnet18(pretrained=True)
# 修改最后的全连接层以适应新的分类任务
num_classes = 10 # 假设新任务有10个类别
model.fc = nn.Linear(model.fc.in_features, num_classes)
1.3.2 参数分组
接下来,我们将模型参数分组,为不同的层设置不同的学习率:
# 定义不同组的学习率
backbone_lr = 1e-4 # 较小的学习率用于预训练的主干网络
classifier_lr = 1e-3 # 较大的学习率用于新的分类器层
# 创建参数组
params = [
{'params': model.conv1.parameters(), 'lr': backbone_lr},
{'params': model.bn1.parameters(), 'lr': backbone_lr},
{'params': model.layer1.parameters(), 'lr': backbone_lr},
{'params': model.layer2.parameters(), 'lr': backbone_lr},
{'params': model.layer3.parameters(), 'lr': backbone_lr},
{'params': model.layer4.parameters(), 'lr': backbone_lr},
{'params': model.fc.parameters(), 'lr': classifier_lr}
]
此处我们对ResNet的各个组件进行了更细致的划分,为不同的层组设置了相应的学习率。这种方法允许我们对模型的学习过程进行更精细的控制。
1.3.3 优化器配置
在确定了参数分组后,我们需要选择合适的优化器并进行配置。这里我们简单的选用Adam优化器。
optimizer = torch.optim.Adam(params)
这种分组策略同样适用于其他PyTorch支持的优化器,PyTorch的优化器会自动识别并应用在参数分组中定义的不同学习率。这种设计使得实现层级学习率变得相对简单。
1.3.4 训练循环
实现了层级学习率后的训练循环保持不变。PyTorch会在后台自动处理不同参数组的学习率:
# 定义损失函数
criterion = nn.CrossEntropyLoss()
# 训练循环
for epoch in range(num_epochs):
model.train()
for inputs, labels in train_loader:
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
# 在每个epoch结束后进行验证
model.eval()
# ... [验证代码]
1.3.5 学习率调度
除了设置初始的层级学习率,我们还可以结合学习率调度器来动态调整学习率。PyTorch提供了多种学习率调度器,如StepLR
、ReduceLROnPlateau
等。以下是一个使用StepLR
的示例:
from torch.optim.lr_scheduler import StepLR
scheduler = StepLR(optimizer, step_size=30, gamma=0.1)
# 在训练循环中更新学习率
for epoch in range(num_epochs):
# ... [训练代码]
scheduler.step()
这将每30个epoch将所有参数组的学习率降低为原来的0.1倍;也可改为其他的,如cosin余弦学习率、step梯度下降法等。
1.4 高级学习率优化技巧
1.4.1 渐进式解冻
在微调预训练模型时,一种有效的策略是渐进式解冻。我们可以先锁定底层,只训练顶层,然后逐步解冻更多的层:
# 初始阶段:只训练分类器
for param in model.parameters():
param.requires_grad = False
model.fc.requires_grad = True
# 训练几个epoch后
model.layer4.requires_grad = True
# 再过几个epoch
model.layer3.requires_grad = True
以此类推,冻结其实意味着学习率为0,也就是不对任何参数进行更新。
1.4.2 层适应学习率
我们上面已经介绍了手动指定固定的学习率,其实我们还可以通过自定义优化器来实现,不同的层采用不同的学习率范围。我们可以实现一个自定义的优化器来自动调整每一层的学习率:
class LayerAdaptiveLR(torch.optim.Adam):
def __init__(self, params, lr=1e-3, betas=(0.9, 0.999), eps=1e-8, weight_decay=0):
super().__init__(params, lr, betas, eps, weight_decay)
self.param_groups = sorted(self.param_groups, key=lambda x: id(x['params'][0]))
def step(self, closure=None):
loss = None
if closure is not None:
loss = closure()
for group in self.param_groups:
for p in group['params']:
if p.grad is None:
continue
grad = p.grad.data
state = self.state[p]
# 根据梯度统计调整学习率
if len(state) == 0:
state['step'] = 0
state['exp_avg'] = torch.zeros_like(p.data)
state['exp_avg_sq'] = torch.zeros_like(p.data)
exp_avg, exp_avg_sq = state['exp_avg'], state['exp_avg_sq']
beta1, beta2 = group['betas']
state['step'] += 1
exp_avg.mul_(beta1).add_(grad, alpha=1 - beta1)
exp_avg_sq.mul_(beta2).addcmul_(grad, grad, value=1 - beta2)
denom = exp_avg_sq.sqrt().add_(group['eps'])
# 动态调整学习率
step_size = group['lr'] * (exp_avg.abs() / denom).mean().item()
p.data.add_(exp_avg, alpha=-step_size)
return loss
# 使用示例
optimizer = LayerAdaptiveLR(model.parameters(), lr=1e-3)
可以看到,上面我们继承自Adam优化器,这里我们不用实现优化过程只针对于针对层的学习率变化即可。
1.5 层级学习率设置总结
层级学习率设置是一种强大的优化技术,特别适用于迁移学习和微调预训练模型的场景。通过精心设计的学习率策略,可以在保留预训练模型通用特征的同时有效地适应新任务。结合其他高级技巧,如渐进式解冻、层适应学习率,可以进一步提升模型的训练效率和性能。
二、冻结某些层的训练参数
参考:Pytorch 冻结预训练模型的某一层
https://www.zhihu.com/question/311095447/answer/589307812
在加载预训练模型的时候,我们有时想冻结前面几层,使其参数在训练过程中不发生变化。
我们需要先知道每一层的名字,通过如下代码打印:
net = Network() # 获取自定义网络结构
for name, value in net.named_parameters():
print('name: {0},\t grad: {1}'.format(name, value.requires_grad))
假设前几层信息如下:
name: cnn.VGG_16.convolution1_1.weight, grad: True
name: cnn.VGG_16.convolution1_1.bias, grad: True
name: cnn.VGG_16.convolution1_2.weight, grad: True
name: cnn.VGG_16.convolution1_2.bias, grad: True
name: cnn.VGG_16.convolution2_1.weight, grad: True
name: cnn.VGG_16.convolution2_1.bias, grad: True
name: cnn.VGG_16.convolution2_2.weight, grad: True
name: cnn.VGG_16.convolution2_2.bias, grad: True
后面的True表示该层的参数可训练,然后我们定义一个要冻结的层的列表:
no_grad = [ 'cnn.VGG_16.convolution1_1.weight',
'cnn.VGG_16.convolution1_1.bias',
'cnn.VGG_16.convolution1_2.weight',
'cnn.VGG_16.convolution1_2.bias'
]
冻结方法如下:
net = Net.CTPN() # 获取网络结构
for name, value in net.named_parameters():
if name in no_grad:
value.requires_grad = False
else:
value.requires_grad = True
冻结后我们再打印每层的信息:
name: cnn.VGG_16.convolution1_1.weight, grad: False
name: cnn.VGG_16.convolution1_1.bias, grad: False
name: cnn.VGG_16.convolution1_2.weight, grad: False
name: cnn.VGG_16.convolution1_2.bias, grad: False
name: cnn.VGG_16.convolution2_1.weight, grad: True
name: cnn.VGG_16.convolution2_1.bias, grad: True
name: cnn.VGG_16.convolution2_2.weight, grad: True
name: cnn.VGG_16.convolution2_2.bias, grad: True
可以看到前两层的weight和bias的requires_grad都为False,表示它们不可训练;最后在定义优化器时,只对requires_grad为True的层的参数进行更新。
optimizer = optim.Adam(filter(lambda p: p.requires_grad, net.parameters()), lr=0.001)