Sieć konwolucyjna w Pytorch – klasyfikacja obrazów CIFAR-10

Tutorial ten pomoże Ci zbudować konwolucyjną sieć neuronową (Convolutional Neural Network) do klasyfikacji obrazów ze zbioru CIFAR-10. Krok po kroku pokazuję jak połączyć warstwy konwolucyjne oraz jaki wpływ na rozmiary obrazu mają „stride” i rozmiar jądra konwolucji.

Wpis ten należy do serii artykułów o PyTorch, w każdym artykule opisuję pewien aspekt tej biblioteki oraz przykłady najpopularniejszych architektur sieci neuronowych.

Do tej pory opublikowałem:

Celem całej serii jest pomoc w nauce i wskazaniu dalszych kierunków rozwoju.

Sieć konwolucyjna – co to jest?

Sieć konwolucyjna (sieć splotowa lub po angielsku Convolutional Neural Network) jest architekturą podobną do klasycznych sieci. Składa się z warstw, które kolejno transformują dane wejściowe wykorzystując operację konwolucji na obrazie.

W klasycznym podejściu (fully connected network) na wartość danego neuronu w warstwie następnej mają wpływ wszystkie neurony z warstw poprzednich. W sieciach konwolucyjnych wykorzystywana jest tylko lokalna informacja z pewnego sąsiedztwa danego piksela.

Zastanówcie się przez moment, czy piksel z prawego dolnego rogu będzie nam w stanie coś powiedzieć o pikselu z lewego górnego rogu? Raczej niewiele. Stąd pomysł wykorzystania tylko lokalnej informacji z okolicy danego piksela idealnie nadaje się do tego operacja konwolucji (splotu). Pozwala ona zakodować informację wokół wybranego piksela (np. okna o rozmiarze 3×3 piksele).

Podejście takie ma także dodatkową zaletę. Mianowicie macierz wag w warstwie konwolucyjnej jest bardzo mała (3×3). Zestawiając to z siecią w pełni połączoną, w której rozmiar macierzy jest rzędu n_input x n_hidden (np. 1024×512) daje to zdecydowaną redukcję ilości parametrów.
Na operację splotu można patrzeć jak na filtr, który przemieszcza się po obrazie i wyciąga z niego lokalne cechy (szczegółowo opisałem to w artykule „Sieć konwolucyjna w Tensorflow”). Właściwie w sieciach splotowych mamy zazwyczaj wiele filtrów, przyjęło się je nazywać także kanałami (channels).
O ile w klasycznym przetwarzaniu obrazów wagi w filtrze konwolucyjnym są specjalnie dobrane to w sieciach wagi te są wyuczane w trakcie treningu. Każdy filtr (kanał) uczy się rozpoznawać czegoś innego ( lini pionowych, poziomych, łuków, lini ukośnych itp.)

Wizualizacja filtrów w sieci konwolucyjnej.
Wizualizacja filtrów w sieci konwolucyjnej. Obraz pochodzi z http://cs231n.github.io/convolutional-networks/

Dodatkowo w sieciach konwolucyjnych stosuję się operację ’poolingu’. Najlepiej o niej myśleć jak o wyciągnięciu najważniejszej informacji z zadanego obszaru obrazu. Operacja max_pooling, czy average polling zmniejszają obraz, w zależności od wielkości rozmiaru okna. Najlepiej gdy zobaczycie to na przykładzie, poniżej operacja max poolingu

sieć konwolucyjna max-pooling
Operacja max-pooling.
Obraz pochodzi z https://computersciencewiki.org/index.php/Max-pooling_/_Pooling

Architektura sieci

Mamy za sobą opis czym jest sieć konwolucyjna. Przejdźmy do stworzenia naszej architektury. Uprzedzam, będzie ona prosta, nie spodziewajcie się najlepszego wyniku klasyfikacji dla CIFAR-10. Architektura ta da wam podstawy do dalszych eksperymentów.

Sieć będzie składała się z trzech warstw konwolucyjnych o rozmiarze filtra (kernel size) 3×3 px. Po każdej zastosujemy nieliniową transformację ReLu, po drugiej i trzeciej warstwie konwolucyjnej zastosujemy operację ’max poolingu’ . Ostatnie dwie warstwy to warstwy liniowe. W skrócie cała architektura preznetuje się następująco:

  • conv1 (kernel size 3×3) – warstwa konwolucyjna, rozmiar filtra 3×3, posiadająca 3 kanały wejściowe (tyle ile ma obraz RGB), 8 kanałów wyjściowych (to już dobrałem sam)
  • relu – funkcja aktywacji (w ramach eksperymentów możecie spróbować inne)
  • conv2 (3×3) – warstwa konwolucyjna, 8 kanałów wejściowych (tyle ile jest kanałów wyjścioowych z warstwy conv1), 16 kanałów wyjściowych
  • relu – funkcja aktywacji
  • max pooling – rozmiar okna 2×2, spowoduje 2-krotne zmniejszenie rozdzielczości z 32 na 16
  • conv3 (3×3) – 16 kanałów wejściowych, 24 wyjściowe
  • relu
  • max pooling – podobnie jak wyżej, rozmiar okna 2×2, spowoduje 2-krotne zmniejszenie rozdzielczości tym razem z 16 na 8
  • fc1 – warstwa liniowa (fully connected) o rozmiarze wejściowym 24*8*8 x 100 ( 24 kanały z ostatniej warstwy conv3, 8*8 to rozdzielczość obrazów zmniejszona już 4-krotnie przez dwie operacji max_pooling). Liczba 100 została dobrana na zasadzie bo tak mi się wydaje że będzie dobrze 🙂
  • output – warstwa wyjściowa o rozmiarze 100×10, 100 to wyjście poprzedniej warstwy a 10, bo mamy do rozpoznania 10 klas. Na wyjściu otrzymamy nieznormalizowane współczynniki przynależności do poszczególnych klas. Im wyższe tym większe prawdopodobieństwo przynależności.

Przygotowanie i wczytanie zbioru CIFAR-10

Jak wczytać dane z CIFAR-10 dokładnie opisałem w poprzednim artykule z serii, w którym opisuję jak stworzyć wielowarstwową sieć neuronową. Nie będę marnował miejsca tylko wrzucam kod bez większego tłumaczenia, jest on identyczny z tym z podlinkowanego wpisu.

transform = transforms.Compose(
    [transforms.ToTensor(),
     transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])

trainset = torchvision.datasets.CIFAR10(root='./data', train=True,
                                        download=True, transform=transform)
trainloader = torch.utils.data.DataLoader(trainset,
                                          batch_size=batch_size,
                                          shuffle=True)

testset = torchvision.datasets.CIFAR10(root='./data', train=False,
                                       download=True, transform=transform)
testloader = torch.utils.data.DataLoader(testset,
                                         batch_size=batch_size,
                                         shuffle=False)

classes = ('plane', 'car', 'bird', 'cat',
           'deer', 'dog', 'frog', 'horse', 'ship', 'truck')

Całość wykorzystuje transformery do przekształcenia danych w locie, klasę dataset oraz dataloaders.

Kod modułu sieci konwolucyjnej

Podstawową jednostką pozwalająca na zbudowanie własnej architektury w Pytorch jest klasa nn.Module. Może ona odzwierciedlać całą sieć lub jej część składową do wielokrotnego wykorzystania.
Stwórzmy klasę ConvNet dziedziczącą po nn.Module. W konstruktorze (__init__) definiujemy moduły (klocki: conv1, conv2, conv3, pool, fc1, output), z których będziemy składać sieć.

Do poprawnego działania musimy zaimplementować metodę forward, to ona określa sposób, w jaki chcemy, aby dane przepływały przez sieć. Metoda ta wywoływana jest w pętli uczącej z przekazaną paczką danych treningowych. Zamiast wywoływać ją jawnie net.forward(inputs) można skorzystać z wywołania obiektu w Python (Python calleble objects) w postaci net(inputs), gdzie net jest instancją klasy ConvNet.

class ConvNet(nn.Module):
    def __init__(self):
        super(ConvNet, self).__init__()

        self.pool = nn.MaxPool2d(2, 2)

        # channel_in=3 channels_out=8
        self.conv1 = nn.Conv2d(in_channels=3, out_channels=8, kernel_size=3, stride=1, padding=1)
        
        self.conv2 = nn.Conv2d(8, 16, kernel_size=3, stride=1, padding=1)

        self.conv3 = nn.Conv2d(16, 24, kernel_size=3, stride=1, padding=1)

        # 24 chaneels by 8x8 pixesl
        self.fc1 = nn.Linear(24 * 8 * 8, 100)

        self.output = nn.Linear(100, 10)

    def forward(self, x):

        x = F.relu(self.conv1(x))

        # max_pooling will resize input from 32 to 16
        x = self.pool(F.relu(self.conv2(x)))
        # max_pooling will resize input from 16 to 8
        x = self.pool(F.relu(self.conv3(x)))

        x = x.view(-1, 24 * 8 * 8)
        x = F.relu(self.fc1(x))
        x = self.output(x)
        return x

Metoda forward może przyjąć tyle parametrów z iloma wywołamy nasz moduł. W tym przykładzie przekazujemy tylko tensor wejściowy (x). Ma on wymiary [4, 3, 32, 32] czyli [batch_size, channels_RGB, width, height].

Cztery obrazy wejściowe (batch) są transformowane przez kolejne warstwy konwolucyjne. Polecam zdebuggowanie tego kawałka kodu i prześledzenie jak zmieniają się rozmiary x.shape. Zwróćcie uwagę na liczbę paczek=4, jak zmienia się liczba kanałów oraz rozmiary obrazka. Powinno to wyglądać następująco:

  • wejscie x =[4, 3, 32, 32] – 4 obrazy RGB o rozdzielczości 32×32 piksele
  • po conv1 x = [4, 8, 32, 32] – 4 „obrazy” o 8 kanałach o rozdzielczości 32×32
  • po conv2 i pooling x= [4, 16, 16, 16]
  • po conv3 i pooling x =[4, 24, 8, 8]
  • po x.view x = [4,1536] – wypłaszczamy x, wszystkie wymiary przechowujące dane „obrazu” – 24 kanały o rozmiarach 8×8 rozciągamy jako jeden długi wektor, robimy to dla 4 takich obrazów z paczki
  • po fc1 x = [4, 100] – rozmiar po pierwszej warstwie w pełni połączeniowej
  • po output x = [4, 10] – rozmiar na wyjściu, dla 4 obrazów mamy rozkład 10 wartości określających przynależność danego obrazu do jednej z 10 klas

Trening sieci neuronowej

Przed rozpoczęciem treningu sieci musimy:

net = ConvNet()
net.to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(net.parameters(), lr=0.001)

Następnie tworzymy pętlę ucząca. Jej zadaniem jest przejście po całym zbiorze danych, pobranie paczki danych (trainloader) uruchomienie modelu ( net(inputs) ) , przy pomocy funkcji starty określenie jak dalece model jest od prawdy ( criterion(outputs, labels) ), obliczenie gradientów (loss.backward) dla parametrów sieci i na końcu aktualizacja wag ( optimizer).

for epoch in range(num_epochs):  # loop over the dataset multiple times
    start_time = datetime.now()
    net.train()
    running_loss = 0.0
    epoch_loss = 0
    for i, data in enumerate(trainloader, 0):
        # get the inputs
        inputs, labels = data
        # move data to device (GPU if enabled, else CPU do nothing)
        inputs, labels = inputs.to(device), labels.to(device)

        # zero the parameter gradients
        optimizer.zero_grad()

        # forward + backward + optimize
        outputs = net(inputs)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()

        # print statistics
        epoch_loss += loss.item()

    epoch_loss = epoch_loss / len(trainloader)
    time_elapsed = datetime.now() - start_time

    # Test the model
    # set our model in the training mode
    net.eval()
    # In test phase, we don't need to compute gradients (for memory efficiency)
    correct = 0
    total = 0
    with torch.no_grad():
        for data in testloader:
            inputs, labels = data
            inputs, labels = inputs.to(device), labels.to(device)
            outputs = net(inputs)
            _, predicted = torch.max(outputs.data, 1)
            total += labels.size(0)
            correct += (predicted == labels).sum().item()

    # Accuracy of the network on the 10000 test images
    acc = correct/total
    print(
        f'Epoch [{epoch+1}/{num_epochs}], Loss: {epoch_loss:.4f} Test acc: {acc} time={time_elapsed}')

Walidacja modelu

Walidacji w tym przykładzie dokonuję w trakcie treningu, tak abyśmy na bieżąco widzieli postępy uczenia. Część walidacyjna zaczyna się od ustawienia sieci w trybie ewaluacyjnym net.eval(). Następnie sprawdzamy dokładność klasyfikacji na zbiorze testowym. Z wykorzystaniem testloader iterujemy po danych testowych i obliczamy predykcję (outputs = net(images) ).
Wyciągamy indeksy występowania największej wartości współczynnika przynależności do klasy
_, predicted = torch.max(outputs.data, 1)
i sprawdzamy, w jakim procencie zgadzają się dla całej paczki. Przykładowy wydruk na konsoli powinien wyglądać następująco:

Epoch [1/5], Loss: 1.3781 Test acc: 0.5827
Epoch [2/5], Loss: 1.0444 Test acc: 0.6432
Epoch [3/5], Loss: 0.9129 Test acc: 0.6623
Epoch [4/5], Loss: 0.8251 Test acc: 0.6738
Epoch [5/5], Loss: 0.7628 Test acc: 0.6643

Podsumowanie

Tutorial ten ma na celu nauczyć Cię tworzenia prostej sieci konwolucyjnej. Chciałem, abyście zwrócili uwagę i zapoznali się, w jaki sposób warstwy konwolucyjne przekształcają dane. Jak poprawnie określić kształty (shapes) poszczególnych warstw. Ja na początku miałem z tym problem, aby zrozumieć jak przekazać dane z jednej warstwy do drugiej. Często dostawałem wyjątki, że rozmiary się nie zgadzają (liczba kanałów lub rozmiary obrazu). Zamieszanie może także wprowadzać operacja ’max pooling’, która zmienia rozmiar obrazu.

Nie omawiałem tego w tej części, ale uprzedzam, że także sama warstwa konwolucyjna może zmieniać rozmiar obrazu. Obraz na wyjściu nie musi mieć już tych samych rozmiarów co wejściowy, związane jest to z arytmetyką działania operacji konwolucji (poczytajcie o stride i padding). Polecam tutaj publikację „A guide to convolution arithmetic for deep learning„, która bardzo przystępnie i obrazowo pokazuje wszystkie przypadki brzegowe.

Przykładowy obraz z publikacji z https://arxiv.org/abs/1603.07285


Cały kod tego przykładu znajduje się na moim github’ie w projekcie “Pytorch neural networks tutorial” w pliku conv_net_cifar.py 

Polecam na przeczytanie README.md w którym jest opisany sposób instalacji wszystkich niezbędnych bibliotek poprzez pipenv.

  1. Install Python.
  2. Install pipenv
  3. Git clone the repository
  4. Install all necessary python packages executing this command in terminal
git clone https://github.com/ksopyla/pytorch_neural_networks.git
cd pytorch_neural_networks
pipenv install


Znajdują się tam lub będą znajdować (zależy kiedy czytasz) także inne zaimplementowane modele i przykłady.

Dajcie znać w komentarzach, jeżeli Wam się przydał lub coś nie jest jasne.

Jeżeli uważasz ten wpis za wartościowy to Zasubskrybuj bloga. Dostaniesz informacje o nowych artykułach.

Join 100 other subscribers

Photo by Alistair MacRobert on Unsplash

Ciekawe, wartościowe, podziel się proszę opinią!

Witryna wykorzystuje Akismet, aby ograniczyć spam. Dowiedz się więcej jak przetwarzane są dane komentarzy.