Derin sinir ağları (Deep neural networks)

Bu bölümde çoklu sinir ağı katmanlarından oluşan derin bir sinir ağı oluşturacağız yığınlar ve bu tür eğitimler için sıklıkla kullanılan MNIST basamak veri setinde görüntü sınıflandırması yapmak için onu eğiteceğiz. Bu örnekte, ek olarak, bir veri kümesini mini partiler halinde işlemek için DataLoader nesneleriyle birlikte PyTorch veri kümelerinin nasıl kullanılabileceğini gösterceğiz.

Cihazı belirtme

Eğitimin yapılmasını istediğimiz cihazı belirtiyoruz. Varsa GPU’yu, yoksa CPU’yu kullanacağız.

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

Veri kümesi

PyTorch ile birlikte gelen torchvision paketinden veri setini indirerek başlıyoruz. Veri kümesi görüntü verilerinden oluşmakta, dolayısıyla bu görüntüleri ToTensor() işlemine, veri kümelerinin yapıcısına bir transformer argümanı olarak ileterek, tensörlere dönüştürüyoruz.

Bu veri kümelerinin her birinde, bir öğe bir örneğe karşılık gelir. Her örnek, örneğin özelliklerinden ve sınıf etiketinden oluşan bir gruptur.

import torch
from torchvision import datasets
import torchvision.transforms as transforms

# verilerin indirileceği sunucudaki konumu 'data_storage' olarak belirtiyoruz. Eğitim veri setini seçiyoruz ve indirilmemişse Torchvision'dan indirmesini istiyoruz. Veri kümesi görüntü verilerinden oluşur, bu nedenle `ToTensor()` dönüşümünü geçirerek onu tensörlere dönüştürürüz.
train_dataset = datasets.MNIST(root='data', train = True, download = True,
    transform=transforms.ToTensor())
test_dataset = datasets.MNIST(root='data', train = False, download = True,
    transform=transforms.ToTensor())

# Her özellik bir tensör şeklindedir (1*28*28), bu da 28*28 toplam özelliğe sahip olduğu anlamına gelir.
num_features = 28*28
num_classes = len(train_dataset.classes)

zeroth_sample = train_dataset[0]
print(f"Eğitim örneğindeki 0. örnek, {zeroth_sample[0].shape} boyutunda özellik tensörünü içerir ve sınıfı {zeroth_sample[1]}'dir. ")

Model Oluşturma

Torch.nn.Module sınıfından miras alan bir sınıf yaratarak derin sinir ağımızı oluşturuyoruz. __init__ fonksiyonunda modele gerekli sinir ağı katmanlarını ve kullanacağımız aktivasyon fonksiyonunu ekliyoruz. Daha sonra ileriye doğru yayılma sürecini forward fonksiyonu ile tanımlayacağız.

Modelimiz değişken sayıda gizli katman içerecektir. Bu katmanları bir torch.nn.ModuleList nesnesinde saklamalıyız, böylece daha sonra bir optimizer nesnesi kullanılarak güncellenebilirler. Ayrıca ileriye doğru yayılma sırasında kullanacağımız doğrusal olmayan aktivasyon fonksiyonunu da saklıyoruz.

İleri yayılım sırasında, sonuncusu hariç tüm katmanlardan sonra ReLU doğrusal olmayan aktivasyon fonksiyonunu uygulamamız gerekiyor. Bunun nedeni, kullanacağımız kaybın yani çapraz entropi kaybının softmax aktivasyonunu uygulayacak olmasıdır.

Son olarak, modu oluşturup modeli GPU’ya gönderiyoruz.

class DeepNeuralNetwork(torch.nn.Module):
    def __init__(self, num_layers, input_features, num_hidden_features, num_classes):
        super(DeepNeuralNetwork, self).__init__()
        # Ağın katmanlarını depolamak için `torch.nn.ModuleList()` türünde bir nesne oluşturuyoruz. Bunu, optimize edicinin sinir ağı katmanlarının parametrelerini güncelleyebilmesi için yapmalıyız.
        self.hidden_layers = torch.nn.ModuleList()
        # İlk katman, her girdi örneği için input_features özelliklerini alır ve num_hidden_feautrue özelliklerini çıkarır.
        self.hidden_layers.append(torch.nn.Linear(input_features, num_hidden_features))

        for _ in range(num_layers-2):
            self.hidden_layers.append(torch.nn.Linear(num_hidden_features, num_hidden_features))

        # Son katman, her örnek için sınıf sayısı kadar çıktı üretir.
        self.hidden_layers.append(torch.nn.Linear(num_hidden_features, num_classes))
        # Katmanlar arasında kullanılacak aktivasyon fonksiyonu olarak ReLU'yu ekliyoruz
        self.relu_activation = torch.nn.ReLU()

    def forward(self, samples):
        # Girdiyi katmanlardan geçiriyoruz ve ardından sonuncusu hariç tüm katmanlar için aktivasyon işlemini uyguluyoruz.
        for layer in self.hidden_layers[:-1]:
            out = layer(samples)
            out = self.relu_activation(out)
        # son katman için aktivasyonu uygulamıyoruz çünkü kullanacağımız kayıp fonksiyonu softmax aktivasyonunu uyguluyor
        out = self.hidden_layers[-1](out)
        return out

num_layers = 4
num_hidden_features = 128
model = DeepNeuralNetwork(num_layers, num_features, num_hidden_features, num_classes).to(device)
print(model)

Optimize edici ve kayıp

Modelimiz için bir Adam optimizer tanımlıyoruz, ona öğrenme oranını ve model parametrelerini iletiyoruz. Optimize edici, bu parametreleri stratejisine göre güncelleyecektir. Kayıp fonksiyonunu ayrıca çapraz entropi kaybı olarak tanımlıyoruz.

learning_rate = 0.01
# "model.parameters()", "model" içindeki tüm eğitilebilir parametreleri döndürür. Bizim durumumuzda, bunlar "model"deki doğrusal katmanın parametreleridir.
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
loss_function = nn.CrossEntropyLoss()

Veri yükleyiciler (Dataloaders)

Veri kümesi büyük olduğundan, verileri örnek yığınlarına bölecek bir DataLoader nesnesi kullanacağız. Bu mini yığınlar, GPU belleğinin tam veri kümesinden daha küçük ve daha yönetilebilir olacaktır.

Veri kümelerini DataLoader yapıcısına iletiyoruz ve parti boyutunu (her mini yığındaki numune sayısı) belirliyoruz. Ayrıca örnekleri karıştırmak istediğimizi de belirtiyoruz.

batch_size = 100
train_loader = torch.utils.data.DataLoader(dataset=train_dataset,
                                           batch_size=batch_size,
                                           shuffle=True)

test_loader = torch.utils.data.DataLoader(dataset=test_dataset,
                                          batch_size=batch_size,
                                          shuffle=False)

Eğitim döngüsü

Belirli sayıda devir (epoch) boyunca çalışacak bir eğitim döngüsü oluşturuyoruz. Bu eğitim döngüsünün içinde, train_loaderın içindeki tüm örnek gruplarını karışık bir sırayla bize sağlayacağı başka bir döngü yapıyoruz. Başka bir deyişle, her adımda, tüm mini yığınları train_loader içerisinde işleyeceğiz.

num_epochs = 2
for epoch in range(num_epochs):
    # Aşağıdaki döngünün her yinelemesi, train_loader'dan "batch_size" boyutunda bir mini toplu iş alacaktır. Her parti bir dizi tensörden oluşur. Birincisi partinin özellik vektörleri, ikincisi ise partinin etiketleridir.
    # "X_train_batch", toplu işin özellik vektörleridir ve "y_train_batch", toplu işin etiketleridir
    for i, (X_train_batch, y_train_batch) in enumerate(train_loader):

        # X_train_batch, model için uygun şekilde şekillendirilmemiş. Şeklini [batch_size, 1, 28, 28] yerine [batch_size, 748] olarak değiştirmeliyiz
        X_train_batch = X_train_batch.reshape(X_train_batch.shape[0], 28*28)
        # Verileri cihaza gönderiyoruz
        X_train_batch = X_train_batch.to(device)
        y_train_batch = y_train_batch.to(device)

        # Batch'i tanımladığımız `forward` fonksiyonunu çağıracak modele geçiriyoruz ve son katmanın çıktısını döndürüyoruz.
        outputs = model(X_train_batch)

        # Kaybı hesaplamak için modelin çıktısını ve partinin etiketlerini kullanıyoruz.
        loss = loss_function(outputs, y_train_batch)

        # Modelin tüm eğitilebilir parametrelerine göre kaybın gradyanını hesaplayacak olan kayıp üzerinde 'backward' işlevini çağırarak geri yayılımı gerçekleştiriyoruz.
        loss.backward()

        # Optimize edici, eğitilebilir parametreleri güncellemek için önceki adımda hesaplanan gradyanları kullanır.
        optimizer.step()

        # Bir sonraki eğitim adımına hazırlanırken tüm parametrelerin gradyanlarını sıfıra ayarlamalıyız.
        optimizer.zero_grad()
        if i%100 == 0:
            print(f"Epoch {epoch}: batch {i}/{len(train_loader)} with loss {loss}")

Değerlendirme

Son olarak, modelin doğruluğunu değerlendireceğiz. Test verilerini mini yığınlar halinde işlemek için test_loader objesini kullanıyoruz. Hesaplamalarımızın gradyan hesaplaması için kullanılmasını önlemek için değerlendirme kodunu torch.no_grad() kod bloğu ile çevreliyoruz. Bu ayrıca, ileriye doğru yayılma sırasında oluşturulan hesaplama grafiğinin kaydedilmesini engellediğinden, bellek tüketimimizi de azaltacaktır.

# Değerlendirme kodu bloğunu bir `torch.no_grad()` çağrısı ile çevreliyoruz, böylece hesaplamalarımız gradyan hesaplaması için kullanılmaz ve bu nedenle bellek açısından daha verimli olur
with torch.no_grad():
    n_correct = 0
    n_samples = 0
    for images, labels in test_loader:
        images = images.reshape(images.shape[0], 28*28).to(device)
        labels = labels.to(device)
        outputs = model(images)

        # "predicted" tensörü, her örnek için en yüksek puanların endekslerini içerecektir. Başka bir deyişle, her örneğin tahmin edilen sınıfını içerecektir.
        _, predicted = torch.max(outputs.data, 1)
        n_samples += labels.size(0)
        n_correct += (predicted == labels).sum().item()

    acc = 100.0 * n_correct / n_samples
    print(f'10000 test örneğinde ağın doğruluğu : {acc} %')