用 Opacus 做差分隐私微调
用 DP-SGD 在研究的标签上微调文本或图像分类器,设定 epsilon 和 delta,并在运行日志和模型卡中查看已消耗的隐私。
v2.14.0 新增
本教程用差分隐私在研究自己的标签上训练分类器,使发布的模型很少泄露任何一个已标注条目的信息。本页面向条目或标签敏感(例如健康记录或私人消息)、并打算共享或发表模型的研究负责人。你将在 hf-text-classification 的参数中添加 dp 块,在商品评论示例上运行该训练器,并从运行日志和模型卡中读取已消耗的隐私。同样的块适用于 hf-image-classification,其他训练器都不接受 dp。
差分隐私中的 epsilon 和 delta
差分隐私是关于数据集上某项计算的数学保证。增加或删除一条记录,任何输出的概率变化都有界,所以输出很少泄露该记录是否存在。Epsilon(ε)就是这个界。epsilon 越小,有该记录和没有该记录时的输出越难区分,因此 epsilon 越小,隐私越强。Delta(δ)是 epsilon 界不成立的概率,Potato 的文档把 delta 描述为“保证不成立的可能性”。
DP-SGD 按 Abadi 等人(2016)的方法,在这一保证之下训练神经网络。每个训练步骤把每个条目的梯度裁剪到最大范数,并给求和结果加上高斯噪声。裁剪限制单个条目能让模型移动多远,噪声则掩盖这个有界的移动。Opacus 是实现 DP-SGD 的 PyTorch 库,只要一次运行带有 dp,Potato 就把训练循环交给 Opacus。Opacus 的 RDP 记账器把每一步消耗的 epsilon 累加起来。
安装训练器和 Opacus
Transformer 训练器需要 Potato 训练 extra 中的 torch 和 transformers。没有任何 Potato extra 包含 Opacus,因为 Opacus 固定了特定的 torch 版本。请在运行训练的机器上同时安装两者:
pip install "potato-annotation[train-text]" # [train-vision] for hf-image-classification
pip install opacus带 dp 的运行在训练开始之前会检查训练机器上是否有 Opacus。因此在 ssh、SLURM 或 agent 后端上,Opacus 要装在负责训练的 GPU 服务器或计算节点上,而不是标注服务器上。这些后端见远程训练。
配置训练器
下面的配置是训练你的第一个模型中的商品评论示例,并把私有训练设为 hf-text-classification 的默认值。请把 data/reviews.json 和 data/gold.json 从 Potato 的 examples/ai-assisted/model-training/ 复制到一个项目文件夹中,并把这份配置作为 config.yaml 保存在那里:
annotation_task_name: "Review Sentiment (private training)"
data_files:
- "data/reviews.json"
item_properties:
id_key: "id"
text_key: "text"
task_dir: "."
output_annotation_dir: "annotation_output"
require_password: false
admin_api_key: "demo-admin-key"
num_annotators_per_item: 1
annotation_schemes:
- annotation_type: "radio"
name: "sentiment"
description: "Is this review positive or negative?"
labels:
- "positive"
- "negative"
sequential_key_binding: true
model_training:
enabled: true
trainer_params:
hf-text-classification:
epochs: 5
batch_size: 16
dp:
epsilon: 8
max_grad_norm: 1.0
writeback:
schemas: ["sentiment"]
include_unlabeled: true
min_confidence: 0.6trainer_params 为 hf-text-classification 的每次运行设定默认值,所以训练页面的 Parameters 字段打开时已经填好了 dp 条目。epsilon: 8 落在 Potato 文档称为常用的 1 到 10 的范围之内。delta 被省略,所以 Potato 把 delta 设为 1 / (10 × train 条目数)。max_grad_norm: 1.0 重复了默认的裁剪值。epochs 和 batch_size 重复了训练器的默认值,因为 Opacus 根据这两者来确定噪声的大小。require_password: false 和明文的管理员密钥只适合本地试用。
dp 接受三个键:
| 键 | 默认值 | 含义 |
|---|---|---|
epsilon | 必填 | 整次运行的隐私预算。epsilon 越小越私密,也会加入越多的噪声。 |
delta | 1 / (10 × train 条目数) | 保证不成立的可能性。delta 必须小于 1 / train 条目数。 |
max_grad_norm | 1.0 | 加噪声之前,每个条目的梯度被裁剪到这个范数。 |
该配置能通过 python -m potato.validate_cli config.yaml --strict。验证器接受 dp 为映射、null 或 false,并拒绝 dp: true。验证器还会检查 dp 内的每个键及其范围。Opacus 以及依赖 train 条目数的检查在一次运行开始时在训练机器上执行,具体见运行开始时的错误。
标注条目并开始一次运行
hf-text-classification 拒绝在少于 20 个 train 条目(min_instances)上拟合。请启动服务器,然后手工标注评论,或者用用户模拟器中的三个模拟标注者,它们根据 gold 文件作答:
potato start config.yaml -p 8000
python -m potato.simulator --server http://localhost:8000 --users 3 \
--strategy gold_standard --competence good=1.0 --max-annotations 40 \
--gold-file data/gold.json --sequential --fast-mode --no-export模拟器标注了 160 条评论中的 120 条。在默认的 splits(70% train、15% val、15% test)和 split_seed: 0 下,我们的运行把 76 条已标注评论放进 train,19 条放进 val,25 条放进 test。因此 delta 默认为 1 / 760,约 0.00132,你设置的任何 delta 都必须小于 1 / 76,约 0.0132。
用管理员密钥打开 http://localhost:8000/admin/training,选择 hf-text-classification 和 sentiment 方案,确认 Parameters 字段显示了 dp 条目,然后点击 Train now。同样的运行也可以从 API 启动:
curl -X POST -H "X-API-Key: demo-admin-key" -H "Content-Type: application/json" \
http://localhost:8000/admin/training/api/runs \
-d '{"trainer": "hf-text-classification", "schemas": ["sentiment"]}'响应中带有 run_id。一次运行自己的 params 会覆盖 trainer_params,所以 "params": {"dp": {"epsilon": 2}} 以更小的预算训练一次运行,"params": {"dp": null} 或 "params": {"dp": false} 则训练一次不带 DP 的运行。两次运行之间请保持 split_seed 不变,这样两者在同样的条目上训练和评分,留出得分可以直接比较。
DP 下的运行日志行
/admin/training/runs/<run_id> 的运行页面列出该次运行的事件和指标。DP 运行会在事件中增加三类行。
第一行指出 Potato 冻结的嵌入表。文本 transformer 的位置嵌入和 token 类型嵌入对整批数据共用一行,而 Opacus 无法通过共用的行计算每个条目的梯度。对默认模型 distilbert/distilroberta-base,被冻结的表是 roberta.embeddings.position_embeddings 和 roberta.embeddings.token_type_embeddings。冻结的表不训练,也不消耗隐私,词嵌入和其他每一层仍然训练。
第二行以 DP-SGD: 开头,给出目标 epsilon、delta、计划的 epoch 数、Opacus 选定的噪声乘数、每个条目的裁剪值和采样率。每一步通过 Poisson 采样抽取一批,所以每个 train 条目以 1 / 每个 epoch 的批数 的概率独立加入一批。76 个 train 条目按每批 16 个,一个 epoch 有 5 批,日志给出的采样率是 0.2000。噪声乘数设定噪声的标准差,Opacus 的 FAQ 给出的常用范围是 0.1 到 2。
每个 epoch 之后,形如 Epoch <n>/<epochs>: loss <loss>, epsilon spent <epsilon> 的一行报告该 epoch 的平均损失和目前已消耗的 epsilon。同一个值按 epoch 记录为 dp_epsilon 指标,运行的最终指标带有整次运行的 dp_epsilon。Opacus 确定噪声的大小,使计划的 epoch 至多消耗目标 epsilon。因 patience(默认为连续两个 epoch 没有更好的 val 得分)而提前停止的运行,消耗的隐私少于目标。
模型卡中的 dp 条目
运行把 model_card.json 写入运行目录 <task_dir>/<output_annotation_dir>/training/runs/<run_id>/ 下的 artifacts/。DP 运行的卡片带有 dp 条目:
| 字段 | 值 |
|---|---|
epsilon_target | 你请求的 epsilon |
epsilon_spent | 该次运行消耗的量,来自 Opacus 的 RDP 记账器 |
delta | 该次运行使用的 delta,可以是你自己设的,也可以是默认值 |
max_grad_norm | 每个条目的裁剪值 |
noise_multiplier | Opacus 选定的噪声乘数 |
accountant | rdp |
covers | train split |
epsilon_spent 计入运行训练过的每个 epoch。保留的检查点之后的 epoch 也计入总量。发布到 Hugging Face Hub 会把 model_card.json 连同模型一起上传,其中的本地路径被替换为文件名,所以 dp 条目随权重一起流转。Hub 上的 README.md 模型卡还有一行“Differential privacy”,给出已消耗的 epsilon 与目标的对比、delta,以及该保证所覆盖的 train 划分。运行没有记录目标或 delta 的值时,这一行会省略它。
选择隐私预算和裁剪值
Potato 的文档给出 epsilon 的常用范围是 1 到 10,并要求 delta 小于 1 / train 条目数。Opacus 的 FAQ 对 delta 给出同样的经验规则,并建议 epsilon 取一个小的常数、delta 取极小值。默认的 delta 1 / (10 × train 条目数) 低于 1 / train 条目数 这个上限,所以大多数研究可以省略 delta。
裁剪值 max_grad_norm 要在两种误差之间权衡。裁剪会改变每个大于阈值的梯度,Opacus 的 FAQ 说裁剪会损害收敛。噪声按裁剪值缩放,所以裁剪值越大,加入的噪声越多。FAQ 还补充说,阈值设错对线性模型的伤害比对深度模型更大。请从默认的 1.0 开始,先比较留出得分,再调整裁剪值。
Opacus 按计划的 epochs 确定噪声的大小,所以同样的 epsilon 分摊到更少的 epoch 上,每一步的噪声就更小。Opacus 的 FAQ 指出,批越大,采样率越高,隐私代价也随采样率升高。FAQ 建议用更大的学习率和更早停止来抵消这一代价。请把 DP 运行的留出得分与同一划分上不带 dp 的运行的得分比较。两个得分之间的差距衡量这一保证在你的数据上的代价。
保证的范围
该保证覆盖 train 划分。改变一个 train 条目,发布的模型可能发生的变化至多是所述的 epsilon。该条目与由其标注者得出的已解析标签一起计算。Potato 按条目陈述该保证,文档没有陈述按标注者的保证。
val 划分在保证之外。每个 epoch 之后,训练器在 val 条目上给模型评分,并保留得分最好的 epoch 的检查点。这一选择读取了 val 条目的标签,没有加噪声,所以模型卡上的 epsilon 对 val 条目不作任何承诺。当 val 条目需要与 train 条目同样的保护时,请把 val 比例设为 0,并把这部分比例移给 test,例如 splits: {train: 0.7, val: 0, test: 0.3}。没有 val 条目时,训练器保留最后一个 epoch,patience 永远不会提前停止运行,留出得分来自 test。保持 train 比例不变就能让 train 中的条目不变,因为 Potato 按每个条目 id 的带种子哈希来分配划分。
模型的标签列表在保证之外。Potato 从数据中出现的标签读取标签列表,所以标注者只在一个条目上用过的标签,仍然会出现在模型的输出中。
DP 下的训练差异
DP-SGD 需要每个条目的梯度,所以 DP 运行与同一训练器的普通运行有以下区别:
- 无论
precision怎么设置,训练都以全精度运行,因为 DP 下混合精度是关闭的。 gradient_accumulation必须为 1。每一步训练一个 Poisson 采样的批。- 量化的基础模型(
lora.quantize)会被拒绝。 - 文本 transformer 的位置嵌入表和 token 类型嵌入表被冻结,运行日志会报告这一点。
- 带有 Opacus 不支持的层(例如 BatchNorm)的模型,会在训练开始时失败,并指出这一层。
只有 hf-text-classification 和 hf-image-classification 接受 dp。其他每个训练器都会拒绝自己不接受的参数,所以在 sklearn-text、setfit 或 hf-token-classification 上使用 dp,在 trainer_params 下会在启动时失败,在一次运行开始时则以 409 失败。
运行开始时的错误
potato validate 在服务器启动之前报告 dp 不接受的键和超出范围的值。一次运行在开始时会再次检查 dp 块,同时检查 Opacus 以及取决于 train 划分的限制。没有通过这些检查的运行会在训练之前以错误结束,错误码为 validation_failed,所有问题合在一条信息中。这些信息来自 Potato 的检查:
| 问题 | 信息 |
|---|---|
| 未安装 Opacus | dp needs opacus, which is not installed (pip install opacus). |
没有 epsilon | dp needs epsilon, the privacy budget for the run (1 to 10 is usual). |
dp 不接受的键 | dp does not take bogus; it takes epsilon, delta and max_grad_norm. |
delta 大于或等于 1 / train 条目数 | dp.delta is 0.05, at least 1 / 76 train items: at that delta one item's data could be released outright. Use less than 0.0131579. |
gradient_accumulation 大于 1 | dp trains one sampled batch per step; set gradient_accumulation to 1. |
设置了 lora.quantize | dp cannot train a quantized (4bit or 8bit) base model; drop lora.quantize. |
空映射 dp: {} 请求不带预算的 DP,并以缺少 epsilon 的信息失败。当 trainer_params 设置了 dp 时,运行的 "dp": null 或 "dp": false 会为该次运行关闭 DP。状态路由 GET /admin/training/api/status 在 errors 下列出同样的信息。
延伸阅读
- 模型训练概览,介绍训练页面和 Hub 发布
- 训练器,介绍
hf-text-classification和hf-image-classification拟合什么 - 模型训练配置,介绍划分、留出得分和回写
- 训练你的第一个模型,介绍不带 GPU 或 DP 的同一个项目
- 远程训练,介绍在 GPU 服务器上运行 DP 微调
- Potato 模型训练参考中的差分隐私(
dp)
参考文献
- Abadi, M., Chu, A., Goodfellow, I., McMahan, H. B., Mironov, I., Talwar, K., and Zhang, L. (2016). Deep learning with differential privacy. Proceedings of the 2016 ACM SIGSAC Conference on Computer and Communications Security, 308-318. https://doi.org/10.1145/2976749.2978318
- Opacus. FAQ. https://opacus.ai/docs/faq