跳转到内容

用 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 版本。请在运行训练的机器上同时安装两者:

bash
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 保存在那里:

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.6

trainer_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 越小越私密,也会加入越多的噪声。
delta1 / (10 × train 条目数)保证不成立的可能性。delta 必须小于 1 / train 条目数。
max_grad_norm1.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 文件作答:

bash
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 启动:

bash
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_multiplierOpacus 选定的噪声乘数
accountantrdp
coverstrain 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 的检查:

问题信息
未安装 Opacusdp needs opacus, which is not installed (pip install opacus).
没有 epsilondp 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 大于 1dp trains one sampled batch per step; set gradient_accumulation to 1.
设置了 lora.quantizedp 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 下列出同样的信息。

延伸阅读

参考文献