Evrişimli sinir ağları (Convolutional neural networks)

Bu bölümde görüntü verileri üzerinde sınıflandırma yapmak için kullanacağımız evrişimsel bir sinir ağı oluşturacağız. Evrişimli sinir ağları, örneğin iki boyutlu görüntüler gibi birden fazla boyutu olan verilerle çalışmak için mükemmeldir. Bu örnekte, görüntüleri boyutsallıklarına saygı gösterecek şekilde işlemek için evrişimli katmanları ve havuz katmanlarını kullanıyoruz.

Veri kümesi

10 farklı nesnenin 60000 görüntüsünü içeren CIFAR10 görüntü veri kümesini kullanacağız. Veri setini, torchvision kitaplığından indireceğiz. Veri kümesi, doğrudan PyTorch katmanlarıyla kullanamadığımız PIL görüntülerini içerir. Bu nedenle, görüntüleri tensörlere dönüştürmek için ToTensor() dönüşümünü kullanıyoruz. Bu, görüntüdeki her pikseli, o pikselin RGB değerlerini temsil eden [0,1] aralığında üç sayıdan oluşan bir tensöre dönüştürecektir. Ayrıca değerleri [-0.5, 0.5] aralığına kaydırmak için Normalize() adlı ikinci bir dönüşüm uygulayacağız.

import torchvision
import torchvision.transforms as transforms

# veriler dönüştürülmeden yüklenecek. Görüntü nesneleri şeklinde olacak.
train_dataset = torchvision.datasets.CIFAR10(root='./data', train=True,
                                        download=True)

first_element = train_dataset[0]
image, label = first_element
print("Verinin değişimden önceki şekli")
print(type(image), image)

# İki dönüşümü uygulayacak tek bir dönüşüm oluşturun:
# 1. ToTensor(): Her görüntüdeki her pikseli RGB değerleri için üç
#                sayıdan oluşan bir tensöre dönüştürün.
# 2. Normalize(): değerleri [0,1] aralığından [-0.5, 0.5] değerine kaydırır
transform = transforms.Compose(
    [transforms.ToTensor(),
     transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])

# veriler yüklenir ve dönüşümler uygulanır
train_dataset = torchvision.datasets.CIFAR10(root='./data', train=True,
                                        download=True, transform=transform)

test_dataset = torchvision.datasets.CIFAR10(root='./data', train=False,
                                       download=True, transform=transform)

first_element = train_dataset[0]
image, label = first_element
print(type(image), image.shape)
print("Her görüntü 32x32 pikseldir ve her pikselin üç değeri vardır")

import matplotlib.pyplot as plt
import numpy as np

# Bu işlev, tensör temsilini kullanarak görüntüyü gösterecektir.
def imshow(img):
    img = img / 2 + 0.5  # unnormalize
    npimg = img.numpy()
    plt.imshow(np.transpose(npimg, (1, 2, 0)))
    plt.show()

print("İlk görüntü bir kurbağa görüntüsü!")
imshow(image)
/assets/pytorch-education/frog.png

Veri yükleyiciler (Dataloaders)

Eğitim ve test döngüleri sırasında kullanacağımız veri kümelerini taşıyabilmek için veri yükleyicileri oluşturuyoruz.

import torch

# her parti 4 görüntüden oluşacaktır
batch_size = 4

# veri yükleyici örnekleri karıştıracak
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=batch_size,
                                          shuffle=True)

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

dataiter = iter(train_loader)
images, labels = dataiter.next()
print(images.shape)

Evrişimli sinir ağı modeli

Modelimiz, görüntü veri tensörlerini alacak ve bunları evrişim katmanları ve havuz katmanları aracılığıyla işleyecektir. Daha sonra, tahmin etmek istediğimiz sınıflar için verileri doğrusal katmanlardan alacağız ve son skorları elde edeceğiz. Bu bölümde, önce evrişim katmanını ve havuzlama katmanını göstereceğiz, ardından torch.nn.Module sınıfından miras alarak tam sinir ağı modelini oluşturacağız.

Evrişimsel katmanlar

Evrişimsel katmanlar çok boyutlu verileri alır ve çok boyutlu bir çıktı üretmek için bir evrişim kullanır. Aşağıdaki örnek, modelimizde kullanacağımız ilk evrişimsel katmanı göstermektedir. Ancak, aşağıdaki örnekte, netlik için 32x32 yerine 9x9 boyutlarında bir giriş görüntüsü kullanıyoruz. Katman, görüntüdeki her 5x5 piksel grubunun üzerinden geçerek ve bunları çıktıda tek bir piksele dönüştürerek 5x5’lik bir filtre eğitir. Ayrıca filtre, girişteki her pikselin üç rengini (kanalını) kullanacak ve çıktıdaki her piksel için 6 çıkış kanalı üretecektir.

/assets/pytorch-education/conv.png

Aşağıda, evrişim katmanının bir girdiye ne yaptığının bir gösterimi verilmiştir. Katmana dört görüntüden oluşan bir toplu iş gönderiyoruz ve dönüştürülmüş bir çıktı alıyoruz:

import torch.nn as nn

conv1 = nn.Conv2d(in_channels=3, out_channels=6, kernel_size=5)
# input_channels = her giriş pikselindeki kanal sayısı
# output_channels = her çıkış pikselindeki kanal sayısı
# kernel_size = filtrenin genişliği ve yüksekliği

dataiter = iter(train_loader)
images, labels = dataiter.next()
print(f"katmandan önce, şekil: {images.shape}")
output = conv1(images)
print(f"katmandan sonra, şekil: {output.shape}")

Havuz katmanları (Pooling layers)

Evrişim katmanlarını çalıştırdıktan sonra, çıktıyı daha küçük bir temsile sıkıştırmak için havuz katmanlarını kullanabiliriz. Modelimizde, bir önceki katmanın çıktısını alacak ve maksimum fonksiyonunu kullanarak sıkıştıracak bir max-pool kullanıyoruz. Aşağıda bir havuzlama katmanı örneği verilmiştir.

/assets/pytorch-education/pool.png

Aşağıda, önceki evrişim katmanının çıktısını alacak ve havuzlama uygulayacak bir maksimum havuz katmanı gösteriyoruz. Katman 2x2’lik ızgaralar alacak ve maksimum değerlerini bulacaktır. Havuzlama katmanının adımı 2’dir, bu nedenle filtre bir seferde 2 konum hareket edecektir. Bu havuzlama prosedürü, girişin tüm kanalları için gerçekleşir.

pool = nn.MaxPool2d(kernel_size=2,stride=2)
# kernel_size = filtrenin genişliği ve yüksekliği
# stride = filtreleme işlemleri arasındaki mesafe

print(f"katmandan önce, şekil: {output.shape}")
output = pool(output)
print(f"katmandan sonra, şekil: {output.shape}")

Tam model

torch.nn.Module sınıfından miras alarak modelimizi oluşturuyoruz. Her evrişim katmanından sonra kullanacağımız iki evrişim katmanı ve tek bir havuz işlevi tanımlıyoruz. Ayrıca, evrişimin çıktısını alacak ve tahmin edilecek sınıf sayısı olan sadece 10 çıktı olana kadar kademeli olarak dönüştürecek üç doğrusal katman tanımlıyoruz.

import torch.nn.functional as F

class CNN(nn.Module):
    def __init__(self):
        super(CNN, self).__init__()
        # ilk evrişim 5x5 boyutlarında bir filtre kullanır, piksel başına 3 giriş
        # kanalı alır ve 6 çıkış kanalı üretir
        self.conv1 = nn.Conv2d(3, 6, 5)
        # 2x2 ızgaralı ve 2 adımlı max-pool kullanıyoruz. Havuz eğitilmediğinden,
        # yalnızca bir örneğine ihtiyacımız var
        self.pool1and2 = nn.MaxPool2d(2, 2)
        # İkinci evrişim 5x5 boyutlarında bir filtre kullanır, ancak 6 giriş kanalı alır
        # ve konum başına 16 çıkış kanalı üretir
        self.conv2 = nn.Conv2d(6, 16, 5)

        # Bu doğrusal katman, üzerine self.pool1and2 uygulandıktan sonra conv2'nin çıktısını
        # alacaktır, bu da girdinin 16*5*5 değerine sahip olacağı anlamına gelir.
        self.fc1 = nn.Linear(16 * 5 * 5, 120)
        self.fc2 = nn.Linear(120, 84)

        # Son doğrusal katman, tahmin edilecek 10 sınıf olduğundan 10 çıktı üretmelidir.
        self.fc3 = nn.Linear(84, 10)

    def forward(self, x):
        # x -> [batch_size, 3, 32, 32]
        output = self.conv1(x) # [batch_size, 6, 28, 28]
        output = self.pool1and2(output) # [batch_size, 6, 14, 14]
        output = F.relu(output) # [batch_size, 6, 14, 14]
        output = self.conv2(output) # [batch_size, 16, 10, 10]
        output = self.pool1and2(output) # [batch_size, 16, 5, 5]
        output = F.relu(output) # [batch_size, 16, 5, 5]
        # Doğrusal katmana beslemek için çıktıyı girdi başına tek bir satır haline getirmeliyiz.
        output = output.reshape(-1, 16 * 5 * 5) # [batch_size, 16*5*5]
        output = F.relu(self.fc1(output))
        output = F.relu(self.fc2(output))
        # Son katmandan sonra bir aktivasyon kullanmayacağız çünkü
        # kayıp işlevi sigmoid aktivasyonunu otomatik olarak uygulayacaktır
        output = self.fc3(output)
        return output

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

model = CNN().to(device)

print(model)

Optimize edici ve kayıp

Modeli eğitmek için kullanılacak optimize edici ve kayıp fonksiyonlarını tanımlıyoruz.

learning_rate = 0.001
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate)

Eğitim döngüsü

Modeli eğitmek, batch_size=4 değeri ile görüntü yığınları oluşturacak olan eğitim seti yükleyiciyi kullanacaktır. Her eğitim dönemi için, modelin eğitiminde tüm eğitim grupları kullanılacaktır. Her parti için, sistem boyunca ileriye doğru bir yayılım, ardından onu optimize etmek için geriye doğru bir yayılım gerçekleştirilecektir. Verileri işlemeden önce cihaza taşıyoruz.

num_epochs = 5

# Train_loader'daki parti sayısı
n_total_steps = len(train_loader)
for epoch in range(num_epochs):

    # Her toplu iş, bir görüntü tensörü ve bu görüntünün etiketlerini içeren
    # bir tensörden oluşur.
    for i, (images, labels) in enumerate(train_loader):
        images = images.to(device)
        labels = labels.to(device)

        # Giriş tensörü şu şekildedir: [batch_size, 3, 32, 32]
        outputs = model(images)

        loss = criterion(outputs, labels)

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        if (i+1) % 2000 == 0:
            print (f'Epoch [{epoch+1}/{num_epochs}], Step [{i+1}/{n_total_steps}], Loss: {loss.item():.4f}')

Değerlendirme

Son olarak, test verilerini kullanarak eğitilen modeli değerlendireceğiz. Test verisi yığınları oluşturacak test yükleyicisini kullanıyoruz. On sınıfın her birinin doğruluğunu ve ayrıca sistemin genel doğruluğunu hesaplıyoruz. Değerlendirmede kullanılan hesaplamanın, hesaplama ve bellek açısından daha verimli olması için, yani hesaplama grafiği oluşturmaması için değerlendirme kodunu torch.no_grad() işlemiyle çevreliyoruz.

classes = ('plane', 'car', 'bird', 'cat',
           'deer', 'dog', 'frog', 'horse', 'ship', 'truck')
with torch.no_grad():
    n_correct = 0
    n_samples = 0
    n_class_correct = [0 for i in range(10)]
    n_class_samples = [0 for i in range(10)]
    for images, labels in test_loader:
        images = images.to(device)
        labels = labels.to(device)
        outputs = model(images)

        _, predicted = torch.max(outputs, 1)
        n_samples += labels.size(0)
        n_correct += (predicted == labels).sum().item()

        for i in range(batch_size):
            label = labels[i]
            pred = predicted[i]
            if (label == pred):
                n_class_correct[label] += 1
            n_class_samples[label] += 1

    acc = 100.0 * n_correct / n_samples
    print(f'Ağın doğruluğu: {acc} %')

    for i in range(10):
        acc = 100.0 * n_class_correct[i] / n_class_samples[i]
        print(f'{classes[i]} doğruluğu: {acc} %')