Come ottimizzare i modelli di rilevamento degli oggetti per domini specifici

Come migliorare i modelli di rilevamento oggetti per settori specifici

Progettare modelli migliori e più veloci per risolvere il tuo problema specifico

Foto di paolo candelo su Unsplash

La rilevazione degli oggetti è ampiamente utilizzata in diversi settori, dall’accademico a quello industriale, grazie alla sua capacità di fornire ottimi risultati a basso costo computazionale. Tuttavia, nonostante l’abbondanza di architetture open-source disponibili pubblicamente, la maggior parte di questi modelli è progettata per affrontare problemi generici e potrebbe non essere adatta a contesti specifici.

A titolo di esempio, possiamo menzionare il dataset Common Objects in Context (COCO), che viene generalmente utilizzato come punto di riferimento per la ricerca in questo campo, influenzando gli iperparametri e i dettagli architettonici dei modelli. Questo dataset comprende 90 classi distinte in diverse condizioni di illuminazione, sfondi e dimensioni. Si scopre che, talvolta, il problema di rilevamento che stai affrontando è relativamente semplice. Potresti voler rilevare solo alcuni oggetti distinti senza molte variazioni di scena o dimensioni. In questo caso, se addestri il tuo modello utilizzando un insieme generico di iperparametri, probabilmente otterrai un modello che comporta costi computazionali superflui.

Con questa prospettiva in mente, l’obiettivo principale di questo articolo è fornire indicazioni per ottimizzare vari modelli di rilevamento degli oggetti per compiti meno complessi. Voglio aiutarti a selezionare una configurazione più efficiente che riduca i costi computazionali senza pregiudicare la mean Average Precision (mAP).

Fornire un pò di contesto

Uno degli obiettivi del mio master era sviluppare un sistema di riconoscimento della lingua dei segni con requisiti computazionali minimi. Un componente cruciale di questo sistema è la fase di preprocessing, che comprende la rilevazione delle mani e del viso dell’interprete, come raffigurato nella figura qui sotto:

Campioni dal dataset HFSL che sono stati creati in questo lavoro. Immagine dell'autore.

Come illustrato, questo problema è relativamente semplice, coinvolgendo solo due classi distinte e tre oggetti che appaiono contemporaneamente nell’immagine. Per questo motivo, il mio obiettivo era ottimizzare gli iperparametri dei modelli per mantenere un’alta mAP riducendo al contempo il costo computazionale, consentendo così un’esecuzione efficiente su dispositivi edge come gli smartphone.

Architetture e configurazione del rilevamento degli oggetti

In questo progetto sono state testate le seguenti architetture di rilevamento degli oggetti: EfficientDetD0, Faster-RCNN, SDD320, SDD640 e YoloV7. Tuttavia, i concetti presentati qui possono essere applicati per adattare varie altre architetture.

Per lo sviluppo del modello, ho utilizzato principalmente Python 3.8 e il framework TensorFlow, ad eccezione di YoloV7, dove è stato impiegato PyTorch. Sebbene la maggior parte degli esempi forniti qui si riferiscano a TensorFlow, puoi adattare questi principi al tuo framework preferito.

In termini di hardware, i test sono stati condotti utilizzando una GPU RTX 3060 e un processore Intel Core i5–10400. Tutto il codice sorgente e i modelli sono disponibili su GitHub.

Ottimizzazione degli rilevatori degli oggetti

Quando si utilizza TensorFlow per il rilevamento degli oggetti, è essenziale capire che tutti gli iperparametri sono memorizzati in un file chiamato “pipeline.config”. Questo file protobuf contiene le configurazioni utilizzate per addestrare e valutare il modello e lo troverai in qualsiasi modello pre-addestrato scaricato da TF Model Zoo, ad esempio. In questo contesto, descriverò le modifiche che ho apportato ai file di pipeline per ottimizzare i rilevatori degli oggetti.

È importante notare che gli iperparametri forniti qui sono stati specificamente progettati per la rilevazione di mani e volti (2 classi, 3 oggetti). Assicurati di adattarli al tuo stesso dominio di problemi.

Semplificazioni generali

La prima modifica che si può applicare a tutti i modelli consiste nella riduzione del numero massimo di previsioni per classe e del numero di bounding box generate, da 100 a 2 e 4 rispettivamente. È possibile ottenere ciò regolando la proprietà “max_number_of_boxes” all’interno dell’oggetto “train_config“:

...train_config {  batch_size: 128  sync_replicas: true  optimizer { ... }  fine_tune_checkpoint: "PERCORSO_DA_CONFIGURARE"  num_steps: 50000  startup_delay_steps: 0.0  replicas_to_aggregate: 8  max_number_of_boxes: 4 # <----------------- cambia questa riga  unpad_groundtruth_tensors: false  fine_tune_checkpoint_type: "classification"  fine_tune_checkpoint_version: V2}...

Dopo aver fatto ciò, cambia “max_total_detections” e “max_detections_per_class” che si trovano all’interno di “post_processing”, il rilevatore di oggetti:

post_processing {  batch_non_max_suppression {    score_threshold: 9.99999993922529e-09    iou_threshold: 0.6000000238418579    max_detections_per_class: 2 # <------------------ cambia questa riga    max_total_detections: 4    # <------------------ cambia questa riga    use_static_shapes: false  }  score_converter: SIGMOID}

Questi cambiamenti sono importanti, soprattutto nel mio caso, poiché ci sono solo tre oggetti e due classi che appaiono contemporaneamente nell’immagine. Riducendo il numero di previsioni, sono necessarie meno iterazioni per eliminare le bounding box sovrapposte attraverso la Non-Maximum Suppression (NMS). Pertanto, se hai un numero limitato di classi da rilevare e oggetti che compaiono nella scena, potrebbe essere una buona idea modificare questo iperparametro.

Sono stati applicati anche aggiustamenti individuali, tenendo conto dei dettagli architettonici specifici di ciascun modello di rilevazione degli oggetti.

Single Shot Multibox Detector (SSD)

È sempre una buona idea testare diverse risoluzioni quando si lavora con la rilevazione degli oggetti. In questo progetto, ho utilizzato due versioni del modello, SSD320 e SSD640, con risoluzioni dell’immagine di input di 320×320 e 640×640 pixel rispettivamente.

Per entrambi i modelli, una delle principali modifiche è stata quella di ridurre la profondità della Feature Pyramid Network (FPN) da 5 a 4 rimuovendo il livello più superficiale. FPN è un meccanismo potente di estrazione delle caratteristiche che opera su più dimensioni delle mappe delle caratteristiche. Tuttavia, per oggetti più grandi, il livello più superficiale, progettato per risoluzioni dell’immagine più alte, potrebbe non essere necessario. Detto ciò, se gli oggetti che stai cercando di rilevare non sono troppo piccoli, probabilmente è una buona idea rimuovere questo livello. Per implementare questa modifica, regola l’attributo “min_level” da 3 a 4 all’interno dell’oggetto “fpn“:

...feature_extractor {  type: "ssd_mobilenet_v2_fpn_keras"  depth_multiplier: 1.0  min_depth: 16  conv_hyperparams {    regularizer { ... }    initializer { ... }    activation: RELU_6    batch_norm {...}  }  use_depthwise: true  override_base_feature_extractor_hyperparams: true  fpn {    min_level: 4 # <------------------ cambia questa riga    max_level: 7    additional_layer_depth: 108 # <------------------ cambia questa riga  }}...

Ho semplificato anche il modello ad alta risoluzione (SSD640) riducendo la “additional_layer_depth” da 128 a 108. Allo stesso modo, ho regolato la profondità del “multiscale_anchor_generator” da 5 a 4 strati per entrambi i modelli, come mostrato di seguito:

...anchor_generator {  multiscale_anchor_generator {    min_level: 4 # <------------------ cambia questa riga    max_level: 7    anchor_scale: 4.0    aspect_ratios: 1.0    aspect_ratios: 2.0    aspect_ratios: 0.5    scales_per_octave: 2  }}...

Finalmente, la rete responsabile della generazione delle previsioni dei bounding box (“box_predictor“) ha ridotto il numero di livelli da 4 a 3. Per quanto riguarda SSD640, la profondità del box predictor è stata ridotta anche da 128 a 96, come mostrato di seguito:

...box_predictor {  weight_shared_convolutional_box_predictor {    conv_hyperparams {      regularizer { ... }      initializer { ... }      activation: RELU_6      batch_norm { ... }    }    depth: 96 # <------------------ cambia questa riga    num_layers_before_predictor: 3 # <------------------ cambia questa riga    kernel_size: 3    class_prediction_bias_init: -4.599999904632568    share_prediction_tower: true    use_depthwise: true  }}...

Queste semplificazioni sono state determinate dal fatto che abbiamo un numero limitato di classi distinte con modelli relativamente semplici da rilevare. Pertanto, è possibile ridurre il numero di livelli e la profondità del modello, poiché anche con meno mappe delle caratteristiche possiamo comunque estrarre efficacemente le caratteristiche desiderate dalle immagini.

EfficientDet-D0

Riguardo a EfficientDet-D0, ho ridotto la profondità del Bidirectional Feature Pyramid Network (Bi-FPN) da 5 a 4. Inoltre, ho diminuito le iterazioni del Bi-FPN da 3 a 2 e le dimensioni del kernel della mappa delle caratteristiche da 64 a 48. Il Bi-FPN è una sofisticata tecnica di fusione delle caratteristiche a multi-scala che può produrre eccellenti risultati. Tuttavia, comporta richieste computazionali più elevate, che possono essere uno spreco di risorse per problemi più semplici. Per implementare gli aggiustamenti sopra citati, semplicemente aggiorna gli attributi dell’oggetto “bifpn” come segue:

...bifpn {      min_level: 4 # <------------------ cambia questa riga      max_level: 7      num_iterations: 2 # <------------------ cambia questa riga      numyaml_filters: 48 # <------------------ cambia questa riga    }...

Inoltre, è importante ridurre anche la profondità del “multiscale_anchor_generator” allo stesso modo in cui abbiamo fatto con SSD. Infine, ho ridotto i livelli della rete del box predictor da 3 a 2:

...box_predictor {  weight_shared_convolutional_box_predictor {    conv_hyperparams {      regularizer { ... }      initializer { ... }      activation: SWISH      batch_norm { ... }      force_use_bias: true    }    depth: 64    num_layers_before_predictor: 2 # <------------------ cambia questa riga    kernel_size: 3    class_prediction_bias_init: -4.599999904632568    use_depthwise: true  }}...

Faster R-CNN

Il modello Faster R-CNN si basa sulla Region Proposal Network (RPN) e sugli anchor box come tecniche principali. Gli anchor sono il punto centrale di una finestra scorrevole che itera sull’ultima mappa delle caratteristiche del backbone CNN. Per ogni iterazione, un classificatore determina la probabilità di una proposta che contiene un oggetto, mentre un regressore regola le coordinate del bounding box. Per garantire che il detector sia invariante rispetto alla traslazione, utilizza tre diverse scale e tre diversi rapporti di aspetto per gli anchor box, il che aumenta il numero di proposte per iterazione.

Anche se questa è una spiegazione sommaria, è evidente che questo modello è notevolmente più complesso degli altri a causa del suo processo di rilevazione in due fasi. Tuttavia, è possibile semplificarlo e migliorarne la velocità mantenendo un’alta precisione.

Per farlo, la prima importante modifica consiste nel ridurre il numero di proposte generate da 300 a 50. Questa riduzione è fattibile perché sono presenti contemporaneamente solo pochi oggetti nell’immagine. Puoi implementare questa modifica regolando la proprietà “first_stage_max_proposals”, come mostrato di seguito:

...first_stage_box_predictor_conv_hyperparams {  op: CONV  regularizer { ... }  initializer { ... }}first_stage_nms_score_threshold: 0.0first_stage_nms_iou_threshold: 0.7first_stage_max_proposals: 50 # <------------------ cambia questa rigafirst_stage_localization_loss_weight: 2.0first_stage_objectness_loss_weight: 1.0initial_crop_size: 14maxpool_kernel_size: 2maxpool_stride: 2...

Dopo averlo fatto, ho eliminato la scala della casella di ancoraggio più grande (2.0) dal modello. Questo cambiamento è stato effettuato perché le mani e il viso mantengono una dimensione costante a causa della distanza fissa dall’interprete alla telecamera, e avere caselle di ancoraggio grandi potrebbe non essere utile per la generazione di proposte. Inoltre, ho rimosso uno dei rapporti di aspetto delle caselle di ancoraggio, dato che gli oggetti hanno forme simili con variazioni minime nel dataset. Questi adattamenti sono visualizzati visivamente di seguito:

generatore_ancore_primo_stadio {  generatore_ancore_griglia {    scale: [0.25, 0.5, 1.0] # <------------------ cambia questa riga    rapporti_aspetto: [0.5, 1.0] # <------------------ cambia questa riga    distanza_altezza: 16    distanza_larghezza: 16  }}

Detto ciò, è fondamentale considerare la dimensione e i rapporti di aspetto dei tuoi oggetti target. Questa considerazione ti permette di eliminare caselle di ancoraggio meno utili e ridurre significativamente il costo computazionale del modello.

YoloV7

Al contrario, sono state apportate poche modifiche a YoloV7 per preservare la funzionalità dell’architettura. La principale modifica ha coinvolto la semplificazione del CNN responsabile dell’estrazione delle caratteristiche, sia nel backbone che nella testa del modello. Per ottenere ciò, ho diminuito il numero di kernel/mappe di caratteristiche per quasi ogni layer convoluzionale, creando il modello seguente:

backbone:  # [da, numero, modulo, argomenti]  [[-1, 1, Conv, [22, 3, 1]],  # 0   [-1, 1, Conv, [44, 3, 2]],  # 1-P1/2         [-1, 1, Conv, [44, 3, 1]],   [-1, 1, Conv, [89, 3, 2]],  # 3-P2/4     [-1, 1, Conv, [44, 1, 1]],   [-2, 1, Conv, [44, 1, 1]],   [-1, 1, Conv, [44, 3, 1]],   [-1, 1, Conv, [44, 3, 1]],   [-1, 1, Conv, [44, 3, 1]],   [-1, 1, Conv, [44, 3, 1]],   [[-1, -3, -5, -6], 1, Concat, [1]],   [-1, 1, Conv, [179, 1, 1]],  # 11   [-1, 1, MP, []],   [-1, 1, Conv, [89, 1, 1]],   [-3, 1, Conv, [89, 1, 1]],   [-1, 1, Conv, [89, 3, 2]],   [[-1, -3], 1, Concat, [1]],  # 16-P3/8     [-1, 1, Conv, [89, 1, 1]],   [-2, 1, Conv, [89, 1, 1]],   [-1, 1, Conv, [89, 3, 1]],   [-1, 1, Conv, [89, 3, 1]],   [-1, 1, Conv, [89, 3, 1]],   [-1, 1, Conv, [89, 3, 1]],   [[-1, -3, -5, -6], 1, Concat, [1]],   [-1, 1, Conv, [512, 1, 1]],  # 24   [-1, 1, MP, []],   [-1, 1, Conv, [89, 1, 1]],   [-3, 1, Conv, [89, 1, 1]],   [-1, 1, Conv, [89, 3, 2]],   [[-1, -3], 1, Concat, [1]],  # 29-P4/16     [-1, 1, Conv, [89, 1, 1]],   [-2, 1, Conv, [89, 1, 1]],   [-1, 1, Conv, [89, 3, 1]],   [-1, 1, Conv, [89, 3, 1]],   [-1, 1, Conv, [89, 3, 1]],   [-1, 1, Conv, [89, 3, 1]],   [[-1, -3, -5, -6], 1, Concat, [1]],   [-1, 1, Conv, [716, 1, 1]],  # 37   [-1, 1, MP, []],   [-1, 1, Conv, [256, 1, 1]],   [-3, 1, Conv, [256, 1, 1]],   [-1, 1, Conv, [256, 3, 2]],   [[-1, -3], 1, Concat, [1]],  # 42-P5/32     [-1, 1, Conv, [128, 1, 1]],   [-2, 1, Conv, [128, 1, 1]],   [-1, 1, Conv, [128, 3, 1]],   [-1, 1, Conv, [128, 3, 1]],   [-1, 1, Conv, [128, 3, 1]],   [-1, 1, Conv, [128, 3, 1]],   [[-1, -3, -5, -6], 1, Concat, [1]],   [-1, 1, Conv, [716, 1, 1]],  # 50  ]# testa yolov7testa:  [[-1, 1, SPPCSPC, [358]], # 51   [-1, 1, Conv, [179, 1, 1]],   [-1, 1, nn.Upsample, [None, 2, 'nearest']],   [37, 1, Conv, [179, 1, 1]], # route backbone P4   [[-1, -2], 1, Concat, [1]],   [-1, 1, Conv, [179, 1, 1]],   [-2, 1, Conv, [179, 1, 1]],   [-1, 1, Conv, [89, 3, 1]],   [-1, 1, Conv, [89, 3, 1]],   [-1, 1, Conv, [89, 3, 1]],   [-1, 1, Conv, [89, 3, 1]],   [[-1, -2, -3, -4, -5

Come discusso in precedenza, rimuovere alcuni livelli e mappe delle caratteristiche dai rilevatori è tipicamente un approccio valido per problemi più semplici, poiché gli estrattori di caratteristiche sono inizialmente progettati per rilevare decine o addirittura centinaia di classi in scenari diversi, richiedendo un modello più robusto per affrontare queste complessità e garantire una grande precisione.

Con queste modifiche, ho ridotto il numero di parametri da 36,4 milioni a soli 14,1 milioni, rappresentando una riduzione di circa il 61%. Inoltre, ho utilizzato una risoluzione di input di 512x512 pixel invece dei suggeriti 640x640 pixel nell'articolo originale.

Consiglio aggiuntivo

Un altro prezioso suggerimento nell'addestramento dei rilevatori di oggetti è utilizzare il modello Kmeans per l'adattamento non supervisionato delle proporzioni delle caselle di ancoraggio, adattando la larghezza e l'altezza delle figure per massimizzare il rapporto di intersezione su unione (IoU) all'interno del set di addestramento. Facendo ciò, possiamo adattare meglio gli ancoraggi al dominio del problema dato, migliorando così la convergenza del modello partendo da rapporti aspetto adeguati. La figura sottostante esemplifica questo processo, confrontando tre caselle di ancoraggio utilizzate di default nell'algoritmo SSD (in rosso) accanto a tre caselle con proporzioni ottimizzate per il riconoscimento delle mani e del viso (in verde).

Confronto tra diverse proporzioni delle caselle di ancoraggio. Immagine dell'autore.

Visualizzazione dei risultati

Ho addestrato e valutato ciascun rilevatore utilizzando il mio dataset chiamato Language dei segni delle mani e del viso (HFSL), prendendo in considerazione la mAP (mean Average Precision) e i frame al secondo (FPS) come metriche principali. La tabella sottostante fornisce un riepilogo dei risultati, con i valori tra parentesi che rappresentano i FPS del rilevatore prima di implementare alcuna delle ottimizzazioni descritte.

Risultati del rilevamento degli oggetti.

Possiamo osservare che la maggior parte dei modelli ha mostrato una significativa riduzione del tempo di inferenza mantenendo un'alta mAP su vari livelli di Intersezione su Unione (IoU). Architetture più complesse, come Faster R-CNN ed EfficientDet, hanno aumentato l'FPS sulla GPU rispettivamente del 200,80% e del 231,78%. Anche le architetture basate su SSD hanno mostrato un enorme miglioramento delle performance, con miglioramenti del 280,23% e del 159,59% per le versioni a 640 e 320 rispettivamente. Considerando YoloV7, sebbene la differenza di FPS sia più evidente sulla CPU, il modello ottimizzato ha il 61% dei parametri in meno, riducendo i requisiti di memoria e rendendolo più adatto per dispositivi edge.

Conclusioni

Ci sono casi in cui le risorse di calcolo sono limitate o le attività devono essere eseguite rapidamente. In tali scenari, possiamo ottimizzare ulteriormente i modelli di rilevamento di oggetti open-source per trovare una combinazione di iperparametri che possa ridurre i requisiti di calcolo senza influire sui risultati, offrendo così una soluzione adatta a diversi domini di problema.

Spero che questo articolo ti abbia aiutato a prendere decisioni migliori per addestrare i tuoi rilevatori di oggetti, ottenendo significativi guadagni di efficienza con uno sforzo minimo. Se alcuni dei concetti spiegati non sono stati chiari, ti consiglio di approfondire il funzionamento dell'architettura del tuo rilevatore di oggetti. Inoltre, considera di sperimentare con diversi valori di iperparametri per ottimizzare ulteriormente i tuoi modelli in base al problema specifico che stai affrontando!