CNN: Sieci splotowe od zera do produkcji
Jak komputer „widzi” kota na zdjęciu? Jak odróżnić znak drogowy od ludzkiej twarzy? Odpowiedź kryje się w Konwolucyjne sieci neuronowe (CNN), architektura głębokiego uczenia się który zrewolucjonizował widzenie komputerowe. Od samochodów autonomicznych po diagnostykę obrazową medyczną, Sieci CNN to niewidzialny silnik stojący za milionami aplikacji, z których korzystamy na co dzień.
W pierwszym artykule z tej serii Widzenie komputerowe z głębokim uczeniem się, zbudujemy Twoje zrozumienie CNN od podstaw: czym są, jak działają filtry splotowe, które architektury przeszły do historii oraz jak wdrożyć i wytrenować kompletny CNN w PyTorch. Po ukończeniu będziesz potrafił klasyfikować prawdziwe obrazy i wprowadzać model do produkcji.
Przegląd serii
| # | Przedmiot | Centrum |
|---|---|---|
| 1 | Jesteś tutaj - CNN: Sieci konwolucyjne | Architektura, szkolenia, wdrożenie |
| 2 | Transfer wiedzy i dostrajanie | Wstępnie wytrenowane modele, adaptacja domeny |
| 3 | Wykrywanie obiektów za pomocą YOLO | Wykrywanie obiektów w czasie rzeczywistym |
| 4 | Semantyczna segmentacja | Klasyfikacja na poziomie pikseli |
| 5 | Generowanie obrazu za pomocą GAN i dyfuzji | Generowanie obrazu syntetycznego |
| 6 | Wdrożenie i optymalizacja brzegowa | Modele na urządzeniach wbudowanych |
Czego się nauczysz
- Jak komputer reprezentuje obrazy (piksele, kanały RGB, tensory)
- Operacja splotu: jądro, mapy cech, przesuwane okno
- Podstawowe elementy CNN: zwoje, łączenie, aktywacje
- Ewolucja architektur: od LeNet-5 do ConvNeXt
- ResNet i pomijanie połączeń: jak rozwiązać znikający gradient
- Pełna implementacja w PyTorch: CNN dla klasyfikacji CIFAR-10
- Transfer uczenia się: ponowne wykorzystanie wstępnie wytrenowanych modeli w ImageNet
- Metryki oceny: dokładność, precyzja, przypominanie, macierz zamieszania
- Wdrożenie: od wyszkolonego modelu do wnioskowania produkcyjnego (ONNX, TorchScript)
1. Od pikseli do funkcji: jak komputer widzi obrazy
Dla nas, ludzi, patrzenie na zdjęcie jest czynnością naturalną. Nasz mózg rozpoznaje natychmiast kształty, kolory, kontury i przedmioty. Ale dla komputera obraz to po prostu siatka liczb. Każdy piksel jest reprezentowany przez wartości liczbowe, które wskazują natężenie światła.
1.1 Obraz jako macierz liczb
Obraz w skali szarości to tablica 2D, w której każda komórka zawiera wartość z zakresu od 0 (czarny)
i 255 (biały). Kolorowy obraz składa się z trzy kanały (Czerwony, Zielony, Niebieski),
każdy z oddzielną matrycą. Kolorowy obraz o wymiarach 224x224 jest zatem tensorem wymiaru
3 x 224 x 224lub 150 528 wartości liczbowych.
Immagine 4x4 (scala di grigi, valori 0-255):
+-----+-----+-----+-----+
| 10 | 20 | 30 | 40 |
+-----+-----+-----+-----+
| 50 | 120 | 180 | 60 |
+-----+-----+-----+-----+
| 70 | 200 | 220 | 80 |
+-----+-----+-----+-----+
| 90 | 100 | 110 | 130 |
+-----+-----+-----+-----+
Immagine a colori (3 canali RGB):
Canale R: [[10, 20, ...], ...] --> tonalita di rosso
Canale G: [[30, 40, ...], ...] --> tonalita di verde
Canale B: [[50, 60, ...], ...] --> tonalita di blu
Tensore finale: shape = (3, H, W) --> (canali, altezza, larghezza)
1.2 dlaczego gęste sieci neuronowe nie wystarczą
Pierwsze podejście, które przychodzi na myśl, polega na spłaszczeniu obrazu do wektora 1D i przepuszczeniu go przez niego do w pełni połączonej (gęstej) sieci neuronowej. W przypadku obrazu o wymiarach 224x224x3 oznaczałoby to warstwę wejściową z 150 528 neuronów. Z ukrytą warstwą 1000 neuronów, miałbyś już 150 milionów parametrów w samej pierwszej warstwie.
Problemy gęstych sieci dla obrazów
- Eksplozja parametrów: Miliony ciężarów już w pierwszej warstwie, zaporowe obliczeniowo
- Brak niezmienności przestrzennej: Jeśli kot przesunie się o 10 pikseli w prawo, sieć już go nie rozpoznaje
- Utrata struktury 2D: Spłaszczenie obrazu niszczy relacje przestrzenne pomiędzy sąsiednimi pikselami
- Nadmierne dopasowanie: Zbyt wiele parametrów przy zbyt małej ilości danych prowadzi do zapamiętywania, a nie uogólniania
CNN rozwiązują wszystkie te problemy, wykorzystując trzy kluczowe spostrzeżenia: miejscowość (wzory wizualne są lokalne), podział obciążeń (ten sam filtr działa w każdym miejscu obrazu) e niezmienność tłumaczenia (krawędź jest krawędzią niezależnie od tego, gdzie się znajduje).
2. Operacja splotu
La skręt jest to operacja matematyczna leżąca u podstaw CNN. Mały filtr (powiedział jądro) przewija obraz wejściowy, obliczając sumę w każdej pozycji ważenie zasłoniętych pikseli. Wynikiem jest nowa tablica o nazwie mapy obiektów, który podkreśla konkretny wzór (krawędź pionowa, krawędź pozioma, narożnik, tekstura).
2.1 Jądro i okno przesuwne
Jądro to mała matryca wag (zwykle 3x3 lub 5x5), która jest przewijana (slajdy) na całym obrazie wejściowym. W każdej pozycji wartości jądra wynoszą pomnożony element po elemencie z podstawowymi pikselami i zsumowany, aby uzyskać pojedynczy wartość na wyjściowej mapie cech.
Input (5x5): Kernel (3x3):
+---+---+---+---+---+ +----+----+----+
| 1 | 2 | 3 | 0 | 1 | | -1 | 0 | 1 |
+---+---+---+---+---+ +----+----+----+
| 0 | 1 | 2 | 3 | 1 | | -1 | 0 | 1 |
+---+---+---+---+---+ +----+----+----+
| 1 | 0 | 1 | 2 | 0 | | -1 | 0 | 1 |
+---+---+---+---+---+ +----+----+----+
| 2 | 1 | 0 | 1 | 3 | (Filtro per bordi verticali)
+---+---+---+---+---+
| 0 | 1 | 2 | 1 | 0 |
+---+---+---+---+---+
Posizione (0,0): applica kernel ai pixel evidenziati [*]
[*1][*2][*3] 0 1
[*0][*1][*2] 3 1 Calcolo:
[*1][*0][*1] 2 0 (-1x1)+(0x2)+(1x3)+(-1x0)+(0x1)+(1x2)+(-1x1)+(0x0)+(1x1)
2 1 0 1 3 = -1 + 0 + 3 + 0 + 0 + 2 - 1 + 0 + 1 = 4
0 1 2 1 0
Output feature map (3x3):
+---+---+---+
| 4 | . | . | <-- Il 4 appena calcolato
+---+---+---+
| . | . | . | Il kernel scorre e calcola ogni cella
+---+---+---+
| . | . | . |
+---+---+---+
Klasyczne typy jądra
| Jądro | Zakres | Wartości (3x3) |
|---|---|---|
| Granica pionowa | Wykryj przejścia pionowe | [-1, 0, 1] powtórzono |
| Granica pozioma | Wykryj przejścia poziome | [-1, -1, -1], [0,0,0], [1,1,1] |
| Ostrzenie | Zwiększa ostrość | [0,-1,0], [-1,5,-1], [0,-1,0] |
| Rozmycie gaussowskie | Rozmycie gaussowskie | [1,2,1], [2,4,2], [1,2,1] / 16 |
Kluczowa różnica między CNN a tradycyjnym przetwarzaniem obrazu polega na tym w CNN wartości jądra nie są ustawiane ręcznie. Sieć uczy się automatycznie optymalne filtry podczas treningu poprzez propagacja wsteczna. Pierwsze warstwy uczą się wykrywać proste krawędzie i tekstury, natomiast warstwy uczą się wykrywać proste krawędzie i tekstury głębsze łączą te cechy w coraz bardziej złożone wzory (oczy, koła, twarze).
3. Elementy CNN
CNN składa się z różnych typów warstw, z których każda pełni określoną rolę. Zrozumienie działania każdego komponentu ma kluczowe znaczenie w projektowaniu efektywnych architektur.
3.1 Warstwa splotowa
Warstwa splotowa stosuje wiele filtrów do danych wejściowych, tworząc mapę obiektów każdy filtr. Jeśli zastosujesz 32 filtry 3x3 do obrazu RGB, otrzymasz 32 mapy obiektów, każdy podkreśla inny wzór. Kluczowe parametry to:
Parametry warstwy splotowej
| Parametr | Opis | Typowe wartości |
|---|---|---|
| Rozmiar jądra | Rozmiar filtra (szerokość x wysokość) | 3x3, 5x5, 7x7 |
| Krok | Krok przewijania filtra | 1, 2 |
| Wyściółka | Piksele dodane do krawędzi wejścia | 0 (ważne), 1 (tak samo dla 3x3) |
| Liczba filtrów | Ile map obiektów znajduje się na wyjściu | 32, 64, 128, 256, 512 |
Formula dimensione output:
output_size = (input_size - kernel_size + 2 * padding) / stride + 1
Esempio: input 32x32, kernel 3x3
Stride=1, Padding=0: (32 - 3 + 0) / 1 + 1 = 30x30 (si riduce)
Stride=1, Padding=1: (32 - 3 + 2) / 1 + 1 = 32x32 (same padding)
Stride=2, Padding=1: (32 - 3 + 2) / 2 + 1 = 16x16 (dimezza)
Stride=1, Padding=1 ("same"):
Input: [A B C D E] Output: [A B C D E] --> stessa dimensione
(con zero-padding ai bordi)
Stride=2 (dimezza la risoluzione):
Input: [A B C D E F] Output: [A C E] --> meta dimensione
(salta un pixel ad ogni passo)
3.2 Funkcja aktywacji (ReLU)
Po każdym splocie stosowana jest nieliniowa funkcja aktywacji. Najczęstsze
to tam jest ReLU (prostowana jednostka liniowa): f(x) = max(0, x).
ReLU resetuje wszystkie wartości ujemne, a dodatnie pozostawia bez zmian. Bez nieliniowości,
sekwencja splotów byłaby równoważna pojedynczej transformacji liniowej,
czyniąc sieć niezdolną do uczenia się złożonych wzorców.
ReLU: f(x) = max(0, x) Semplice, veloce, standard
LeakyReLU: f(x) = x se x>0, 0.01x altrimenti Evita "dying ReLU"
GELU: f(x) = x * Phi(x) Usata nei Transformers, smooth
Swish: f(x) = x * sigmoid(x) Usata in EfficientNet
Input feature map: Dopo ReLU:
+----+-----+----+ +----+----+----+
| -3 | 5 | -1 | | 0 | 5 | 0 |
+----+-----+----+ +----+----+----+
| 2 | -7 | 4 | | 2 | 0 | 4 |
+----+-----+----+ +----+----+----+
| -2 | 1 | -5 | | 0 | 1 | 0 |
+----+-----+----+ +----+----+----+
3.3 Łączenie warstw
Łączenie zmniejsza wymiary przestrzenne map obiektów, zmniejszając liczbę parametrów i uczynienie sieci bardziej odporną na niewielkie zmiany położenia funkcji. Dwa typy główne są Maksymalne łączenie (przyjmuje maksymalną wartość w każdym oknie) tj Średnie łączenie (obliczyć średnią).
Input (4x4): Output (2x2):
+----+----+----+----+ +----+----+
| 12 | 20| 30| 0 | | 20 | 30 | max(12,20,0,8)=20
+----+----+----+----+ --> +----+----+ max(30,0,2,14)=30
| 0 | 8 | 2 | 14 | | 15 | 16 |
+----+----+----+----+ +----+----+
| 15 | 2 | 3 | 16 |
+----+----+----+----+ Riduce 4x4 --> 2x2
| 1 | 6 | 7 | 8 | (dimezza altezza e larghezza)
+----+----+----+----+
Max Pooling: prende il massimo --> preserva le feature più forti
Avg Pooling: calcola la media --> effetto di smoothing
Global Average Pooling: media su tutta la feature map --> un singolo valore per canale
3.4 Normalizacja wsadowa
La Normalizacja wsadowa (BatchNorm) normalizuje dane wyjściowe każdej warstwy tak, że ma zerową średnią i jednostkową wariancję. To stabilizuje trening, pozwala wyższe tempo uczenia się i działa jako lekki regulator. W praktyce to pasuje warstwę BatchNorm po każdym splocie, przed aktywacją.
4. Typowa architektura CNN
Standardowy CNN ma powtarzający się schemat: bloki ekstrakcji cech (convolution + aktywacja + łączenie), po których następują w pełni połączone warstwy w celu ostatecznej klasyfikacji. Wraz z głębokością mapy obiektów zmniejszają się pod względem przestrzennym, ale zwiększają się ich liczby kanałów, oddając coraz bardziej abstrakcyjne wzorce.
Input Immagine (3 x 32 x 32)
|
v
[Conv2d 3->32, 3x3, pad=1] --> [BatchNorm] --> [ReLU] --> [MaxPool 2x2]
| Feature maps: 32 x 16 x 16
v
[Conv2d 32->64, 3x3, pad=1] --> [BatchNorm] --> [ReLU] --> [MaxPool 2x2]
| Feature maps: 64 x 8 x 8
v
[Conv2d 64->128, 3x3, pad=1] --> [BatchNorm] --> [ReLU] --> [MaxPool 2x2]
| Feature maps: 128 x 4 x 4
v
[Flatten] --> Vettore di 128 * 4 * 4 = 2048 valori
|
v
[Linear 2048 -> 256] --> [ReLU] --> [Dropout 0.5]
|
v
[Linear 256 -> 10] --> Output: 10 classi (es. CIFAR-10)
|
v
[Softmax] --> Probabilità per ogni classe: [0.02, 0.01, 0.85, ...]
Flusso delle dimensioni:
(3, 32, 32) -> (32, 16, 16) -> (64, 8, 8) -> (128, 4, 4) -> (2048) -> (256) -> (10)
[immagine] [bordi, texture] [parti] [oggetti] [decisione] [classe]
Hierarchia wyuczonych funkcji
- Warstwa 1-2 (niski poziom): Obramowania, gradienty kolorów, proste tekstury
- Warstwa 3-5 (poziom średni): Narożniki, kontury, części obiektów (oczy, koła)
- Warstwa 6+ (wysoki poziom): Kompletne obiekty, sceny, abstrakcyjne koncepcje
Hierarchia ta pojawia się automatycznie podczas szkolenia. Nie ma potrzeby aby powiedzieć sieci, czego ma szukać: filtry dostosowują się do danych.
5. Ewolucja architektur CNN
Historia CNN naznaczona jest rewolucyjnymi architekturami, z których każda wprowadziła pomysły, które zmieniły tę dziedzinę. Znajomość tej ewolucji jest podstawą zrozumienia nowoczesne wybory architektoniczne.
Kalendarium architektur CNN
| Rok | Architektura | Kluczowa innowacja | Top 1 ImageNet |
|---|---|---|---|
| 1998 | LeNet-5 | Pierwszy praktyczny CNN (rozpoznawanie cyfr) | Nie dotyczy |
| 2012 | AlexNet | Trening GPU, ReLU, Dropout | 63,3% |
| 2014 | VGGNet | Głębokie sieci z jednolitymi filtrami 3x3 | 74,5% |
| 2014 | GoogLeNet/Incepcja | Moduły początkowe, wieloskalowe, równoległe | 74,8% |
| 2015 | ResNet | Pomiń połączenia, sieci ponad 152 warstwowe | 78,6% |
| 2019 | Efektywna sieć | Skalowanie złożone (głębokość+szerokość+rozdzielczość) | 84,4% |
| 2022 | ConvNeXt | Zmodernizowane CNN inspirowane Vision Transformers | 87,8% |
5.1 LeNet-5 (1998) - Pionier
Zaprojektowany przez Yanna LeCuna do rozpoznawania cyfr odręcznych (MNIST), LeNet-5 i pierwszą CNN, która odniosła praktyczny sukces. Udowodniono to przy zaledwie 5 warstwach i 60 000 parametrach że sploty mogą uczyć się cech dyskryminacyjnych z obrazów. Służył do automatycznie odczytuje czeki bankowe.
5.2 AlexNet (2012) - Rewolucja
AlexNet wygrał konkurs ImageNet 2012 z ogromną przewagą, redukując błąd z 26% do 16%. Kluczowe innowacje: szkolenie GPU (dwie karty NVIDIA GTX 580), funkcjonalność Aktywacja ReLU zamiast esicy, Dropout w celu uregulowania i zwiększenia danych. Wynik ten przekonał środowisko akademickie i przemysł, że głębokie uczenie się działa.
5.3 VGGNet (2014) – Głębokość ma znaczenie
VGG wykazało, że głębsze sieci dają lepsze wyniki. Jego kluczowa idea i radykalna w prostocie: używaj tylko filtrów skumulowanych 3x3. Dwie kolejne warstwy 3x3 mają to samo pole recepcyjne co pojedyncza warstwa 5x5, ale z coraz mniejszą liczbą parametrów nieliniowość. VGG-16 ma 16 warstw i 138 milionów parametrów.
5.4 EfficientNet (2019) – Inteligentne skalowanie
EfficientNet wprowadził skalowanie złożone: zamiast po prostu zwiększać głębokość (jak VGG) lub szerokość, skaluje wszystkie trzy wymiary równomiernie (głębokość, szerokość, rozdzielczość wejściowa) ze zrównoważonymi współczynnikami. EfficientNet-B0 osiąga w ImageNet dokładność na poziomie 77,1% przy zaledwie 5,3 milionach parametrów – wynika z raportu niespotykane dotąd parametry dokładności.
5.5 ConvNeXt (2022) – Zmodernizowana CNN
ConvNeXt pokazuje, że sieci CNN zmodernizowane przy użyciu technik inspirowanych transformatorami wizyjnymi, mogą konkurować (i przewyższać) architektury transformatorowe. Innowacje obejmują: Jądra separowane wgłębnie 7x7, LayerNorm zamiast BatchNorm, aktywacja GELU, oraz projekt „izomorficzny” z etapami o rosnącym rozmiarze. ConvNeXt V2, w wersji E-ConvNeXt-Tiny, osiąga 80,6% Top-1 przy zaledwie 2,0 GFLOP, doskonały do wdrożenia wydajny.
6. ResNet i pomiń połączenia
ResNet (sieci resztkowe), zaproponowany przez He i in. w 2015 roku rozwiązano jeden z podstawowych problemów głębokiego uczenia się: problem degradacji. Przed ResNet dodanie warstw do głębokiej sieci powodowało, że wyniki były raczej gorsze niż lepsze, nawet na planie treningowym. Rozwiązanie jest równie eleganckie, co proste.
6.1 Problem zanikającego gradientu
Podczas propagacji wstecznej gradienty są wielokrotnie mnożone warstwy sieci. Jeśli te mnożenia dają wartości mniejsze niż 1, gradienty „zanikają” wykładniczo, gdy rozprzestrzeniają się w kierunku początkowych warstw. W przypadku 50 lub więcej warstw gradienty stają się tak małe, że kilka pierwszych warstw się zatrzymuje uczyć się. Ostatnie badania potwierdzają, że bez pomijania połączeń, L2 gradientów drastycznie spada w początkowych warstwach, przy pomijaniu połączeń pozostają jednolite w całej sieci.
Rete profonda SENZA skip connections (50 layer):
Layer 50 Layer 49 Layer 48 ... Layer 2 Layer 1
grad=1.0 * 0.8 * 0.8 ... * 0.8 * 0.8
Gradiente al layer 1: 0.8^49 = 0.00001 --> quasi zero!
I primi layer NON apprendono.
Rete profonda CON skip connections (ResNet):
Il gradiente ha un "percorso diretto" attraverso le skip connections.
Non viene moltiplicato ripetutamente per valori piccoli.
Gradiente al layer 1: ~0.5 --> i layer apprendono normalmente!
6.2 Rozwiązanie: uczenie się resztkowe
Genialny pomysł ResNet jest prosty: zamiast tworzyć blok, naucz się transformacji
kompletny H(x), każesz mu uczyć się tylko różnica (resztkowy)
odnośnie wejścia: F(x) = H(x) - x. Dane wyjściowe bloku stają się
y = F(x) + x, Gdzie x jest to wejście, które „przeskakuje” blok
jeden pominąć połączenie (lub połączenie skrótowe).
Blocco Standard: Residual Block:
x x ------+
| | |
v v | (skip connection)
[Conv 3x3] [Conv 3x3] |
| | |
[BatchNorm] [BatchNorm] |
| | |
[ReLU] [ReLU] |
| | |
[Conv 3x3] [Conv 3x3] |
| | |
[BatchNorm] [BatchNorm] |
| | |
v v |
H(x) = output F(x) + x <--+
|
[ReLU]
|
v
output
Se F(x) = 0, l'output è semplicemente x (identità).
La rete può "saltare" un blocco se non serve.
Questo rende il training di reti profonde stabile.
ponieważ to działa
Nauka pozostałości F(x) = 0 (tj. „nic nie rób”) jest znacznie łatwiejsze
niż nauka całkowitej transformacji tożsamości. Jeśli warstwa nie jest przydatna, sieć
po prostu naucz się F(x) = 0 i przekazuje dane wejściowe bez zmian. To pozwala
do budowania sieci składających się z setek warstw bez pogorszenia wydajności.
7. Szkolenie CNN
Szkolenie CNN oznacza znalezienie optymalnych wartości dla wszystkich jąder (filtrów) oraz ciężary w pełni połączonych warstw. Dzieje się to poprzez proces iteracyjny przejście w przód, obliczanie strat i wsteczna propagacja gradientu.
7.1 Funkcja straty
W przypadku klasyfikacji obrazu standardową funkcją straty jest Strata między entropią. Mierzy, jak bardzo prawdopodobieństwa przewidywane przez sieć odbiegają od rzeczywistych etykiet. Doskonała prognoza daje stratę = 0; powstaje całkowicie błędna prognoza strata zmierzająca do nieskończoności.
Etichetta reale (one-hot): [0, 0, 1, 0, 0, 0, 0, 0, 0, 0] (classe "gatto" = indice 2)
Predizione buona: [0.02, 0.03, 0.85, 0.02, 0.01, 0.02, 0.01, 0.02, 0.01, 0.01]
Loss = -log(0.85) = 0.16 --> Loss bassa, predizione corretta
Predizione cattiva: [0.30, 0.25, 0.05, 0.10, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05]
Loss = -log(0.05) = 3.00 --> Loss alta, predizione sbagliata
7.2 Optymalizatory
Optymalizator aktualizuje wagi sieci w kierunku zmniejszającym straty. Najczęściej używane to:
Porównanie optymalizatorów
| Optymalizator | Charakterystyka | Kiedy go używać |
|---|---|---|
| SGD + dynamika | Prosta, solidna i stabilna zbieżność | Długi trening, maksymalna dokładność końcowa |
| Adama | Adaptacyjna szybkość uczenia się, szybka konwergencja | Prototypowanie, małe/średnie sieci |
| Adam W | Adam z prawidłowym spadkiem wagi | Nowoczesny standard, rekomendowany dla CNN |
7.3 Rozszerzanie danych
La powiększanie danych jest to podstawowa technika zapobiegania nadmiernemu dopasowaniu i poprawić generalizację sieci. Polega na zastosowaniu losowych przekształceń do trenowania obrazów (obroty, odwrócenia, przycinanie, zmiany jasności) w celu tworzenia wariacji syntetyczne bez gromadzenia nowych danych.
Immagine Originale: Trasformazioni:
+-------+ [Flip orizzontale] --> Immagine specchiata
| Gatto | [Rotazione +-15 gradi]--> Leggera rotazione
| --o-- | [Random Crop] --> Ritaglio casuale
| /|\ | [Color Jitter] --> Variazione colori
+-------+ [Gaussian Noise] --> Aggiunta rumore
[Cutout/Erasing] --> Maschera rettangolare casuale
[MixUp] --> Media pesata di 2 immagini
[CutMix] --> Porzione di un'immagine sovrapposta
Effetto: da 50.000 immagini di training, ogni epoca vede variazioni
diverse, come se avessi milioni di immagini uniche.
8. Kompletne wdrożenie w PyTorch
Przejdźmy od teorii do praktyki. Wdrożymy pełny CNN do klasyfikacji obrazy zbioru danych CIFAR-10 (60 000 obrazów 32x32 w 10 klasach: samolot, samochód, ptak, kot, jeleń, pies, żaba, koń, statek, ciężarówka).
8.1 Konfiguracja i zbiór danych
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
from torch.utils.data import DataLoader
# Trasformazioni con data augmentation per il training
train_transform = transforms.Compose([
transforms.RandomHorizontalFlip(p=0.5),
transforms.RandomCrop(32, padding=4),
transforms.ColorJitter(brightness=0.2, contrast=0.2),
transforms.ToTensor(),
transforms.Normalize(
mean=[0.4914, 0.4822, 0.4465], # Media CIFAR-10
std=[0.2470, 0.2435, 0.2616] # Std CIFAR-10
),
])
# Trasformazioni per il test (nessuna augmentation)
test_transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize(
mean=[0.4914, 0.4822, 0.4465],
std=[0.2470, 0.2435, 0.2616]
),
])
# Caricamento dataset
train_dataset = torchvision.datasets.CIFAR10(
root='./data', train=True, download=True, transform=train_transform
)
test_dataset = torchvision.datasets.CIFAR10(
root='./data', train=False, download=True, transform=test_transform
)
# DataLoader con batching e shuffling
train_loader = DataLoader(train_dataset, batch_size=128, shuffle=True, num_workers=4)
test_loader = DataLoader(test_dataset, batch_size=128, shuffle=False, num_workers=4)
CLASSES = ['aereo', 'auto', 'uccello', 'gatto', 'cervo',
'cane', 'rana', 'cavallo', 'nave', 'camion']
8.2 Definicja modelu
class ResidualBlock(nn.Module):
"""Blocco residuale con skip connection."""
def __init__(self, in_channels: int, out_channels: int, stride: int = 1):
super().__init__()
self.conv1 = nn.Conv2d(
in_channels, out_channels, kernel_size=3,
stride=stride, padding=1, bias=False
)
self.bn1 = nn.BatchNorm2d(out_channels)
self.conv2 = nn.Conv2d(
out_channels, out_channels, kernel_size=3,
stride=1, padding=1, bias=False
)
self.bn2 = nn.BatchNorm2d(out_channels)
self.relu = nn.ReLU(inplace=True)
# Skip connection: se le dimensioni cambiano, usa conv 1x1
self.shortcut = nn.Sequential()
if stride != 1 or in_channels != out_channels:
self.shortcut = nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernel_size=1,
stride=stride, bias=False),
nn.BatchNorm2d(out_channels)
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
identity = self.shortcut(x)
out = self.relu(self.bn1(self.conv1(x)))
out = self.bn2(self.conv2(out))
out = out + identity # Skip connection
out = self.relu(out)
return out
class CIFAR10CNN(nn.Module):
"""CNN con blocchi residuali per classificazione CIFAR-10."""
def __init__(self, num_classes: int = 10):
super().__init__()
# Primo layer convoluzionale
self.conv1 = nn.Conv2d(3, 32, kernel_size=3, padding=1, bias=False)
self.bn1 = nn.BatchNorm2d(32)
self.relu = nn.ReLU(inplace=True)
# Blocchi residuali con profondità crescente
self.layer1 = self._make_layer(32, 64, num_blocks=2, stride=1)
self.layer2 = self._make_layer(64, 128, num_blocks=2, stride=2)
self.layer3 = self._make_layer(128, 256, num_blocks=2, stride=2)
# Classificatore finale
self.global_avg_pool = nn.AdaptiveAvgPool2d((1, 1))
self.dropout = nn.Dropout(0.3)
self.fc = nn.Linear(256, num_classes)
def _make_layer(
self, in_ch: int, out_ch: int, num_blocks: int, stride: int
) -> nn.Sequential:
layers = [ResidualBlock(in_ch, out_ch, stride)]
for _ in range(1, num_blocks):
layers.append(ResidualBlock(out_ch, out_ch, stride=1))
return nn.Sequential(*layers)
def forward(self, x: torch.Tensor) -> torch.Tensor:
x = self.relu(self.bn1(self.conv1(x))) # (B, 32, 32, 32)
x = self.layer1(x) # (B, 64, 32, 32)
x = self.layer2(x) # (B, 128, 16, 16)
x = self.layer3(x) # (B, 256, 8, 8)
x = self.global_avg_pool(x) # (B, 256, 1, 1)
x = x.view(x.size(0), -1) # (B, 256)
x = self.dropout(x)
x = self.fc(x) # (B, 10)
return x
8.3 Pętle treningowe
def train_model(
model: nn.Module,
train_loader: DataLoader,
test_loader: DataLoader,
epochs: int = 50,
lr: float = 0.01
) -> dict:
"""Addestra il modello e restituisce la storia del training."""
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = model.to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(
model.parameters(), lr=lr, momentum=0.9, weight_decay=1e-4
)
scheduler = optim.lr_scheduler.OneCycleLR(
optimizer, max_lr=lr, epochs=epochs,
steps_per_epoch=len(train_loader)
)
history = {'train_loss': [], 'test_loss': [], 'test_acc': []}
for epoch in range(epochs):
# --- Training ---
model.train()
running_loss = 0.0
for images, labels in train_loader:
images, labels = images.to(device), labels.to(device)
optimizer.zero_grad()
outputs = model(images)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
scheduler.step()
running_loss += loss.item()
avg_train_loss = running_loss / len(train_loader)
# --- Evaluation ---
model.eval()
test_loss = 0.0
correct = 0
total = 0
with torch.no_grad():
for images, labels in test_loader:
images, labels = images.to(device), labels.to(device)
outputs = model(images)
loss = criterion(outputs, labels)
test_loss += loss.item()
_, predicted = outputs.max(1)
total += labels.size(0)
correct += predicted.eq(labels).sum().item()
avg_test_loss = test_loss / len(test_loader)
accuracy = 100.0 * correct / total
history['train_loss'].append(avg_train_loss)
history['test_loss'].append(avg_test_loss)
history['test_acc'].append(accuracy)
print(
f"Epoch [{epoch+1}/{epochs}] "
f"Train Loss: {avg_train_loss:.4f} | "
f"Test Loss: {avg_test_loss:.4f} | "
f"Accuracy: {accuracy:.2f}%"
)
return history
# Esecuzione
model = CIFAR10CNN(num_classes=10)
history = train_model(model, train_loader, test_loader, epochs=50, lr=0.1)
# Output atteso dopo 50 epoche con OneCycleLR:
# Test Accuracy: ~92-93%
Zalecane parametry treningowe dla CIFAR-10
| Parametr | Wartość | Motywacja |
|---|---|---|
| Rozmiar partii | 128 | Równowaga pomiędzy prędkością i stabilnością nachylenia |
| Szybkość uczenia się | 0,1 (z OneCycleLR) | SGD z planowaniem osiąga najwyższą dokładność |
| Zanik masy | 1e-4 | Regularyzacja L2 zapobiegająca nadmiernemu dopasowaniu |
| Epoki | 50 | Dzięki OneCycleLR wystarczy 50 epok, aby się zbiegły |
| Rezygnacje | 0,3 | Dodatkowa regularyzacja przed ostatnią warstwą |
9. Przenieś naukę
W praktyce rzadko szkoli się CNN od zera. The nauka transferu umożliwia ponowne wykorzystanie wstępnie wytrenowanych modeli w dużych zbiorach danych (takich jak ImageNet z 1,2 milionami obrazów i 1000 klas) i dostosuj je do konkretnego problemu. To zmniejsza drastycznie skraca czas szkolenia, ilość potrzebnych danych i poprawia wydajność.
9.1 Ekstrakcja cech a dostrajanie
Dwie strategie uczenia się transferowego
| Strategia | Jak to działa | Kiedy go używać |
|---|---|---|
| Ekstrakcja cech | Zamroź wszystkie wstępnie wyszkolone warstwy, wytrenuj tylko końcowy klasyfikator | Mało danych (<1000 obrazów), domena podobna do ImageNet |
| Dostrajanie | Odblokuj ostatnie N warstw i przetrenuj ponownie z niską szybkością uczenia się | Dostępnych jest więcej danych, domena inna niż ImageNet |
import torchvision.models as models
def create_transfer_model(
num_classes: int,
freeze_backbone: bool = True
) -> nn.Module:
"""Crea un modello con transfer learning da ResNet-18."""
# Carica ResNet-18 pre-addestrato su ImageNet
model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)
# Strategia 1: Feature Extraction (congela il backbone)
if freeze_backbone:
for param in model.parameters():
param.requires_grad = False
# Sostituisci il classificatore finale
num_features = model.fc.in_features # 512 per ResNet-18
model.fc = nn.Sequential(
nn.Dropout(0.3),
nn.Linear(num_features, 256),
nn.ReLU(),
nn.Dropout(0.2),
nn.Linear(256, num_classes)
)
return model
# Feature extraction: solo il classificatore viene addestrato
feature_model = create_transfer_model(num_classes=10, freeze_backbone=True)
# Fine-tuning: tutto il modello viene riaddestrato
finetune_model = create_transfer_model(num_classes=10, freeze_backbone=False)
# Per il fine-tuning, usa un learning rate più basso
optimizer = optim.AdamW([
{'params': finetune_model.layer4.parameters(), 'lr': 1e-4},
{'params': finetune_model.fc.parameters(), 'lr': 1e-3},
], weight_decay=1e-2)
Guida al Transfer Learning:
Dati simili a ImageNet Dati diversi da ImageNet
(oggetti, animali, scene) (medico, satellite, micro)
+---------------------------+---------------------------+
Pochi dati | Feature Extraction | Feature Extraction |
(<1.000 img) | Congela tutto, addestra FC | + Aumenta data augment. |
+---------------------------+---------------------------+
Molti dati | Fine-tuning ultimi layer | Fine-tuning completo |
(>5.000 img) | LR basso per il backbone | o training da zero |
+---------------------------+---------------------------+
10. Metryki oceny
Sama dokładność nie wystarczy, aby zrozumieć, czy CNN działa dobrze. Z niezrównoważonymi zbiorami danych (np. 95% klasa A, 5% klasa B), model, który zawsze przewiduje „klasę A”, ma 95% dokładności ale jest to całkowicie bezużyteczne. Potrzebne są bardziej szczegółowe wskaźniki.
Metryki klasyfikacji obrazów
| Metryczny | Co mierzy | Formuła |
|---|---|---|
| Dokładność | Procent poprawnych przewidywań w stosunku do całości | TP + TN / Razem |
| Precyzja | Ile z pozytywnych przewidywań jest prawidłowych? | TP / (TP + FP) |
| Przypomnienie sobie czegoś | Ile spośród prawdziwych pozytywów udało się znaleźć? | TP / (TP + FN) |
| Wyniki F1 | Harmoniczna średnia precyzji i zapamiętywania | 2 * (P * R) / (P + R) |
Confusion Matrix (predizioni vs realta):
Predetto: aereo auto uccel gatto cervo cane rana caval nave camion
Reale:
aereo [92] 1 2 0 0 0 1 0 3 1
auto 0 [95] 0 0 0 0 0 0 1 4
uccello 3 0 [85] 3 2 2 3 1 1 0
gatto 0 1 2 [78] 1 12 3 2 0 1
cervo 1 0 3 2 [88] 1 2 3 0 0
Interpretazione:
- Diagonale = predizioni corrette (più alto = meglio)
- Fuori diagonale = errori (confusioni tra classi)
- gatto vs cane: 12 gatti classificati come cani --> classi "confuse"
from sklearn.metrics import (
classification_report,
confusion_matrix
)
import numpy as np
def evaluate_model(
model: nn.Module,
test_loader: DataLoader,
device: torch.device,
class_names: list[str]
) -> dict:
"""Valuta il modello e restituisce metriche dettagliate."""
model.eval()
all_preds = []
all_labels = []
with torch.no_grad():
for images, labels in test_loader:
images = images.to(device)
outputs = model(images)
_, predicted = outputs.max(1)
all_preds.extend(predicted.cpu().numpy())
all_labels.extend(labels.numpy())
preds_array = np.array(all_preds)
labels_array = np.array(all_labels)
# Report dettagliato per classe
report = classification_report(
labels_array, preds_array,
target_names=class_names, output_dict=True
)
# Confusion matrix
cm = confusion_matrix(labels_array, preds_array)
# Accuracy globale
accuracy = np.mean(preds_array == labels_array) * 100
print(f"Accuracy globale: {accuracy:.2f}%")
print(classification_report(
labels_array, preds_array, target_names=class_names
))
return {'accuracy': accuracy, 'report': report, 'confusion_matrix': cm}
11. Wdrożenie: od wyszkolonego modelu do produkcji
Wyszkolenie modela to tylko połowa pracy. Aby wprowadzić go do produkcji, trzeba go wyeksportować w zoptymalizowanym formacie utwórz interfejs API wnioskowania i skonteneryzuj wszystko do skalowalnego wdrożenia.
11.1 Eksport modelu
Formaty eksportu
| Format | Używać | Zalety |
|---|---|---|
| TorchScript | Wnioskowanie PyTorch bez Pythona | Brak zależności w Pythonie, pełna serializacja |
| ONNX | Format uniwersalny, wieloramowy | Kompatybilny z TensorRT, OpenVINO, CoreML |
| TensorRT | Wnioskowanie zoptymalizowane dla procesorów graficznych NVIDIA | Do 5 razy szybszy niż natywny PyTorch |
import torch.onnx
def export_model(model: nn.Module, export_path: str) -> None:
"""Esporta il modello in TorchScript e ONNX."""
model.eval()
dummy_input = torch.randn(1, 3, 32, 32)
# --- TorchScript ---
scripted_model = torch.jit.trace(model, dummy_input)
scripted_model.save(f"{export_path}/model_scripted.pt")
print("TorchScript salvato.")
# --- ONNX ---
torch.onnx.export(
model,
dummy_input,
f"{export_path}/model.onnx",
input_names=['image'],
output_names=['prediction'],
dynamic_axes={
'image': {0: 'batch_size'},
'prediction': {0: 'batch_size'}
},
opset_version=17
)
print("ONNX salvato.")
export_model(model, './exports')
11.2 API wnioskowania z FastAPI
# inference_api.py
from fastapi import FastAPI, UploadFile
from PIL import Image
import torch
import torchvision.transforms as transforms
import io
app = FastAPI(title="CNN Image Classifier")
# Carica il modello TorchScript
model = torch.jit.load("./exports/model_scripted.pt")
model.eval()
CLASSES = ['aereo', 'auto', 'uccello', 'gatto', 'cervo',
'cane', 'rana', 'cavallo', 'nave', 'camion']
preprocess = transforms.Compose([
transforms.Resize((32, 32)),
transforms.ToTensor(),
transforms.Normalize(
mean=[0.4914, 0.4822, 0.4465],
std=[0.2470, 0.2435, 0.2616]
),
])
@app.post("/predict")
async def predict(file: UploadFile):
"""Classifica un'immagine caricata."""
image_bytes = await file.read()
image = Image.open(io.BytesIO(image_bytes)).convert("RGB")
tensor = preprocess(image).unsqueeze(0)
with torch.no_grad():
outputs = model(tensor)
probabilities = torch.softmax(outputs, dim=1)
confidence, predicted = probabilities.max(1)
return {
"class": CLASSES[predicted.item()],
"confidence": round(confidence.item() * 100, 2),
"all_probabilities": {
name: round(prob.item() * 100, 2)
for name, prob in zip(CLASSES, probabilities[0])
}
}
# Esecuzione: uvicorn inference_api:app --host 0.0.0.0 --port 8000
11.3 Konteneryzacja za pomocą Dockera
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY exports/ ./exports/
COPY inference_api.py .
EXPOSE 8000
CMD ["uvicorn", "inference_api:app", "--host", "0.0.0.0", "--port", "8000"]
# Build: docker build -t cnn-classifier .
# Run: docker run -p 8000:8000 cnn-classifier
# Test: curl -X POST -F "file=@cat.jpg" http://localhost:8000/predict
Pipeline: Training --> Export --> Container --> Deploy
[Training] [Export] [Container] [Deploy]
PyTorch + GPU --> TorchScript/ONNX --> Docker --> Kubernetes
50 epoche ~10 MB file FastAPI Auto-scaling
~92% accuracy Ottimizzato Health checks Load balancer
No dipendenza Python GPU opzionale Monitoring
Alternativa serverless:
Export ONNX --> AWS Lambda + ONNX Runtime --> API Gateway
Pro: Pay-per-use, zero infrastruttura da gestire
Contro: Cold start (~2s), limite 250MB package
Wnioski i dalsze kroki
W tym artykule opracowaliśmy kompleksowe zrozumienie konwolucyjnych sieci neuronowych, zaczynając od podstaw (jak komputer widzi obrazy, operacja splotu) w górę do praktycznego wdrożenia (CNN z pozostałymi blokami w PyTorch) i wdrożenia do produkcji (TorchScript, ONNX, FastAPI, Docker).
Widzieliśmy, jak ewolucja architektur, od LeNet-5 w 1998 r. do ConvNeXt w 2022 r., ma stopniowo poprawianą wydajność dzięki pomysłom takim jak pomijanie połączeń (ResNet), skalowanie złożone (EfficientNet) i konstrukcja inspirowana transformatorem (ConvNeXt).
Kluczowe punkty do zapamiętania
- CNN wykorzystują miejscowość, podział obciążeń e niezmienność przestrzenna aby efektywnie przetwarzać obrazy
- Standardowa architektura opiera się na schemacie: Conv + BatchNorm + ReLU + Pooling, powtarzanym wraz ze wzrostem głębokości
- Le pominąć połączenia (ResNet) są niezbędne do uczenia głębokich sieci bez zanikających gradientów
- Il nauka transferu jest prawie zawsze lepsze niż trenowanie od zera, zwłaszcza przy małej ilości danych
- La powiększanie danych jest to niezbędne dla uogólnienia i kosztuje zero w zakresie gromadzenia danych
- W celu produkcji eksport do ONNX o TorchScript i konteneryzuj za pomocą Dockera
W kolejnym artykule z tej serii zagłębimy się w temat Transfer wiedzy i dostrajanie: jak wybrać odpowiedni, wstępnie wytrenowany model, strategie progresywnego dostrajania, adaptacja domeny i zaawansowane techniki, takie jak destylacja wiedzy. W trzecim artykule zmierzymy się z Wykrywanie obiektów za pomocą YOLO, system detekcji obiektów najczęściej używany w czasie rzeczywistym w przemyśle.
Dodatkowe zasoby
- Oryginalny papier ResNet: „Głębokie uczenie się szczątkowe na potrzeby rozpoznawania obrazów” (He i in., 2015)
- Papierowe ConvNeXt: „Sieć ConvNet na lata 2020.” (Liu i in., 2022)
- Paper EfficientNet: „EfficientNet: nowe podejście do skalowania modeli dla CNN” (Tan i Le, 2019)
- Dokumentacja PyTorcha: Poradniki dotyczące CNN i Torchvision
- CS231n Stanforda: Konwolucyjne sieci neuronowe do rozpoznawania wizualnego (kurs online)
- Środowisko wykonawcze ONNX: Zoptymalizowana dokumentacja wnioskowania międzyplatformowego







