Glavna ideja FHE-a je da možemo računati nad šifrovanim podacima, bez njihovog otkrivanja. Postupak je sledeći:
Server ne poseduje tajni ključ, pa ne vidi ni ulazne podatke ni rezultat.
U sistemima poput aukcija ili glasanja, tajni ključ ne bi trebalo da ima jedna osoba, već se često deli između više članova.
U ovom notebooku koristimo biblioteke koje razvija kompanija Zama, jedna od najpoznatijih kompanija u oblasti Fully Homomorphic Encryption-a.
Njihovi alati omogućavaju da pišemo običnu Python funkciju, a zatim je kompajliramo u FHE kolo.
U prvom delu koristimo concrete-python.
U ML delu koristimo concrete-ml, biblioteku namenjenu mašinskom učenju nad šifrovanim podacima. Ona omogućava da se modeli prvo koriste na standardan način, a zatim kompajliraju tako da se predikcija može izvršiti u FHE režimu.
Zato razlikujemo tri režima:
clear — obična predikcija bez FHE-a,simulate — simulacija FHE kola bez enkripcije: ne štiti podatke, ali pokazuje kakav rezultat možemo očekivati kada isto kolo pokrenemo u pravom FHE režimu.execute — pravo FHE izvršavanje nad šifrovanim podacima.# Ako paketi nisu instalirani, pokrenuti:
# pip install concrete-python concrete-ml pandas numpy scikit-learn torch
Posmatramo scenario u kojem korisnik zna funkciju koja se izvršava, ali želi da se računanje izvrši na strani servera nad šifrovanim podacima. U naprednijem scenariju u kojem korisnik ne zna funkciju unapred, FHE sam po sebi nije dovoljan, pa se obično kombinuje sa pristupima kao što su MPC, trusted execution okruženja i zero-knowledge dokazi kako bi se omogućilo sigurno računanje nad podacima.
Računamo
$$ f(x,y)=3x+2y+1. $$
from concrete import fhe
# funkcija koju želimo da server izvrši nad šifrovanim podacima
def private_score(x, y):
return 3 * x + 2 * y + 1
# server kompajlira funkciju u FHE kolo
compiler = fhe.Compiler(private_score,{"x": "encrypted", "y": "encrypted"})
# inputset predstavlja primere ulaza na osnovu kojih FHE kompajler određuje opseg vrednosti
# i pravi odgovarajuće kolo. Ovde su x i y u opsegu od 0 do 7.
inputset = [(x, y) for x in range(8) for y in range(8)]
circuit = compiler.compile(inputset)
# korisnik generiše ključeve
circuit.keygen()
x_private = 4
y_private = 6
# korisnik šifruje podatke
encrypted_input = circuit.encrypt(x_private, y_private)
# server izvršava računanje nad šifrovanim podacima
encrypted_result = circuit.run(encrypted_input)
# korisnik dešifruje rezultat
result = circuit.decrypt(encrypted_result)
print("Rezultat:", result)
print("Stvarni rezultat:", 3 * x_private + 2 * y_private + 1)
Rezultat: 25 Stvarni rezultat: 25
Imamo tri učesnika: Ana, Bojan i Marko. Svako ima svoju ponudu, ali ne želimo da otkrijemo same ponude. Želimo samo da saznamo ko je pobedio.
Radi jednostavnosti, pretpostavljamo da su ponude različite. Ako postoje jednake ponude, u realnom sistemu bi unapred moralo da se definiše pravilo za izjednačenje.
Poenta primera:
0, 1 ili 2,import numpy as np
def auction_winner(ana, bojan, marko):
highest_bid = np.maximum(np.maximum(ana, bojan), marko)
# Ako je Ana pobednik, oba izraza su 0, pa je rezultat 0.
# Ako je Bojan pobednik, prvi izraz je 1, a drugi 0, pa je rezultat 1.
# Ako je Marko pobednik, prvi izraz je 0, a drugi 1, pa je rezultat 2.
return (bojan == highest_bid) + 2 * (marko == highest_bid)
compiler = fhe.Compiler(auction_winner, {"ana": "encrypted", "bojan": "encrypted", "marko": "encrypted"})
bid_values = range(0, 11)
# trebalo bi dodatno obraditi i slučaj kada imamo izjednačene ponude
auction_inputset = [
(ana, bojan, marko)
for ana in bid_values
for bojan in bid_values
for marko in bid_values
]
# kompajliranje fhe kola
auction_circuit = compiler.compile(auction_inputset)
# generisanje ključeva korisnika
auction_circuit.keygen()
# ponude
ana_bid = 7
bojan_bid = 9
marko_bid = 5
# šifrovanje ponuda
encrypted_ana, encrypted_bojan, encrypted_marko = auction_circuit.encrypt(ana_bid, bojan_bid, marko_bid)
# Izvršavanje aukcije nad šifrovanim ponudama
encrypted_winner_code = auction_circuit.run(
encrypted_ana, encrypted_bojan, encrypted_marko)
# dešifrovanje pobednika aukcije
winner_code = auction_circuit.decrypt(encrypted_winner_code)
winner_names = {
0: "Ana",
1: "Bojan",
2: "Marko"}
print("Pobednik:", winner_names[int(winner_code)])
Pobednik: Bojan
U ovom primeru smo sve ponude šifrovali istim ključem. U realnosti, njihove ponude moraju biti šifrovane istim javnim ključem aukcijske aplikacije ili kroz napredniji protokol sa zajedničkim/threshold dešifrovanjem.
Radimo sa insurance.csv.
Cilj je da predvidimo charges, odnosno medicinski trošak osiguranika. Ulazi mogu biti osetljivi podaci: godine, BMI, broj dece, region itd.
import pandas as pd
df = pd.read_csv("insurance.csv")
print("Dimenzije baze:", df.shape)
df.head()
Dimenzije baze: (1338, 7)
| age | sex | bmi | children | smoker | region | charges | |
|---|---|---|---|---|---|---|---|
| 0 | 19 | female | 27.900 | 0 | yes | southwest | 16884.92400 |
| 1 | 18 | male | 33.770 | 1 | no | southeast | 1725.55230 |
| 2 | 28 | male | 33.000 | 3 | no | southeast | 4449.46200 |
| 3 | 33 | male | 22.705 | 0 | no | northwest | 21984.47061 |
| 4 | 32 | male | 28.880 | 0 | no | northwest | 3866.85520 |
Radimo samo osnovne stvari:
charges delimo sa 1000.Model će dakle predviđati trošak u hiljadama dolara.
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import mean_absolute_error
target_col = "charges"
X_raw = df.drop(columns=[target_col])
y_dollars = df[target_col].to_numpy()
y_thousands = y_dollars / 1000.0
X = pd.get_dummies(X_raw, drop_first=True)
X_train, X_test, y_train, y_test = train_test_split(
X, y_thousands, test_size=0.2, random_state=42)
x_scaler = StandardScaler()
X_train_scaled = x_scaler.fit_transform(X_train).astype(np.float32)
X_test_scaled = x_scaler.transform(X_test).astype(np.float32)
# Ovo koristimo samo za poređenje clear/simulate/execute na celoj bazi.
X_all_scaled = x_scaler.transform(X).astype(np.float32)
y_all = y_thousands.astype(np.float32)
y_train = y_train.astype(np.float32)
y_test = y_test.astype(np.float32)
print("Broj ulaznih promenljivih nakon one-hot kodiranja:", X_train_scaled.shape[1])
print("Broj instanci u celoj bazi:", X_all_scaled.shape[0])
Broj ulaznih promenljivih nakon one-hot kodiranja: 8 Broj instanci u celoj bazi: 1338
Ovo je prvi ML primer.
Ovde je važno da razdvojimo tri faze:
fit — treniranje modela,compile — pretvaranje istreniranog modela u FHE kolo,predict — predikcija, koja može da se pokrene u režimu clear, simulate ili execute.U ovom notebooku treniranje nije FHE. Model se trenira nad običnim podacima. FHE koristimo u fazi predikcije, kada želimo da model obradi šifrovane ulazne podatke.
Korak
model.compile(X_calibration)
pravi FHE kolo za izvršavanje predikcije nad šifrovanim ulazima.
from concrete.ml.sklearn import LinearRegression as FHELinearRegression
fhe_linear_model = FHELinearRegression(n_bits=12)
# Treniranje FHE-kompatibilne linearne regresije
fhe_linear_model.fit(X_train_scaled, y_train)
X_calibration = X_train_scaled[:200]
# Kompajliranje modela u FHE kolo
fhe_linear_model.compile(X_calibration)
<concrete.fhe.compilation.circuit.Circuit at 0x34f2d55b0>
from time import perf_counter
# Poređenje clear, simulate i execute režima na celoj bazi
# Clear predikcija
start = perf_counter()
linear_clear_all = fhe_linear_model.predict(X_all_scaled)
linear_clear_time = perf_counter() - start
# FHE simulacija
start = perf_counter()
linear_sim_all = fhe_linear_model.predict(X_all_scaled, fhe="simulate")
linear_sim_time = perf_counter() - start
# FHE execute
start = perf_counter()
linear_execute_all = fhe_linear_model.predict(X_all_scaled, fhe="execute")
linear_execute_time = perf_counter() - start
linear_summary = pd.DataFrame({
"mode": ["clear", "simulate", "execute"],
"MAE": [
mean_absolute_error(y_all, linear_clear_all),
mean_absolute_error(y_all, linear_sim_all),
mean_absolute_error(y_all, linear_execute_all)],
"time_seconds": [
linear_clear_time,
linear_sim_time,
linear_execute_time]})
linear_comparison = pd.DataFrame({
"actual_dollars": np.ravel(y_all),
"clear_prediction": np.ravel(linear_clear_all),
"simulate_prediction": np.ravel(linear_sim_all),
"execute_prediction": np.ravel(linear_execute_all)
})
display(linear_summary)
display(linear_comparison.head(10))
| mode | MAE | time_seconds | |
|---|---|---|---|
| 0 | clear | 4.201058 | 0.001085 |
| 1 | simulate | 4.201058 | 0.231899 |
| 2 | execute | 4.201058 | 4.219427 |
| actual_dollars | clear_prediction | simulate_prediction | execute_prediction | |
|---|---|---|---|---|
| 0 | 16.884924 | 25.198774 | 25.198774 | 25.198774 |
| 1 | 1.725552 | 3.822363 | 3.822363 | 3.822363 |
| 2 | 4.449462 | 6.983230 | 6.983230 | 6.983230 |
| 3 | 21.984470 | 3.805894 | 3.805894 | 3.805894 |
| 4 | 3.866855 | 5.632206 | 5.632206 | 5.632206 |
| 5 | 3.756622 | 4.047443 | 4.047443 | 4.047443 |
| 6 | 8.240589 | 10.919882 | 10.919882 | 10.919882 |
| 7 | 7.281506 | 7.825227 | 7.825227 | 7.825227 |
| 8 | 6.406411 | 8.456061 | 8.456061 | 8.456061 |
| 9 | 28.923138 | 11.822928 | 11.822928 | 11.822928 |
Za linearnu regresiju sada poredimo sva tri režima na svim instancama iz baze.
Dobili smo iste rezultate u clear, simulate i execute režimu jer je model samo linearna funkcija i posle kvantizacije se ista računanja rade na isti način i u običnom i u FHE okruženju. Razlike se obično pojavljuju tek kod složenijih modela, npr. kada postoje nelinearne funkcije, ili kada se zbog ograničene preciznosti i kvantizacije mora raditi aproksimacija računanja, pa FHE izvršavanje više ne može da bude potpuno identično običnom izračunavanju.
Sada radimo istu ideju, ali model nije linearna regresija, nego mala potpuno povezana neuronska mreža.
Ne jurimo najbolju arhitekturu. Poenta je samo da vidimo da i neuronska mreža može da se kompajlira i izvršava u FHE režimu.
Kod neuronske mreže ostavljamo execute poređenje samo na jednoj instanci, jer pravo FHE izvršavanje neuronske mreže može biti znatno sporije od linearne regresije.
import torch
import torch.nn as nn
from concrete.ml.sklearn import NeuralNetRegressor
np.random.seed(42)
torch.manual_seed(42)
X_train_nn = X_train_scaled.astype(np.float32)
X_test_nn = X_test_scaled.astype(np.float32)
# NeuralNetRegressor očekuje y kao matricu oblika (broj_instanci, broj_izlaza)
y_train_nn = y_train.reshape(-1, 1).astype(np.float32)
fhe_nn_model = NeuralNetRegressor(
# Broj slojeva neuronske mreže
module__n_layers=2,
# Aktivaciona funkcija između slojeva
module__activation_function=nn.ReLU,
# broj neurona u skriven sloju
module__n_hidden_neurons_multiplier=10,
# Broj bitova za kvantizaciju težina modela
# Manji broj bitova znači brže FHE izvršavanje, ali potencijalno slabiju preciznost
module__n_w_bits=3,
# Broj bitova za kvantizaciju aktivacija
# I ovde je kompromis između brzine FHE-a i tačnosti modela
module__n_a_bits=3,
# Broj bitova za akumulirane vrednosti tokom računanja
# Ako je premalo, model može izgubiti preciznost; ako je previše, FHE je sporiji
module__n_accum_bits=8, max_epochs=15, batch_size=64, lr=0.01, train_split=None,verbose=0)
# Treniranje FHE-kompatibilne neuronske mreže
fhe_nn_model.fit(X_train_nn, y_train_nn)
# Kompajliranje neuronske mreže u FHE kolo
fhe_nn_model.compile(X_train_nn[:200])
<concrete.fhe.compilation.circuit.Circuit at 0x343edd430>
# Clear predikcija neuronske mreže
nn_clear_pred = fhe_nn_model.predict(X_test_nn[:20])
# FHE simulacija neuronske mreže
nn_sim_pred = fhe_nn_model.predict(X_test_nn[:20], fhe="simulate")
# Pravo FHE izvršavanje neuronske mreže
nn_execute_pred = fhe_nn_model.predict(X_test_nn[:20], fhe="execute")
pd.DataFrame({
"actual_value": np.ravel(y_test[:10]),
"clear_prediction": np.ravel(nn_clear_pred[:10]),
"fhe_simulate_prediction": np.ravel(nn_sim_pred[:10]),
"fhe_execute_prediction": np.ravel(nn_execute_pred[:10])
})
| actual_value | clear_prediction | fhe_simulate_prediction | fhe_execute_prediction | |
|---|---|---|---|---|
| 0 | 9.095068 | 13.0 | 13.0 | 13.0 |
| 1 | 5.272176 | 10.0 | 10.0 | 10.0 |
| 2 | 29.330982 | 28.0 | 28.0 | 28.0 |
| 3 | 9.301893 | 10.0 | 10.0 | 10.0 |
| 4 | 33.750290 | 28.0 | 28.0 | 28.0 |
| 5 | 4.536259 | 19.0 | 19.0 | 19.0 |
| 6 | 2.117339 | 4.0 | 4.0 | 4.0 |
| 7 | 14.210536 | 19.0 | 19.0 | 19.0 |
| 8 | 3.732625 | 7.0 | 7.0 | 7.0 |
| 9 | 10.264442 | 13.0 | 13.0 | 13.0 |