Architetture Multi-Task Una Guida Completa

Architetture Multi-Task Guida Completa

Modelli leggeri per l’Inferenza Multi-Task in Tempo Reale

Foto di Julien Duduoglu su Unsplash

Introduzione

Ti sei mai chiesto come addestrare una rete neurale profonda per fare molte cose? Un tale modello è definito Architettura Multi-Task e può avere vantaggi rispetto a un approccio tradizionale che utilizza modelli individuali per ogni compito. Un’Architettura Multi-Task è un sottoinsieme di Apprendimento Multi-Task che è un approccio generale per addestrare un modello o un insieme di modelli per eseguire contemporaneamente più compiti.

In questo post impareremo come addestrare un singolo modello per eseguire contemporaneamente compiti di classificazione e regressione. Il codice per questo post può essere trovato su GitHub. Ecco una panoramica:

  • Motivazione — Perché dovremmo farlo?
  • Approccio — Come lo faremo?
  • Architettura del Modello
  • Approccio di Addestramento
  • Inferenza — Verificare le prestazioni e imparare da un fallimento interessante
  • Conclusioni

Motivazione

Perché vorremmo usare un modello leggero? Non diminuirà le prestazioni? Se non stiamo distribuendo ai dispositivi edge, non dovremmo utilizzare il modello più grande possibile?

Le applicazioni edge necessitano di modelli leggeri per eseguire inferenze in tempo reale con un basso consumo di energia. Altre applicazioni possono trarne vantaggio, ma come? Un beneficio trascurato dei modelli leggeri è il loro minor requisito di calcolo. In generale, questo può ridurre l’utilizzo del server e quindi diminuire il consumo di energia. Questo ha l’effetto complessivo di ridurre i costi e abbassare le emissioni di carbonio, queste ultime potrebbero diventare un problema importante nel futuro dell’IA.

I modelli leggeri possono aiutare a ridurre i costi e abbassare le emissioni di carbonio attraverso un minor consumo di energia

Detto ciò, un’architettura multi-task è solo uno strumento nel toolbox, e tutti i requisiti del progetto dovrebbero essere presi in considerazione prima di decidere quali strumenti utilizzare. Ora immergiamoci in un esempio su come addestrare uno di questi!

Approccio

Per costruire la nostra Architettura Multi-Task, copriremo in modo generale l’approccio di questo articolo, in cui è stato addestrato un singolo modello per la segmentazione e la stima della profondità simultaneamente. L’obiettivo sottostante era quello di eseguire questi compiti in modo rapido ed efficiente, con un compromesso rappresentato da una perdita accettabile delle prestazioni. Nell’Apprendimento Multi-Task, di solito raggruppiamo compiti simili insieme. Durante l’addestramento, possiamo anche aggiungere un compito ausiliario che potrebbe aiutare l’apprendimento del nostro modello, ma potremmo decidere di non usarlo durante l’inferenza [1, 2]. Per semplicità, non utilizzeremo alcun compito ausiliario durante l’addestramento.

La Profondità e la Segmentazione sono entrambi compiti di predizione densa e presentano somiglianze. Ad esempio, la profondità di un singolo oggetto sarà probabilmente consistente in tutte le aree dell’oggetto, formando una distribuzione molto stretta. L’idea principale è che ogni singolo oggetto debba avere il proprio valore di profondità e dovremmo essere in grado di riconoscere gli oggetti individuali semplicemente guardando una mappa di profondità. Allo stesso modo, dovremmo essere in grado di riconoscere gli stessi oggetti individuali guardando una mappa di segmentazione. Sebbene probabilmente ci saranno alcuni valori anomali, assumiamo che questa relazione sia valida.

Dataset

Utilizzeremo il dataset City Scapes per fornire immagini di input (fotocamera sinistra), maschere di segmentazione e mappe di profondità. Per le mappe di segmentazione, scegliamo di utilizzare le etichette di addestramento standard, con 19 classi + 1 categoria non etichettata.

Preparazione della Mappa di Profondità — disparità predefinita

Le mappe di disparità create con SteroSGBM sono disponibili sul sito web di CityScapes. La disparità descrive la differenza dei pixel tra gli oggetti come visti dalla prospettiva di ciascuna fotocamera stereo ed è inversamente proporzionale alla profondità che può essere calcolata con:

Calcolo della profondità City Scapes, le unità sono tra parentesi. Fonte: Autore.

Tuttavia, le mappe di disparità predefinite sono rumorose con molti buchi corrispondenti a profondità infinita e una porzione in cui il veicolo ego è sempre mostrato. Un approccio comune per pulire queste mappe di disparità prevede:

  1. Ritaglia il 20% inferiore insieme alle parti dei bordi sinistro e superiore
  2. Ridimensiona alla scala originale
  3. Applica un filtro di smoothing
  4. Esegue l’inpainting

Una volta pulita la disparità, possiamo calcolare la profondità, che si traduce in:

Figura 1. Dati di profondità da City Scapes. Fonte Autore.

I dettagli precisi di questo approccio esulano dall’ambito di questo post, ma se sei interessato ecco un video di spiegazione su YouTube.

La fase di ritaglio e ridimensionamento significa che la mappa di disparità (così come la profondità) non si allineerà esattamente con l’immagine di input. Anche se potremmo fare lo stesso ritaglio e ridimensionamento con l’immagine di input per correggere questo problema, abbiamo optato per esplorare un nuovo approccio.

Preparazione della mappa di profondità – Disparità CreStereo

Abbiamo esplorato l’uso di CreStereo per produrre mappe di disparità di alta qualità sia dalle immagini sinistra che destra. CreStereo è un modello avanzato in grado di prevedere mappe di disparità fluide da coppie di immagini stereo. Questo approccio introduce un paradigma noto come distillazione della conoscenza, in cui CreStereo è una rete insegnante e il nostro modello sarà la rete studente (almeno per la stima della profondità). I dettagli di questo approccio esulano dall’ambito di questo post, ma ecco un link YouTube se sei interessato.

In generale, le mappe di profondità di CreStereo hanno un rumore minimo, quindi non c’è bisogno di ritagliare e ridimensionare. Tuttavia, il veicolo ego presente nelle maschere di segmentazione potrebbe causare problemi di generalizzazione, quindi è stato rimosso il 20% inferiore su tutte le immagini di addestramento. Un esempio di addestramento è mostrato di seguito:

Figura 2. Esempio di addestramento. Fonte Autore.

Ora che abbiamo i nostri dati, vediamo l’architettura.

Architettura del modello

Seguendo [1], l’architettura sarà composta da un backbone/encoder MobileNet, un decoder LightWeight RefineNet e Heads per ogni singolo compito. L’architettura complessiva è mostrata di seguito nella figura 3.

Figura 3. Architettura del modello. Fonte.

Per l’encoder/backbone, utilizzeremo un MobileNetV3 e passeremo le connessioni skip alle risoluzioni 1/4, 1/8, 1/16 e 1/32 al Light Weight Refine Net. Infine, l’output viene passato a ciascuna head che è responsabile di un compito diverso. Nota come possiamo anche aggiungere ulteriori compiti a questa architettura se volessimo.

Figura 4. (Sinistra) Architettura dettagliata Encoder-Decoder Multi-Task. (Destra) dettagli dei blocchi LightWeight RefineNet. Modificato da Fonte.

Per implementare l’encoder, utilizziamo un encoder MobileNetV3 pre-addestrato, in cui passeremo l’encoder MobileNetV3 a un modulo PyTorch personalizzato. L’output della sua funzione forward è un ParameterDict di connessioni skip per l’input al LightWeight Refine Net. Il frammento di codice di seguito mostra come fare questo.

class MobileNetV3Backbone(nn.Module):    def __init__(self, backbone):        super().__init__()        self.backbone = backbone        def forward(self, x):        """ Passes input theough MobileNetV3 backbone feature extraction layers            layers to add connections to                - 1:  1/4 res                - 3:  1/8 res                - 7, 8:  1/16 res                - 10, 11: 1/32 res           """        skips = nn.ParameterDict()        for i in range(len(self.backbone) - 1):            x = self.backbone[i](x)            # add skip connection outputs            if i in [1, 3, 7, 8, 10, 11]:                skips.update({f"l{i}_out" : x})        return skips

Il LightWeight RefineNet Decoder è molto simile a quello implementato in [1], tranne che con alcune modifiche per renderlo compatibile con MobileNetV3 anziché MobileNetV2. Notiamo anche che la parte del decoder è composta dalle heads Segmentation e Depth. Il codice completo per il modello è disponibile su GitHub. Possiamo assemblare il modello nel seguente modo:

from torchvision.models import mobilenet_v3_small    mobilenet = mobilenet_v3_small(weights='IMAGENET1K_V1')encoder = MobileNetV3Backbone(mobilenet.features)decoder = LightWeightRefineNet(num_seg_classes)model = MultiTaskNetwork(encoder, freeze_encoder=False).to(device)

Approccio di addestramento

Dividiamo l’addestramento in tre fasi, la prima a risoluzione 1/4, la seconda a risoluzione 1/2 e l’ultima a risoluzione completa. Tutti i pesi sono stati aggiornati, dal momento che congelare i pesi dell’encoder non sembrava produrre buoni risultati.

Trasformazioni

Durante ogni fase, eseguiamo ritagli casuali, jitter del colore, inversioni casuali e normalizzazione. L’immagine di input sinistra viene normalizzata utilizzando la media e la deviazione standard di ImageNet.

Trasformazione della profondità

In generale, le mappe di profondità contengono principalmente valori più piccoli, poiché la maggior parte delle informazioni contenute in una mappa di profondità consiste in oggetti e superfici vicine alla telecamera. Poiché la mappa di profondità ha la maggior parte della sua profondità concentrata intorno a valori più bassi (vedi sinistra della figura 4 qui sotto), sarà necessario trasformarla affinché possa essere appresa efficacemente da una rete neurale. La mappa di profondità viene tagliata tra 0 e 250, ciò è dovuto al fatto che i dati di disparità/profondità stereo a grandi distanze sono tipicamente poco affidabili e in questo caso vogliamo un modo per scartarli. Quindi prendiamo il logaritmo naturale e lo dividiamo per 5 per ridurre la distribuzione intorno a un intervallo più piccolo di numeri. Consulta questo notebook per ulteriori dettagli.

Figura 4. Sinistra - Distribuzione della profondità tagliata. Destra - Distribuzione della profondità trasformata. La profondità è stata campionata da 64 maschere di profondità di addestramento casuali a dimensioni complete. Fonte autore.

Sarò sincero, non ero sicuro del modo migliore per trasformare i dati di profondità. Se c’è un modo migliore o se lo fareste diversamente, sarei interessato a saperne di più nei commenti :).

Funzioni di perdita

Manteniamo le funzioni di perdita semplici, Cross Entropy Loss per la segmentazione e Mean Squared Error per la stima della profondità. Le aggiungiamo insieme senza ponderazione e le ottimizziamo congiuntamente.

Learning Rate

Utilizziamo un learning rate coseno-annealed One Cycle con un valore massimo di 5e-4 e addestriamo per 150 epoche a risoluzione 1/4. Il notebook utilizzato per l’addestramento si trova qui.

Figura 5. Learning Rate coseno-annealed One Cycle. Fonte autore.

Poi ottimizziamo ulteriormente a risoluzione 1/2 per 25 epoche e nuovamente a risoluzione completa per altre 25 epoche, entrambe con un learning rate di 5e-6. Nota che abbiamo dovuto ridurre la dimensione del batch ogni volta che ottimizziamo ulteriormente a una risoluzione maggiore.

Inferenza

Per l’inferenza, normalizziamo l’immagine di input e facciamo passare in avanti il modello. La figura 6 mostra i risultati dell’addestramento sia per i dati di convalida che per i dati di test

Figura 6. Risultati dell'inferenza (i primi 2 sono dal set di test e gli ultimi 2 sono dal set di convalida). Fonte autore.

In generale sembra che il modello sia in grado di segmentare ed stimare la profondità quando ci sono oggetti più grandi in un’immagine. Quando sono presenti oggetti più dettagliati come i pedoni, il modello tende ad avere difficoltà a segmentarli completamente. Il modello è in grado di stimare la loro profondità con un certo grado di precisione.

Un Interessante Fallimento

Il basso della figura 6 mostra un interessante caso di fallimento nel segmentare completamente il lampione sul lato sinistro dell’immagine. La segmentazione copre solo la metà inferiore del lampione, mentre la profondità mostra che la metà inferiore del lampione è molto più vicina rispetto alla metà superiore. Il fallimento della profondità potrebbe essere dovuto al fatto che i pixel inferiori corrispondono generalmente ad una profondità più vicina; notare la linea dell’orizzonte intorno al pixel 500, esiste una chiara divisione tra i pixel più vicini e quelli più lontani. Sembra che questo tipo di distorsione potrebbe essere trapelata nel compito di segmentazione del modello. Questo tipo di distorsione dovrebbe essere preso in considerazione durante l’addestramento di modelli multi-task.

Nell’apprendimento multi-task, i dati di addestramento di un compito possono influenzare le prestazioni su un altro compito

Distribuzioni di Profondità

Vediamo come la profondità predetta è distribuita rispetto alla verità. Per semplicità, utilizzeremo solo un campione di 94 coppie di mappe di profondità vere/predette a piena risoluzione.

Figura 8. Distribuzioni delle mappe di profondità vere (sinistra) e predette (destra), ognuna con 1000 bin. Fonte: Autore.

Sembra che il modello abbia imparato due distribuzioni, una distribuzione con un picco intorno a 4 e una distribuzione con un picco intorno a 30. Notare che l’artefatto di clipping non sembra fare alcuna differenza. La distribuzione complessiva contiene una coda lunga che è caratteristica del fatto che solo una piccola parte di un’immagine conterrà dati di profondità lontani.

La distribuzione di profondità predetta è molto più uniforme rispetto alla verità. La rugosità della distribuzione di verità potrebbe derivare dal fatto che ogni oggetto contiene valori di profondità simili. Potrebbe essere possibile utilizzare queste informazioni per applicare una sorta di regolarizzazione per obbligare il modello a seguire questo paradigma, ma questo sarà per un’altra volta.

Bonus: Velocità di Inferenza

Dato che questo è un modello leggero progettato per la velocità, vediamo quanto velocemente può eseguire inferenze sulla GPU. Il codice sottostante è stato modificato da questo articolo. In questo test, l’immagine di input è stata ridimensionata a 400×1024.

# trova il backend ottimale per eseguire le convoluzionitorch.backends.cudnn.benchmark = True# ridimensiona a metà dimensionirescaled_sample = Rescale(400, 1024)(sample)rescaled_left = rescaled_sample['left'].to(DEVICE)# INIZIALIZZA I REGISTRATORIstarter, ender = torch.cuda.Event(enable_timing=True), torch.cuda.Event(enable_timing=True)repetitions = 300timings = np.zeros((repetitions,1))# AVVIO GPU-WARM-UPfor _ in range(10):    _, _ = model(rescaled_left.unsqueeze(0))# MISURA LA PERFORMANCEwith torch.no_grad():    for rep in range(repetitions):        starter.record()        _, _ = model(rescaled_left.unsqueeze(0))        ender.record()        # ATTENDI LA SINCRONIZZAZIONE DELLA GPU        torch.cuda.synchronize()        curr_time = starter.elapsed_time(ender)        timings[rep] = curr_timemean_syn = np.sum(timings) / repetitionsstd_syn = np.std(timings)print(mean_syn, std_syn)

Il test di inferenza mostra che questo modello può eseguire a una velocità di 18,69 +/- 0,44 ms, ovvero circa 55Hz. È importante notare che si tratta solo di un prototipo Python eseguito su un laptop con una GPU NVIDIA RTX 3060, l’hardware diverso influenzerà la velocità di inferenza. Dovremmo anche notare che un SDK come Torch-TensorRt potrebbe fornire un significativo aumento di velocità se implementato su una GPU NVIDIA.

Conclusioni

In questo post abbiamo appreso come l’apprendimento multi-task può ridurre i costi e le emissioni di carbonio. Abbiamo imparato come costruire un’architettura multi-task leggera in grado di eseguire contemporaneamente la classificazione e la regressione sul dataset CityScapes. Abbiamo anche sfruttato CreStereo e Knowledge Distillation per aiutare il nostro modello ad imparare a prevedere migliori mappe di profondità.

Questo modello leggero rappresenta un compromesso in cui sacrificiamo alcune prestazioni per la velocità ed efficienza. Anche con questo compromesso, il modello addestrato è stato in grado di prevedere risultati ragionevoli di profondità e segmentazione sui dati di test. Inoltre, è stato in grado di imparare a prevedere una distribuzione di profondità simile alle mappe di profondità di verità.

Riferimenti

[1] Nekrasov, Vladimir, et al. ‘Real-Time Joint Semantic Segmentation and Depth Estimation Using Asymmetric Annotations’. CoRR, vol. abs/1809.04766, 2018, http://arxiv.org/abs/1809.04766

[2] Standley, Trevor, et al. ‘Quali compiti dovrebbero essere appresi insieme nell’apprendimento multitask?’ CoRR, vol. abs/1905.07553, 2019, http://arxiv.org/abs/1905.07553

[3] Cordts, M., Omran, M., Ramos, S., Rehfeld, T., Enzweiler, M., Benenson, R., Franke, U., Roth, S., & Schiele, B. (2016). Il dataset cityscapes per la comprensione semantica delle scene urbane. Conferenza IEEE sul riconoscimento dei modelli e della visione artificiale (CVPR) del 2016. https://doi.org/10.1109/cvpr.2016.350