Privacy Differenziale e Federated Learning: Architettura di Pipeline ML Sanitari Conformi HIPAA
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.