引言和背景
差分隐私随机梯度下降(DP-SGD)是训练具有差分隐私的机器学习模型的典型方法。它对其非隐私版本随机梯度下降(SGD)进行了以下两个修改。
-
每样本梯度裁剪:在每个小批量中裁剪梯度,确保其范数不超过预指定的值,“裁剪范数”,C,在每次迭代中。
-
噪声添加:在每次迭代中,将预指定的方差的高斯噪声添加到平均剪裁梯度中,取决于剪裁范数和隐私参数。
第一个变化,按样本梯度剪裁,引入了额外的复杂性,因为通常它需要实例化每个样本的梯度。
Opacus 是 DP-SGD 的 PyTorch 实现。Opacus 通过使用钩子函数来解决上述任务,允许在特定事件上干预,例如正向和反向传播。有关 Opacus 的更多详细信息,我们鼓励读者回顾之前的博客文章:《DP-SGD 算法解析》、《Opacus 中的高效按样本梯度计算》和《Opacus 中更多层的高效按样本梯度计算》。
虽然 Opacus 与原始方法相比提供了显著的效率提升,但实例化每个样本梯度的内存成本是显著的。特别是,内存使用量与批量大小乘以可训练参数的数量成正比。因此,内存限制 Opacus 只能用于小批量大小和/或小模型,这极大地限制了其应用范围。
我们向 Opacus 引入了快速梯度裁剪和幽灵裁剪,这使得开发者和研究人员可以在不实例化每个样本的梯度的情况下执行梯度裁剪。例如,这允许在单个 16GB GPU 上微调 BERT 的 700 万个参数,批大小为 1024,内存与使用 PyTorch(不应用 DP-SGD)相当。相比之下,Opacus 的先前版本在相同设置下支持的最大批大小约为 256。我们提供了一个教程,说明了如何使用快速梯度裁剪在 Opacus 中完成上述任务。
快速梯度裁剪和幽灵裁剪
这些技术的关键思想基于以下观察:假设已知每个样本的梯度范数,则可以通过对重新加权的损失函数 $\bar{L}$ 进行反向传播来实现梯度裁剪。该损失函数定义为 $\bar{L} = \sum_{i} R_{i} L_{i}$,其中 $R_i = \min\left(\frac{C}{C_i}, 1\right)$ 是从每个样本的梯度范数 $C_i$ 计算出的裁剪系数,而 $L_i$ 是每个样本的损失。
上述想法乍一看可能看似循环,因为它似乎需要实例化每个样本的梯度来计算每个样本的梯度范数。然而,对于神经网络架构中广泛使用的某些组件,如全连接/线性层,确实可以在单个反向传播过程中获得每个样本的梯度范数,而无需每个样本的梯度。这表明了一个涉及两个反向传播过程的流程:第一个用于计算每个样本的梯度范数,第二个用于计算聚合(非每个样本)的裁剪梯度。第二个反向传播仅仅是标准的批量反向传播。
图 1:vanilla Opacus(左上)、Fast Gradient Clipping(右上)和 Ghost clipping(下)的比较。我们用红色标记了成为内存瓶颈的梯度实例化。对于 vanilla Opacus,它必须实例化每个样本的梯度。Fast Gradient Clipping 为每个层实例化每个样本的梯度以计算其范数,一旦反向传播移动到下一层,该范数就会被立即释放。Ghost Clipping 直接从每个样本的激活梯度和每个样本的激活中进行操作,避免了梯度实例化的需求。
Fast Gradient Clipping
在 Fast Gradient Clipping 中,每个样本的梯度范数计算分为三个步骤:
- 对于每一层,实例化每个样本的梯度并计算其范数。
- 每个样本的梯度随后立即被丢弃。
- 将每一层的(平方)每个样本梯度范数相加,以获得整体(平方)每个样本梯度范数。
幽灵剪裁
扩展快速梯度剪裁的方法,幽灵剪裁利用了对于线性层,每个样本的梯度范数可以通过激活梯度和激活来计算的事实。特别是,设 backprops
和 activations
分别为每个样本的激活梯度和激活,维度分别为 batch_size ✕ output_width
和 batch_size ✕ input_width
。每个样本的梯度是这两个的外积,这需要 O(batch_size ✕ input_width ✕ output_width)
时间和空间。
鬼剪裁技巧实际上计算的是 backprops
和 activations
的(平方)范数,逐样本计算,并取它们的乘积,从而得到梯度的(平方)范数。这需要 O(batch-size ✕ (input_width + output_width))
时间,并需要 O(batch-size)
空间来存储。由于每个样本的激活和每个样本的激活梯度已经存储,因此只需要额外的内存来存储范数。
快速梯度剪裁与鬼剪裁之间的关系
- 快速梯度剪裁和鬼剪裁是互补的技术。快速梯度剪裁可以应用于任何类型的层,而鬼剪裁是针对支持层的一种严格更好的技术。
- 我们的实现会自动在层不支持鬼剪裁时切换到快速梯度剪裁。
如何在 Opacus 中使用快速梯度裁剪
训练循环与标准 PyTorch 循环相同。与 Opacus 之前一样,我们使用 PrivacyEngine()
来“净化”模型和优化器。要启用幽灵裁剪,使用 grad_sample_mode="ghost"
参数。此外, make_private()
将损失准则作为额外输入并对其进行净化。这允许我们在 loss.backward()
中隐藏两次反向传递和损失缩放。
from opacus import PrivacyEngine
criterion = nn.CrossEntropyLoss() # example loss function
privacy_engine = PrivacyEngine()
model_gc, optimizer_gc, criterion_gc, train_loader, = privacy_engine.make_private(
module=model,
optimizer=optimizer,
data_loader=train_loader,
noise_multiplier=noise_multiplier
max_grad_norm=max_grad_norm,
criterion=criterion,
grad_sample_mode="ghost",
)
# The training loop below is identical to that of PyTorch
for input_data, target_data in train_loader:
output_gc = model_gc(input_data) # Forward pass
optimizer_gc.zero_grad()
loss = criterion_gc(output_gc, target_data)
loss.backward()
optimizer_gc.step() # Add noise and update the model
在内部,在第一次传递之前,我们启用钩子,这允许我们捕获与正向和反向调用对应的层值。它们用于计算每个样本的梯度范数。然后我们计算裁剪系数,缩放损失函数并禁用钩子,这使我们能够使用标准的 PyTorch 反向传递。
内存复杂度分析
考虑具有以下特性的多层神经网络:
L:层数
d:最大层宽
B:批量大小
K:不支持/非线性层的数量
与普通(PyTorch)SGD 相比,DP-SGD 与 Ghost Clipping 的内存开销是可加的 O(BL),需要存储所有层的样本梯度范数。此外,如果存在不支持层(如果 K≥1),则需要额外的 O(Bd 2 )内存来实例化该层的梯度。
内存基准测试
我们提供了各种设置下的内存使用结果。
微调 BERT
我们考虑了在文本分类任务中私有地微调 BERT 的最后三层的问题。基础模型有超过 1000 万个参数,其中我们微调了最后三层,即 BertEncoder,
BertPooler,
和 Classifier
,大约包含 760 万个参数。实验在一个 P100 GPU 上运行,内存为 16GB。
下表报告了各种方法每迭代的最大内存和时间:
批处理大小 | |||||||||
B = 32 | B = 128 | B = 512 | B = 1024 | B = 2048 | |||||
内存 | 时间 | 内存 | 时间 | 内存 | 时间 | 内存 | 时间 | ||
PyTorch 随机梯度下降 | 236 兆字节 | 0.15 秒 | 1.04 GB | 0.55 秒 | 5.27 GB | 2.1 秒 | 12.7 GB | 4.2 秒 | OOM |
DP-SGD | 1,142 MB | 0.21 秒 | 4.55 GB | 0.68 秒 | OOM | OOM | OOM | ||
FGC DP-SGD | 908 MB | 0.21 秒 | 3.6GB | 0.75 秒 | OOM | OOM | OOM | ||
GC DP-SGD | 362 兆字节 | 0.21 秒 | 1.32 吉字节 | 0.67 秒 | 5.27 GB | 2.5 秒 | 12.7 GB | 5 秒 | OOM |
在峰值内存占用方面,DP-SGD > FGC DP-SGD ≫ GC DP-SGD ≈ PyTorch SGD。此外,运行时间相似,因为大多数参数被冻结,正向传播占据了大部分时间。
合成设置:内存分析
我们考虑以下设置来分析 PyTorch SGD、Vanilla DP-SGD 和 Ghost Clipping,GC DP-SGD 使用的内存。
- 2 层全连接神经网络
- 输入: 5120
- 隐藏: 2560
- 输出: 1280
- 模型参数总数 = 15.6M
- 模型大小 = 62.5 MB
- 批处理大小,如下表所示的不同值。
下表总结了每种方法在训练循环各个阶段的内存最大增加(以 MB 为单位)。
批处理大小 | 方法 | 模型到 GPU | 前向 | 第一次反向 | 第二次反向 | 优化步 |
32 | PyTorch SGD | 62.5 | 0.5 | 62.5 | 无效 | 0 |
香草 DP-SGD | 62.5 | 0.47 | 3,663 | 无 | 162.5 | |
GC DP-SGD | 62.5 | 0.47 | 63.13 | 50 | 125 | |
217 | PyTorch SGD | 62.5 | 1920 | 1932.5 | 无 | 0 |
香草 DP-SGD | OOM | |||||
GC DP-SGD | 62.5 | 1920 | 2625 | 1932.5 | 125 |
行业应用案例
我们在 Meta 内部的一个用例上测试了 Ghost Clipping DP-SGD,该用例包含一个大约 100B 大小的模型,具有 4000 万个可训练参数。我们的初步结果表明,Ghost Clipping SGD 将 vanilla DP-SGD 的内存减少了 95%,并且与 PyTorch SGD 具有可比的内存使用量。
结论
在本文中,我们描述了 Opacus 中 Fast Gradient Clipping 和 Ghost Clipping 的实现,这些实现使得机器学习模型在差分隐私的情况下能够进行内存高效的训练。目前,Ghost Clipping 的实现仅适用于线性层,但正如系列的第 3 部分所述,它可以扩展到“广义”线性层,如卷积和多头注意力。当前的这些技术需要两个显式的反向传播步骤,这会增加运行时间。我们将探索 Ghost Clipping 之上的发展,例如用于缓解的 Book-Keeping 算法。
想了解更多关于 Opacus 的信息,请访问 opacus.ai 和 github.com/pytorch/opacus。
致谢
我们感谢 Iden Kalemaj、Darren Liu、Karthik Prasad、Hao Shi、Igor Shilov、Davide Testuggine、Eli Uriegas、Haicheng Wang 和 Richard Zou 提供的宝贵反馈和建议。
-
有方法可以将鬼剪裁扩展到非线性层。