Fully Homomorphic Encryption¶

Glavna ideja FHE-a je da možemo računati nad šifrovanim podacima, bez njihovog otkrivanja. Postupak je sledeći:

  • korisnik generiše javni i tajni ključ
  • privatne podatke šifruje pomoću javnog ključa
  • šifrovane podatke šalje serveru
  • server računa nad šifrovanim podacima
  • rezultat je i dalje šifrovan
  • korisnik ga dešifruje pomoću tajnog ključa

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.

Biblioteke koje koristimo¶

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.
In [ ]:
# Ako paketi nisu instalirani, pokrenuti:
# pip install concrete-python concrete-ml pandas numpy scikit-learn torch

FHE primer¶

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. $$

In [ ]:
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

Anonimna aukcija¶

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:

  • ulaz su tri šifrovane ponude,
  • FHE kolo poredi ponude dok su šifrovane,
  • izlaz je samo šifra pobednika: 0, 1 ili 2,
  • same ponude se ne otkrivaju.
In [ ]:
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.

Baza podataka¶

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.

In [ ]:
import pandas as pd

df = pd.read_csv("insurance.csv")
print("Dimenzije baze:", df.shape)
df.head()
Dimenzije baze: (1338, 7)
Out[ ]:
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

Priprema podataka¶

Radimo samo osnovne stvari:

  1. kategoričke promenljive pretvaramo u 0/1 kolone,
  2. ulazne promenljive standardizujemo,
  3. ciljnu promenljivu charges delimo sa 1000.

Model će dakle predviđati trošak u hiljadama dolara.

In [ ]:
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

FHE linearna regresija¶

Ovo je prvi ML primer.

Ovde je važno da razdvojimo tri faze:

  1. fit — treniranje modela,
  2. compile — pretvaranje istreniranog modela u FHE kolo,
  3. 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.

In [ ]:
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)
Out[ ]:
<concrete.fhe.compilation.circuit.Circuit at 0x34f2d55b0>
In [ ]:
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

Šta znači poređenje na celoj bazi?¶

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.

FHE potpuno povezana neuronska mreža¶

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.

In [ ]:
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])
Out[ ]:
<concrete.fhe.compilation.circuit.Circuit at 0x343edd430>
In [ ]:
# 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])
})
Out[ ]:
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