Vai al contenuto principale
AI/ML

Privacy Differenziale e Federated Learning: Architettura di Pipeline ML Sanitari Conformi HIPAA

6 min lettura
LD
Lucio Durán
Engineering Manager & AI Solutions Architect
Disponibile anche in: English, Español

L'Architettura

Ospedale A ──┐
Ospedale B ──┤
Ospedale C ──┼──→ [Server Aggregazione Sicura (AWS)] ──→ [Modello Globale]
Ospedale D ──┤ ↑ solo gradienti crittografati
Ospedale E ──┘ ↓ solo aggiornamenti aggregati

Ogni ospedale esegue un nodo di training locale. Il server centrale non vede mai dati grezzi, gradienti grezzi, o aggiornamenti di modello individuali. Vede solo il risultato dell'aggregazione sicura — la somma crittografata di tutti gli aggiornamenti.

Privacy Differenziale: La Teoria che Serve Davvero

La privacy differenziale è una definizione matematica, non una tecnica. Un algoritmo randomizzato M soddisfa la (ε, δ)-privacy differenziale se per tutti i dataset D e D' che differiscono in un record, e per tutti i possibili output S:

P[M(D) ∈ S] ≤ e^ε · P[M(D') ∈ S] + δ

In parole semplici: rimuovere o aggiungere i dati di qualsiasi singolo paziente cambia la distribuzione dell'output al massimo di un fattore e^ε, più una probabilità di fallimento trascurabile δ.

Per il deep learning, applichiamo questo attraverso DP-SGD:

import torch
from opacus import PrivacyEngine

model = PneumoniaResNet(pretrained=True)
optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.9)
data_loader = torch.utils.data.DataLoader(hospital_dataset, batch_size=64)

privacy_engine = PrivacyEngine()
model, optimizer, data_loader = privacy_engine.make_private_with_epsilon(
 module=model,
 optimizer=optimizer,
 data_loader=data_loader,
 epochs=50,
 target_epsilon=8.0,
 target_delta=1e-5,
 max_grad_norm=1.0,
)

for epoch in range(50):
 for batch in data_loader:
 images, labels = batch
 optimizer.zero_grad()
 output = model(images)
 loss = F.binary_cross_entropy_with_logits(output, labels)
 loss.backward()
 optimizer.step() # Opacus gestisce clipping + rumore internamente

 epsilon = privacy_engine.get_epsilon(delta=1e-5)
 print(f"Epoch {epoch}: ε = {epsilon:.2f}")
 if epsilon > 8.0:
 print("Budget di privacy esaurito. Fermo il training.")
 break

Le decisioni chiave sui parametri che richiedono sperimentazione attenta:

  • max_grad_norm=1.0: Troppo basso e clippi segnale utile. Troppo alto e serve più rumore per lo stesso ε.
  • target_epsilon=8.0: Una scelta pragmatica. ε=1.0 è il gold standard accademico ma costa circa l'8% di accuracy.
  • target_delta=1e-5: Pratica standard — δ deve essere minore di 1/N dove N è la dimensione del dataset.

Rényi DP per il Tracking del Budget

Qui la maggior parte dei tutorial sbaglia la matematica. Se esegui DP-SGD per 100 epoch, il costo naive di privacy è 100 × ε_per_passo. Questa è la composizione base, ed è drammaticamente pessimistica.

from opacus.accountants import RDPAccountant

accountant = RDPAccountant()

for epoch in range(num_epochs):
 for step in range(steps_per_epoch):
 accountant.step(
 noise_multiplier=noise_multiplier,
 sample_rate=batch_size / len(dataset),
 )

 epsilon = accountant.get_epsilon(delta=1e-5)
 print(f"Dopo epoch {epoch}: ε = {epsilon:.2f} (RDP)")

In un pipeline rappresentativo, la composizione base dà ε=12.4 dopo 50 epoch. RDP dava ε=4.7 per la stessa identica esecuzione. Non è un miglioramento marginale — è la differenza tra un risultato pubblicabile e un paper rifiutato.

Federated Learning con PySyft

import syft as sy

hospital_a = sy.orchestra.launch(
 name="hospital-a",
 port=8081,
 dev_mode=False,
 reset=True,
)

dataset = sy.Dataset(
 name="chest-xray-hospital-a",
 description="Radiografie toraciche de-identificate, 2020-2025",
 asset_list=[
 sy.Asset(
 name="images",
 data=real_image_tensor, # Non lascia mai questo nodo
 mock=generate_mock_images(), # Dati sintetici per il testing
 ),
 sy.Asset(
 name="labels",
 data=real_labels,
 mock=generate_mock_labels(),
 ),
 ],
)
hospital_a.upload_dataset(dataset)

Il coordinatore centrale orchestra il training senza accedere ai dati:

hospitals = [
 sy.login(url="https://hospital-a.internal:8081", email="ml@coordinator.org", password="..."),
 sy.login(url="https://hospital-b.internal:8082", email="ml@coordinator.org", password="..."),
 sy.login(url="https://hospital-c.internal:8083", email="ml@coordinator.org", password="..."),
 sy.login(url="https://hospital-d.internal:8084", email="ml@coordinator.org", password="..."),
 sy.login(url="https://hospital-e.internal:8085", email="ml@coordinator.org", password="..."),
]

@sy.syft_function_single_use(
 images=hospitals[0].datasets["chest-xray-hospital-a"]["images"],
 labels=hospitals[0].datasets["chest-xray-hospital-a"]["labels"],
)
def train_local(images, labels):
 import torch
 from opacus import PrivacyEngine

 model = load_global_model()
 optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
 data_loader = make_loader(images, labels, batch_size=64)

 privacy_engine = PrivacyEngine()
 model, optimizer, data_loader = privacy_engine.make_private_with_epsilon(
 module=model, optimizer=optimizer, data_loader=data_loader,
 epochs=1, target_epsilon=0.5, target_delta=1e-5, max_grad_norm=1.0,
 )

 model.train()
 for batch_images, batch_labels in data_loader:
 optimizer.zero_grad()
 loss = F.binary_cross_entropy_with_logits(model(batch_images), batch_labels)
 loss.backward()
 optimizer.step()

 return extract_model_updates(model)

Aggregazione Sicura

Anche con la privacy differenziale sull'aggiornamento di ogni ospedale, il coordinatore potrebbe apprendere qualcosa sulla distribuzione dei dati di un ospedale specifico dal suo aggiornamento individuale.

import numpy as np

def secure_aggregate(hospital_updates, threshold=3):
 n = len(hospital_updates)
 masked_updates = []

 for i in range(n):
 mask = np.zeros_like(hospital_updates[i])
 for j in range(n):
 if i == j:
 continue
 shared_seed = derive_shared_seed(private_keys[i], public_keys[j])
 pairwise_mask = prng_from_seed(shared_seed, shape=mask.shape)

 if i < j:
 mask += pairwise_mask
 else:
 mask -= pairwise_mask

 masked_updates.append(hospital_updates[i] + mask)

 aggregate = sum(masked_updates) / n
 return aggregate

Quando il coordinatore somma tutti gli aggiornamenti mascherati, le maschere pairwise si cancellano perfettamente, lasciando solo la media reale degli aggiornamenti di tutti gli ospedali.

Il Problema Non-IID

I dati ospedalieri reali sono selvaggiamente non-IID. L'Ospedale A è un centro pediatrico. L'Ospedale C è una struttura geriatrica. L'Ospedale E è una clinica rurale — attrezzature di imaging diverse, demografia diversa.

FedAvg standard crolla con dati non-IID. La soluzione: FedProx con un termine prossimale:

def train_local_fedprox(model, global_model, data_loader, mu=0.01):
 model.train()
 global_params = {name: param.clone() for name, param in global_model.named_parameters()}

 for batch_images, batch_labels in data_loader:
 optimizer.zero_grad()
 loss = F.binary_cross_entropy_with_logits(model(batch_images), batch_labels)

 proximal_loss = 0.0
 for name, param in model.named_parameters():
 proximal_loss += ((param - global_params[name]) ** 2).sum()
 loss += (mu / 2) * proximal_loss

 loss.backward()
 optimizer.step()

Questo singolo cambiamento ha portato la convergenza da "mai" a 30 round.

Risultati che Hanno Superato l'Audit

| Metrica | Centralizzato | Federato (no DP) | Federato + DP (ε=8) | |---------|---------------|--------------------|-----------------------| | AUC | 0.961 | 0.942 | 0.934 | | Sensibilità | 0.923 | 0.911 | 0.897 | | Specificità | 0.954 | 0.938 | 0.931 | | PPV | 0.891 | 0.872 | 0.858 |

Il calo del 2.7% nell'AUC da centralizzato a federato+DP è clinicamente accettabile per uno strumento di screening. L'auditor HIPAA ha specificamente evidenziato la garanzia formale di privacy (ε=8.0, δ=1e-5) come punto di forza — non avevano mai visto un limite quantitativo di privacy, solo "abbiamo anonimizzato i dati".

Test di Attacco Membership Inference

from ml_privacy_meter import run_population_metric_attack

attack_results = run_population_metric_attack(
 target_model=federated_dp_model,
 population_data=held_out_data,
 member_data=training_data_sample,
 num_shadow_models=10,
)

# Senza DP: Attack AUC = 0.71
# Con DP ε=8: Attack AUC = 0.53 (appena sopra il random)
# Con DP ε=1: Attack AUC = 0.51 (essenzialmente random)

Lezioni Pratiche

I costi di comunicazione dominano. Ogni round federato invia aggiornamenti del modello (~180 MB per un ResNet-50) da cinque ospedali attraverso canali crittografati. Quasi 1 GB per round. Comprimere i gradienti con top-k sparsification riduce la comunicazione a ~5 MB per round.

I budget di privacy sono a senso unico. Una volta speso epsilon, è perso per sempre per quel dataset. È comune bruciare budget significativo durante l'hyperparameter tuning. La soluzione: fare tutto il tuning su dati sintetici e eseguire solo il training finale su dati reali.

I dipartimenti IT degli ospedali sono il vero collo di bottiglia. L'ingegneria ML richiede tipicamente 6 settimane. Ottenere che cinque dipartimenti IT ospedalieri aprano porte firewall, approvino container Docker e allocchino risorse GPU può richiederne 10.

Questo tipo di pipeline opera in produzione, riaddestrandosi mensilmente con dati freschi da tutti gli ospedali partecipanti. Nessun dato paziente lascia mai la rete ospedaliera. E la garanzia di privacy è matematicamente dimostrabile, non solo una promessa in una privacy policy.

privacy-differenzialefederated-learningpysyftml-sanitarioprivacyaggregazione-sicura

Strumenti menzionati in questo articolo

AWSProva AWS
AnthropicProva Anthropic
Divulgazione: Alcuni link in questo articolo sono link di affiliazione. Se ti registri tramite questi, potrei guadagnare una commissione senza costi aggiuntivi per te. Raccomando solo strumenti che uso e di cui mi fido personalmente.
Compartir
Seguime