E se potessimo spiegare facilmente modelli eccessivamente complessi?

E se potessimo spiegare in modo semplice modelli eccessivamente complessi?

Generare spiegazioni contrefattuali è diventato molto più facile con CFNOW, ma cosa sono le spiegazioni contrefattuali e come posso usarle?

Immagine generata con il modello Diffusione Illusoria con testo CFNOW come illusione (cerca di socchiudere gli occhi e guardare da una certa distanza) | Immagine dell'autore utilizzando il modello Diffusione Stabile (licenza)

Questo articolo si basa sul seguente articolo: https://www.sciencedirect.com/science/article/abs/pii/S0377221723006598

Ecco l’indirizzo del repository CFNOW: https://github.com/rmazzine/CFNOW

Se stai leggendo questo, potresti sapere quanto sia fondamentale l’Intelligenza Artificiale (AI) nel nostro mondo oggi. Tuttavia, è importante sottolineare che le approcci di apprendimento automatico apparentemente efficaci e innovativi, combinati con la loro diffusa popolarità, possono portare a conseguenze impreviste/desiderabili.

Ciò ci porta a comprendere perché l’Intelligenza Artificiale Esplicabile (XAI) è un componente cruciale per garantire lo sviluppo etico e responsabile dell’AI. Quest’area dimostra che spiegare modelli composti da milioni o addirittura miliardi di parametri non è una questione banale. La risposta a questo è complessa, poiché esistono numerosi metodi che rivelano diversi aspetti del modello, con LIME [1] e SHAP [2] che sono esempi popolari.

Tuttavia, la complessità delle spiegazioni generate da questi metodi può risultare in grafici o analisi intricate, che potenzialmente possono portare a interpretazioni errate da parte di persone non esperte. Un possibile modo per ovviare a questa complessità è un metodo semplice e naturale per spiegare le cose chiamato Spiegazioni Contrefattuali [3].

Le Spiegazioni Contrefattuali sfruttano un comportamento umano naturale per spiegare le cose – creando “mondi alternativi” in cui modificare alcuni parametri può cambiare l’esito. È una pratica comune, probabilmente hai già fatto qualcosa del genere – “se solo mi fossi svegliato un po’ prima, non avrei perso l’autobus”, questo tipo di spiegazione evidenzia le principali ragioni di un risultato in modo semplice.

Andando più in profondità, le contrefattuali vanno oltre semplici spiegazioni; possono fungere da guida per i cambiamenti, aiutare a risolvere anomalie comportamentali e verificare se alcune caratteristiche possono potenzialmente modificare le previsioni (senza essere così impattanti sul punteggio). Questa natura multifunzionale enfatizza l’importanza di spiegare le proprie previsioni. Non si tratta solo di un problema di intelligenza artificiale responsabile; è anche un percorso per migliorare i modelli e utilizzarli al di là dello scopo delle previsioni. Un aspetto notevole delle spiegazioni contrefattuali è la loro natura basata sulle decisioni, che le rende direttamente corrispondenti a un cambiamento nella previsione [6], a differenza di LIME e SHAP, che sono più adatti a spiegare punteggi.

Dati i evidenti vantaggi, potresti chiederti perché le contrefattuali non siano più popolari. È una domanda valida! I principali ostacoli all’adozione diffusa delle spiegazioni contrefattuali sono tre [4, 5]: (1) l’assenza di algoritmi di generazione di contrefattuali compatibili e facili da usare, (2) l’inefficienza degli algoritmi nella generazione di contrefattuali, (3) e la mancanza di una rappresentazione visuale completa.

Ma ho una buona notizia per te! Un nuovo pacchetto, CFNOW (CounterFactuals NOW o CounterFactual Nearest Optimal Wololo), sta intervenendo per affrontare queste sfide. CFNOW è un versatile pacchetto Python in grado di generare molteplici contrefattuali per diversi tipi di dati come input tabulari, immagini e testuali (embedding). Adotta un approccio indipendente dal modello, richiedendo solo un minimo di dati – (1) il punto fattuale (punto da spiegare) e (2) la funzione di previsione.

Inoltre, CFNOW è strutturato per consentire lo sviluppo e l’integrazione di nuove strategie per trovare e perfezionare i contrefattuali basandosi su logiche personalizzate. Dispone anche di CounterPlots, una nuova strategia per rappresentare visivamente le spiegazioni contrefattuali.

Al centro di CFNOW c’è un framework che converte i dati in una struttura singola gestibile dal generatore CF. Seguendo questo principio, un processo a due fasi individua e ottimizza il controfattuale trovato. Per evitare minimi locali, il pacchetto implementa Tabu Search, un metodo matheuristico, che consente di esplorare nuove regioni in cui la funzione obiettivo potrebbe essere ottimizzata in modo migliore.

Le sezioni successive di questo testo si concentreranno su come CFNOW può essere utilizzato in modo efficiente per generare spiegazioni per classificatori tabulari, di immagini e testuali (embedding).

Classificatori Tabulari

Qui mostriamo le solite cose, hai dati tabulari con più tipi di dati. Nell’esempio qui sotto, useremo un set di dati che contiene dati numerici continui, dati binari categorici e dati codificati in one-hot per mostrare CFNOW nella sua piena potenza.

Prima di tutto, è necessario installare il pacchetto CFNOW, il requisito è una versione di Python superiore a 3.8:

pip install cfnow

(qui trovi il codice completo per questo esempio: https://colab.research.google.com/drive/1GUsVfcM3I6SpYCmsBAsKMsjVdm-a6iY6?usp=sharing)

In questa prima parte, creeremo un classificatore con il dataset Adult. Non c’è molto di nuovo qui:

import warnings
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score

warnings.filterwarnings("ignore", message="X does not have valid feature names, but RandomForestClassifier was fitted with feature names")

Importiamo pacchetti di base per creare il modello di classificazione e disabilitiamo anche gli avvisi relativi a fare previsioni senza i nomi delle colonne.

Poi, procediamo a scrivere il classificatore, dove la classe 1 rappresenta un reddito inferiore o uguale a 50k (<=50K) e la classe 0 rappresenta un reddito elevato.

# Crea il classificatore
import warnings
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score

warnings.filterwarnings("ignore", message="X does not have valid feature names, but RandomForestClassifier was fitted with feature names")

# Carica il dataset Adult
dataset_url = "https://archive.ics.uci.edu/ml/machine-learning-databases/adult/adult.data"
column_names = ['age', 'workclass', 'fnlwgt', 'education', 'education-num', 'marital-status',
                'occupation', 'relationship', 'race', 'sex', 'capital-gain', 'capital-loss',
                'hours-per-week', 'native-country', 'income']
data = pd.read_csv(dataset_url, names=column_names, na_values=" ?", skipinitialspace=True)

# Elimina le righe con valori mancanti
data = data.dropna()

# Identifica le caratteristiche categoriche non binarie
non_binary_categoricals = [column for column in data.select_dtypes(include=['object']).columns
                            if len(data[column].unique()) > 2]
binary_categoricals = [column for column in data.select_dtypes(include=['object']).columns
                        if len(data[column].unique()) == 2]
cols_numericals = [column for column in data.select_dtypes(include=['int64']).columns]

# Applica la codifica one-hot alle caratteristiche categoriche non binarie
data = pd.get_dummies(data, columns=non_binary_categoricals)

# Converti le caratteristiche categoriche binarie in numeri
# Questo binarizzerà anche la variabile di destinazione (income)
for bc in binary_categoricals:
    data[bc] = data[bc].apply(lambda x: 1 if x == data[bc].unique()[0] else 0)

# Suddividi il dataset in caratteristiche e variabile di destinazione
X = data.drop('income', axis=1)
y = data['income']

# Suddividi il dataset in set di addestramento e di test
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# Inizializza un classificatore RandomForestClassifier
clf = RandomForestClassifier(random_state=42)

# Addestra il classificatore
clf.fit(X_train, y_train)

# Effettua previsioni sul set di test
y_pred = clf.predict(X_test)

# Valuta il classificatore
accuracy = accuracy_score(y_test, y_pred)
print("Accuracy:", accuracy)

Con il codice sopra, creiamo un dataset, lo pre-processiamo, creiamo un modello di classificazione e facciamo una previsione e una valutazione sul set di test.

Ora, prendiamo un punto (il primo dal set di test) e verifichiamo la sua previsione:

clf.predict([X_test.iloc[0]])# Risultato: 0 -> Alto reddito

Adesso è il momento di utilizzare CFNOW per calcolare come possiamo modificare questa previsione minimamente modificando le caratteristiche:

from cfnow import find_tabular# Successivamente, utilizziamo CFNOW per generare la modifica minima per cambiare la classificazione.cf_res = find_tabular(    factual=X_test.iloc[0],    feat_types={c: 'num' if c in cols_numericals else 'cat' for c in X.columns},    has_ohe=True,    model_predict_proba=clf.predict_proba,    limit_seconds=60)

Il codice sopra:

  • factualAggiunge l’istanza fattuale come pd.Series
  • feat_typesSpecifica i tipi di caratteristiche (“num” per numeriche continue e “cat” per categoriche)
  • has_oheIndica che abbiamo caratteristiche OHE (rileva automaticamente le caratteristiche OHE aggregando quelle che hanno lo stesso prefisso seguito da un underscore, ad esempio, country_brazil, country_usa, country_ireland).
  • model_predict_probaInclusa una funzione di previsione
  • limit_secondsDefinisce una soglia di tempo totale per l’esecuzione, questo è importante perché la fase di ottimizzazione può continuare all’infinito (il valore predefinito è di 120 secondi)

Quindi, dopo un po’ di tempo, possiamo valutare la classe del miglior controfattuale (primo indice di cf_res.cfs)

clf.predict([cf_obj.cfs[0]])# Risultato: 1 -> Basso reddito

E qui arrivano alcune differenze con CFNOW, poiché integra anche CounterPlots, possiamo tracciare i loro grafici e ottenere informazioni più approfondite come quelle di seguito riportate:

CounterShapley Chart per il nostro CF | Immagine dell'autore

Il grafico CounterShapley qui sotto mostra l’importanza relativa di ogni caratteristica per generare la previsione del controfattuale. Qui abbiamo alcune intuizioni interessanti che mostrano che lo stato civile (se combinato) rappresenta più del 50% del contributo alla classe del CF.

Greedy Chart per il nostro CF | Immagine dell'autore

Il grafico Greedy mostra qualcosa di molto simile al CounterShapley, la differenza principale qui è la sequenza dei cambiamenti. Mentre il CounterShapley non considera alcuna sequenza specifica (calcolando i contributi utilizzando i valori di Shapley), il grafico Greedy utilizza la strategia più avida per modificare l’istanza fattuale, ogni passo modificando la caratteristica che contribuisce di più alla classe CF. Questo può essere utile per situazioni in cui viene fornita una guida in modo avido (ogni passo sceglie l’approccio migliore per raggiungere l’obiettivo).

Constellation Chart per il nostro CF | Immagine dell'autore

Infine, abbiamo l’analisi più complessa, il grafico Constellation. Nonostante la sua apparenza intimidatoria, è in realtà abbastanza semplice interpretarlo. Ogni grande punto rosso rappresenta una singola modifica delle caratteristiche (rispetto all’etichetta), e i puntini più piccoli rappresentano la combinazione di due o più caratteristiche. Infine, il grande punto blu rappresenta il punteggio CF. Qui possiamo vedere che l’unico modo per ottenere un CF con queste caratteristiche è modificarle tutte ai loro rispettivi valori (cioè non esiste un sottoinsieme che genera un CF). Possiamo anche approfondire e indagare la relazione tra le caratteristiche e trovare potenziali modelli interessanti.

In questo caso particolare, è stato interessante osservare che una previsione di alto reddito cambierebbe se la persona fosse donna, divorziata e con un figlio proprio. Questo controfattuale può portare a ulteriori discussioni sugli impatti economici su diversi gruppi sociali.

Classificatori di immagini

Come già accennato, CFNOW può lavorare con diversi tipi di dati, quindi può anche generare controfattuali per i dati delle immagini. Tuttavia, cosa significa avere un controfattuale per un dataset di immagini?

La risposta può variare perché ci sono diversi modi in cui puoi generare controfattuali. Può essere sostituire singoli pixel con rumore casuale (un metodo utilizzato dagli attacchi avversari) o qualcosa di più complesso, che coinvolge metodi avanzati di segmentazione.

CFNOW utilizza un metodo di segmentazione chiamato quickshift, che è un metodo affidabile e veloce per rilevare segmenti “semantici”. Tuttavia, è possibile integrare (e ti invito a farlo) altre tecniche di segmentazione.

La sola rilevazione del segmento non è sufficiente per generare spiegazioni controfattuali. Abbiamo anche bisogno di modificare i segmenti, sostituendoli con versioni modificate. Per questa modificazione, CFNOW ha quattro opzioni definite nel parametro replace_mode, in cui possiamo avere: (predefinito) blur – che aggiunge un filtro sfocato ai segmenti sostituiti, mean che sostituisce i segmenti con il colore medio, random che li sostituisce con rumore casuale, e inpaint, che ricostruisce l’immagine basandosi sui pixel del vicinato.

Se vuoi l’intero codice puoi trovarlo qui: https://colab.research.google.com/drive/1M6bEP4x7ilSdh01Gs8xzgMMX7Uuum5jZ?usp=sharing

Di seguito mostrerò l’implementazione del codice CFNOW per questo tipo di dati:

Prima di tutto, installiamo ancora una volta il pacchetto CFNOW se non lo hai ancora fatto.

pip install cfnow

Ora, aggiungiamo alcuni pacchetti aggiuntivi per caricare un modello pre-allenato:

pip install torch torchvision Pillow requests

Quindi carichiamo i dati, carichiamo il modello pre-allenato e creiamo una funzione di previsione che sia compatibile con il formato dei dati che CFNOW deve ricevere:

import requestsimport numpy as npfrom PIL import Imagefrom torchvision import models, transformsimport torch# Carica un modello ResNet pre-allenatomodel = models.resnet50(pretrained=True)model.eval()# Definisci la trasformazione dell'immaginetransform = transforms.Compose([    transforms.Resize(256),    transforms.CenterCrop(224),    transforms.ToTensor(),    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),])# Prendi un'immagine dal webimage_url = "https://upload.wikimedia.org/wikipedia/commons/thumb/4/41/Sunflower_from_Silesia2.jpg/320px-Sunflower_from_Silesia2.jpg"response = requests.get(image_url, stream=True)image = np.array(Image.open(response.raw))def predict(images):    if len(np.shape(images)) == 4:        # Converti la lista di array numpy in un batch di tensori        input_images = torch.stack([transform(Image.fromarray(image.astype('uint8'))) for image in images])    elif len(np.shape(images)) == 3:        input_images = transform(Image.fromarray(images.astype('uint8')))    else:        raise ValueError("L'input deve essere una lista di immagini o un'immagine singola.")        # Controlla se è disponibile una GPU e se non lo è, usa una CPU    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")    input_images = input_images.to(device)    model.to(device)        # Esegui l'elaborazione    with torch.no_grad():        outputs = model(input_images)        # Restituisci un array di punteggi di previsione per ogni immagine    return torch.asarray(outputs).cpu().numpy()LABELS_URL = "https://raw.githubusercontent.com/anishathalye/imagenet-simple-labels/master/imagenet-simple-labels.json"def predict_label(outputs):    # Carica le etichette usate dal modello pre-allenato    labels = requests.get(LABELS_URL).json()        # Ottieni le etichette previste    predicted_idxs = [np.argmax(od) for od in outputs]    predicted_labels = [labels[idx.item()] for idx in predicted_idxs]        return predicted_labels# Verifica la previsione per l'immaginelabel_prevista = predict([np.array(image)])print("Etichette previste:", predict_label(label_prevista))

La maggior parte del lavoro di codifica è legata alla costruzione del modello, all’ottenimento dei dati e al loro adattamento, perché per generare controfattuali con CFNOW abbiamo solo bisogno di:

dalla cfnow import find_imagecf_img = find_image(img=immagine, modello_previsione=previsione)cf_img_hl = cf_img.cfs[0]print("Etichette previste:", prevedi_etichetta(previsione([cf_img_hl])))# Mostra l'immagine CFImage.fromarray(cf_img_hl.astype('uint8'))

Nell’esempio sopra, abbiamo utilizzato tutti i parametri opzionali predefiniti, pertanto abbiamo utilizzato quickshift per segmentare l’immagine e sostituire i segmenti con immagini sfocate. Come risultato, abbiamo questa previsione di fatto di seguito:

Al seguente:

Allora, quali sono i risultati di questa analisi? In realtà, i controfattuali delle immagini possono essere strumenti estremamente utili per rilevare come il modello sta effettuando le classificazioni. Questo può essere applicato nei casi in cui: (1) vogliamo verificare perché il modello ha effettuato delle classificazioni corrette – garantendo che stia utilizzando correttamente le caratteristiche dell’immagine: in questo caso, anche se ha classificato erroneamente il girasole come una margherita, possiamo vedere che sfocando il fiore (e non una caratteristica dello sfondo) la previsione cambia. Può anche (2) aiutare a diagnosticare immagini errate, il che può portare a una migliore comprensione per l’elaborazione delle immagini e/o l’acquisizione dei dati.

Classificatori Testuali

Infine, abbiamo classificatori testuali basati su embeddings. Anche se i classificatori testuali semplici (che utilizzano una struttura di dati più simile a dati tabulari) possono utilizzare il generatore di controfattuali tabulari, sui classificatori testuali basati su embeddings ciò non è così chiaro.

La giustificazione è che gli embeddings hanno un numero variabile di ingressi e parole che possono influire notevolmente sul punteggio di previsione e sulla classificazione.

CFNOW risolve questo problema con due strategie: (1) rimuovendo le prove o (2) aggiungendo antonimi. La prima strategia è diretta, per misurare l’impatto di ogni parola sul testo, semplicemente le rimuoviamo e vediamo quali dobbiamo rimuovere per invertire la classificazione. Mentre aggiungendo antonimi, possiamo possibilmente mantenere una struttura semantica (perché rimuovere una parola può danneggiarla gravemente).

Quindi, il codice di seguito mostra come utilizzare CFNOW in questo contesto.

Se desideri tutto il codice, puoi controllarlo qui: https://colab.research.google.com/drive/1ZMbqJmJoBukqRJGqhUaPjFFRpWlujpsi?usp=sharing

Prima di tutto, installa il pacchetto CFNOW:

pip install cfnow

Poi, installa i pacchetti necessari per la classificazione testuale:

pip install transformers

In seguito, come nelle sezioni precedenti, per prima cosa costruiremo il classificatore:

from transformers import DistilBertTokenizer, DistilBertForSequenceClassification
from transformers import pipeline
import numpy as np

# Carica il modello pre-addestrato e il tokenizzatore per l'analisi del sentimento
model_name = "distilbert-base-uncased-finetuned-sst-2-english"
tokenizer = DistilBertTokenizer.from_pretrained(model_name)
model = DistilBertForSequenceClassification.from_pretrained(model_name)

# Definisci il pipeline di analisi del sentimento
sentiment_analysis = pipeline("sentiment-analysis", model=model, tokenizer=tokenizer)

# Definisci un dataset semplice
text_factual = "Mi è piaciuto questo film perché era divertente ma i miei amici non l'hanno apprezzato perché era troppo lungo e noioso."

result = sentiment_analysis(text_factual)

print(f"{text_factual}: {result[0]['label']} (confidenza: {result[0]['score']:.2f})")

def pred_score_text(list_text):
  if type(list_text) == str:
    sa_pred = sentiment_analysis(list_text)[0]
    sa_score = sa_pred['score']
    sa_label = sa_pred['label']
    return sa_score if sa_label == "POSITIVE" else 1.0 - sa_score
  return np.array([sa["score"] if sa["label"] == "POSITIVE" else 1.0 - sa["score"] for sa in sentiment_analysis(list_text)])

Per questo codice, vedremo che il nostro testo reale ha un sentimento NEGATIVO con una confidenza elevata (≥0.9), quindi proviamo a generare un controfattuale:

from cfnow import find_text

cf_text = find_text(text_input=text_factual, textual_classifier=pred_score_text)
result_cf = sentiment_analysis(cf_text.cfs[0])
print(f"CF: {cf_text.cfs[0]}: {result_cf[0]['label']} (confidenza: {result_cf[0]['score']:.2f})")

Con il codice sopra, cambiando una sola parola (ma), la classificazione è passata da NEGATIVA a POSITIVA con alta confidenza. Questo dimostra come i controfattuali possano essere utili, perché anche piccole modifiche possono influire sulla comprensione di come il modello predice le frasi e/o aiutare a identificare comportamenti indesiderati.

Conclusioni

Questo è stato un’introduzione (relativamente) breve a CFNOW e spiegazioni controfattuali. Esiste un’ampia (e crescente) letteratura sui controfattuali che dovresti sicuramente consultare se vuoi approfondire l’argomento. Questo articolo fondamentale [3], scritto dal mio supervisore di dottorato, il Prof. David Martens, offre una buona introduzione alle spiegazioni controfattuali. Inoltre, ci sono ottime recensioni come quella scritta da Verma et al. [7]. In sintesi, le spiegazioni controfattuali sono un modo facile e conveniente per spiegare le decisioni complesse degli algoritmi di apprendimento automatico e possono fare molto di più delle semplici spiegazioni se applicate correttamente. CFNOW offre un modo semplice, veloce e flessibile per generare spiegazioni controfattuali, consentendo ai professionisti non solo di spiegare, ma anche di sfruttare al massimo il potenziale dei loro dati e del loro modello.

Riferimenti:

[1] — https://github.com/marcotcr/lime[2] — https://github.com/shap/shap[3] — https://www.jstor.org/stable/26554869[4] — https://www.mdpi.com/2076-3417/11/16/7274[5] — https://arxiv.org/pdf/2306.06506.pdf[6] — https://arxiv.org/abs/2001.07417[7] — https://arxiv.org/abs/2010.10596