Zum Inhalt springen

Differentially privates Fine-Tuning mit Opacus

Einen Text- oder Bildklassifikator mit DP-SGD auf den Labels deiner Studie fine-tunen, Epsilon und Delta festlegen und den verbrauchten Datenschutz im Lauf-Log und in der Modellkarte ablesen.

Neu in v2.14.0

Dieses Tutorial trainiert einen Klassifikator mit Differential Privacy auf den eigenen Labels einer Studie, sodass das veröffentlichte Modell wenig über ein einzelnes annotiertes Item verrät. Die Seite richtet sich an Studienverantwortliche, deren Items oder Labels sensibel sind (z. B. Gesundheitsakten oder private Nachrichten) und die das Modell weitergeben oder veröffentlichen wollen. Dabei wird den Parametern von hf-text-classification ein dp-Block hinzugefügt, der Trainer auf dem Produktbewertungs-Beispiel ausgeführt und der verbrauchte Datenschutz aus dem Lauf-Log und der Modellkarte abgelesen. Derselbe Block funktioniert bei hf-image-classification, und kein anderer Trainer nimmt dp an.

Epsilon und Delta in der Differential Privacy

Differential Privacy ist eine mathematische Garantie für eine Berechnung auf einem Datensatz. Das Hinzufügen oder Entfernen eines Datensatzes ändert die Wahrscheinlichkeit jeder Ausgabe nur um einen begrenzten Betrag, sodass die Ausgabe wenig darüber verrät, ob dieser Datensatz vorhanden war. Epsilon (ε) ist die Schranke. Ein kleineres Epsilon macht die Ausgaben mit und ohne den Datensatz schwerer unterscheidbar, und ein kleineres Epsilon bietet deshalb stärkeren Datenschutz. Delta (δ) ist die Wahrscheinlichkeit, dass die Epsilon-Schranke verletzt wird, und die Dokumentation von Potato beschreibt Delta als „die Wahrscheinlichkeit, dass die Garantie nicht gilt“.

DP-SGD trainiert ein neuronales Netz unter dieser Garantie, nach der Methode von Abadi et al. (2016). Jeder Trainingsschritt beschneidet den Gradienten jedes Items auf eine maximale Norm und addiert gaußsches Rauschen zur Summe. Die Beschneidung begrenzt, wie weit ein Item das Modell verschieben kann, und das Rauschen verdeckt diese begrenzte Verschiebung. Opacus ist eine PyTorch-Bibliothek, die DP-SGD implementiert, und Potato übergibt die Trainingsschleife an Opacus, sobald ein Lauf dp trägt. Der RDP-Accountant von Opacus summiert das Epsilon auf, das jeder Schritt verbraucht.

Den Trainer und Opacus installieren

Die Transformer-Trainer brauchen torch und transformers aus den Trainings-Extras von Potato. Kein Potato-Extra enthält Opacus, weil Opacus bestimmte torch-Versionen festschreibt. Installiere beides auf der Maschine, auf der der Lauf trainiert:

bash
pip install "potato-annotation[train-text]"   # [train-vision] for hf-image-classification
pip install opacus

Ein Lauf mit dp prüft vor dem Trainingsstart, ob Opacus auf der Trainingsmaschine vorhanden ist. Auf einem ssh-, SLURM- oder Agent-Backend gehört Opacus deshalb auf den GPU-Server oder Rechenknoten, der trainiert, und nicht auf den Annotationsserver. Remote-Training behandelt diese Backends.

Den Trainer konfigurieren

Die folgende Konfiguration ist das Produktbewertungs-Beispiel aus Dein erstes Modell trainieren, mit privatem Training als Standard für hf-text-classification. Kopiere data/reviews.json und data/gold.json aus examples/ai-assisted/model-training/ von Potato in einen Projektordner und speichere diese Konfiguration dort als 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 legt die Standardwerte für jeden Lauf von hf-text-classification fest, deshalb öffnet sich das Feld Parameters der Training-Seite mit dem bereits ausgefüllten dp-Eintrag. epsilon: 8 liegt im Bereich von 1 bis 10, den die Dokumentation von Potato als üblich bezeichnet. delta fehlt, deshalb setzt Potato Delta auf 1 / (10 × Train-Items). max_grad_norm: 1.0 wiederholt die Standardbeschneidung. epochs und batch_size wiederholen die Standardwerte des Trainers, weil Opacus das Rauschen aus beiden bemisst. require_password: false und der einfache Admin-Schlüssel eignen sich nur für einen lokalen Test.

dp nimmt drei Schlüssel an:

SchlüsselStandardBedeutung
epsilonerforderlichDas Datenschutzbudget für den ganzen Lauf. Ein kleineres Epsilon ist privater und fügt mehr Rauschen hinzu.
delta1 / (10 × Train-Items)Die Wahrscheinlichkeit, dass die Garantie nicht gilt. Delta muss unter 1 / Train-Items liegen.
max_grad_norm1.0Der Gradient jedes Items wird auf diese Norm beschnitten, bevor das Rauschen addiert wird.

Die Konfiguration besteht python -m potato.validate_cli config.yaml --strict. Der Validator akzeptiert für dp ein Mapping, null oder false und weist dp: true zurück. Der Validator prüft außerdem jeden Schlüssel innerhalb von dp und seinen Wertebereich. Opacus und die Prüfungen, die von der Zahl der Train-Items abhängen, laufen beim Start eines Laufs auf der Maschine, die trainiert, wie Fehler beim Start eines Laufs auflistet.

Items labeln und einen Lauf starten

hf-text-classification weigert sich, auf weniger als 20 Train-Items zu fitten (min_instances). Starte den Server und labele dann Bewertungen von Hand oder mit drei simulierten Annotatoren aus dem Nutzersimulator, die aus der Gold-Datei antworten:

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

Der Simulator labelt 120 der 160 Bewertungen. Mit den Standard-splits (70 % Train, 15 % Val, 15 % Test) und split_seed: 0 legte unser Lauf 76 gelabelte Bewertungen in Train, 19 in Val und 25 in Test. Delta ist deshalb standardmäßig 1 / 760, etwa 0,00132, und jedes selbst gesetzte Delta muss unter 1 / 76 liegen, etwa 0,0132.

Öffne http://localhost:8000/admin/training mit dem Admin-Schlüssel, wähle hf-text-classification und das Schema sentiment, prüfe, dass das Feld Parameters den dp-Eintrag zeigt, und klicke auf Train now. Derselbe Lauf startet über die 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"]}'

Die Antwort trägt die run_id. Die eigenen params eines Laufs überschreiben trainer_params, deshalb trainiert "params": {"dp": {"epsilon": 2}} einen Lauf mit kleinerem Budget, und "params": {"dp": null} oder "params": {"dp": false} trainiert einen Lauf ohne DP. Lass split_seed zwischen den beiden Läufen unverändert, damit beide auf denselben Items trainieren und bewerten und die Holdout-Scores direkt vergleichbar sind.

Lauf-Log-Zeilen unter DP

Die Lauf-Seite unter /admin/training/runs/<run_id> listet die Ereignisse und Metriken des Laufs auf. Ein DP-Lauf fügt den Ereignissen drei Arten von Zeilen hinzu.

Die erste Zeile nennt die Embedding-Tabellen, die Potato einfriert. Die Positions- und Token-Typ-Embeddings eines Text-Transformers haben eine Zeile, die der ganze Batch teilt, und Opacus kann über eine geteilte Zeile keinen Gradienten pro Item berechnen. Beim Standardmodell distilbert/distilroberta-base sind die eingefrorenen Tabellen roberta.embeddings.position_embeddings und roberta.embeddings.token_type_embeddings. Eingefrorene Tabellen werden nicht trainiert und verbrauchen keinen Datenschutz, und die Wort-Embeddings und jede andere Schicht werden weiter trainiert.

Die zweite Zeile beginnt mit DP-SGD: und nennt das Ziel-Epsilon, Delta, die geplanten Epochen, den von Opacus gewählten Rauschmultiplikator, die Beschneidung pro Item und die Sampling-Rate. Jeder Schritt zieht einen Batch per Poisson-Sampling, sodass jedes Train-Item unabhängig mit der Wahrscheinlichkeit 1 / Batches pro Epoche in einen Batch gelangt. Bei 76 Train-Items in Batches von 16 hat eine Epoche 5 Batches, und das Log gibt eine Sampling-Rate von 0.2000 an. Der Rauschmultiplikator legt die Standardabweichung des Rauschens fest, und die FAQ von Opacus nennt 0,1 bis 2 als üblichen Bereich.

Nach jeder Epoche meldet eine Zeile der Form Epoch <n>/<epochs>: loss <loss>, epsilon spent <epsilon> den mittleren Verlust der Epoche und das bisher verbrauchte Epsilon. Derselbe Wert wird pro Epoche als Metrik dp_epsilon festgehalten, und die abschließenden Metriken des Laufs tragen dp_epsilon für den ganzen Lauf. Opacus bemisst das Rauschen so, dass die geplanten Epochen höchstens das Ziel-Epsilon verbrauchen. Ein Lauf, der wegen patience früh stoppt (standardmäßig zwei Epochen ohne besseren Val-Score), verbraucht weniger als das Ziel.

Der dp-Eintrag in der Modellkarte

Der Lauf schreibt model_card.json nach artifacts/ im Lauf-Verzeichnis, <task_dir>/<output_annotation_dir>/training/runs/<run_id>/. Die Karte eines DP-Laufs hat einen dp-Eintrag:

FeldWert
epsilon_targetDas angeforderte epsilon
epsilon_spentWas der Lauf verbraucht hat, nach dem RDP-Accountant von Opacus
deltaDas Delta, das der Lauf verwendet hat, das eigene oder der Standard
max_grad_normDie Beschneidung pro Item
noise_multiplierDer von Opacus gewählte Rauschmultiplikator
accountantrdp
coverstrain split

epsilon_spent zählt jede Epoche, die der Lauf trainiert hat. Epochen nach dem behaltenen Checkpoint zählen zur Summe. Auf dem Hugging Face Hub veröffentlichen lädt model_card.json mit dem Modell hoch, wobei lokale Pfade durch Dateinamen ersetzt werden, sodass der dp-Eintrag mit den Gewichten wandert. Die Modellkarte README.md auf dem Hub hat außerdem eine Zeile „Differential privacy“ mit dem verbrauchten Epsilon im Vergleich zum Ziel, dem Delta und dem Train-Split, den die Garantie abdeckt. Die Zeile lässt das Ziel oder das Delta weg, wenn der Lauf den Wert nicht festgehalten hat.

Das Datenschutzbudget und die Beschneidung wählen

Die Dokumentation von Potato nennt 1 bis 10 als üblichen Bereich für Epsilon und verlangt ein Delta unter 1 / Train-Items. Die FAQ von Opacus nennt dieselbe Faustregel für Delta und empfiehlt eine kleine Konstante für Epsilon bei winzigem Delta. Das Standard-Delta von 1 / (10 × Train-Items) liegt unter der Obergrenze von 1 / Train-Items, sodass die meisten Studien Delta weglassen können.

Die Beschneidung, max_grad_norm, wägt zwei Fehler gegeneinander ab. Das Beschneiden verändert jeden Gradienten, der größer als die Schwelle ist, und laut der FAQ von Opacus beeinträchtigt das Beschneiden die Konvergenz. Das Rauschen wird an der Beschneidung skaliert, deshalb fügt eine größere Beschneidung mehr Rauschen hinzu. Die FAQ ergänzt, dass eine falsche Schwelle lineare Modelle stärker schädigt als tiefe. Beginne mit dem Standard von 1.0 und vergleiche Holdout-Scores, bevor die Beschneidung abgestimmt wird.

Opacus bemisst das Rauschen für die geplanten epochs, sodass dasselbe Epsilon, auf weniger Epochen verteilt, weniger Rauschen in jedem Schritt bedeutet. Die FAQ von Opacus merkt an, dass ein größerer Batch die Sampling-Rate erhöht und mit der Sampling-Rate die Datenschutzkosten. Die FAQ schlägt eine größere Lernrate und früheres Stoppen vor, um die Kosten auszugleichen. Vergleiche den Holdout-Score eines DP-Laufs mit dem Score eines Laufs ohne dp auf demselben Split. Die Lücke zwischen den beiden Scores misst, was die Garantie auf deinen Daten kostet.

Reichweite der Garantie

Die Garantie deckt den Train-Split ab. Wird ein Train-Item geändert, ändert sich das, was das veröffentlichte Modell sein könnte, höchstens um das angegebene Epsilon. Das Item zählt zusammen mit dem Label, das aus den Annotatoren des Items aufgelöst wurde. Potato formuliert die Garantie pro Item, und die Dokumentation formuliert keine Garantie pro Annotator.

Der Val-Split liegt außerhalb der Garantie. Nach jeder Epoche bewertet der Trainer das Modell auf den Val-Items und behält den Checkpoint der am besten bewerteten Epoche. Diese Wahl liest die Labels der Val-Items ohne hinzugefügtes Rauschen, deshalb macht das Epsilon auf der Modellkarte kein Versprechen für Val-Items. Brauchen Val-Items denselben Schutz wie Train-Items, setze den Val-Anteil auf 0 und verschiebe den Anteil nach Test, z. B. splits: {train: 0.7, val: 0, test: 0.3}. Ohne Val-Items behält der Trainer die letzte Epoche, patience stoppt den Lauf nie früh, und der Holdout-Score stammt von Test. Ein unveränderter Train-Anteil hält dieselben Items in Train, weil Potato Splits über einen geseedeten Hash der ID jedes Items zuweist.

Die Label-Liste des Modells liegt außerhalb der Garantie. Potato liest die Label-Liste aus den Labels, die in den Daten vorkommen, deshalb erscheint ein Label, das Annotatoren nur bei einem einzigen Item verwendet haben, weiterhin unter den Ausgaben des Modells.

Unterschiede im Training unter DP

DP-SGD braucht einen Gradienten für jedes Item, deshalb unterscheidet sich ein DP-Lauf von einem gewöhnlichen Lauf desselben Trainers in diesen Punkten:

  • Das Training läuft in voller Genauigkeit, was auch immer precision sagt, weil gemischte Genauigkeit unter DP abgeschaltet ist.
  • gradient_accumulation muss 1 sein. Jeder Schritt trainiert einen per Poisson gesampelten Batch.
  • Ein quantisiertes Basismodell (lora.quantize) wird zurückgewiesen.
  • Die Positions- und Token-Typ-Embedding-Tabellen eines Text-Transformers werden eingefroren, wie das Lauf-Log meldet.
  • Ein Modell mit einer Schicht, die Opacus nicht unterstützt, etwa BatchNorm, scheitert beim Trainingsstart unter Nennung der Schicht.

Nur hf-text-classification und hf-image-classification nehmen dp an. Jeder andere Trainer weist einen Parameter zurück, den er nicht annimmt, deshalb scheitert dp bei sklearn-text, setfit oder hf-token-classification unter trainer_params beim Boot und mit einem 409, wenn ein Lauf startet.

Fehler beim Start eines Laufs

potato validate meldet vor dem Serverstart einen Schlüssel, den dp nicht annimmt, und einen Wert außerhalb des Bereichs. Ein Lauf prüft den dp-Block beim Start erneut, zusammen mit Opacus und den Grenzen, die vom Train-Split abhängen. Ein Lauf, der diese Prüfungen nicht besteht, endet vor dem Training mit einem Fehler, mit dem Code validation_failed und jedem Problem in einer Meldung. Die Meldungen stammen aus den Prüfungen von Potato:

ProblemMeldung
Opacus nicht installiertdp needs opacus, which is not installed (pip install opacus).
Kein epsilondp needs epsilon, the privacy budget for the run (1 to 10 is usual).
Ein Schlüssel, den dp nicht annimmtdp does not take bogus; it takes epsilon, delta and max_grad_norm.
delta bei oder über 1 / Train-Itemsdp.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 über 1dp trains one sampled batch per step; set gradient_accumulation to 1.
lora.quantize gesetztdp cannot train a quantized (4bit or 8bit) base model; drop lora.quantize.

Ein leeres Mapping, dp: {}, verlangt DP ohne Budget und scheitert mit der Meldung zum fehlenden Epsilon. Setzt trainer_params dp, schaltet ein "dp": null oder "dp": false eines Laufs DP für diesen Lauf ab. Die Status-Route, GET /admin/training/api/status, listet dieselben Meldungen unter errors auf.

Referenzen