Konvolutivne neuronske mreže¶

Preporučujem da pogledate sledeći materijal:
https://www.youtube.com/watch?v=KuXjwB4LzSA

Delovi materijala su preuzeti iz skripte "Mašinsko učenje" profesora Mladena Nikolića

Konvolutivne neuronske mreže zasnivaju se na sposobnosti mreža da konstruišu atribute, ali ne iz već definisanih atributa, već prvenstveno iz sirovog signala. Nazivaju se konvolutivnim zato što uče filtere čijom primenom detektuju određena svojstva signala.

U obradi signala su dugo korišćeni filteri koje su ručno dizajnirali inženjeri i istraživači, na primer filteri za detekciju ivica na slikama. Značaj konvolutivnih mreža upravo je u tome što ne zahtevaju da čovek unapred odredi koja su svojstva signala važna. Umesto toga, mreža sama, na osnovu podataka i zadatka koji rešava, uči koji filteri i koja svojstva signala su najrelevantniji. Ipak, ovakva fleksibilnost nosi i određene izazove, pre svega složeniju optimizaciju i potrebu za većom količinom podataka.

Ovaj tip mreža nalazi široku primenu u obradi različitih vrsta signala, a naročito u obradi slika, pa ćemo ih opisivati upravo na takvom primeru. Struktura konvolutivnih mreža obično podrazumeva smenjivanje dva tipa slojeva: konvolutivnog sloja (convolutional layer) i sloja agregacije (pooling layer), pri čemu se ista vrsta sloja može pojaviti i više puta zaredom.

Konvolutivni sloj sadrži više izlaznih mapa dobijenih procesom konvolucije ulazne slike sa filterima, kako bi se izdvojile karakteristične strukture na slici, kao što su ivice, prelazi i jednostavni oblici. Konvolucija podrazumeva pomeranje matrice filtera duž ulazne slike, pri čemu se pri svakom pomeraju računa odgovarajuća kombinacija elemenata posmatranog dela slike i filtera. Izlazi konvolutivnog sloja se zatim transformišu nelinearnom aktivacionom funkcijom. Rezultat je nova, filtrirana slika, odnosno matrica manjih dimenzija koja sadrži informacije o prisustvu određenih obrazaca u ulazu.

Neka su $f$ i $g$ dve matrice dimenzija $m \times n$ i $p \times q$. Konvolucija ovih matrica, u oznaci $f \ast g$, jeste matrica definisana formulom

$$ (f \ast g)_{ij} = \sum_{k=0}^{p-1} \sum_{l=0}^{q-1} f_{i-k, j-l} g_{k,l}. $$

U našem slučaju, $f$ predstavlja ulaznu matricu podataka, a $g$ predstavlja filter.

slika1.png

Kao što vidimo, konvolucijom se smanjuje dimenzija podataka, tj. gube se atributi. Da bismo to izbegli, možemo proširiti matricu nulama. Bolji način je proširivanje matrice vrednostima koje se već nalaze na obodu. Takođe, prilikom računanja konvolucije, filter se duž slike ne mora pomerati za jedan piksel, već za neki korak.
Na filtriranim slikama u nižim slojevima mreže se prepoznaju jednostavniji oblici, dok se u višim slojevima mogu razaznati i neke složenije konture koje sa nalaze na polaznoj slici.

slika2.png

U prethodnim razmatranjima nije diskutovano da li filter deluje samo nad tačno jednim kanalom prethodnog sloja ili nad više. U praksi se implementiraju višekanalne operacije konvolucije koje omogućavaju da filter deluje nad više kanala iz prethodnog sloja odjednom. Ovo je ilustrovano sledećom slikom.

slika6.png

Sledeći sloj koji smo pomenuli je sloj agregacije. Sloj agregacije ukrupnjuje informacije koje dobija iz prethodnog sloja, obično tako što računa neku jednostavnu funkciju agregacije susednih jedinica prethodnog sloja, poput maksimuma ili proseka. Ukoliko agregira, na primer, $3 \times 3$ piksela, onda je broj izlaza ovog sloja 9 puta manji od broja izlaza prethodnog. Maksimum je najpopularniji izbor funkcije agregacije. Ukoliko $3 \times 3$ jedinice prethodnog sloja predstavljaju ulaz u jednu jedinicu sloja agregacije i ako ona računa njihov maksimum, dolazi do zanemarivanja informacije o tome gde je precizno neko svojstvo pronađeno, ali se ne gubi informacija da je pronađeno. Ovakva vrsta zanemarivanja informacije često ne šteti cilju koji treba postići. Na primer, ako su na slici pronađeni oko, uvo, usta i nos, informacija o tačnoj poziciji najverovatnije nije bitna za odlučivanje da li se na slici nalazi lice. Naime, u realnosti, šanse da slika koja sadrži navedene elemente ne predstavlja lice su izuzetno male. Ipak, ukoliko je potrebno napraviti mrežu koja igra igru u kojoj su pozicije objekata na ekranu bitne, nije poželjno koristiti agregaciju. Uloga agregacije je smanjenje broja računskih operacija u višim slojevima, a i smanjenje broja parametara (koji može biti ogroman) u potpuno povezanoj mreži koja sledi za slojevima konvolucije i agregacije. Sve to rezultuje smanjenjem računske zahtevnosti pri optimizaciji i smanjenjem fleksibilnosti modela.

slika3.png

Broj slojeva i dimenzije filtera su hiperparametri.
Kao što smo rekli, na izlaze poslednjeg od tih slojeva se obično nadovezuje potpuno povezana mreža, koja uči nad atributima koje prethodni slojevi konstruišu. To je prikazano na sledećoj slici.

slika4.png

Kvaliteti i mane¶

Konvolutivne neuronske mreže predstavljaju veoma uspešan model za obradu signala u mašinskom učenju, naročito kada su u pitanju slike, zvuk i vremenske serije. Njihove dobre performanse posledica su nekoliko važnih osobina.

Prva važna osobina su proređene interakcije. To znači da je svaka jedinica povezana samo sa malim brojem jedinica iz prethodnog sloja, umesto sa svim jedinicama, kao kod potpuno povezanih mreža.

Druga važna osobina je deljenje parametara. Sve jedinice unutar jednog kanala koriste iste parametre, određene filterom tog kanala. Kada parametri ne bi bili deljeni, kao kod potpuno povezanih mreža, model bi učio posebne parametre za različite delove slike, čime bi pojedinim regionima davao posebnu semantiku. To na prvi pogled može delovati korisno, ali u mnogim zadacima nije poželjno. Na primer, kod detekcije lica na slici ne želimo da mreža nauči da nos mora da bude tačno na sredini slike, jer lice može biti pomereno ulevo, udesno, gore ili dole. Deljenje parametara omogućava da se isti filter koristi na svim pozicijama, pa mreža može da prepozna određeni obrazac bez obzira na njegov položaj.

Treća važna osobina jeste specijalizovanost za topologiju signala. I drugi modeli mašinskog učenja mogu da se primene na slike ili zvuk, ali oni često ne uzimaju u obzir susednost elemenata ulaza. Na primer, ako bi se slika posmatrala samo kao niz piksela, raspored piksela bio bi za model proizvoljan. Konvolutivne mreže su, međutim, konstruisane upravo tako da iskoriste činjenicu da su susedni pikseli međusobno povezani i da njihov prostorni raspored nosi važnu informaciju.

Još jedna korisna osobina, iako nije direktno vezana za performanse, jeste delimična interpretabilnost. Naime, moguće je vizualizovati obrasce na koje pojedini filteri najjače reaguju, što nam donekle pomaže da razumemo šta je mreža naučila.

S druge strane, konvolutivne mreže imaju i određena ograničenja. Jedno od njih je osetljivost na neke transformacije, poput rotacije i skaliranja. Drugi problem odnosi se na dimenzije ulaza. Pošto potpuno povezana mreža na kraju konvolutivnog dela očekuje ulaz fiksne dimenzije, i izlaz konvolutivnog dela mora imati tačno određene dimenzije. Zbog toga se u osnovnim varijantama konvolutivnih mreža obično zahteva da i svi ulazi budu istih dimenzija.

Postoje različiti pristupi kojima se ovi problemi mogu ublažiti.

Kada želimo veću otpornost na određene transformacije, jedna mogućnost je da agregaciju ne vršimo samo unutar jednog kanala, već da koristimo više kanala koji mogu naučiti da prepoznaju različito transformisane varijante istog obrasca. Tada agregacija maksimumom može da prijavi prisustvo obrasca čak i ako se on pojavi u nešto izmenjenom obliku.

Problem različitih dimenzija ulaza takođe se može rešavati na više načina. Neki pristupi su složeniji i zahtevaju izmene same arhitekture modela. Neki jednostavniji pristupi, kao što je skaliranje slike na unapred zadatu veličinu, mogu dovesti do gubitka informacije. Dodatno, pošto su konvolutivne mreže već osetljive na transformacije poput skaliranja, ovakvo rešenje nije idealno.

Jedan razuman kompromis jeste da rezolucija agregacije zavisi od dimenzija ulaza. Ovakav pristup se obično koristi na poslednjem nivou agregacije. Na primer, ako se za slike dimenzija $1000 \times 500$ na poslednjem nivou traži maksimum po blokovima dimenzija $3 \times 3$, onda bi se za slike dimenzija $2000 \times 1000$ mogao tražiti maksimum po blokovima dimenzija $6 \times 6$. Na taj način broj izlaznih vrednosti može ostati isti, pa i dimenzija ulaza u završni potpuno povezani deo mreže ostaje nepromenjena.

Pored problema koji su specifični za konvolutivne mreže, i dalje su prisutni i opštiji problemi koji se javljaju kod potpuno povezanih mreža, kao što su složena optimizacija, potreba za velikom količinom podataka i opasnost od preprilagođavanja. Iako su neki od tih problema ublaženi zahvaljujući strukturi konvolutivnih mreža, u praksi se često grade veoma velike mreže sa mnogo parametara, pa ti izazovi ponovo dolaze do izražaja.

Gde su danas CNN modeli?¶

Konvolutivne neuronske mreže su i dalje veoma važne u computer vision-u. One su prirodno prilagođene radu sa slikama, efikasno prepoznaju lokalne obrasce i i dalje se mnogo koriste u zadacima klasifikacije, detekcije i segmentacije.

Sa druge strane, transformeri su postali veoma važni zato što mogu uspešno da modeluju i globalne odnose u podacima. Kod slika, transformeri često posmatraju manje oblasti slike (patch-evi), a model uči odnose između njih. Zato su transformeri doneli novu perspektivu u obradu slika, naročito kod savremenih velikih modela.

Danas se u praksi koriste:

  • CNN modeli,
  • transformeri,
  • različiti hibridni pristupi koji kombinuju ideje iz oba sveta.

Primer¶

Demonstriraćemo upotrebu konvolutivnih neuronskih mreža na bazi ručno pisanih brojeva MNIST. MNIST sadrži crno-bele slike dimenzije 28x28 piksela na kojima se nalazi neka od cifara od 0 do 9.

Demonstriraćemo istu konvolutivnu neuronsku mrežu na dva načina:

  1. TensorFlow/Keras implementacija, koja je kompaktnija i pogodna za prvi susret sa neuronskim mrežama.
  2. PyTorch, koji zahteva nešto više koda, ali daje veću kontrolu nad time kako se model trenira.

Prvo ćemo učitati biblioteke koje će nam biti potrebne u nastavku.

In [ ]:
import random
import numpy as np
from matplotlib import pyplot as plt

import tensorflow as tf
from tensorflow import keras
from tensorflow.keras.datasets import mnist
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Input, Dense, Dropout, Flatten, Conv2D, MaxPooling2D
from tensorflow.keras.losses import CategoricalCrossentropy
from tensorflow.keras.optimizers import Adam

import torch
from torch import nn
from torch.utils.data import TensorDataset, DataLoader

Učitavanje podataka¶

Radićemo sa MNIST skupom podataka. Skup sadrži 60000 slika za treniranje i 10000 slika za testiranje. Slike su crno-bele, dimenzije 28 × 28 piksela, i predstavljaju ručno pisane cifre od 0 do 9.

In [ ]:
(X_train_full, y_train_full), (X_test_raw, y_test_raw) = mnist.load_data()

print("Dimenzije trening skupa:", X_train_full.shape)
print("Dimenzije test skupa:", X_test_raw.shape)
Dimenzije trening skupa: (60000, 28, 28)
Dimenzije test skupa: (10000, 28, 28)

Prikazaćemo nekoliko primera iz trening skupa.

In [ ]:
plt.imshow(X_train_full[0], cmap='gray')
plt.title(f"Cifra: {y_train_full[0]}")
plt.show()
plt.show()
In [ ]:
plt.imshow(X_train_full[1], cmap='gray')
plt.title(f"Cifra: {y_train_full[1]}")
plt.show()
plt.show()
In [ ]:
val_size = 1000 # iz trening skupa izdvajamo validacioni skup
# u ovom slučaju nećemo uzimati validacioni skup veličine 20% vaze, pošto nam je i 1000 instanci sasvim relevantno za
# računanje tačnosti modela

X_train_raw = X_train_full[:-val_size]
y_train_raw = y_train_full[:-val_size]

X_val_raw = X_train_full[-val_size:]
y_val_raw = y_train_full[-val_size:]

print("Trening skup:", X_train_raw.shape)
print("Validacioni skup:", X_val_raw.shape)
print("Test skup:", X_test_raw.shape)
Trening skup: (59000, 28, 28)
Validacioni skup: (1000, 28, 28)
Test skup: (10000, 28, 28)

TensorFlow/Keras implementacija¶

Kod TensorFlow/Keras biblioteke slike se najčešće čuvaju u channels last formatu: (broj_instanci, visina, širina, broj_kanala).

Pošto MNIST slike imaju samo jedan kanal, svaki ulaz će imati dimenzije (28, 28, 1).

In [ ]:
img_rows, img_cols = X_train_raw.shape[1], X_train_raw.shape[2]
number_of_classes = len(np.unique(y_train_full))

X_train_tf = X_train_raw.astype('float32') / 255.0
X_val_tf = X_val_raw.astype('float32') / 255.0
X_test_tf = X_test_raw.astype('float32') / 255.0

X_train_tf = X_train_tf[..., np.newaxis]
X_val_tf = X_val_tf[..., np.newaxis]
X_test_tf = X_test_tf[..., np.newaxis]

y_train_tf = keras.utils.to_categorical(y_train_raw, number_of_classes)
y_val_tf = keras.utils.to_categorical(y_val_raw, number_of_classes)
y_test_tf = keras.utils.to_categorical(y_test_raw, number_of_classes)

input_shape = (img_rows, img_cols, 1)

print("Dimenzije TensorFlow trening ulaza:", X_train_tf.shape)
print("Dimenzije TensorFlow trening izlaza:", y_train_tf.shape)
Dimenzije TensorFlow trening ulaza: (59000, 28, 28, 1)
Dimenzije TensorFlow trening izlaza: (59000, 10)

Naš model će imati dve konvolucije, jedan agregacioni sloj, regularizaciju pomoću Dropout, a zatim i potpuno povezani klasifikacioni deo.

In [ ]:
model_tf = Sequential([
    Input(shape=input_shape),
    Conv2D(filters=32, kernel_size=(3, 3), padding='same', activation='relu'),
    Conv2D(filters=64, kernel_size=(3, 3), padding='same', activation='relu'),
    MaxPooling2D(pool_size=(2, 2)),
    Dropout(0.25),
    Flatten(),
    Dense(128, activation='relu'),
    Dropout(0.5),
    Dense(number_of_classes, activation='softmax')])

Prikaz modela i broj parametara koje mreža treba da nauči možemo dobiti pozivom metode summary().

In [ ]:
model_tf.summary()
Model: "sequential_1"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 conv2d_2 (Conv2D)           (None, 28, 28, 32)        320       
                                                                 
 conv2d_3 (Conv2D)           (None, 28, 28, 64)        18496     
                                                                 
 max_pooling2d_1 (MaxPooling  (None, 14, 14, 64)       0         
 2D)                                                             
                                                                 
 dropout_2 (Dropout)         (None, 14, 14, 64)        0         
                                                                 
 flatten_1 (Flatten)         (None, 12544)             0         
                                                                 
 dense_2 (Dense)             (None, 128)               1605760   
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 conv2d_2 (Conv2D)           (None, 28, 28, 32)        320       
                                                                 
 conv2d_3 (Conv2D)           (None, 28, 28, 64)        18496     
                                                                 
 max_pooling2d_1 (MaxPooling  (None, 14, 14, 64)       0         
 2D)                                                             
                                                                 
 dropout_2 (Dropout)         (None, 14, 14, 64)        0         
                                                                 
 flatten_1 (Flatten)         (None, 12544)             0         
                                                                 
 dense_2 (Dense)             (None, 128)               1605760   
                                                                 
 dropout_3 (Dropout)         (None, 128)               0         
                                                                 
 dense_3 (Dense)             (None, 10)                1290      
                                                                 
=================================================================
Total params: 1,625,866
Trainable params: 1,625,866
Non-trainable params: 0
_________________________________________________________________

Za funkciju gubitaka koristićemo kategoričku unakrsnu entropiju, a za optimizaciju Adamov algoritam.

In [ ]:
model_tf.compile(loss=CategoricalCrossentropy(),
    optimizer=Adam(learning_rate=0.001), metrics=['accuracy'])

Mrežu ćemo trenirati u 8 epoha, a veličina batch-a je 128. To znači da kroz trening skup prolazimo ukupno 8 puta, pri čemu uzimamo skupove od po 128 instanci i radimo stohastički gradijentni spust. Tačnost modela pratimo i na trening i na validacionom skupu.

In [ ]:
history_tf = model_tf.fit(X_train_tf, y_train_tf,
    batch_size=128, epochs=8, verbose=1, validation_data=(X_val_tf, y_val_tf))
# vreme: oko 6 minuta
Epoch 1/8
461/461 [==============================] - 41s 88ms/step - loss: 0.2466 - accuracy: 0.9238 - val_loss: 0.0735 - val_accuracy: 0.9870
Epoch 2/8
461/461 [==============================] - 42s 91ms/step - loss: 0.0891 - accuracy: 0.9735 - val_loss: 0.0575 - val_accuracy: 0.9900
Epoch 3/8
461/461 [==============================] - 42s 91ms/step - loss: 0.0625 - accuracy: 0.9810 - val_loss: 0.0592 - val_accuracy: 0.9890
Epoch 4/8
461/461 [==============================] - 42s 92ms/step - loss: 0.0525 - accuracy: 0.9841 - val_loss: 0.0533 - val_accuracy: 0.9920
Epoch 5/8
461/461 [==============================] - 43s 93ms/step - loss: 0.0426 - accuracy: 0.9866 - val_loss: 0.0566 - val_accuracy: 0.9910
Epoch 6/8
461/461 [==============================] - 49s 107ms/step - loss: 0.0382 - accuracy: 0.9884 - val_loss: 0.0436 - val_accuracy: 0.9930
Epoch 7/8
461/461 [==============================] - 47s 101ms/step - loss: 0.0335 - accuracy: 0.9895 - val_loss: 0.0541 - val_accuracy: 0.9920
Epoch 8/8
461/461 [==============================] - 49s 106ms/step - loss: 0.0298 - accuracy: 0.9904 - val_loss: 0.0599 - val_accuracy: 0.9930

Možemo nacrtati i grafike funkcije gubitka i tačnosti u toku treniranja.

In [ ]:
plt.figure(figsize=(12, 4))

plt.subplot(1, 2, 1)
plt.title('TensorFlow/Keras: funkcija gubitka')
plt.plot(np.linspace(1, 8, 8), history_tf.history['loss'], label='train')
plt.plot(np.linspace(1, 8, 8), history_tf.history['val_loss'], label='val')
plt.xlabel('Epoha')
plt.legend(loc='best')

plt.subplot(1, 2, 2)
plt.title('TensorFlow/Keras: tačnost')
plt.plot(np.linspace(1, 8, 8), history_tf.history['accuracy'], label='train')
plt.plot(np.linspace(1, 8, 8), history_tf.history['val_accuracy'], label='val')
plt.xlabel('Epoha')
plt.legend(loc='best')

plt.tight_layout()
plt.show()

Model ćemo oceniti na test skupu.

In [ ]:
score_tf = model_tf.evaluate(X_test_tf, y_test_tf, verbose=0)
print('Test loss:', score_tf[0]) # funkcija gubitaka
print('Test accuracy:', score_tf[1]) # tačnost modela
Test loss: 0.027447756379842758
Test accuracy: 0.991599977016449

PyTorch¶

U PyTorch-u se obično eksplicitnije pišu pojedini koraci rada sa modelom. Posebno su važni sledeći pojmovi:

  • tenzor kao osnovna struktura podataka,
  • Dataset i DataLoader za rad sa podacima,
  • klasa nn.Module za definisanje modela,

Jedna od prvih tehničkih razlika u odnosu na TensorFlow/Keras je zapis slika: u PyTorch-u se za konvolutivne mreže najčešće koristi channels first format, tj.
(broj_instanci, broj_kanala, visina, širina).

Pripremićemo iste podatke, samo sada u obliku pogodnom za PyTorch. U PyTorch-u kod klasifikacije ostavljamo ciljne vrednosti kao cele brojeve od 0 do 9, umesto da ih prevodimo u one-hot zapis.

In [ ]:
X_train_torch = torch.tensor(X_train_raw.astype('float32') / 255.0).unsqueeze(1)
# unsqueeze dodaje dimenziju channel-a
X_val_torch = torch.tensor(X_val_raw.astype('float32') / 255.0).unsqueeze(1)
X_test_torch = torch.tensor(X_test_raw.astype('float32') / 255.0).unsqueeze(1)

y_train_torch = torch.tensor(y_train_raw, dtype=torch.long)
y_val_torch = torch.tensor(y_val_raw, dtype=torch.long)
y_test_torch = torch.tensor(y_test_raw, dtype=torch.long)

train_dataset = TensorDataset(X_train_torch, y_train_torch)
val_dataset = TensorDataset(X_val_torch, y_val_torch)
test_dataset = TensorDataset(X_test_torch, y_test_torch)

train_loader = DataLoader(train_dataset, batch_size=128, shuffle=True)
val_loader = DataLoader(val_dataset, batch_size=128, shuffle=False)
test_loader = DataLoader(test_dataset, batch_size=128, shuffle=False)

print("Dimenzije PyTorch trening ulaza:", X_train_torch.shape)
print("Dimenzije PyTorch trening izlaza:", y_train_torch.shape)
Dimenzije PyTorch trening ulaza: torch.Size([59000, 1, 28, 28])
Dimenzije PyTorch trening izlaza: torch.Size([59000])
In [ ]:
device = torch.device("cpu")   # mogućnosti su najčešće:
                               # "cpu"  -> izvršavanje na procesoru
                               # "cuda" -> izvršavanje na NVIDIA GPU-u
                               # "mps"  -> izvršavanje na Apple GPU-u

Definisaćemo isti model, sada kroz klasu koja nasleđuje nn.Module. Pošto ćemo za funkciju gubitka koristiti CrossEntropyLoss, izlazni sloj neće imati softmax aktivaciju, ta funkcija je već ugrađena u samu definiciju gubitka.

In [ ]:
class CNNPyTorch(nn.Module):
    def __init__(self, number_of_classes=10):
        super().__init__()

        self.features = nn.Sequential(
            nn.Conv2d(in_channels=1, out_channels=32, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.Conv2d(in_channels=32, out_channels=64, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2),
            nn.Dropout(p=0.25))

        self.classifier = nn.Sequential(
            nn.Flatten(),
            nn.Linear(64 * 14 * 14, 128),
            nn.ReLU(),
            nn.Dropout(p=0.5),
            nn.Linear(128, number_of_classes))

    def forward(self, x):
        x = self.features(x)
        x = self.classifier(x)
        return x


model_torch = CNNPyTorch(number_of_classes=number_of_classes).to(device)
print(model_torch)

number_of_parameters = sum(p.numel() for p in model_torch.parameters() if p.requires_grad)
print("Broj parametara:", number_of_parameters)
CNNPyTorch(
  (features): Sequential(
    (0): Conv2d(1, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (1): ReLU()
    (2): Conv2d(32, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (3): ReLU()
    (4): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (5): Dropout(p=0.25, inplace=False)
  )
  (classifier): Sequential(
    (0): Flatten(start_dim=1, end_dim=-1)
    (1): Linear(in_features=12544, out_features=128, bias=True)
    (2): ReLU()
    (3): Dropout(p=0.5, inplace=False)
    (4): Linear(in_features=128, out_features=10, bias=True)
  )
)
Broj parametara: 1625866

Za PyTorch deo eksplicitno definišemo funkciju gubitka, optimizator i funkcije za jednu epohu treniranja i evaluacije.

In [ ]:
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model_torch.parameters(), lr=0.001)


def run_epoch(model, data_loader, loss_fn, optimizer=None, device='cpu'):
    if optimizer is None:
        model.eval()
    else:
        model.train()

    total_loss = 0.0
    total_correct = 0
    total_examples = 0

    with torch.set_grad_enabled(optimizer is not None):
        for X_batch, y_batch in data_loader:
            X_batch = X_batch.to(device)
            y_batch = y_batch.to(device)

            logits = model(X_batch)
            loss = loss_fn(logits, y_batch)

            if optimizer is not None:
                optimizer.zero_grad()
                loss.backward()
                optimizer.step()

            total_loss += loss.item() * X_batch.size(0)
            predictions = logits.argmax(dim=1)
            total_correct += (predictions == y_batch).sum().item()
            total_examples += X_batch.size(0)

    average_loss = total_loss / total_examples
    accuracy = total_correct / total_examples

    return average_loss, accuracy

Sada možemo da istreniramo model. Kao i u TensorFlow/Keras delu, pratićemo i gubitak i tačnost na trening i validacionom skupu.

In [ ]:
history_torch = {
    'loss': [],
    'accuracy': [],
    'val_loss': [],
    'val_accuracy': []
}

for epoch in range(8):
    train_loss, train_accuracy = run_epoch(
        model_torch, train_loader, loss_fn, optimizer=optimizer, device=device)
    val_loss, val_accuracy = run_epoch(
        model_torch, val_loader, loss_fn, optimizer=None, device=device
    )

    history_torch['loss'].append(train_loss)
    history_torch['accuracy'].append(train_accuracy)
    history_torch['val_loss'].append(val_loss)
    history_torch['val_accuracy'].append(val_accuracy)

    print(
        f"Epoha {epoch + 1:02d}/{8} | "
        f"train_loss={train_loss:.4f}, train_acc={train_accuracy:.4f} | "
        f"val_loss={val_loss:.4f}, val_acc={val_accuracy:.4f}"
    )
Epoha 01/8 | train_loss=0.2891, train_acc=0.9109 | val_loss=0.0848, val_acc=0.9840
Epoha 02/8 | train_loss=0.1013, train_acc=0.9702 | val_loss=0.0796, val_acc=0.9870
Epoha 03/8 | train_loss=0.0742, train_acc=0.9776 | val_loss=0.0744, val_acc=0.9850
Epoha 04/8 | train_loss=0.0617, train_acc=0.9819 | val_loss=0.0670, val_acc=0.9910
Epoha 05/8 | train_loss=0.0523, train_acc=0.9840 | val_loss=0.0708, val_acc=0.9890
Epoha 06/8 | train_loss=0.0455, train_acc=0.9861 | val_loss=0.0736, val_acc=0.9910
Epoha 07/8 | train_loss=0.0434, train_acc=0.9867 | val_loss=0.0819, val_acc=0.9910
Epoha 08/8 | train_loss=0.0386, train_acc=0.9881 | val_loss=0.0699, val_acc=0.9910

Grafički možemo prikazati kako su se menjali gubitak i tačnost tokom treniranja PyTorch modela.

In [ ]:
plt.figure(figsize=(12, 4))

plt.subplot(1, 2, 1)
plt.title('PyTorch: funkcija gubitka')
plt.plot(np.linspace(1, 8, 8), history_torch['loss'], label='train')
plt.plot(np.linspace(1, 8, 8), history_torch['val_loss'], label='val')
plt.xlabel('Epoha')
plt.legend(loc='best')

plt.subplot(1, 2, 2)
plt.title('PyTorch: tačnost')
plt.plot(np.linspace(1, 8, 8), history_torch['accuracy'], label='train')
plt.plot(np.linspace(1, 8, 8), history_torch['val_accuracy'], label='val')
plt.xlabel('Epoha')
plt.legend(loc='best')

plt.tight_layout()
plt.show()

I na kraju ćemo evaluirati model na test skupu.

In [ ]:
test_loss_torch, test_accuracy_torch = run_epoch(
    model_torch, test_loader, loss_fn, optimizer=None, device=device)

print('PyTorch test loss:', test_loss_torch)
print('PyTorch test accuracy:', test_accuracy_torch)
PyTorch test loss: 0.027140886462142227
PyTorch test accuracy: 0.991