Kuzushiji-MNIST Image Classification menggunakan Convolutional Neural Network (LeNet5) – Pytorch version

Kuzushiji-MNIST Image Classification menggunakan Convolutional Neural Network (LeNet5) – Pytorch version

Dalam artikel kali ini, kita akan membuat model machine learning untuk image classification atau klasifikasi citra dengan menggunakan Convolutional Neural Network. arsitektur yang akan digunakan adalah LeNet5 yang dibuat oleh Yann Lecun.

tahapan-tahapan yang akan dilakukan adalah:

  1. Load dataset, kita akan menggunakan dataset Kuzushiji MNIST.
  2. membuat dataloader untuk keperluan training dan validasi model
  3. Membuat convolutional neural network dengan arsitektur LeNet5
  4. proses training dan validasi model LeNet5
  5. plot hasil training dan validasi.

Load Datset Kuzushiji-MNIST

Kuzushiji MNIST adalah dataset citra yang berisi huruf hiragana yang terbagi ke dalam 10 kelas. Masing-masing kelas memiliki 6000 data training dan 1000 data testing. Dataset ini dapat di-download dengan menggunakan modul datasets dari torchvision.

import torch
from torchvision import datasets, transforms

berikutnya buat variable transformer yang berisi fungsi preprocessing untuk data citra.

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

tahap berikutnya, kita akan mengunduh dan membaca dataset training dan testing. Tahap ini akan memberikan 4 inputan pada argument yang dimiliki modul datasets. Argument pertama yaitu root, argument ini untuk lokasi folder penyimpanan dataset Kuzushiji-MNIST. Untuk membaca data training, nilai yang diberikan pada argument train adalah True. Jika script ini baru pertama kali dijalankan, maka kita harus memberikan nilai True pada argument download. Untuk argument transform, kita menggunakan variable transformer yang telah dibuat sebelumnya.

trainset = datasets.KMNIST(root='../../Dataset/MNIST_Kuzushiji',
                           download=False, train=True,
                           transform=transformer)

testset = datasets.KMNIST(root='../../Dataset/MNIST_Kuzushiji',
                          download=False, train=False, 
                          transform=transformer)

Membuat DataLoader training dan testing

Untuk membaca data dari dataset, kita akan menggunakan modul Dataloader. Modul Dataloader menjadikan dataset yang dibuat menjadi iterable sehingga kita dapat mengakses dataset dengan mudah. Pada saat memanggil modul DataLoader, kita akan menggunakan nilai 64 pada argument batch_size dan True untuk argument shuffle. Argument batch_size untuk menentukan jumlah gambar yang akan dibaca secara sekaligus. Pada kasus ini, setiap iterasi DataLoader akan membaca 64 citra sekaligus. Argument shuffle ini untuk membuat DataLoader membaca citra pada dataset secara acak. Modul DataLoader juga dapat digunakan untuk multiprocess data loading dengan menentukan argument num_workers dengan jumlah process yang diinginkan.

trainloader = torch.utils.data.DataLoader(trainset, batch_size=64, 
                                          shuffle=True)

testloader = torch.utils.data.DataLoader(testset, batch_size=64,
                                         shuffle=True)

Membuat Arsitektur LeNet5

Sebelum membuat model LeNet5, kita akan membuat helper function yang nantinya dapat membantu di-saat membuat model dan dalam proses training dan testing. Untuk membuat helper function ini, kita membutuhkan modul numpy dan modul nn dari Pytorch jadi perlu untuk memanggil kedua modul tsb.

import torch.nn as nn
import numpy as np

helper function : conv2d_output_shape()

helper function pertama yang akan di-buat adalah conv2d_output_shape. Fungsi ini berfungsi untuk menghitung ukuran output dari proses Conv2d (Convolusi 2 Dimensi). Referensi rumus yang akan digunakan pada fungsi ini dapat dibaca pada link ini.

def conv2d_output_shape(H_in, W_in, convLayer, pool=2):
    kernel_size = convLayer.kernel_size
    stride = convLayer.stride
    padding = convLayer.padding
    dilation = convLayer.dilation
    H_out = (H_in + 2*padding[0] - dilation[0]*(kernel_size[0]-1) - 1)/stride[0] + 1
    W_out = (W_in + 2*padding[1] - dilation[1]*(kernel_size[1]-1) - 1)/stride[1] + 1
    H_out = np.floor(H_out)
    W_out = np.floor(W_out)
    if pool:
        H_out = H_out / pool
        W_out = W_out / pool
    return int(H_out), int(W_out)

helper function : fit_accuracy()

helper function kedua adalah fit_accuracy() untuk menghitung akurasi dari prediksi model yang akan dibuat. Fungsi ini bertujuan agar dapat memantau performa model LeNet5 selama proses training dan testing.

def fit_accuracy(outputs, targets):
    _, outputs_class = torch.max(outputs, dim=1)
    corrects = sum(targets == outputs_class).sum().item()
    accuracy = corrects / len(targets) * 100
    return accuracy

helper function : plot_samples()

helper function ketiga adalah fungsi untuk menampilkan beberapa sample dari dataset yang digunakan.

from random import randint

def plot_samples(dataset, nrows=5, ncols=5):
    
    fig = plt.figure(figsize=(ncols,nrows))
    plt.subplots_adjust(hspace=0, wspace=0)
    
    for i in range(int(nrows*ncols)):
        idx = randint(0, len(dataset)-1)
        image, target = dataset[idx]
        
        np_image = image.numpy()
        np_image = np.transpose(np_image,(1,2,0))
        np_image = np_image.squeeze()
        
        ax = plt.subplot(nrows, ncols, i+1)
        ax.imshow(np_image, interpolation='nearest', cmap='gray')
        ax.axis('off')
        
    plt.show()

script berikut dapat dijalankan untuk menggunakan helper function plot_samples()

plot_samples(trainset, 5, 20)

layer-layer pada LeNet5

Langkah berikutnya adalah merancang model Convolutional Neural Network dengan arsitetukr LeNet-5. Untuk mengetahui lebih lanjut mengenai LeNet5. silahkan kunjungi link berikut ini. Arsitektur LeNet5 dapat dilihat pada ilustrasi dibawah ini.

Tahap ini membutuhkan modul nn dan modul functional dari Pytorch untuk merancang artsitektur LeNet5.

import torch.nn as nn
import torch.nn.functional as F

setelah itu buat Python class dengan nama LeNet5. Disini kita akan menggunakan konsep inheritance agar Objek class dapat menggunakan method-method yang ada di nn.Module. Objek class LeNet5 akan menggunakan dua method yaitu __init__() dan forward().

class LeNet5(nn.Module):

    def __init__(self):
        pass

    def forward(self):
        pass

Pada method __init__(), kita akan memberikan argumen h_in, w_in, c_in dan num_classes:height, width, channels dan jumlah kelas atau kategori. Selain itu, perlu juga membuat komponen-komponen yang membentuk arsitektur LeNet5. Untuk membuat komponen-komponen ini, kita menggunakan nn.Conv2d (convolution 2 dimensi) dan nn.Linear (fungsi linear). Perlu juga menggunakan helper function yang telah dibuat sebelumnya untuk menghitung jumlah fitur setelah mengubah output dari komponen Convolution terakhir menjadi 1 dimensi. Hasil perhitungan ini akan disimpan pada attribut flatten_features. Dengan atribut tersebut, kita dapat menentukan jumlah input features pada komponen Linear pertama. Method __init__() yang digunakan akan seperti berikut.

def __init__(self, h_in, w_in, c_in, num_classes):
        super(LeNet5, self).__init__()

        self.conv1 = nn.Conv2d(in_channels=c_in, out_channels=6, kernel_size=5, stride=1)
        h, w = conv2d_output_shape(h_in, w_in, self.conv1, pool=2)

        self.conv2 = nn.Conv2d(in_channels=6, out_channels=16, kernel_size=5, stride=1)
        h, w = conv2d_output_shape(h, w, self.conv2, pool=2)

        self.flatten_features = h * w * 16

        self.Linear1 = nn.Linear(in_features=self.flatten_features, out_features=120)

        self.Linear2 = nn.Linear(in_features=120, out_features=84)

        self.Linear3 = nn.Linear(in_features=84, out_features=num_classes)

Method forward() berfungsi untuk melakukan proses forward propagation, karena itu perlu untuk mengisi method forward() dengan urutan proses yang sesuai dengan arsitektur LeNet5. Proses-proses tersebut adalah:

  1. method forward() menerima input citra yang disimpan pada argumen x,
  2. argumen x akan menjadi input untuk layer convolution pertama self.conv1,
  3. output dari layer convolution pertama akan digunakan sebagai input untuk fungsi aktivasi torch.tanh,
  4. berikutnya adalah proses Average Pooling dengan menggunakan method F.avg_pool2d(),
  5. output dari proses Average Pooling akan menjadi input untuk layer convolution kedua self.conv2,
  6. kemudian akan diaktivasi dengan menggunakan torch.tanh,
  7. sama seperti layer convolution pertama, hasil dari proses aktivasi akan melalui proses Average Pooling F.avg_pool2d(),
  8. hasil dari layer convolution kedua akan diubah ke dalam bentuk 1 dimensi agar sesuai dengan input yang diperlukan pada tahap berikutnya yaitu self.Linear1,
  9. setelah itu kita akan menggunakan fungsi aktivasi torch.tanh pada self.Linear1,
  10. output dari tahap 9 akan menjadi input pada layer terakhir yaitu self.Linear2,
  11. output dari self.Linear2 akan dikembalikan dengan menggunakan keyword return.

hasil akhir dari method forward() akan seperti berikut.

def forward(self, x):
        x = self.conv1(x)
        x = F.avg_pool2d(torch.tanh(x), 2)

        x = self.conv2(x)
        x = F.avg_pool2d(torch.tanh(x), 2)

        x = x.view(x.shape[0], -1)

        x = torch.tanh(self.Linear1(x))

        x = torch.tanh(self.Linear2(x))

        x = self.Linear3(x)

        return x
hasil keseluran class LeNet5 akan seperti berikut :
# LeNet5 by Yann Lecun
import torch.nn as nn
import torch.nn.functional as F

class LeNet5(nn.Module):
    def __init__(self, h_in, w_in, c_in, num_classes):
        super(LeNet5, self).__init__()
        
        self.conv1 = nn.Conv2d(in_channels=c_in, out_channels=6, kernel_size=5, stride=1)
        h, w = conv2d_output_shape(h_in, w_in, self.conv1, pool=2)
        
        self.conv2 = nn.Conv2d(in_channels=6, out_channels=16, kernel_size=5, stride=1)
        h, w = conv2d_output_shape(h, w, self.conv2, pool=2)
        
        self.flatten_features = h * w * 16
        
        self.Linear1 = nn.Linear(in_features=self.flatten_features, out_features=120)
        
        self.Linear2 = nn.Linear(in_features=120, out_features=84)
        
        self.Linear3 = nn.Linear(in_features=84, out_features=num_classes)
    
    def forward(self, x):
        x = self.conv1(x)
        x = F.avg_pool2d(torch.tanh(x), 2)
        
        x = self.conv2(x)
        x = F.avg_pool2d(torch.tanh(x), 2)
        
        x = x.view(x.shape[0], -1)
        
        x = torch.tanh(self.Linear1(x))
        
        x = torch.tanh(self.Linear2(x))
        
        x = self.Linear3(x)
        
        return x

Proses training dan testing model LeNet5

Tahap berikutnya adalah membuat model dengan menggunakan class LeNet5 yang telah dirancang sebelumnya. Perlu diketahui, class LeNet5 memerlukan input berupa height, width, jumlah channel dan jumlah kelas dari data citra yang akan digunakan.

inputs, targets = next(iter(trainloader))

c_in = inputs[0].shape[0]
h_in = inputs[0].shape[1]
w_in = inputs[0].shape[2]

model = LeNet5(h_in, w_in, c_in, num_classes=10)

Setelah inisialisasi model selesai, berikutnya adalah menentukan loss function dan algoritma optimization yang akan digunakan dalam proses training model. Motode yang akan digunakan adalah Cross Entropy Loss dan algoritma Stochastic Gradient Descent. Untuk proses training yang lebih cepat, kita akan memanfaatkan perangkat gpu (Graphical Processing Unit) untuk proses komputasi dan optimasi model.

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

model = model.to(device)

loss_fn = nn.CrossEntropyLoss()

optimizer = torch.optim.SGD(model.parameters(), lr=0.03, momentum=0.9)

Sekarang kita akan membuat fungsi fit_classifier(). Fungsi ini akan melakukan proses training dan testing model LeNet5. Pada setiap epoch-nya, nilai loss dan akurasi dari proses training dan testing model akan disimpan di dalam variable python list training_loss, validation_loss, training_acc, dan validation_acc. Untuk menghitung akurasi dari prediksi model, kita akan menggunakan helper function yaitu fit_accuracy().

Proses training model akan seperti berikut :

model.train() # ini untuk mengubah parameter-parameter di dalam model menjadi trainable

        for inputs, targets in trainloader: # membaca data citra dari dataset training

            optimizer.zero_grad() # mengosongkan nilai gradient dari proses sebelumnya

            inputs, targets = inputs.to(device), targets.to(device) # memindahkan variable inputs dan targets ke GPU, proses ini mengubah kedua variable tersebut menjadi CUDA tensor

            outputs = model(inputs) # forward propagation

            loss = loss_fn(outputs, targets) # menghitung nilai loss dari output model dan nilai sebenarnya yaitu targets

            loss.backward() # menghitung nilai gradient pada tiap parameter model a.k.a Backward Propagation

            optimizer.step() # update nilai parameter-parameter model berdasarkan gradient yang telah dihitung

            train_loss += loss.item() # akumulasi nilai loss dari semua data training

            train_acc += fit_accuracy(outputs, targets) # akumulasi akurasi dari semua data training

        training_loss.append(train_loss/len(trainloader)) # menghitung dan menyimpan rata-rata nilai loss pada tiap epoch

        training_acc.append(train_acc/len(trainloader)) # menghitung dan menyimpan rata-rata nilai akurasi pada tiap epoch

pada proses testing model akan seperti berikut :

model.eval() # ini untuk mengatur atribut parameter model (requires_grad) menjadi False

        for inputs, targets in testloader: # membaca data citra dari dataset testing

            inputs, targets = inputs.to(device), targets.to(device) # memindahkan variable inputs dan targets ke GPU, proses ini mengubah kedua variable tersebut menjadi CUDA tensor

            outputs = model(inputs) # forward propagation

            loss = loss_fn(outputs, targets)  # menghitung nilai loss dari output model dan nilai sebenarnya yaitu targets 

            test_loss += loss.item() # akumulasi nilai loss dari semua data testing

            test_acc += fit_accuracy(outputs, targets)  # akumulasi akurasi dari semua data testing

        testing_loss.append(test_loss/len(testloader)) # menghitung dan menyimpan rata-rata nilai loss pada tiap epoch

        testing_acc.append(test_acc/len(testloader)) # menghitung dan menyimpan rata-rata akurasi pada tiap epoch

Secara keseluruhan, fungsi fit_classifier akan seperti berikut :

def fit_classifier(model, trainloader, testloader, loss_fn, optimizer, device, epochs=5):
    
    
    training_loss = []
    testing_loss = []
    training_acc = []
    testing_acc = []
    
    for epoch in range(epochs):
        
        train_loss = 0.0
        test_loss = 0.0
        train_acc = 0.0
        test_acc = 0.0
        
        #training
        model.train()
        for inputs, targets in trainloader:
            
            optimizer.zero_grad()
            inputs, targets = inputs.to(device), targets.to(device)
            outputs = model(inputs)
            loss = loss_fn(outputs, targets)
            loss.backward()
            optimizer.step()
            train_loss += loss.item()
            train_acc += fit_accuracy(outputs, targets)
        
        training_loss.append(train_loss/len(trainloader))
        training_acc.append(train_acc/len(trainloader))
        
        #validation
        model.eval()
        for inputs, targets in testloader:
            
            inputs, targets = inputs.to(device), targets.to(device)
            outputs = model(inputs)
            loss = loss_fn(outputs, targets)
            test_loss += loss.item()
            test_acc += fit_accuracy(outputs, targets)
        
        testing_loss.append(test_loss/len(testloader))
        testing_acc.append(test_acc/len(testloader))
        
        print(f'epoch:{epoch+1}/{epochs}, train_loss:{training_loss[-1]:.3f}, train_acc:{training_acc[-1]:.3f}, test_loss:{testing_loss[-1]:.3f}, test_acc:{testing_acc[-1]:.3f}')
        
    return (training_loss, training_acc, testing_loss, testing_acc)
        

Untuk menjalankan proses training dan testing, kita akan memanggil fungsi fit_classififer dan menyimpan nilai loss dan akurasi ke variable results.

results = fit_classifier(model, trainloader, testloader, loss_fn, optimizer, device=device, epochs=25)

output dari fungsi fit_classifier()

epoch:1/25, train_loss:0.481, train_acc:85.011, test_loss:0.548, test_acc:83.808
epoch:2/25, train_loss:0.164, train_acc:95.021, test_loss:0.348, test_acc:89.819
epoch:3/25, train_loss:0.113, train_acc:96.568, test_loss:0.340, test_acc:90.257
epoch:4/25, train_loss:0.089, train_acc:97.278, test_loss:0.334, test_acc:90.894
epoch:5/25, train_loss:0.075, train_acc:97.733, test_loss:0.306, test_acc:91.899
epoch:6/25, train_loss:0.060, train_acc:98.139, test_loss:0.329, test_acc:91.610
epoch:7/25, train_loss:0.050, train_acc:98.446, test_loss:0.305, test_acc:92.406
epoch:8/25, train_loss:0.040, train_acc:98.714, test_loss:0.309, test_acc:92.327
epoch:9/25, train_loss:0.036, train_acc:98.847, test_loss:0.295, test_acc:93.053
epoch:10/25, train_loss:0.029, train_acc:99.112, test_loss:0.312, test_acc:92.864
epoch:11/25, train_loss:0.023, train_acc:99.270, test_loss:0.315, test_acc:92.924
epoch:12/25, train_loss:0.018, train_acc:99.495, test_loss:0.332, test_acc:92.814
epoch:13/25, train_loss:0.013, train_acc:99.652, test_loss:0.326, test_acc:92.904
epoch:14/25, train_loss:0.010, train_acc:99.743, test_loss:0.317, test_acc:93.232
epoch:15/25, train_loss:0.010, train_acc:99.730, test_loss:0.339, test_acc:93.173
epoch:16/25, train_loss:0.006, train_acc:99.887, test_loss:0.334, test_acc:93.232
epoch:17/25, train_loss:0.005, train_acc:99.878, test_loss:0.329, test_acc:93.471
epoch:18/25, train_loss:0.003, train_acc:99.958, test_loss:0.346, test_acc:93.491
epoch:19/25, train_loss:0.003, train_acc:99.953, test_loss:0.343, test_acc:93.332
epoch:20/25, train_loss:0.001, train_acc:99.993, test_loss:0.339, test_acc:93.531
epoch:21/25, train_loss:0.001, train_acc:99.993, test_loss:0.344, test_acc:93.531
epoch:22/25, train_loss:0.001, train_acc:99.985, test_loss:0.350, test_acc:93.551
epoch:23/25, train_loss:0.001, train_acc:99.997, test_loss:0.348, test_acc:93.611
epoch:24/25, train_loss:0.001, train_acc:100.000, test_loss:0.363, test_acc:93.382
epoch:25/25, train_loss:0.000, train_acc:100.000, test_loss:0.354, test_acc:93.551

In [30]:

trainloss, trainacc, validloss, validacc = results

Grafik Hasil Proses training dan testing

Berikutnya kita akan menampilkan grafik hasil training dan hasil testing.

import matplotlib.pyplot as plt
%matplotlib inline

plt.figure(figsize=(15,5))
plt.subplot(1,2,1)
plt.plot(trainloss, label='training loss')
plt.plot(validloss, label='testing loss')
plt.title('Loss')
plt.legend()

plt.subplot(1,2,2)
plt.plot(trainacc, label='training acc')
plt.plot(validacc, label='testing acc')
plt.title('Accuracy')
plt.legend()
plt.show()

hasil dari proses training dan testing menunjukkan performa akurasi terbaik model adalah 93.611 % pada data testing dan 99.997 % pada data training pada epoch 23. Selisih antara akurasi testing dan training terbilang cukup jauh, atau bisa juga disebut dengan overfitting. Performa model dapat ditingkatkan dengan hyperparamater tuning yaitu mengubah nilai-nilai parameter untuk proses training seperti jumlah epoch, learning ratemomentum atau menerapkan metode L2 Regularization yang biasa disebut juga weight decay.

dalam artikel ini kita telah membahas mengenai implementasi convolutional neural network untuk klasifikasi huruf hiragana pada dataset Kuzushiji-MNIST. Arsitektur yang digunakan pada implementasi ini adalah LeNet5. Semoga artikel ini dapat membantu dalam pengembangan project-project Machine Learning Anda berikutnya.

Leave a Reply