Analisi del Sentimento su Dati Crittografati con Crittografia Omomorfica

Sentiment Analysis on Encrypted Data with Homomorphic Encryption.

È ben noto che un modello di analisi del sentiment determina se un testo è positivo, negativo o neutro. Tuttavia, questo processo di solito richiede l’accesso al testo non criptato, il che può comportare preoccupazioni per la privacy.

La crittografia omomorfica è un tipo di crittografia che consente di effettuare calcoli su dati criptati senza bisogno di decifrarli prima. Ciò lo rende particolarmente adatto per applicazioni in cui i dati personali e potenzialmente sensibili degli utenti sono a rischio (ad esempio, l’analisi del sentiment dei messaggi privati).

Questo post sul blog utilizza la libreria Concrete-ML, che consente ai data scientist di utilizzare modelli di machine learning in ambienti di crittografia omomorfica (FHE) senza alcuna conoscenza preventiva di crittografia. Forniamo un tutorial pratico su come utilizzare la libreria per costruire un modello di analisi del sentiment su dati criptati.

Il post copre:

  • transformers
  • come utilizzare i transformers con XGBoost per eseguire l’analisi del sentiment
  • come fare l’addestramento
  • come utilizzare Concrete-ML per trasformare le previsioni in previsioni su dati criptati
  • come distribuire nel cloud utilizzando un protocollo client/server

Ultimo ma non meno importante, concluderemo con una demo completa su Hugging Face Spaces per mostrare questa funzionalità in azione.

Configurare l’ambiente

Assicurati prima di tutto che pip e setuptools siano aggiornati eseguendo:

pip install -U pip setuptools

Ora possiamo installare tutte le librerie richieste per questo blog con il seguente comando.

pip install concrete-ml transformers datasets

Utilizzo di un dataset pubblico

Il dataset che utilizziamo in questo notebook può essere trovato qui.

Per rappresentare il testo per l’analisi del sentiment, abbiamo scelto di utilizzare una rappresentazione nascosta del transformer in quanto fornisce un’alta precisione per il modello finale in modo molto efficiente. Per un confronto di questa rappresentazione rispetto a una procedura più comune come l’approccio TF-IDF, consulta questo notebook completo.

Possiamo iniziare aprendo il dataset e visualizzando alcune statistiche.

from datasets import load_datasets
train = load_dataset("osanseviero/twitter-airline-sentiment")["train"].to_pandas()
text_X = train['text']
y = train['airline_sentiment']
y = y.replace(['negative', 'neutral', 'positive'], [0, 1, 2])
pos_ratio = y.value_counts()[2] / y.value_counts().sum()
neg_ratio = y.value_counts()[0] / y.value_counts().sum()
neutral_ratio = y.value_counts()[1] / y.value_counts().sum()
print(f'Proporzione di esempi positivi: {round(pos_ratio * 100, 2)}%')
print(f'Proporzione di esempi negativi: {round(neg_ratio * 100, 2)}%')
print(f'Proporzione di esempi neutri: {round(neutral_ratio * 100, 2)}%')

L’output appare quindi come segue:

Proporzione di esempi positivi: 16,14%
Proporzione di esempi negativi: 62,69%
Proporzione di esempi neutri: 21,17%

La proporzione di esempi positivi e neutri è piuttosto simile, sebbene abbiamo significativamente più esempi negativi. Teniamo presente questo per selezionare la metrica di valutazione finale.

Ora possiamo dividere il nostro dataset in set di addestramento e di test. Utilizzeremo un valore di seed per assicurarci che sia perfettamente riproducibile.

from sklearn.model_selection import train_test_split
text_X_train, text_X_test, y_train, y_test = train_test_split(text_X, y,
    test_size=0.1, random_state=42)

Rappresentazione del testo utilizzando un transformer

I transformers sono reti neurali spesso addestrate per prevedere le prossime parole che appariranno in un testo (questo compito è comunemente chiamato apprendimento auto-supervisionato). Possono anche essere sottoposti a un fine-tuning su alcuni sotto-task specifici in modo tale da specializzarsi e ottenere risultati migliori su un determinato problema.

Sono strumenti potenti per tutti i tipi di compiti di elaborazione del linguaggio naturale. Infatti, possiamo sfruttare la loro rappresentazione per qualsiasi testo e alimentarla a un modello di machine learning più adatto alla crittografia omomorfica per la classificazione. In questo notebook, utilizzeremo XGBoost.

Iniziamo importando i requisiti per i transformers. Qui, utilizziamo la popolare libreria di Hugging Face per ottenere rapidamente un transformer.

Il modello che abbiamo scelto è un transformer BERT, sottoposto a un fine-tuning sul dataset Stanford Sentiment Treebank.

import torch
from transformers import AutoModelForSequenceClassification, AutoTokenizer
device = "cuda:0" if torch.cuda.is_available() else "cpu"
# Carica il tokenizer (converte il testo in token)
tokenizer = AutoTokenizer.from_pretrained("cardiffnlp/twitter-roberta-base-sentiment-latest")

# Carica il modello pre-addestrato
transformer_model = AutoModelForSequenceClassification.from_pretrained(
   "cardiffnlp/twitter-roberta-base-sentiment-latest"
)

Questo dovrebbe scaricare il modello, che è ora pronto per essere utilizzato.

Utilizzare la rappresentazione nascosta per un determinato testo può essere complicato all’inizio, principalmente perché si potrebbe affrontare questo problema con molti approcci diversi. Di seguito è riportato l’approccio che abbiamo scelto.

Innanzitutto, suddividiamo il testo in token. La tokenizzazione significa dividere il testo in token (una sequenza di caratteri specifici che possono anche essere parole) e sostituire ciascuno di essi con un numero. Successivamente, inviamo il testo tokenizzato al modello transformer, che restituisce una rappresentazione nascosta (output dei livelli di auto-attenzione che vengono spesso utilizzati come input per i livelli di classificazione) per ciascuna parola. Infine, facciamo la media delle rappresentazioni di ciascuna parola per ottenere una rappresentazione a livello di testo.

Il risultato è una matrice di forma (numero di esempi, dimensione nascosta). La dimensione nascosta è il numero di dimensioni nella rappresentazione nascosta. Per BERT, la dimensione nascosta è 768. La rappresentazione nascosta è un vettore di numeri che rappresenta il testo e può essere utilizzato per molte diverse attività. In questo caso, lo utilizzeremo per la classificazione con XGBoost successivamente.

import numpy as np
import tqdm
# Funzione che trasforma una lista di testi nella loro rappresentazione
# appresa dal transformer.
def text_to_tensor(
   list_text_X_train: list,
   transformer_model: AutoModelForSequenceClassification,
   tokenizer: AutoTokenizer,
   device: str,
) -> np.ndarray:
   # Tokenizza ogni testo nella lista uno per uno
   tokenized_text_X_train_split = []
   tokenized_text_X_train_split = [
       tokenizer.encode(text_x_train, return_tensors="pt")
       for text_x_train in list_text_X_train
   ]

   # Invia il modello al dispositivo
   transformer_model = transformer_model.to(device)
   output_hidden_states_list = [None] * len(tokenized_text_X_train_split)

   for i, tokenized_x in enumerate(tqdm.tqdm(tokenized_text_X_train_split)):
       # Passa i token attraverso il modello transformer e ottieni le rappresentazioni nascoste
       # Mantieni solo l'ultimo stato del livello nascosto per ora
       output_hidden_states = transformer_model(tokenized_x.to(device), output_hidden_states=True)[
           1
       ][-1]
       # Fai la media sull'asse dei token per ottenere una rappresentazione a livello di testo.
       output_hidden_states = output_hidden_states.mean(dim=1)
       output_hidden_states = output_hidden_states.detach().cpu().numpy()
       output_hidden_states_list[i] = output_hidden_states

   return np.concatenate(output_hidden_states_list, axis=0)

# Ora vettorizziamo il testo utilizzando il transformer
list_text_X_train = text_X_train.tolist()
list_text_X_test = text_X_test.tolist()

X_train_transformer = text_to_tensor(list_text_X_train, transformer_model, tokenizer, device)
X_test_transformer = text_to_tensor(list_text_X_test, transformer_model, tokenizer, device)

Questa trasformazione del testo (da testo a rappresentazione del transformer) deve essere eseguita sulla macchina client in quanto la crittografia viene eseguita sulla rappresentazione del transformer.

Classificazione con XGBoost

Ora che abbiamo costruito correttamente i nostri set di addestramento e test per addestrare un classificatore, il passo successivo è l’addestramento del nostro modello FHE. Qui sarà molto semplice, utilizzando uno strumento di ottimizzazione degli iperparametri come GridSearch di scikit-learn.

from concrete.ml.sklearn import XGBClassifier
from sklearn.model_selection import GridSearchCV
# Costruiamo il nostro modello
model = XGBClassifier()

# Una gridsearch per trovare i migliori parametri
parameters = {
    "n_bits": [2, 3],
    "max_depth": [1],
    "n_estimators": [10, 30, 50],
    "n_jobs": [-1],
}

# Ora che abbiamo una rappresentazione per ogni tweet, possiamo addestrare un modello su di essi.
grid_search = GridSearchCV(model, parameters, cv=5, n_jobs=1, scoring="accuracy")
grid_search.fit(X_train_transformer, y_train)

# Controlla l'accuratezza del miglior modello
print(f"Miglior punteggio: {grid_search.best_score_}")

# Controlla i migliori iperparametri
print(f"Migliori parametri: {grid_search.best_params_}")

# Estrai il miglior modello
best_model = grid_search.best_estimator_

L’output è il seguente:

Miglior punteggio: 0.8378111718275654
Migliori parametri: {'max_depth': 1, 'n_bits': 3, 'n_estimators': 50, 'n_jobs': -1}

Ora vediamo come si comporta il modello sul set di test.

from sklearn.metrics import ConfusionMatrixDisplay
# Calcola le metriche sul set di test
y_pred = best_model.predict(X_test_transformer)
y_proba = best_model.predict_proba(X_test_transformer)

# Calcola e plotta la matrice di confusione
matrix = confusion_matrix(y_test, y_pred)
ConfusionMatrixDisplay(matrix).plot()

# Calcola l'accuratezza
accuracy_transformer_xgboost = np.mean(y_pred == y_test)
print(f"Accuratezza: {accuracy_transformer_xgboost:.4f}")

Con il seguente output:

Accuratezza: 0.8504

Predizione su dati criptati

Ora prevediamo su testo criptato. L’idea qui è che cripteremo la rappresentazione data dal transformer anziché il testo grezzo stesso. In Concrete-ML, puoi farlo molto velocemente impostando il parametro execute_in_fhe=True nella funzione di previsione. Questa è solo una funzione per sviluppatori (principalmente utilizzata per verificare il tempo di esecuzione del modello FHE). Vedremo come far funzionare ciò in un contesto di deployment un po’ più avanti.

import time
# Compila il modello per ottenere il motore di inferenza FHE
# (questo potrebbe richiedere alcuni minuti a seconda del modello selezionato)
start = time.perf_counter()
best_model.compile(X_train_transformer)
end = time.perf_counter()
print(f"Tempo di compilazione: {end - start:.4f} secondi")

# Scriviamo un esempio personalizzato e prevediamo in FHE
tested_tweet = ["AirFrance è fantastico, quasi quanto Zama!"]
X_tested_tweet = text_to_tensor(tested_tweet, transformer_model, tokenizer, device)
clear_proba = best_model.predict_proba(X_tested_tweet)

# Ora prevediamo con FHE su un singolo tweet e stampiamo il tempo impiegato
start = time.perf_counter()
decrypted_proba = best_model.predict_proba(X_tested_tweet, execute_in_fhe=True)
end = time.perf_counter()
fhe_exec_time = end - start
print(f"Tempo di inferenza FHE: {fhe_exec_time:.4f} secondi")

L’output diventa:

Tempo di compilazione: 9.3354 secondi
Tempo di inferenza FHE: 4.4085 secondi

È anche necessario verificare che le previsioni FHE siano uguali alle previsioni chiare.

print(f"Probabilità dall'inferenza FHE: {decrypted_proba}")
print(f"Probabilità dal modello chiaro: {clear_proba}")

Questo output legge:

Probabilità dall'inferenza FHE: [[0.08434131 0.05571389 0.8599448 ]]
Probabilità dal modello chiaro: [[0.08434131 0.05571389 0.8599448 ]]

Deployment

A questo punto, il nostro modello è completamente addestrato e compilato, pronto per essere distribuito. In Concrete-ML, puoi utilizzare un’API di deployment per farlo facilmente:

# Salviamo il modello da inviare successivamente a un server
from concrete.ml.deployment import FHEModelDev
fhe_api = FHEModelDev("sentiment_fhe_model", best_model)
fhe_api.save()

Queste poche righe sono sufficienti per esportare tutti i file necessari sia per il client che per il server. Puoi controllare il notebook che spiega questa API di deployment in dettaglio qui .

Esempio completo in uno spazio di Hugging Face

Puoi anche dare un’occhiata all’applicazione finale su Hugging Face Space . L’applicazione client è stata sviluppata con Gradio mentre il server viene eseguito con Uvicorn ed è stato sviluppato con FastAPI .

Il processo è il seguente:

  • L’utente genera una nuova chiave privata/pubblica

  • L’utente digita un messaggio che verrà codificato, quantizzato e criptato

  • Il server riceve i dati criptati e avvia la previsione sui dati criptati, utilizzando la chiave pubblica di valutazione
  • Il server invia le previsioni criptate e il client può decifrarle utilizzando la sua chiave privata

Conclusioni

Abbiamo presentato un modo per sfruttare la potenza dei transformer in cui la rappresentazione viene poi utilizzata per:

  1. addestrare un modello di machine learning per classificare i tweet, e
  2. effettuare previsioni su dati criptati utilizzando questo modello con FHE.

Il modello finale (rappresentazione Transformer + XGboost) ha una precisione finale dell’85%, che è superiore al transformer stesso con una precisione dell’80% (si prega di consultare questo notebook per i confronti).

Il tempo di esecuzione FHE per ogni esempio è di 4,4 secondi su una CPU con 16 core.

I file per il deployment vengono utilizzati per un’app di analisi del sentiment che consente a un client di richiedere previsioni di analisi del sentiment da un server mantenendo i dati criptati lungo tutta la catena di comunicazione.

Concrete-ML (Non dimenticare di darci una stella su Github ⭐️💛) consente di costruire facilmente modelli di ML e convertirli nell’equivalente FHE per poter effettuare previsioni su dati criptati.

Speriamo che abbiate apprezzato questo post e fateci sapere i vostri pensieri/feedback!

E un grazie speciale ad Abubakar Abid per il suo precedente consiglio su come costruire il nostro primo Hugging Face Space!