
Przed Tobą znowu zadanie detekcji obiektów. Priorytetem jest szybkość działania przy akceptowalnej precyzji. Użyj architektury YOLOv3 i dokonaj dalszego treningu. Precyzja (mAp75) powinna wynosić ponad 0,95. Jednak prędkość działania nadal jest zbyt niska. Cholera.
Dziś pominiemy kwantyzację. A w rozwinięciu przyjrzymy się Model Pruning — przycinanie zbędnych części sieci w celu przyspieszenia działania bez utraty precyzji. Przykładowo — skąd, ile i jak można usunąć. Zbadamy, jak zrobić to ręcznie i gdzie można to zautomatyzować. Na końcu — repozytorium na keras.
Wprowadzenie
W poprzedniej pracy, w permiskim Macroscopie, nabrałem nawyku — zawsze śledzić czas działania algorytmów. A czas działania sieci zawsze sprawdzać przez filtr adekwatności. Zazwyczaj state-of-the-art w produkcji nie przechodzi tego filtr, co doprowadziło mnie do Pruning.
Pruning — temat stary, o którym mówiono na w 2017 roku. Główna idea — zmniejszenie rozmiaru wytrenowanej sieci bez utraty precyzji poprzez usunięcie różnych węzłów. Brzmi świetnie, ale rzadko słyszę o jego zastosowaniach. Może brakuje implementacji, nie ma artykułów w języku rosyjskim, albo po prostu wszyscy uważają pruning za tajemnicę i milczą.
Ale przejdźmy do rozbioru.
Zerknijmy na biologię.
Lubię, gdy w Deep Learning zaglądają pomysły, które przyszły z biologii. Można im zaufać, tak jak ewolucji (a czy wiesz, że ReLU jest bardzo podobna do ?)
Proces Model Pruning jest również bliski biologii. Reakcję sieci można porównać do plastyczności mózgu. Kilka ciekawych przykładów można znaleźć w książce :
- Mózg kobiety, która od urodzenia miała tylko jedną półkulę, sam przearanżował się, by pełnić funkcje brakującej półkuli.
- Chłopak odgryzł sobie część mózgu, odpowiadającą za wzrok. Z czasem inne części mózgu przejęły te funkcje. (nie próbujemy tego powtarzać)
Tak i z Twojego modelu można usunąć część słabych splotów. W ostateczności pozostałe sploty pomogą zastąpić usunięte.
Lubisz Transfer Learning czy uczysz od podstaw?
Opcja numer jeden. Używasz Transfer Learning z Yolov3, Retina, Mask-RCNN lub U-Net. Jednak najczęściej nie musimy rozpoznawać 80 klas obiektów, jak w COCO. W mojej praktyce wszystko ogranicza się do 1-2 klas. Można założyć, że architektura dla 80 klas jest w tym przypadku nadmierna. Nasuwa się myśl, że architekturę trzeba zmniejszyć. Chciałbym to zrobić bez utraty istniejących wcześniej wyuczonych wag.
Opcja numer dwa. Może masz dużo danych i zasobów obliczeniowych lub po prostu potrzebujesz super dostosowanej architektury. To nie ma znaczenia. Ale uczysz sieć od podstaw. Zwykły porządek — patrzymy na strukturę danych, dobieramy NADMIERNIE mocną architekturę i wprowadzamy drop-outy, aby zapobiec przeuczeniu. Widziałem drop-outy na poziomie 0.6, Karl.
W obu przypadkach sieć można zmniejszyć. Zostało to udowodnione. Teraz przejdźmy do wyjaśnienia, co to jest odcinanie pruning.
Ogólny algorytm
Postanowiliśmy, że możemy usunąć niektóre konwolucje. Wygląda to bardzo prosto:

Usunięcie dowolnej konwolucji to stres dla sieci, który zwykle prowadzi do pewnego wzrostu błędu. Z jednej strony, ten wzrost błędu jest wskaźnikiem tego, jak dobrze usuwamy konwolucje (na przykład, duży wzrost mówi, że coś robimy źle). Ale niewielki wzrost jest jak najbardziej dopuszczalny i często usuwa się go dzięki lekkiej dalszej nauce z małym LR. Dodajemy krok dalszej nauki:

Teraz musimy zrozumieć, kiedy chcemy zatrzymać nasz cykl Uczenia<->Odcinania. Możliwe są egzotyczne warianty, gdy musimy zmniejszyć sieć do określonego rozmiaru i szybkości działania (na przykład dla urządzeń mobilnych). Jednak najczęściej jest to kontynuacja cyklu, dopóki błąd nie wzrośnie powyżej dopuszczalnego poziomu. Dodajemy warunek:

Tak więc algorytm staje się jasny. Pozostaje nam rozwiązać, jak określić usuwane konwolucje.
Wyszukiwanie usuwanych konwolucji
Musimy usunąć niektóre konwolucje. Rwać się na oślep i »strzelać« do wszelkich - to zła idea, chociaż może działać. Ale skoro mamy głowę, można pomyśleć i spróbować wyróżnić do usunięcia „słabe” konwolucje. Opcji jest kilka:
- . Idea głosi, że konwolucje o małych wartościach wag wnoszą niewielki wkład w ostateczne podejmowanie decyzji.
- Najmniejszy wskaźnik L1 z uwzględnieniem średniej i odchylenia standardowego. Uzupełniamy oceną charakteru rozkładu.
- . Bardziej precyzyjna identyfikacja mało znaczących konwolucji, ale bardzo czasochłonna i kosztowna w zasoby.
- Inne
Każda z opcji ma prawo do życia i swoje cechy realizacji. Tutaj omówimy wariant z najmniejszą miarą L1
Ręczny proces dla YOLOv3
W pierwotnej architekturze znajdują się bloki resztkowe. Jednak niezależnie od tego, jak świetne są dla głębokich sieci, mogą nam w pewnym stopniu przeszkadzać. Problematyczne jest to, że nie można usuwać konwolucji z różnymi indeksami w tych warstwach:

Dlatego wyodrębniamy warstwy, z których możemy swobodnie usuwać konwolucje:

Teraz zbudujmy cykl pracy:
- Zrzucamy aktywacje
- Szacujemy, ile wyciąć
- Wycinamy
- Uczymy przez 10 epok z LR=1e-4
- Testowanie
Zrzucanie konwolucji jest przydatne, aby ocenić, jaką część możemy usunąć na danym etapie. Przykłady zrzutów:

Widzimy, że praktycznie wszędzie 5% konwolucji ma bardzo niską normę L1 i możemy je usunąć. Na każdym etapie taki zrzut powtarzano i dokonywano oceny, z jakich warstw i ile można wyciąć.
Cały proces zmieścił się w 4 krokach (tu i wszędzie liczby odnoszą się do RTX 2060 Super):
| Krok | mAp75 | Liczba parametrów, mln | Rozmiar sieci, mb | Od pierwotnego, % | Czas przetwarzania, ms | Warunek cięcia |
|---|---|---|---|---|---|---|
| 0 | 0.9656 | 60 | 241 | 100 | 180 | — |
| 1 | 0.9622 | 55 | 218 | 91 | 175 | 5% wszystkich |
| 2 | 0.9625 | 50 | 197 | 83 | 168 | 5% wszystkich |
| 3 | 0.9633 | 39 | 155 | 64 | 155 | 15% dla warstw z 400+ konwolucjami |
| 4 | 0.9555 | 31 | 124 | 51 | 146 | 10% dla warstw z 100+ konwolucjami |
W 2 kroku pojawił się jeden pozytywny efekt — w pamięci zmieścił się rozmiar batcha 4, co znacząco przyspieszyło proces dalszego uczenia.
W 4 kroku proces został zatrzymany, ponieważ nawet długotrwałe dalsze uczenie nie podniosło mAp75 do dawnych wartości.
Ostatecznie udało się przyspieszyć inference o 15%, zmniejszyć rozmiar o 35% i nie stracić dokładności.
Automatyzacja dla prostszych architektur
Dla prostszych architektur sieci (bez warunkowych bloków add, concatenate i residual) można spokojnie kierować się przetwarzaniem wszystkich warstw konwolucyjnych i zautomatyzować proces wycinania konwolucji.
Taki wariant zaimplementowałem .
Jest to proste: potrzebujesz tylko funkcji strat, optymalizatora i generatorów batchy:
import pruning
from keras.optimizers import Adam
from keras.utils import Sequence
train_batch_generator = BatchGenerator...
score_batch_generator = BatchGenerator...
opt = Adam(lr=1e-4)
pruner = pruning.Pruner("config.json", "categorical_crossentropy", opt)
pruner.prune(train_batch, valid_batch)W razie potrzeby można zmienić parametry konfiguracji:
{
"input_model_path": "model.h5",
"output_model_path": "model_pruned.h5",
"finetuning_epochs": 10, # liczba epok do treningu między krokami przycinania
"stop_loss": 0.1, # strata do zatrzymania procesu
"pruning_percent_step": 0.05, # część konwolucji do usunięcia na każdym kroku przycinania
"pruning_standart_deviation_part": 0.2 # przesunięcie do ograniczenia części przycinania
}Dodatkowo wprowadzono ograniczenie na podstawie odchylenia standardowego. Celem jest ograniczenie liczby usuwanych, wykluczając konwolucje o już 'wystarczających' miarach L1:

W ten sposób pozwalamy usunąć tylko słabe konwolucje z rozkładów podobnych do prawego i nie wpływać na usuwanie z rozkładów podobnych do lewego:

W miarę zbliżania się rozkładu do normalnego, współczynnik pruning_standart_deviation_part można dostosować do:

Rekomenduję tolerancję na poziomie 2 sigm. Można również nie kierować się tą cechą, pozostawiając wartość < 1.0.
Na wyjściu otrzymujemy wykres rozmiaru sieci, strat i czasu przetwarzania sieci przez cały test, znormalizowany do 1.0. Na przykład tutaj rozmiar sieci został zmniejszony prawie dwukrotnie bez utraty jakości (mała sieć konwolucyjna z 100k wagami):

Czas przetwarzania podlega normalnym fluktuacjom i praktycznie się nie zmienił. Istnieje na to wyjaśnienie:
- Liczba konwolucji zmienia się z wygodnych (32, 64, 128) na mniej wygodne dla kart graficznych — 27, 51 itd. Mogę się mylić, ale prawdopodobnie ma to wpływ.
- Architektura nie jest szeroka, ale sekwencyjna. Zmniejszając szerokość, nie ingerujemy w głębokość. W ten sposób zmniejszamy obciążenie, ale nie zmieniamy szybkości.
Dlatego poprawa przejawia się w zmniejszeniu obciążenia CUDA podczas przetwarzania o 20-30%, ale nie w zmniejszeniu czasu przetwarzania.
Podsumowanie
Pochylmy się nad tym. Rozważaliśmy 2 warianty przycinania — dla YOLOv3 (kiedy trzeba działać ręcznie) i dla sieci z prostszymi architekturami. Widać, że w obu przypadkach można osiągnąć zmniejszenie rozmiaru sieci i przyspieszenie bez utraty dokładności. Wyniki:
- Zmniejszenie rozmiaru
- Przyspieszenie przetwarzania
- Zmniejszenie obciążenia CUDA
- W konsekwencji, ekologiczność (Optymalizujemy przyszłe wykorzystanie zasobów obliczeniowych. Gdzieś cieszy się jedna )
Aneks
- Po kroku przycinania można również przeprowadzić kwantyzację (na przykład z TensorRT)
- Tensorflow oferuje możliwości dla . Działa.
- chcę rozwijać i będę wdzięczny za pomoc
Źródło: habr.com
