All’interno delle reti neurali con l’eXplainable Artificial Intelligence

reti neurali

Le reti neurali hanno apportato notevoli migliorie in molti campi, dalla visione artificiale, passando per i chatbot (come mostra il recente caso di ChatGPT), per arrivare alla diagnosi precoce di malattie rare. Tuttavia, queste tecniche vengono spesso usate come delle black box: in tal senso, comprenderne il meccanismo interno potrebbe aiutare i ricercatori a migliorarne il funzionamento, limandone eventuali debolezze. In questo articolo, ci focalizzeremo sulle reti convoluzionali, vedendo come visualizzare ciò che accade al loro interno mediante l’uso di Python e TensorFlow.

Introduzione

L’avvento del Deep Learning ha reso possibile migliorie impensabili fino a pochi anni fa. In tal senso, la Computer Vision è probabilmente uno tra gli ambiti che ha tratto maggior beneficio dalle nuove tecniche disponibili: pensiamo, infatti, ai grandi avanzamenti in termini di classificazione ed individuazione di oggetti nelle immagini e nei video, cui stiamo assistendo negli ultimi anni (ne abbiamo già parlato in questo articolo). Questo ha però posto le basi per una nuova sfida: interpretare ciò che accade quando una rete neurale predice un risultato, allo scopo di comprenderne (e migliorarne) il funzionamento. Ciò nasce dal fatto che i modelli di rete neurale sono stati spesso trattati come delle scatole nere, per le quali non è facilmente desumibile il come pervengano ad un determinato risultato. Di conseguenza, la domanda che la comunità scientifica si è posta è: come è possibile fidarsi delle decisioni di una rete neurale o, più in generale, di un modello di Machine Learning, se non possiamo validare il percorso che lo ha portato ad assumere tali decisioni? Per rispondere a questa domanda è stata sviluppata un’intera branca dell’Intelligenza Artificiale riguardante la cosiddetta explainability, e chiamata eXplainable Artificial Intelligence, o XAI. La XAI definisce un gran numero di metodi per comprendere ciò che accade all’interno dei modelli di Machine e Deep Learning, tra cui ricordiamo i metodi LIME, SHAP, e quelli basati sulla valutazione del gradiente interno alle reti neurali a più layer. Ed è proprio uno di questi ultimi che andremo ad utilizzare per valutare come una rete neurale convoluzionale (di cui abbiamo già parlato in precedenza) giunga a determinati risultati. In particolare, per aiutarci ad interpretare i risultati raggiunti dal nostro modello e comprendere quali parti di un’immagine “osservi” per determinare la sua predizione, utilizzeremo le cosiddette mappe di attivazione o, in inglese, Class Activation Maps (CAMs). Queste non sono altro che una rappresentazione grafica delle zone maggiormente “salienti” dell’immagine, ovvero quelle verso le quali la rete neurale pone maggiore attenzione durante la fase di inferenza del risultato. Per estrarre le mappe di attivazione esistono diversi approcci: quello che useremo è chiamato Grad-CAM, crasi che sta per Gradient-weighted CAMs. Per Grad-CAM vedremo soltanto l’implementazione Python mediante Keras e TensorFlow: qualora vogliate scendere nei dettagli matematici dell’algoritmo, vi rimando all’articolo scientifico scritto da Selvaraju nel 2017.

Perché “spiegare” una rete neurale?

Per comprendere il perché sia necessario “spiegare” i risultati ottenuti da una rete neurale, è opportuno ricorrere ad un simpatico aneddoto diventato molto comune nella comunità della Computer Vision. Il racconto si apre con l’Esercito che contatta un team di ricercatori richiedendo un software in grado di individuare i carri armati nascosti nella boscaglia. Per far questo, ai ricercatori viene fornito un dataset contenente cento immagini, la metà delle quali contiene dei carri armati mimetizzati tra gli alberi, e l’altra contenente soltanto boscaglia. I ricercatori iniziano a lavorare, ed effettuano la procedura standard di addestramento: il dataset viene suddiviso in insiemi di training e test bilanciati, ed un modello di rete neurale convoluzionale viene addestrato ottenendo un’accuratezza (sia di training, sia di validazione) del 100%. Il team, entusiasta del risultato ottenuto, porta i risultati all’Esercito, affermando di aver finalmente risolto il problema dell’individuazione dei carri armati mimetizzati. Tuttavia, dopo poche settimane, i ricercatori ricevono una chiamata spiacevole dall’Esercito. I militari, infatti, affermano di essere estremamente insoddisfatti delle performance ottenute dal tool. Perplessi, i ricercatori decidono di rivedere i loro esperimenti, addestrando diversi modelli con diversi sottoinsiemi di immagini, ed usando tecniche di cross-validazione per verificare la bontà dei risultati ottenuti. E, nonostante le diverse prove, nulla cambia: l’accuratezza in laboratorio rimane al 100%, mentre le performance sul campo sono terribili. Ad un certo punto, però, uno dei ricercatori decide di osservare con più attenzione il dataset messo a disposizione dall’esercito, scoprendo la vera causa alla base del malfunzionamento del tool. Infatti, le foto dei carri armati mimetizzati erano state catturate in giorni assolati, mentre le foto senza carri armati erano state prese in giorni nuvolosi. Nella pratica, il tool sviluppato dai ricercatori individuava con assoluta certezza la presenza di…nuvole. Ovviamente, questa storia non è reale: tuttavia, possiamo comprendere come capire ciò che sta osservando un modello possa avere estrema rilevanza. Infatti, se i ricercatori avessero usato un algoritmo XAI come Grad-CAM, avrebbero notato che il modello si focalizzava non sulla boscaglia, ma sul cielo.

Grad-CAM in Python

Una volta compresa l’importanza di “comprendere” come una rete neurale giunge ad un risultato, possiamo passare ad implementare Grad-CAM all’interno del nostro codice. Grad-CAM è in grado di funzionare con qualsiasi architettura contenga un layer di tipo convoluzionale. Infatti, il suo funzionamento è vincolato all’individuazione dell’ultimo layer convoluzionale presente nella rete, a partire dal quale vengono esaminate le informazioni relative ai valori del gradiente per determinare le mappe di attivazione. L’output del metodo è, per l’appunto, una mappa di attivazione visualizzata per una determinata classe di oggetti, mediante la quale potremo visualizzare le parti dell’immagine che la rete convoluzionale sta “osservando” e che, di conseguenza, utilizza per determinare il risultato della classificazione.

Un esempio pratico

Passiamo adesso a creare un piccolo programma Python che ci permetta di implementare il metodo GradCAM ed applicarlo ad una rete neurale mediante le librerie Keras e TensorFlow. Per prima cosa, quindi, dovremo configurare il nostro ambiente di sviluppo; se non lo avete mai fatto, ecco un paio di articoli che potranno aiutarci: questo e questo. Ai nostri scopi, utilizzeremo il package manager Pipenv per installare le librerie richieste. Per prima cosa, scarichiamo il codice presente in questa repository GitHub, e portiamoci mediante terminale all’interno della cartella appena scaricata, e creiamo l’ambiente virtuale mediante pipenv.

cd eos-xai
pipenv install

Diamo un’occhiata alla struttura del progetto, mostrata in Figura 1.

Struttura del codice

Figura 1: Struttura del codice

Abbiamo quindi:

  • il package algs, all’interno del quale è contenuto il modulo gradcam
  • la cartella imgs, nella quale sono contenute tre immagini in formato JPEG che ci serviranno per le nostre prove
  • lo script run, che conterrà il codice da eseguire

Il modulo gradcam

Passiamo adesso ad analizzare il codice, partendo dal modulo gradcam. All’apice del modulo, importiamo i package e le funzioni necessarie.

from tensorflow.keras.models import Model
import tensorflow as tf
import numpy as np
import cv2

In particolare, useremo la classe Model di Keras, che ci permette di definire il modello da usare per applicare il metodo GradCAM. Inoltre, sfrutteremo le librerie OpenCV, TensorFlow e NumPy.

Successivamente, andremo a definire la classe GradCAM, che avrà tre attributi:

  • base_model, ovvero un modello Keras che servirà come base per la classificazione delle immagini;
  • cls_idx, ovvero l’indice della classe che useremo per definire le mappe di attivazione;
  • layer_name, valore opzionale, che servirà nel caso si voglia definire nel dettaglio il layer convoluzionale per il quale visualizzare la mappa di attivazione.

Con particolare riferimento all’ultimo attributo, ricordiamo che GradCAM sfrutta normalmente l’ultimo layer convoluzionale della rete; tuttavia, nessuno ci impedisce di visualizzare le mappe di attivazione per qualsiasi layer convoluzionale. In tal senso, se non specificheremo un valore per l’attributo layer_name, la classe inferirà in automatico l’ultimo layer convoluzionale usando il metodo get_conv_layer.

@staticmethod
def get_conv_layer(model):
    for layer in reversed(model.layers):
        if len(layer.output_shape) == 4:
            return layer.name
        raise ValueError('Il modello non ha un layer convoluzionale.')

Questo metodo, definito come statico, non fa altro che iterare sulla lista dei layer della rete, ottenibile mediante l’attributo layers del modello, partendo dall’ultimo ed andando verso il primo. Non appena trova un layer il cui output è a quattro dimensioni (mediante l’attributo output_shape), questo viene restituito; ciò è legato al fatto che i layer con output a quattro dimensioni sono convoluzionali (o, al più, di pooling).

Passiamo adesso alla definizione del metodo get_heatmap, che rappresenta il cuore del GradCAM, e che accetta come argomento l’immagine per la quale vogliamo visualizzare le mappe di attivazione. Per prima cosa, il metodo costruisce una versione “ridotta” del modello base, il cui input sarà proprio l’immagine, ma il cui output consisterà nell’uscita del layer definito dall’attributo layer_name.

def get_heatmap(self, image):
    expl_model = Model(
        inputs=[self.base_model.inputs],
        outputs=[
            self.base_model.get_layer(self.layer_name).output,
            self.base_model.output
        ])

Una volta definita questa versione ridotta del modello, dovremo calcolare i gradienti necessari all’implementazione del GradCAM. Per farlo, utilizzeremo un oggetto di TensorFlow chiamato GradientTape (possiamo trovare la reference a questo indirizzo), il quale implementa il concetto di differenziazione automatica, ovvero una procedura informatica che permette di calcolare automaticamente le derivate di un valore. Creiamo quindi un’istanza di un oggetto di tipo GradientTape chiamata tape, ed usiamo un costrutto with per utilizzarla.

with tf.GradientTape() as tape:
    inputs = tf.cast(image, tf.float32)
    (conv_out, preds) = expl_model(inputs)
    loss = preds[:, self.cls_idx]

All’interno del with, convertiamo l’immagine di ingresso in formato in virgola mobile a 32 bit, e calcoliamo sia i valori in uscita all’ultimo layer (conv_out), sia il risultato della predizione (preds) del modello definito al passo precedente. A questo punto, andremo ad estrarre la loss associata alle predizioni per lo specifico indice di classe cui siamo interessati, che risulterà utile a calcolare il gradiente mediante l’apposito metodo.

[...]

ATTENZIONE: quello che hai appena letto è solo un estratto, l'Articolo Tecnico completo è composto da ben 2790 parole ed è riservato agli ABBONATI. Con l'Abbonamento avrai anche accesso a tutti gli altri Articoli Tecnici che potrai leggere in formato PDF per un anno. ABBONATI ORA, è semplice e sicuro.

Scarica subito una copia gratis

Scrivi un commento

Send this to a friend