跳轉到內容

用 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 下列出同樣的資訊。

延伸閱讀

參考文獻