GPT2源码(1)——梯度累积和梯度裁剪
近期要用GPT2手搭一个项目,今天手撕了GPT2源码,解决了一些困惑的地方,总结一下!
梯度累积和梯度裁剪是深度学习中常用的两种技术,用于优化训练过程。以下结合代码详细分析其原理及利弊:
一、梯度累积(Gradient Accumulation)
原理
-
目标:在显存有限的情况下,通过多次小批量计算梯度并累加,模拟大批量训练的效果。
-
代码逻辑:
if args.gradient_accumulation_steps > 1: loss = loss / args.gradient_accumulation_steps # 平均损失 loss.backward() # 梯度累积- 小批量拆分:将一个大批量(batch)拆分为多个小批量(micro-batch)。
- 梯度累加:每个 micro-batch 计算梯度后,梯度会累积到模型参数中(不立即更新参数)。
- 归一化损失:将损失除以累积步数,确保每个 micro-batch 的梯度量级与大 batch 一致。
- 参数更新:累积足够步数后,调用
optimizer.step()更新参数,并清空梯度。
-
数学意义:
假设累积步数为 NNN,每个 micro-batch 的梯度为∇Li\nabla L_i∇Li,则总梯度为:
∇L=∑i=1N∇LiN×N=∑i=1N∇Li \nabla L = \sum_{i=1}^N \frac{\nabla L_i}{N} \times N = \sum_{i=1}^N \nabla L_i ∇L=i=1∑NN∇Li×N=i=1∑N∇Li
等效于直接计算大 batch 的梯度。
利弊
- 优点:
- 节省显存:允许在显存不足时使用更大的等效批量。
- 训练稳定:大批量通常能提供更准确的梯度方向。
- 缺点:
- 更新延迟:参数更新频率降低,可能影响收敛速度。
- 超参数敏感:需根据有效批量调整学习率(例如,线性缩放规则)。
二、梯度裁剪(Gradient Clipping)
原理
- 目标:防止梯度爆炸,避免参数更新过大导致训练不稳定。
- 代码逻辑:
torch.nn.utils.clip_grad_norm_(model.parameters(), args.max_grad_norm)- 计算范数:计算所有参数的梯度张量的 L2 范数。
- 缩放梯度:若范数超过阈值
max_grad_norm,则按比例缩放梯度,使其范数等于阈值。
利弊
- 优点:
- 稳定训练:防止梯度爆炸,尤其对 RNN、Transformer 等结构有效。
- 通用性:几乎不增加计算成本,适用于大多数模型。
- 缺点:
- 阈值敏感:过小的阈值会抑制梯度,导致收敛缓慢;过大的阈值可能无法防止爆炸。
- 信息损失:极端情况下可能破坏梯度方向。
三、代码潜在问题分析
-
梯度裁剪位置错误:
当前代码在每个 micro-batch 反向传播后立即裁剪梯度,导致多次裁剪。正确做法应是累积完所有梯度后裁剪一次。例如:for micro_batch in accumulated_steps: loss.backward() # 梯度累加 clip_grad_norm_(model.parameters(), max_grad_norm) # 统一裁剪 optimizer.step()- 错误影响:每个 micro-batch 的梯度被单独裁剪,累加后的总梯度可能过小或方向扭曲。
-
梯度归一化:
loss = loss / gradient_accumulation_steps是正确的,确保梯度量级与大批量一致。
总结
- 梯度累积:牺牲更新频率换取显存节省,需合理调整学习率。
- 梯度裁剪:简单有效防止梯度爆炸,但需谨慎选择阈值。
- 代码改进:将梯度裁剪移至所有梯度累积完成后执行,避免多次裁剪。
四、关于梯度累积两个问题的详细解释
以下代码中的梯度累计,假如累积步数设置为4,下面的loss是累积了4步的总loss吗,为什么还要除以4。还有一个问题是1个batch的损失会不会重复使用用,下一个batch计算损失之后,会把这个batch的损失与前面3个batch的损失一起计算这个loss,还是要等着后面的3个batch计算完损失,再一起构成这个loss呢?
if args.gradient_accumulation_steps > 1:
loss = loss / args.gradient_accumulation_steps # 平均损失
loss.backward() #
梯度累积以下是两个问题的解答
问题1:累积步数为4时,为什么要将 loss 除以4?
核心原理
- 梯度累积的本质:在多次小批量(micro-batch)计算中累加梯度,而不是累加损失值。
- PyTorch的梯度行为:默认情况下,
loss.backward()会将梯度累加到参数的.grad属性中,而不是覆盖。 - 归一化的目的:通过
loss = loss / 4,确保每个 micro-batch 的梯度量级与“等效大批量”一致。
数学推导
假设:
- 单个 micro-batch 的损失为 LiL_iLi,梯度为 ∇Li\nabla L_i∇Li。
- 累积步数 N=4N=4N=4,等效大批量的总损失为 L=∑i=14LiL = \sum_{i=1}^4 L_iL=∑i=14Li。
直接大批量训练的梯度:
∇L=∑i=14∇Li \nabla L = \sum_{i=1}^4 \nabla L_i ∇L=i=1∑4∇Li
梯度累积时的梯度:
- 若每个 micro-batch 的损失为 Li/4L_i/4Li/4,则梯度为 ∇(Li/4)=∇Li/4\nabla (L_i/4) = \nabla L_i /4∇(Li/4)=∇Li/4。
- 累积4步后的总梯度:
∑i=14∇(Li/4)=∑i=14∇Li4=14∑i=14∇Li \sum_{i=1}^4 \nabla (L_i/4) = \sum_{i=1}^4 \frac{\nabla L_i}{4} = \frac{1}{4} \sum_{i=1}^4 \nabla L_i i=1∑4∇(Li/4)=i=1∑44∇Li=41i=1∑4∇Li - 参数更新量公式:
参数更新量=η×14∑∇Li \text{参数更新量} = \eta \times \frac{1}{4} \sum \nabla L_i 参数更新量=η×41∑∇Li - 这等效于直接使用大批量时的梯度 ( \sum \nabla L_i ),但学习率需要放大4倍(即线性缩放规则)。
关键结论
- 除以4:通过归一化损失,使得梯度累积后的总梯度与大批量训练的梯度量级一致。
- 学习率调整:若使用梯度累积,通常需要将学习率乘以累积步数(例如,原学习率 η→η×4\eta \rightarrow \eta \times 4η→η×4)。
问题2:每个 batch 的损失是否会重复使用?
梯度累积的流程
假设累积步数 N=4N=4N=4,一个完整的训练流程如下:
optimizer.zero_grad() # 清空梯度
for i in range(4): # 累积4个 micro-batch
inputs, labels = next_batch()
outputs = model(inputs)
loss = criterion(outputs, labels)
loss = loss / 4 # 归一化损失
loss.backward() # 梯度累积到参数的.grad中
optimizer.step() # 参数更新(等效大批量)
optimizer.zero_grad() # 清空梯度,准备下一轮累积
关键点
-
Micro-batch 独立性:
- 每个 micro-batch 的损失独立计算,梯度独立累积。
- 不会重复使用之前的损失值,每个 micro-batch 的数据是独立的(例如,从数据集中顺序取4个不同的子批量)。
-
梯度累加方式:
- 每次
loss.backward()将当前 micro-batch 的梯度 累加 到参数的.grad中。 - 参数更新(
optimizer.step())仅在累积4步后执行一次。
- 每次
-
数据流示例:
- Batch 1: 计算 loss1 → 梯度1/4 → 累积梯度 = 梯度1/4
- Batch 2: 计算 loss2 → 梯度2/4 → 累积梯度 = 梯度1/4 + 梯度2/4
- Batch 3: 计算 loss3 → 梯度3/4 → 累积梯度 = 梯度1/4 + 梯度2/4 + 梯度3/4
- Batch 4: 计算 loss4 → 梯度4/4 → 累积梯度 = 梯度1/4 + 梯度2/4 + 梯度3/4 + 梯度4/4
- 最终参数更新量 = 学习率 × (梯度1 + 梯度2 + 梯度3 + 梯度4)/4
结论
- 不重复使用损失:每个 micro-batch 的损失仅用于计算当前梯度,不会被重复累加。
- 同步性:必须等待4个 micro-batch 全部计算完成,才能执行参数更新。
五、潜在问题与改进
代码中的隐患
- 梯度裁剪位置:若在每个 micro-batch 反向传播后立即裁剪梯度(如原代码所示),会导致多次裁剪,破坏梯度方向。正确做法应在累积完所有梯度后裁剪一次:
# 错误:每个 micro-batch 裁剪一次 for i in range(4): loss.backward() clip_grad_norm_() # 错误! # 正确:累积完所有梯度后裁剪一次 for i in range(4): loss.backward() clip_grad_norm_() # 正确 optimizer.step()
梯度累积的优缺点
- 优点:允许在显存不足时模拟大批量训练,提升训练稳定性。
- 缺点:
- 参数更新频率降低,可能影响收敛速度。
- 需根据累积步数调整学习率(如线性缩放规则:学习率 × 累积步数)。
总结
- 为什么除以4:通过归一化梯度量级,使累积后的梯度与大批量训练等效。
- 数据独立性:每个 micro-batch 的损失独立计算,梯度累加但不重复使用。
- 实现要点:梯度裁剪应在所有梯度累积完成后执行,避免多次裁剪。
更多推荐
所有评论(0)