Comprensione delle reti di capsule: la nuova affascinante architettura dell'IA

Le reti neurali convoluzionali hanno fatto un lavoro straordinario, ma sono radicate in problemi. È tempo che iniziamo a pensare a nuove soluzioni o miglioramenti - e ora, inserisci le capsule.

In precedenza, ho discusso brevemente di come le reti di capsule combattono alcuni di questi problemi tradizionali. In passato da alcuni mesi mi sono immerso in tutto ciò che riguarda le capsule. Penso che sia il momento in cui tutti cerchiamo di capire meglio come funzionano effettivamente le capsule.

Al fine di rendere più facile seguire insieme, ho costruito uno strumento di visualizzazione che permette di vedere ciò che sta accadendo in ogni strato. Questo è associato a una semplice implementazione della rete. Tutto può essere trovato su GitHub qui.

Questa è l'architettura CapsNet. Non preoccuparti se non capisci ancora cosa significhi. Lo esaminerò strato per strato, con tutti i dettagli che riesco a evocare.

Parte 0: The Input

L'input in CapsNet è l'immagine reale fornita alla rete neurale. In questo esempio l'immagine di input è alta 28 pixel e larga 28 pixel. Ma le immagini sono in realtà 3 dimensioni e la terza dimensione contiene i canali di colore.

L'immagine nel nostro esempio ha un solo canale di colore, perché è in bianco e nero. La maggior parte delle immagini con cui hai familiarità ha 3 o 4 canali, per Rosso-Verde-Blu e possibilmente un canale aggiuntivo per Alpha o trasparenza.

Ognuno di questi pixel è rappresentato come un valore compreso tra 0 e 255 e memorizzato in una matrice 28x28x1 [28, 28, 1]. Più è luminoso il pixel, maggiore è il valore.

Parte 1a: Convoluzioni

La prima parte di CapsNet è un livello convoluzionale tradizionale. Che cos'è uno strato convoluzionale, come funziona e qual è il suo scopo?

L'obiettivo è quello di estrarre alcune caratteristiche estremamente basilari dall'immagine di input, come bordi o curve.

Come possiamo farlo?

Pensiamo a un vantaggio:

Se osserviamo alcuni punti sull'immagine, possiamo iniziare a prendere uno schema. Concentrati sui colori a sinistra ea destra del punto che stiamo osservando:

Potresti notare che hanno una differenza maggiore se il punto è un bordo:

255-114 = 141
114 - 153 = -39
153 - 153 = 0
255 - 255 = 0

E se attraversassimo ciascun pixel dell'immagine e ne sostituissimo il valore con il valore della differenza dei pixel a sinistra e a destra di esso? In teoria, l'immagine dovrebbe diventare tutta nera ad eccezione dei bordi.

Potremmo farlo facendo un ciclo attraverso ogni pixel nell'immagine:

per pixel nell'immagine {
  risultato [pixel] = immagine [pixel - 1] - immagine [pixel + 1]
}

Ma questo non è molto efficiente. Possiamo invece usare qualcosa chiamato "convoluzione". Tecnicamente parlando, è una "correlazione incrociata", ma a tutti piace chiamarli convoluzioni.

Una convoluzione essenzialmente sta facendo la stessa cosa del nostro loop, ma sfrutta la matematica della matrice.

Una convoluzione viene eseguita allineando una piccola "finestra" nell'angolo dell'immagine che ci consente di vedere solo i pixel in quell'area. Facciamo quindi scorrere la finestra su tutti i pixel dell'immagine, moltiplicando ciascun pixel per un set di pesi e quindi sommando tutti i valori presenti in quella finestra.

Questa finestra è una matrice di pesi, chiamata "kernel".

Ci preoccupiamo solo di 2 pixel, ma quando avvolgiamo la finestra attorno a loro incapsulerà il pixel tra di loro.

Finestra:
┌─────────────────────────────────────┐
│ left_pixel middle_pixel right_pixel │
└─────────────────────────────────────┘

Riesci a pensare a una serie di pesi che possiamo moltiplicare per questi pixel in modo che la loro somma si sommi al valore che stiamo cercando?

Finestra:
┌─────────────────────────────────────┐
│ left_pixel middle_pixel right_pixel │
└─────────────────────────────────────┘
(w1 * 255) + (w2 * 255) + (w3 * 114) = 141

Spoiler di seguito!

 │ │ │
 │ │ │
 │ │ │
 │ │ │
 │ │ │
\ │ / \ │ / \ │ /
 V V V

Possiamo fare qualcosa del genere:

Finestra:
┌─────────────────────────────────────┐
│ left_pixel middle_pixel right_pixel │
└─────────────────────────────────────┘
(1 * 255) + (0 * 255) + (-1 * 114) = 141

Con questi pesi, il nostro kernel sarà simile al seguente:

kernel = [1 0 -1]

Tuttavia, i kernel sono generalmente quadrati, quindi possiamo riempirlo con più zeri per apparire così:

kernel = [
  [0 0 0]
  [1 0 -1]
  [0 0 0]
]

Ecco una bella gif per vedere una convoluzione in azione:

Nota: la dimensione dell'output è ridotta della dimensione del kernel più 1. Ad esempio: (7 - 3) + 1 = 5 (più su questo nella prossima sezione)

Ecco come appare l'immagine originale dopo aver fatto una convoluzione con il kernel che abbiamo creato:

Potresti notare che mancano un paio di bordi. In particolare, quelli orizzontali. Per evidenziarli, avremmo bisogno di un altro kernel che guardi i pixel sopra e sotto. Come questo:

kernel = [
  [0 1 0]
  [0 0 0]
  [0 -1 0]
]

Inoltre, entrambi questi kernel non funzionano bene con bordi di altri angoli o bordi sfocati. Per questo motivo, utilizziamo molti kernel (nella nostra implementazione CapsNet, utilizziamo 256 kernel). E i kernel sono normalmente più grandi per consentire più spazio di manovra (i nostri kernel saranno 9x9).

Questo è l'aspetto di uno dei kernel dopo l'allenamento del modello. Non è molto ovvio, ma questa è solo una versione più grande del nostro rilevatore di bordi che è più robusta e trova solo bordi che vanno dal chiaro allo scuro.

kernel = [
  [0,02 -0,01 0,01 -0,05 -0,08 -0,14 -0,16 -0,22 -0,02]
  [0,01 0,02 0,03 0,02 0,00 -0,06 -0,14 -0,28 0,03]
  [0,03 0,01 0,02 0,01 0,03 0,01 -0,11 -0,22 -0,08]
  [0,03 -0,01 -0,02 0,01 0,04 0,07 -0,11 -0,24 -0,05]
  [-0,01 -0,02 -0,02 0,01 0,06 0,12 -0,13 -0,31 0,04]
  [-0,05 -0,02 0,00 0,05 0,08 0,14 -0,17 -0,29 0,08]
  [-0,06 0,02 0,00 0,07 0,07 0,04 -0,18 -0,10 0,05]
  [-0,06 0,01 0,04 0,05 0,03 -0,01 -0,10 -0,07 0,00]
  [-0,04 0,00 0,04 0,05 0,02 -0,04 -0,02 -0,05 0,04]
]

Nota. Ho arrotondato i valori perché sono piuttosto grandi, ad esempio 0,01783941

Fortunatamente, non dobbiamo scegliere manualmente questa raccolta di kernel. Questo è ciò che la formazione fa. I kernel iniziano tutti vuoti (o in uno stato casuale) e continuano a essere modificati nella direzione che rende l'output più vicino a ciò che vogliamo.

Questo è ciò che i 256 kernel finito per assomigliare (io li colorato come pixel in modo che sia più facile da digerire). Più i numeri sono negativi, più sono blu. 0 è verde e positivo è giallo:

256 kernel (9x9)

Dopo aver filtrato l'immagine con tutti questi kernel, finiamo con una grossa pila di 256 immagini di output.

Parte 1b: ReLU

ReLU (formalmente noto come Unità lineare rettificata) può sembrare complicato, ma in realtà è abbastanza semplice. ReLU è una funzione di attivazione che accetta un valore. Se è negativo diventa zero e se è positivo rimane lo stesso.

Nel codice:

x = max (0, x)

E come grafico:

Applichiamo questa funzione a tutti gli output delle nostre convoluzioni.

Perché lo facciamo? Se non applichiamo una sorta di funzione di attivazione all'output dei nostri layer, l'intera rete neurale potrebbe essere descritta come una funzione lineare. Ciò significherebbe che tutte queste cose che stiamo facendo sono in qualche modo inutili.

L'aggiunta di una non linearità ci consente di descrivere tutti i tipi di funzioni. Esistono molti tipi diversi di funzioni che potremmo applicare, ma ReLU è il più popolare perché è molto economico da eseguire.

Ecco gli output del layer ReLU Conv1:

256 uscite (20x20 pixel)

Parte 2a: PrimaryCaps

Il livello PrimaryCaps inizia come un normale livello di convoluzione, ma questa volta stiamo contorcendo lo stack di 256 output delle precedenti convoluzioni. Quindi invece di avere un kernel 9x9, abbiamo un kernel 9x9x256.

Quindi cosa stiamo cercando esattamente?

Nel primo strato di convoluzioni cercavamo bordi e curve semplici. Ora stiamo cercando forme leggermente più complesse dai bordi che abbiamo trovato in precedenza.

Questa volta il nostro "passo" è 2. Ciò significa che invece di spostare 1 pixel alla volta, facciamo i passi di 2. Viene scelto un passo più grande in modo da poter ridurre le dimensioni del nostro input più rapidamente:

Nota: la dimensione dell'output sarebbe normalmente 12, ma la dividiamo per 2, a causa del passo. Ad esempio: ((20-9) + 1) / 2 = 6

Convoleremo le uscite altre 256 volte. Quindi finiremo con uno stack di 256 output 6x6.

Ma questa volta non siamo soddisfatti solo di alcuni vecchi numeri orribili.

Tagliamo il mazzo in 32 mazzi con 8 carte per mazzo.

Possiamo chiamare questo mazzo un "livello capsula".

Ogni strato di capsula ha 36 "capsule".

Se stai tenendo il passo (e sei un mago della matematica), ciò significa che ogni capsula ha una matrice di 8 valori. Questo è ciò che possiamo chiamare un "vettore".

Ecco di cosa sto parlando:

Queste "capsule" sono il nostro nuovo pixel.

Con un singolo pixel, potremmo solo archiviare la sicurezza di trovare o meno un vantaggio in quel punto. Più alto è il numero, maggiore è la fiducia.

Con una capsula possiamo memorizzare 8 valori per posizione! Questo ci dà l'opportunità di memorizzare più informazioni rispetto al semplice fatto di trovare una forma in quel punto. Ma quali altri tipi di informazioni vorremmo archiviare?

Quando guardi la forma qui sotto, cosa puoi dirmi al riguardo? Se dovessi dire a qualcun altro come ridisegnarlo e non potrebbero guardarlo, cosa diresti?

Questa immagine è estremamente semplice, quindi ci sono solo alcuni dettagli per descrivere la forma:

  • Tipo di forma
  • Posizione
  • Rotazione
  • Colore
  • Taglia

Possiamo chiamare questi "parametri di istanziazione". Con immagini più complesse finiremo per avere bisogno di maggiori dettagli. Possono includere posa (posizione, dimensione, orientamento), deformazione, velocità, albedo, tonalità, trama e così via.

Potresti ricordare che quando abbiamo creato un kernel per il rilevamento dei bordi, ha funzionato solo su un angolo specifico. Avevamo bisogno di un kernel per ogni angolo. Potremmo cavarcela quando trattiamo i bordi perché ci sono pochissimi modi per descriverli. Una volta arrivati ​​al livello delle forme, non vogliamo avere un kernel per ogni angolo di rettangoli, ovali, triangoli e così via. Diventerebbe ingombrante e peggiorerebbe ancora quando si trattano forme più complicate che hanno rotazioni tridimensionali e caratteristiche come l'illuminazione.

Questo è uno dei motivi per cui le reti neurali tradizionali non gestiscono molto bene le rotazioni invisibili:

Mentre andiamo dai bordi alle forme e dalle forme agli oggetti, sarebbe bello se avessimo più spazio per memorizzare queste informazioni extra utili.

Ecco un confronto semplificato di 2 livelli capsula (uno per i rettangoli e l'altro per i triangoli) vs 2 output pixel tradizionali:

Come un tradizionale vettore 2D o 3D, questo vettore ha un angolo e una lunghezza. La lunghezza descrive la probabilità e l'angolo descrive i parametri di istanza. Nell'esempio sopra, l'angolo corrisponde effettivamente all'angolo della forma, ma normalmente non è così.

In realtà non è realmente possibile (o almeno facile) visualizzare i vettori come sopra, perché questi vettori sono di 8 dimensioni.

Poiché abbiamo tutte queste informazioni extra in una capsula, l'idea è che dovremmo essere in grado di ricreare l'immagine da loro.

Sembra fantastico, ma come convincere la rete a voler davvero imparare queste cose?

Durante l'addestramento di una CNN tradizionale, ci preoccupiamo solo se il modello prevede o meno la giusta classificazione. Con una rete di capsule, abbiamo qualcosa chiamato "ricostruzione". Una ricostruzione prende il vettore che abbiamo creato e cerca di ricreare l'immagine di input originale, dato solo questo vettore. Quindi classifichiamo il modello in base alla vicinanza della ricostruzione all'immagine originale.

Ne parlerò più in dettaglio nelle prossime sezioni, ma ecco un semplice esempio:

Parte 2b: schiacciare

Dopo che avremo le nostre capsule, eseguiremo un'altra funzione di non linearità (come ReLU), ma questa volta l'equazione è un po 'più coinvolta. La funzione ridimensiona i valori del vettore in modo che cambi solo la lunghezza del vettore, non l'angolo. In questo modo possiamo rendere il vettore tra 0 e 1 in modo che sia una probabilità effettiva.

Ecco come si presentano le lunghezze dei vettori delle capsule dopo lo schiacciamento. A questo punto è quasi impossibile indovinare ciò che ogni capsula sta cercando.

Tieni presente che ogni pixel è in realtà un vettore di lunghezza 8

Parte 3: Instradamento per accordo

Il prossimo passo è decidere quali informazioni inviare al livello successivo. Nelle reti tradizionali, probabilmente faremmo qualcosa di simile al "pool massimo". Il pool massimo è un modo per ridurre le dimensioni passando al livello successivo solo il pixel attivato più alto nella regione.

Tuttavia, con le reti di capsule faremo qualcosa chiamato routing di comune accordo. Il miglior esempio di questo è l'esempio della barca e della casa illustrato da Aurélien Géron in questo eccellente video. Ogni capsula tenta di prevedere le attivazioni del livello successivo in base a se stessa:

Guardando queste previsioni, quale oggetto sceglieresti di passare al livello successivo (non conoscendo l'input)? Probabilmente la barca, vero? sia la capsula del rettangolo che la capsula del triangolo concordano su come sarebbe la barca. Ma non sono d'accordo su come apparirebbe la casa, quindi non è molto probabile che l'oggetto sia una casa.

Con l'instradamento concordato, trasmettiamo solo le informazioni utili e buttiamo via i dati che aggiungerebbero solo rumore ai risultati. Questo ci offre una selezione molto più intelligente rispetto alla semplice scelta del numero più grande, come nel pooling massimo.

Con le reti tradizionali, le funzionalità fuori posto non la fanno impazzire:

Con le reti di capsule, le funzionalità non sarebbero d'accordo tra loro:

Spero che funzioni intuitivamente. Tuttavia, come funziona la matematica?

Abbiamo 10 diverse classi di cifre che stiamo prevedendo:

0, 1, 2, 3, 4, 5, 6, 7, 8, 9

Nota: nell'esempio dell'imbarcazione e della casa stavamo prevedendo 2 oggetti, ma ora ne prevediamo 10.

A differenza della barca e dell'esempio della casa, le previsioni non sono in realtà immagini. Invece, stiamo cercando di prevedere il vettore che descrive l'immagine.

Le previsioni della capsula per ogni classe vengono fatte moltiplicando il suo vettore per una matrice di pesi per ogni classe che stiamo cercando di prevedere.

Ricorda che abbiamo 32 strati di capsule e ogni strato di capsule ha 36 capsule. Ciò significa che abbiamo un totale di 1.152 capsule.

cap_1 * weight_for_0 = previsione
cap_1 * weight_for_1 = previsione
cap_1 * weight_for_2 = previsione
cap_1 * ...
cap_1 * weight_for_9 = previsione
cap_2 * weight_for_0 = previsione
cap_2 * weight_for_1 = previsione
cap_2 * weight_for_2 = previsione
cap_2 * ...
cap_2 * weight_for_9 = previsione
...
cap_1152 * weight_for_0 = previsione
cap_1152 * weight_for_1 = previsione
cap_1152 * weight_for_2 = previsione
cap_1152 * ...
cap_1152 * weight_for_9 = previsione

Si finirà con un elenco di 11.520 previsioni.

Ogni peso è in realtà una matrice 16x8, quindi ogni previsione è una moltiplicazione di matrice tra il vettore della capsula e questa matrice di peso:

Come puoi vedere, la nostra previsione è un vettore di 16 gradi.

Da dove viene il 16? È una scelta arbitraria, proprio come 8 era per le nostre capsule originali.

Ma va notato che vogliamo aumentare il numero di dimensioni delle nostre capsule più in profondità entriamo nella rete. Ciò dovrebbe avere senso in modo intuitivo, perché più approfondiamo, più diventano complesse le nostre funzionalità e più parametri abbiamo bisogno per ricrearle. Ad esempio, avrai bisogno di più informazioni per descrivere un intero volto di un semplice occhio di una persona.

Il prossimo passo è capire quale di queste 11.520 previsioni sono maggiormente d'accordo.

Può essere difficile visualizzare una soluzione a questo quando pensiamo in termini di vettori ad alta dimensione. Per motivi di sanità mentale, iniziamo facendo finta che i nostri vettori siano solo punti nello spazio bidimensionale:

Iniziamo calcolando la media di tutti i punti. Ogni punto inizia con uguale importanza:

Possiamo quindi misurare la distanza tra ogni punto dalla media. Più il punto è lontano dalla media, meno importante diventa quel punto:

Quindi ricalcoliamo la media, questa volta tenendo conto dell'importanza del punto:

Finiamo per attraversare questo ciclo 3 volte:

Come puoi vedere, mentre attraversiamo questo ciclo, i punti che non sono d'accordo con gli altri iniziano a scomparire. I punti concordanti più alti finiscono per passare al livello successivo con le attivazioni più alte.

Parte 4: DigitCaps

Dopo l'accordo, finiamo con dieci vettori 16 dimensionali, un vettore per ogni cifra. Questa matrice è la nostra previsione finale. La lunghezza del vettore è la sicurezza della cifra trovata - più lunga è, meglio è. Il vettore può anche essere usato per generare una ricostruzione dell'immagine di input.

Ecco come appaiono le lunghezze dei vettori con l'ingresso di 4:

Il quinto blocco è il più luminoso, il che significa elevata sicurezza. Ricorda che 0 è la prima classe, il che significa che 4 è la nostra classe prevista.

Parte 5: Ricostruzione

La parte di ricostruzione dell'implementazione non è molto interessante. Sono solo alcuni livelli completamente collegati. Ma la ricostruzione stessa è molto interessante e divertente con cui giocare.

Se ricostruiamo i nostri 4 input dal suo vettore, questo è ciò che otteniamo:

Se manipoliamo i cursori (il vettore), possiamo vedere come ogni dimensione influenza il 4:

Consiglio di clonare il repository di visualizzazione per giocare con input diversi e vedere come i cursori influenzano la ricostruzione:

git clone https://github.com/bourdakos1/CapsNet-Visualization.git
cd CapsNet-Visualization
pip install -r Requisiti.txt

Esegui lo strumento:

python run_visualization.py

Quindi punta il tuo browser a: http: // localhost: 5000

Pensieri finali

Penso che le ricostruzioni dalle reti di capsule siano sbalorditive. Anche se il modello attuale è addestrato solo su cifre semplici, mi fa correre la mente con le possibilità che un'architettura maturata addestrata su un set di dati più ampio potrebbe raggiungere.

Sono molto curioso di vedere come la manipolazione dei vettori di ricostruzione di un'immagine più complicata possa influenzarla. Per questo motivo, il mio prossimo progetto è far funzionare le reti di capsule con i set di dati CIFAR e smallNORB.

Grazie per aver letto! In caso di domande, contattaci all'indirizzo bourdakos1@gmail.com, connettiti con me su LinkedIn o seguimi su Medium e Twitter.

Se trovi utile questo articolo, significherebbe molto se gli dessi degli applausi e lo condividessi per aiutare gli altri a trovarlo! E sentiti libero di lasciare un commento qui sotto.