Implementare la matematica nei paper di deep learning in un efficiente codice PyTorch SimCLR Contrastive Loss

Implementare la matematica dei paper di deep learning in codice PyTorch efficiente per SimCLR Contrastive Loss.

Imparare a implementare formule matematiche avanzate in codice PyTorch performante.

Foto di Jeswin Thomas su Unsplash

Introduzione

Uno dei modi migliori per approfondire la comprensione della matematica dietro i modelli di deep learning e le funzioni di perdita, nonché un ottimo modo per migliorare le tue competenze in PyTorch, è abituarsi a implementare da soli articoli di deep learning.

I libri e gli articoli di blog possono aiutarti a iniziare a codificare e a imparare le basi di ML / DL, ma dopo aver studiato alcuni di essi e aver acquisito familiarità con le attività di routine nel campo, ti renderai presto conto che sei da solo nel tuo percorso di apprendimento e troverai la maggior parte delle risorse online noiose e troppo superficiali. Tuttavia, sono convinto che se puoi studiare nuovi articoli di deep learning man mano che vengono pubblicati e comprendere le parti matematiche necessarie (non necessariamente tutte le dimostrazioni matematiche dietro le teorie degli autori) e sei un programmatore capace che può implementarle in un codice efficiente, niente può fermarti nel rimanere aggiornato nel campo e imparare nuove idee.

Implementazione della Loss Contrastiva

Illustrerò la mia routine e i passaggi che seguo per implementare la matematica negli articoli di deep learning utilizzando un esempio non banale: la Loss Contrastiva nell’articolo SimCLR.

Ecco la formulazione matematica della loss:

Loss Contrastiva (NT-Xent) dall'articolo SimCLR | da https://arxiv.org/pdf/2002.05709.pdf

Sono d’accordo che la sola vista della formula potrebbe sembrare spaventosa! E potresti pensare che ci devono essere molte implementazioni di PyTorch pronte su GitHub, quindi usiamole 🙂 E sì, hai ragione. Ci sono decine di implementazioni online. Tuttavia, penso che questo sia un buon esempio per praticare questa competenza e potrebbe servire come un buon punto di partenza.

Passaggi per implementare la matematica nel codice

La mia routine per implementare la matematica negli articoli in codice PyTorch efficiente è la seguente:

  1. Comprendere la matematica, spiegarla in termini semplici
  2. Implementare una versione iniziale utilizzando semplici cicli “for” in Python, senza moltiplicazioni matriciali complesse per ora
  3. Convertire il tuo codice in codice PyTorch efficiente e compatibile con le matrici

OK, passiamo direttamente al primo passaggio.

Passaggio 1: Comprendere la matematica e spiegarla in termini semplici

Suppongo che tu abbia una conoscenza di base dell’algebra lineare e che tu sia familiare con le notazioni matematiche. Se non lo sei, puoi utilizzare questo strumento per sapere cosa rappresentano e cosa fanno ciascuno di questi simboli in matematica, semplicemente disegnando il simbolo. Puoi anche consultare questa fantastica pagina di Wikipedia in cui vengono descritte la maggior parte delle notazioni. Queste sono le opportunità in cui impari cose nuove, cercando e leggendo ciò che è necessario al momento in cui ne hai bisogno. Credo che sia un modo più efficiente di apprendere, invece di iniziare con un libro di matematica da zero e abbandonarlo dopo qualche giorno 🙂

Tornando al nostro lavoro. Come il paragrafo sopra la formula aggiunge più contesto, nella strategia di apprendimento SimCLR si parte con N immagini e si trasformano ciascuna di esse 2 volte per ottenere viste aumentate di quelle immagini (2 * N immagini ora). Successivamente, si passano queste 2 * N immagini attraverso un modello per ottenere vettori di embedding per ciascuna di esse. Ora, si desidera rendere i vettori di embedding delle 2 viste aumentate della stessa immagine (una coppia positiva) più vicini nello spazio di embedding (e fare lo stesso per tutte le altre coppie positive). Un modo per misurare quanto simili (vicini, nella stessa direzione) siano due vettori è utilizzare la Similarità Cosinusoidale, che è definita come sim(u, v) (cerca la definizione nell’immagine sopra).

In termini semplici, la formula descrive che per ogni elemento nel nostro batch, che è l’embedding di una delle viste aumentate di un’immagine, (Ricorda: il batch contiene tutti gli embedding delle viste aumentate di diverse immagini → se partiamo con N immagini, il batch avrà una dimensione di 2*N), prima troviamo l’embedding dell’altra vista aumentata di quell’immagine per creare una coppia positiva. Successivamente, calcoliamo la similarità cosinica di questi due embedding ed eleviamo il risultato a potenza (il numeratore della formula). Poi, calcoliamo l’elevazione a potenza della similarità cosinica di tutte le altre coppie che possiamo costruire con il nostro primo vettore di embedding con il quale abbiamo iniziato (ad eccezione della coppia con se stesso, è quello che significa 1[k!=i] nella formula), e li sommiamo per costruire il denominatore. Ora possiamo dividere il numeratore per il denominatore e prendere il logaritmo naturale di questo e invertire il segno! Ora abbiamo la perdita del primo elemento nel nostro batch. Dobbiamo semplicemente ripetere lo stesso processo per tutti gli altri elementi nel batch e poi prendere la media per poter chiamare il metodo .backward() di PyTorch per calcolare i gradienti.

Step 2: Implementazione utilizzando codice Python semplice, con cicli “for”

Implementazione Python semplice, utilizzando cicli “for” lenti

Andiamo attraverso il codice. Supponiamo di avere due immagini: A e B. La variabile aug_views_1 contiene gli embedding (ognuno di dimensione 3) di una vista aumentata di queste due immagini (A1 e B1), così come aug_views_2 (A2 e B2); quindi, il primo elemento di entrambe le matrici è relativo all’immagine A e il secondo elemento di entrambe è relativo all’immagine B. Concateniamo le due matrici nella matrice delle proiezioni (che contiene 4 vettori: A1, B1, A2, B2).

Per mantenere la relazione dei vettori nella matrice delle proiezioni, definiamo un dizionario pos_pairs per memorizzare quali due elementi sono correlati nella matrice concatenata. (presto spiegherò cosa significa la cosa F.normalize()!)

Come puoi vedere nelle righe successive di codice, sto passando attraverso gli elementi nella matrice delle proiezioni in un ciclo “for”, trovo il vettore correlato utilizzando il nostro dizionario e quindi calcolo la similarità cosinica. Potresti chiederti perché non dividere per la dimensione dei vettori, come suggerisce la formula della similarità cosinica. Il punto è che prima di iniziare il ciclo, utilizzando la funzione F.normalize, sto normalizzando tutti i vettori nella nostra matrice delle proiezioni in modo che abbiano dimensione 1. Quindi, non c’è bisogno di dividere per la dimensione nella riga in cui stiamo calcolando la similarità cosinica.

Dopo aver costruito il nostro numeratore, trovo tutti gli altri indici dei vettori nel batch (ad eccezione dello stesso indice i), per calcolare le similarità cosiniche che compongono il denominatore. Infine, calcolo la perdita dividendo il numeratore per il denominatore, applicando la funzione log e invertendo il segno. Assicurati di giocare con il codice per capire cosa succede in ogni riga.

Step 3: Conversione in un codice PyTorch efficiente e adatto alle matrici

Il problema con l’implementazione precedente in Python è che è troppo lenta per essere utilizzata nel nostro processo di addestramento; dobbiamo eliminare i cicli “for” lenti e convertirlo in moltiplicazioni di matrici e manipolazioni di array per sfruttare il potere di parallelizzazione.

Implementazione in PyTorch

Vediamo cosa succede in questo frammento di codice. Questa volta, ho introdotto i tensori labels_1 e labels_2 per codificare le classi arbitrarie a cui appartengono queste immagini, poiché abbiamo bisogno di un modo per codificare la relazione tra le immagini A1, A2 e B1, B2. Non importa se scegli le etichette 0 e 1 (come ho fatto io) o diciamo 5 e 8.

Dopo aver concatenato sia gli embedding che le etichette, iniziamo creando una matrice sim_matrix contenente la similarità cosinica di tutte le possibili coppie.

Come appare la sim_matrix: le celle verdi contengono le nostre coppie positive, le celle arancioni sono le coppie che devono essere ignorate nel denominatore | Visualizzazione dell'autore

La visualizzazione sopra riportata è tutto ciò di cui avete bisogno 🙂 per capire come funziona il codice e perché stiamo eseguendo i passaggi. Considerando la prima riga della matrice sim_matrix, possiamo calcolare la perdita per il primo elemento nel batch (A1) nel seguente modo: dobbiamo dividere A1A2 (esponenziato) per la somma di A1B1, A1A2 e A1B2 (ognuno esponenziato per primo) e mantenere il risultato nel primo elemento di un tensore che memorizza tutte le perdite. Quindi, dobbiamo prima creare una maschera per individuare le celle verdi nella visualizzazione sopra riportata. Le due righe di codice che definiscono la variabile mask fanno esattamente questo. Il numeratore viene calcolato moltiplicando la nostra matrice sim_matrix per la maschera appena creata e quindi sommando gli elementi di ogni riga (dopo aver applicato la maschera, ci sarà solo un elemento diverso da zero in ogni riga; ossia le celle verdi). Per calcolare il denominatore, dobbiamo sommare su ogni riga, ignorando le celle arancioni sulla diagonale. Per farlo, useremo il metodo .diag() dei tensori di PyTorch. Il resto è autoesplicativo!

Bonus: Utilizzare gli assistenti AI (ChatGPT, Copilot, …) per implementare la formula

Abbiamo ottimi strumenti a nostra disposizione per aiutarci a comprendere e implementare la matematica nei paper di deep learning. Ad esempio, è possibile chiedere a ChatGPT (o altri strumenti simili) di implementare il codice in PyTorch dopo avergli fornito la formula del paper. Sulla base della mia esperienza, ChatGPT può essere molto utile e fornire le migliori risposte finali con meno tentativi ed errori se riesci a portarti in qualche modo all’implementazione ingenua a loop for in stile pythonico. Dai a ChatGPT quella implementazione ingenua e chiedigli di convertirla in un codice PyTorch efficiente che utilizza solo moltiplicazioni di matrici e manipolazioni di tensori; rimarrai sorpreso dalla risposta 🙂

Ulteriori letture

Vi incoraggio a dare un’occhiata alle seguenti due ottime implementazioni della stessa idea per imparare come è possibile estendere questa implementazione per considerare situazioni più sfumate, come nel contesto del supervised contrastive learning.

  1. Supervised Contrastive Loss, di Guillaume Erhard
  2. SupContrast, di Yonglong Tian

Su di me

Sono Moein Shariatnia, uno sviluppatore di machine learning e studente di medicina, specializzato nell’utilizzo di soluzioni di deep learning per applicazioni di imaging medico. La mia ricerca riguarda principalmente l’indagine sulla generalizzabilità dei modelli di deep learning in diverse circostanze. Non esitate a contattarmi tramite e-mail, Twitter o LinkedIn.