AI & GPU
Jak łatwo zrozumieć ResNet w PyTorch

Jak łatwo zrozumieć ResNet w PyTorch

Wprowadzenie do ResNet'a

Czym jest ResNet?

ResNet, czyli Sieć Neuronowa Residualna to architektura głębokiego uczenia wprowadzona w 2015 roku przez badaczy z Microsoftu. Zaprojektowano ją w celu rozwiązania problemu znikającego/eksplodującego gradientu, który jest powszechnym problemem podczas treningu bardzo głębokich sieci neuronowych.

  1. Sieć Neuronowa Residualna: ResNet to rodzaj sieci neuronowej, która wykorzystuje "skip connections" lub "residual connections" w celu umożliwienia trenowania znacznie głębszych modeli. Te "skip connections" pozwalają sieci na pominięcie pewnych warstw, co efektywnie tworzy "skrót", który pomaga w łagodzeniu problemu znikającego gradientu.

  2. Rozwiązanie problemu znikającego/eksplodującego gradientu: W bardzo głębokich sieciach neuronowych gradienty używane do wstecznej propagacji mogą zarówno zanikać (stawać się bardzo małe), jak i eksplodować (zmieniać się bardzo duże), w miarę jak są propagowane wstecz przez sieć. Może to utrudniać efektywne uczenie sieci, zwłaszcza w głębszych warstwach. "Skip connections" w ResNet pomagają rozwiązać ten problem, umożliwiając płynniejszy przepływ gradientów przez sieć.

Zalety ResNet'a

  1. Poprawiona wydajność głębokich sieci neuronowych: "Skip connections" w ResNet umożliwiają trening znacznie głębszych sieci neuronowych, co może prowadzić do znaczącego wzrostu wydajności w różnorodnych zadaniach, takich jak klasyfikacja obrazów, wykrywanie obiektów i segmentacja semantyczna.

  2. Szybsza zbieżność podczas treningu: "Skip connections" w ResNet mogą również pomóc sieci osiągnąć szybszą zbieżność podczas procesu treningu, ponieważ pozwalają gradientom bardziej efektywnie przepływać przez sieć.

Implementacja ResNet'a w PyTorch'u

Konfiguracja środowiska

  1. Instalacja PyTorch'a: Aby rozpocząć implementację ResNet w PyTorch'u, musisz najpierw zainstalować bibliotekę PyTorch. Możesz pobrać i zainstalować PyTorch ze strony internetowej (https://pytorch.org/ (opens in a new tab)) w zależności od swojego systemu operacyjnego i wersji Pythona.

  2. Importowanie niezbędnych bibliotek: Po zainstalowaniu PyTorch'a, musisz zaimportować niezbędne biblioteki do swojego projektu. Zazwyczaj obejmuje to PyTorch, NumPy i inne biblioteki, które mogą być potrzebne do przetwarzania danych, wizualizacji itp.

import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
import numpy as np
import matplotlib.pyplot as plt

Definiowanie architektury ResNet'a

Zrozumienie podstawowych bloków budujących

  1. Warstwy konwolucyjne: ResNet, podobnie jak wiele innych modeli głębokiego uczenia, wykorzystuje warstwy konwolucyjne jako podstawowe bloki budujące do ekstrakcji cech.

  2. Batch Normalization: ResNet wykorzystuje także warstwy Batch Normalization, które mają na celu stabilizację procesu trenowania i poprawienie wydajności modelu.

  3. Funkcje aktywacji: Architektura ResNet zazwyczaj wykorzystuje funkcję aktywacji ReLU (Rectified Linear Unit), która wprowadza nieliniowość do modelu.

  4. Warstwy pooling'u: ResNet może również zawierać warstwy pooling'u, takie jak max-pooling lub average-pooling, w celu zmniejszenia przestrzennych wymiarów map cech i wprowadzenia inwariancji translacyjnej.

Implementacja bloku ResNet

  1. Residual Connection: Kluczowym innowacyjnym elementem ResNet jest "residual connection", która pozwala sieci na pominięcie pewnych warstw poprzez dodanie wejścia warstwy do jej wyjścia. Pomaga to w łagodzeniu problemu znikającego gradientu.
class ResNetBlock(nn.Module):
    def __init__(self, in_channels, out_channels, stride=1):
        super(ResNetBlock, self).__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.relu = nn.ReLU(inplace=True)
        self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=1, padding=1, bias=False)
        self.bn2 = nn.BatchNorm2d(out_channels)
        
        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):
        residual = self.shortcut(x)
        out = self.conv1(x)
        out = self.bn1(out)
        out = self.relu(out)
        out = self.conv2(out)
        out = self.bn2(out)
        out += residual
        out = self.relu(out)
        return out
  1. Shortcut Connection: Oprócz połączenia "residual connection", ResNet wykorzystuje także "shortcut connection", aby dopasować wymiary wejścia i wyjścia bloku ResNet, jeżeli jest to konieczne.

Konstruowanie pełnego modelu ResNet'a

  1. Stosowanie bloków ResNet'a: Aby stworzyć pełny model ResNet'a, należy zastosować wiele bloków ResNet'a, dostosowując liczbę warstw i liczbę filtrów w każdym bloku.

  2. Dostosowywanie liczby warstw: Modele ResNet występują w różnych wariantach, takich jak ResNet-18, ResNet-34, ResNet-50, ResNet-101 i ResNet-152, które mają różną liczbę warstw. Liczba warstw wpływa na złożoność i wydajność modelu.

Implementacja ResNet-18 w PyTorch'u

Definiowanie modelu ResNet-18

  1. Warstwa wejściowa: Warstwa wejściowa modelu ResNet-18 zazwyczaj przyjmuje obraz o określonym rozmiarze, na przykład 224x224 pikseli.

  2. Warstwy konwolucyjne: Początkowe warstwy konwolucyjne modelu ResNet-18 wydobywają podstawowe cechy z obrazu wejściowego.

  3. Bloki ResNet: Istota modelu ResNet-18 polega na składaniu wielu bloków ResNet, które wykorzystują "residual connections" do umożliwienia treningu głębszej sieci.

  4. Warstwa w pełni połączona: Po blokach konwolucyjnych i ResNet, model będzie mieć warstwę w pełni połączoną, która wykonuje ostateczne zadanie klasyfikacji lub predykcji.

  5. Warstwa wyjściowa: Warstwa wyjściowa modelu ResNet-18 będzie miała określoną liczbę jednostek odpowiadającą liczbie klas w rozwiązywanym problemie.

class ResNet18(nn.Module):
    def __init__(self, num_classes=10):
        super(ResNet18, self).__init__()
        self.in_channels = 64
        self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3, bias=False)
        self.bn1 = nn.BatchNorm2d(64)
        self.relu = nn.ReLU(inplace=True)
        self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
 
        self.layer1 = self._make_layer(64, 64, 2, stride=1)
        self.layer2 = self._make_layer(64, 128, 2, stride=2)
        self.layer3 = self._make_layer(128, 256, 2, stride=2)
        self.layer4 = self._make_layer(256, 512, 2, stride=2)
 
        self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
        self.fc = nn.Linear(512, num_classes)
 
    def _make_layer(self, in_channels, out_channels, num_blocks, stride):
        layers = []
        layers.append(ResNetBlock(in_channels, out_channels, stride))
        self.in_channels = out_channels
        for i in range(1, num_blocks):
            layers.append(ResNetBlock(out_channels, out_channels))
        return nn.Sequential(*layers)
 
    def forward(self, x):
        x = self.conv1(x)
        x = self.bn1(x)
        x = self.relu(x)
        x = self.maxpool(x)
 
        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.layer4(x)
 
        x = self.avgpool(x)
        x = x.view(x.size(0), -1)
        x = self.fc(x)
        return x

Inicjalizacja modelu

Aby utworzyć instancję modelu ResNet-18, po prostu można utworzyć obiekt klasy ResNet18:

model = ResNet18(num_classes=10)

Wypisanie podsumowania modelu

Można wydrukować podsumowanie architektury modelu ResNet-18, używając funkcji summary() z biblioteki torchsummary:

from torchsummary import summary
summary(model, input_size=(3, 224, 224))

Dostarczy to szczegółowy przegląd warstw modelu, w tym liczby parametrów i kształtu wyjściowego każdej warstwy.

Trenowanie modelu ResNet-18

Przygotowanie zbioru danych

Pobieranie i ładowanie zbioru danych

W tym przykładzie będziemy używać zbioru danych CIFAR-10, który jest powszechnie stosowany do zadań klasyfikacji obrazów. Można pobrać zbiór danych za pomocą modułu torchvision.datasets.CIFAR10:

# Pobierz i załaduj zbiór danych CIFAR-10
train_dataset = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=transforms.ToTensor())
test_dataset = torchvision.datasets.CIFAR10(root='./data', train=False, download=True, transform=transforms.ToTensor())

Przetwarzanie danych

Przed trenowaniem modelu należy przetworzyć dane, takie jak normalizacja wartości pikseli i zastosowanie technik augmentacji danych:

# Zdefiniuj transformacje danych
transform_train = transforms.Compose([
    transforms.RandomCrop(32, padding=4),
    transforms.RandomHorizontalFlip(),
    transforms.ToTensor(),
    transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010))
])
 
transform_test = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010))
])
 
# Utwórz wczytywacze danych
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=128, shuffle=True, num_workers=2)
test_loader = torch.utils.data.DataLoader(test_dataset, batch_size=100, shuffle=False, num_workers=2)

Definiowanie pętli trenowania

Ustawienie urządzenia (CPU lub GPU)

Aby skorzystać z przyspieszenia GPU, można przenieść model i dane na GPU:

device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
model = model.to(device)

Definiowanie funkcji straty i optymalizatoraNastępnie będziesz musiał zdefiniować funkcję straty i optymalizator do użycia podczas procesu szkolenia:

criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.1, momentum=0.9, weight_decay=5e-4)

Implementacja pętli szkoleniowej

Pętla szkoleniowa będzie obejmować następujące kroki:

  1. Przekazanie przez model
  2. Obliczenie straty
  3. Propagacja wsteczna gradientów
  4. Aktualizacja parametrów modelu
  5. Śledzenie straty szkoleniowej i dokładności
num_epochs = 100
train_losses = []
train_accuracies = []
val_losses = []
val_accuracies = []
 
for epoch in range(num_epochs):
    # Faza szkoleniowa
    model.train()
    running_loss = 0.0
    correct = 0
    total = 0
    for i, (inputs, labels) in enumerate(train_loader):
        inputs, labels = inputs.to(device), labels.to(device)
        
        optimizer.zero_grad()
        outputs = model(inputs)
        loss = criterion(outputs, labels)
 
## Optymalizacja modelu
 
### Regularyzacja
 
Regularyzacja to technika stosowana w uczeniu głębokim w celu zapobieżenia nadmiernemu dopasowaniu modelów. Nadmierne dopasowanie występuje, gdy model dobrze działa na danych szkoleniowych, ale nie potrafi uogólnić się do nowych, niewidzianych danych. Techniki regularyzacji pomagają modelowi uogólniać się lepiej poprzez wprowadzenie kary za złożoność lub dodawanie szumu do procesu szkoleniowego.
 
Jedną popularną techniką regularyzacji jest regularyzacja L2, znana również jako degradacja wag. Ta metoda dodaje term w karze do funkcji straty, który jest proporcjonalny do kwadratowej wielkości wag modelu. Funkcję straty z regularyzacją L2 można zapisać jako:
 

loss = original_loss + lambda * sum(w^2)


w której `lambda` to siła regularyzacji, a `w` to wagi modelu.

Inną popularną techniką regularyzacji jest Dropout. Dropout losowo ustawia część aktywacji w warstwie na zero podczas szkolenia, co efektywnie zmniejsza pojemność modelu i zmusza go do uczenia bardziej wytrzymałych cech. Pomaga to w zapobieganiu nadmiernemu dopasowaniu i może poprawić zdolność modelu do uogólniania.

Oto przykład implementacji Dropout w modelu PyTorch:

```python
import torch.nn as nn

class MyModel(nn.Module):
    def __init__(self):
        super(MyModel, self).__init__()
        self.fc1 = nn.Linear(64, 128)
        self.dropout = nn.Dropout(p=0.5)
        self.fc2 = nn.Linear(128, 10)

    def forward(self, x):
        x = self.fc1(x)
        x = self.dropout(x)
        x = self.fc2(x)
        return x

W tym przykładzie warstwa Dropout jest stosowana po pierwszej warstwie w pełni połączonej, z współczynnikiem wyłączania wynoszącym 0,5, co oznacza, że 50% aktywacji zostanie losowo ustawionych na zero podczas szkolenia.

Algorytmy optymalizacji

Wybór algorytmu optymalizacji może mieć znaczący wpływ na wydajność i zbieżność modelu uczącego się głębokiego uczenia. Oto kilka popularnych algorytmów optymalizacji stosowanych w uczeniu głębokim:

Stochastyczny Gradient Descent (SGD)

SGD to najprostszy algorytm optymalizacji, w którym obliczane są gradienty dla pojedynczego przykładu szkoleniowego lub małej partii przykładów, a następnie wagi są aktualizowane. SGD może działacz powoli, ale jest prosty i skuteczny.

import torch.optim as optim
 
model = MyModel()
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9)

Adam

Adam (Adaptive Moment Estimation) to bardziej zaawansowany algorytm optymalizacji, który oblicza adaptacyjne współczynniki uczenia dla każdego parametru. Łączy on korzyści ze strony momentum i RMSProp, co czyni go popularnym wyborem dla wielu zadań uczenia głębokiego.

optimizer = optim.Adam(model.parameters(), lr=0.001)

AdaGrad

AdaGrad (Adaptive Gradient) to algorytm optymalizacji, który dostosowuje współczynnik uczenia dla każdego parametru na podstawie historycznych gradientów. Jest skuteczny dla danych rzadkich, ale może cierpieć z powodu agresywnego zmniejszania współczynnika uczenia z czasem.

optimizer = optim.Adagrad(model.parameters(), lr=0.01)

RMSProp

RMSProp (Root Mean Square Propagation) to kolejny adaptacyjny algorytm optymalizacji współczynnika uczenia, który utrzymuje ruchomą średnią kwadratowych gradientów. Jest szczególnie przydatny dla celów niestacjonarnych, takich jak te występujące w rekurencyjnych sieciach neuronowych.

optimizer = optim.RMSprop(model.parameters(), lr=0.001, alpha=0.99)

Wybór algorytmu optymalizacji zależy od konkretnego problemu, struktury modelu i charakterystyki danych. Często dobrze jest eksperymentować z różnymi algorytmami i porównywać ich wydajność w przypadku Twojego zadania.

Transfer Learning

Transfer learning to technika polegająca na wykorzystaniu modelu nauczonego na dużym zbiorze danych jako punktu startowego dla modelu na różnym, ale powiązanym zadaniu. Może to być szczególnie przydatne, gdy docelowy zbiór danych jest mały, ponieważ umożliwia modelowi wykorzystanie cech nauczonej na większym zbiorze danych.

Jednym z powszechnych podejść transferowych w uczeniu głębokim jest użycie wstępnie nauczonego modelu, takiego jak te dostępne dla popularnych zadań wizji komputerowej lub przetwarzania języka naturalnego, i dostrojenie modelu na docelowym zbiorze danych. Polega to na zamrożeniu niższych warstw wstępnie nauczonego modelu i trenowaniu tylko wyższych warstw na nowych danych.

Oto przykład dostrojenia wcześniej nauczonego modelu ResNet do zadania klasyfikacji obrazów w PyTorch:

import torchvision.models as models
import torch.nn as nn
 
# Wczytaj wstępnie nauczony model ResNet
resnet = models.resnet18(pretrained=True)
 
# Zamroź parametry modelu wstępnie nauczonego
for param in resnet.parameters():
    param.requires_grad = False
 
# Zamień ostatnią warstwę na nową w pełni połączoną warstwę
num_features = resnet.fc.in_features
resnet.fc = nn.Linear(num_features, 10)  # Zakładając 10 klas
 
# Trenuj nowy model na nowym zbiorze danych
optimizer = optim.Adam(resnet.fc.parameters(), lr=0.001)

W tym przykładzie najpierw wczytujemy wstępnie nauczony model ResNet18 i zamrażamy parametry niższych warstw. Następnie zamieniamy ostatnią w pełni połączoną warstwę na nową warstwę z odpowiednią liczbą wyjść dla naszego zadania (w tym przypadku 10 klas). Na końcu trenujemy model używając optymalizatora Adam, aktualizując tylko parametry nowej warstwy w pełni połączonej.

Transfer learning może znacząco poprawić wydajność modeli uczenia głębokiego, zwłaszcza gdy docelowy zbiór danych jest mały. To potężna technika, która może oszczędzić czas i zasoby podczas tworzenia modelu.

Interpretowalność modelu

W miarę rozwoju i upowszechniania się modeli uczących się głęboko, wzrost znaczenia interpretowalności modeli staje się coraz ważniejszy. Interpretowalność odnosi się do zdolności do zrozumienia i wyjaśnienia wewnętrznego procesu podejmowania decyzji przez model.

Jedną z popularnych technik poprawiających interpretowalność modelu jest stosowanie mechanizmów uwagi (ang. attention). Uwaga pozwala modelowi skupić się na najważniejszych częściach danych wejściowych podczas podejmowania decyzji, a jej działanie można zwizualizować, aby zrozumieć, na których cechach model się opiera.

Oto przykład implementacji mechanizmu uwagi w modelu PyTorch do zadania przetwarzania języka naturalnego:

import torch.nn as nn
import torch.nn.functional as F
 
class AttentionModel(nn.Module):
    def __init__(self, vocab_size, embedding_dim, hidden_dim):
        super(AttentionModel, self).__init__()
        self.embedding = nn.Embedding(vocab_size, embedding_dim)
        self.lstm = nn.LSTM(embedding_dim, hidden_dim, bidirectional=True, batch_first=True)
        self.attention = nn.Linear(hidden_dim * 2, 1)
 
    def forward(self, input_ids):
        # Osadź dane wejściowe
        embedded = self.embedding(input_ids)
 
        # Przepuść osadzone dane wejściowe przez LSTM
        lstm_output, _ = self.lstm(embedded)
 
        # Oblicz wagi uwagi (attention)
        attention_weights = F.softmax(self.attention(lstm_output), dim=1)
 
        # Oblicz ważoną sumę wyników LSTM
        context = torch.sum(attention_weights * lstm_output, dim=1)
 
        return context

W tym przykładzie mechanizm uwagi jest zaimplementowany jako warstwa liniowa, która pobiera wyjścia LSTM jako wejście i generuje zestaw wag uwagi. Następnie wykorzystuje się te wagi do obliczenia ważonej sumy wyjść LSTM, która jest ostatecznym wynikiem modelu.

Poprzez wizualizację wag uwagi można uzyskać wgląd w to, na jakie części danych model skupia się podczas podejmowania decyzji. Może to pomóc w zrozumieniu procesu podejmowania decyzji przez model i w identyfikacji potencjalnych stronniczości lub obszarów do poprawy.

Inną techniką poprawiającą interpretowalność modelu jest analiza znaczenia cech. Polega to na identyfikowaniu najważniejszych cech, których model używa do podejmowania decyzji. Jedną popularną metodą jest wartości Shapley, które pozwalają na kwantyfikację wkładu każdej cechy do wyników modelu.

Poprawa interpretowalności modelu to ważny obszar badań w dziedzinie uczenia głębokiego, ponieważ może pomóc budować zaufanie do tych potężnych modeli i zapewnić odpowiedzialną ich użyteczność.

Podsumowanie

W tym samouczku omówiliśmy szereg zagadnień związanych z uczeniem głębokim, w tym optymalizację modelu, transfer learning i interpretowalność modelu. Omówiliśmy techniki takie jak regularyzacja, algorytmy optymalizacji i mechanizmy uwagi, oraz zapewniliśmy przykłady, jak zaimplementować te koncepty w PyTorch.

W miarę rozwoju i coraz szerszego stosowania uczenia głębokiego, ważne jest zrozumienie tych zaawansowanych zagadnień i umiejętność ich stosowania we własnych projektach. Opanowanie tych technik pozwoli Ci lepiej radzić sobie przy tworzeniu wydajnych, solidnych i interpretowalnych modeli uczenia głębokiego, które mogą rozwiązywać wiele różnych problemów.

Pamiętaj, że uczenie głębokie jest dziedziną dynamicznie zmieniającą się i ważne jest, aby być na bieżąco z najnowszymi badaniami i najlepszymi praktykami. Kontynuuj eksplorowanie, eksperymentowanie i uczenie się, a stanie się ekspertem w dziedzinie uczenia głębokiego.