1. 理解optimizer.param_groups的核心结构

当你第一次接触PyTorch的优化器时,可能会被param_groups这个属性搞得一头雾水。其实它的设计非常巧妙,我用一个实际项目中的例子来解释。去年我在做一个图像分类项目时,需要为不同层设置不同的学习率,这时候param_groups就派上了大用场。

param_groups本质上是一个Python列表,列表中的每个元素都是一个字典。这个字典包含了优化器需要的所有参数信息。比如你用Adam优化器,典型的param_groups结构是这样的:

[{
    'params': [参数张量列表],
    'lr': 0.001,  # 学习率
    'betas': (0.9, 0.999),  # 动量系数
    'eps': 1e-08,  # 数值稳定项
    'weight_decay': 0,  # 权重衰减
    'amsgrad': False,  # 是否使用AMSGrad变体
    'maximize': False  # 是否最大化目标
}]

这里有个容易踩坑的地方:很多人以为param_groups[0]['params']就是模型参数本身,其实它是一个包含Parameter对象的列表。我在调试时就犯过这个错误,试图直接修改这个列表导致模型无法训练。

2. 动态调参的四种实战场景

2.1 学习率预热(Learning Rate Warmup)

在训练初期直接使用较大的学习率可能会导致训练不稳定。我通常在Transformer模型中使用warmup策略,具体实现是这样的:

def adjust_learning_rate(optimizer, epoch, warmup_epochs=5, base_lr=0.001):
    if epoch < warmup_epochs:
        lr = base_lr * (epoch + 1) / warmup_epochs
    else:
        lr = base_lr
    for group in optimizer.param_groups:
        group['lr'] = lr

这个简单的线性warmup策略在我的NLP项目中效果很好。对于视觉任务,你可能需要调整warmup的epoch数。

2.2 分层学习率设置

当微调预训练模型时,通常希望底层保持较小的学习率,顶层使用较大的学习率。这是我常用的实现方式:

def set_layerwise_lr(model, base_lr=1e-5, top_lr=1e-3):
    params = []
    # 为不同层设置不同的学习率
    for name, param in model.named_parameters():
        if 'classifier' in name or 'fc' in name:  # 分类层
            params.append({'params': param, 'lr': top_lr})
        else:  # 特征提取层
            params.append({'params': param, 'lr': base_lr})
    return optim.Adam(params)

2.3 训练中动态冻结层

有时候我们需要在训练过程中冻结某些层。这是我实现动态冻结的代码片段:

def freeze_layers(optimizer, layer_names):
    for group in optimizer.param_groups:
        # 过滤需要冻结的参数
        group['params'] = [p for p in group['params'] 
                          if not any(name in str(p) for name in layer_names)]
        # 设置requires_grad=False
        for p in group['params']:
            p.requires_grad = False

2.4 周期性学习率调整

余弦退火学习率是我在图像超分辨率任务中常用的策略:

def cosine_annealing(optimizer, epoch, max_epoch, base_lr, min_lr=1e-6):
    for group in optimizer.param_groups:
        lr = min_lr + 0.5 * (base_lr - min_lr) * (
            1 + math.cos(math.pi * epoch / max_epoch))
        group['lr'] = lr

3. 高级技巧与性能优化

3.1 参数组的高效管理

当模型有大量参数组时,直接遍历param_groups可能会影响性能。我推荐使用字典来管理:

param_group_dict = {
    'backbone': {'params': [], 'lr': 1e-5},
    'head': {'params': [], 'lr': 1e-3}
}

# 填充参数
for name, param in model.named_parameters():
    if 'backbone' in name:
        param_group_dict['backbone']['params'].append(param)
    else:
        param_group_dict['head']['params'].append(param)

optimizer = optim.Adam(list(param_group_dict.values()))

3.2 梯度裁剪与参数组

梯度裁剪时,你可能希望对不同参数组使用不同的裁剪阈值:

def clip_grad_by_group(optimizer, max_norm_dict):
    for group in optimizer.param_groups:
        group_name = group.get('name', 'default')
        max_norm = max_norm_dict.get(group_name, 1.0)
        torch.nn.utils.clip_grad_norm_(group['params'], max_norm)

3.3 自定义优化器参数

除了标准参数,你还可以在param_groups中添加自定义字段:

optimizer.param_groups[0]['custom_field'] = 'value'

这个技巧在我实现一些特殊优化策略时非常有用。

4. 调试与常见问题解决

4.1 参数组验证技巧

在修改param_groups后,我通常会验证参数是否按预期设置:

def check_param_groups(optimizer):
    for i, group in enumerate(optimizer.param_groups):
        print(f"Group {i}:")
        print(f"  Learning rate: {group['lr']}")
        print(f"  Parameters count: {len(group['params'])}")
        print(f"  First param shape: {group['params'][0].shape}")

4.2 常见错误排查

  1. 参数重复问题:确保同一个参数不会出现在多个组中
  2. 学习率不更新:检查是否在每次step()前更新了lr
  3. 梯度消失:确认参数组的requires_grad设置正确

4.3 性能监控

我通常会记录每个参数组的学习情况:

class ParamGroupMonitor:
    def __init__(self, optimizer):
        self.optimizer = optimizer
        self.history = {i: [] for i in range(len(optimizer.param_groups))}
    
    def record(self):
        for i, group in enumerate(self.optimizer.param_groups):
            grad_norm = sum(p.grad.norm() for p in group['params'] if p.grad is not None)
            self.history[i].append((group['lr'], grad_norm))

这个监控器帮助我发现了很多训练中的异常情况。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐