Semester lalu di salah satu tugas kuliah Computer Vision, kita diminta untuk membuat model CNN dengan dataset CIFAR10. Awalnya ngira ini kayak MNIST dataset yang satset train nggak sampai 10 epoch dapet akurasi >90%. Ternyata dataset ini lebih kompleks. Menariknya, dari satu tugas sederhana ini, Pak Dosen mengenalkan para mahasiswa tentang beragam strategi di deep learning.
Di tutorial ini kita akan coba membuat model machine learning untuk dataset CIFAR10. Kita akan buat modelnya dari scratch menggunakan PyTorch dan tidak menggunakan pre-trained model agar kita bisa lebih dalam memahami apa yang terjadi. Kita juga akan menarget (sebagai tantangan) untuk mendapatkan akurasi >85% dalam 40 epoch.
Source code tutorial ini tersedia dalam bentuk Google Colab di sini (akan terus dilengkapi komentar dan catatan pada notebook tersebut)
Catatan: Artikel ini tidak membahas dasar-dasar deep learning menggunakan PyTorch. Bagi yang ingin tahu dasar deep learning atau dasar PyTorch bisa mengunjungi artikel terkait di blog ini:
- Jaringan Saraf Tiruan dan Deep Learning
- Tutorial dasar Pytorch
- Jaringan Saraf Tiruan menggunakan Pytorch
- CNN menggunakan Pytorch
CIFAR10 Dataset
Dataset CIFAR10 adalah dataset citra berukuran 32×32 pixel berwarna. Sesuai namanya, CIFAR10 terdiri dari 10 kelas (kucing, burung, …, kapal). Jelas dataset ini berbeda dari MNIST yang hanya hitam putih. Selain itu jika dilihat di paper-paper, rata-rata akurasi yang diperoleh untuk CIFAR10 adalah sekitar 92% (dibanding MNIST yang bisa sampai >98%). Meskipun begitu, dataset ini masih tergolong mudah dan cocok untuk buat latihan klasifikasi citra. Dataset CIFAR10 bisa didownload langsung menggunakan perintah dari PyTorch.
1. Arsitektur Dasar
Untuk arsitektur dasar, kita akan membuat CNN sederhana yang terdiri dari 3 layer konvolusi dan 2 fully connected layer. Tapi sebelumnya, langkah pertama kita adalah download dan normalisasi dataset terlebih dahulu.
Data Preparation
# dataset transform functions
train_transform = transforms.Compose(
[transforms.ToTensor(), # convert image to tensor
transforms.Normalize((0.4914, 0.4822, 0.4465), (0.247, 0.243, 0.261))]) # normalization
test_transform = transforms.Compose(
[transforms.ToTensor(), # convert image to tensor
transforms.Normalize((0.4914, 0.4822, 0.4465), (0.247, 0.243, 0.261))]) # normalization
# download dataset
train_set = datasets.CIFAR10("cifar10", train=True, download=True, transform=train_transform)
test_set = datasets.CIFAR10("cifar10", train=False, download=True, transform=test_transform)
# create pytorch data loader
# we set 256 images for a single batch
train_loader = torch.utils.data.DataLoader(train_set, batch_size=256, shuffle=True)
test_loader = torch.utils.data.DataLoader(test_set, batch_size=256, shuffle=True)
Karena dataset ini berukuran tidak terlalu besar, kita akan menggunakan batch size yang cukup besar, yakni 256 gambar, dalam sekali training, harapannya model bisa lebih cepet optimal. Kita juga normalisasi data CIFAR10 menggunakan rekomendasi yang ada di sini.
Arsitektur
Arsitektur dasar yang kita buat kira-kira seperti ini jika dituliskan dalam kode Pytorch:
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = nn.Conv2d(3, 128, kernel_size=3, stride=1, padding=1)
self.conv2 = nn.Conv2d(128, 256, kernel_size=3, stride=1, padding=1)
self.conv3 = nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1)
self.pool = nn.MaxPool2d(kernel_size=2, stride=2)
self.do = nn.Dropout() # dropout layer
self.fc1 = nn.Linear(4096, 1024)
self.fco = nn.Linear(1024, 10)
def forward(self, x): # forward propagation
batch_size = x.shape[0]
x = self.pool(torch.relu(self.conv1(x)))
x = self.pool(torch.relu(self.conv2(x)))
x = self.pool(torch.relu(self.conv3(x)))
x = x.reshape(batch_size, -1)
x = self.do(torch.relu(self.fc1(x))) # dropout layer
x = self.fco(x)
return x
Atau seperti ini jika divisualisasikan:

Jika diperhatikan, ada beberapa catatan pada arsitektur tersebut:
- Jumlah feature map yang kita gunakan cukup besar, yakni 128 pada konvolusi pertama dan 256 pada konvolusi kedua dan ketiga. Hal ini mengingat CIFAR10 dataset yang lebih kompleks dibanding MNIST dan kita menarget untuk mendapat akurasi >85% dalam waktu singkat, karenanya walau arsitektur dasar, tapi akan kita buat cukup besar.
- Terdapat layer dropout pada salah satu fully-connected layer, hal ini bertujuan untuk mengurangi overfitting model deep learning.
Optimizer
Selanjutnya kita train model tersebut selama 40 epoch menggunakan Stochastic Gradient Descent (SGD) dengan learning rate 0.01, lalu kita tambahkan momentum 0.9, weight decay 0.0005 dan juga menggunakan learning rate scheduler, Cosine Annealing. Apa maksud setiap pengaturan itu? kita bahas satu-satu
epoch = 40 criterion = nn.CrossEntropyLoss() optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.9, weight_decay=0.0005) scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=epoch)
Momentum
Penggunaan momentum pada SGD seperti memberi “dorongan” agar model optimal lebih cepat. Pada Pytorch, rumus SGD akan berubah menjadi seperti ini jika menggunakan momentum:
$$ \theta_t = \theta_{t-1} – \alpha (\mu g_{t-1} + g_t) $$
$\theta$ adalah parameter (bobot / bias model), $\alpha$ adalah learning rate, $g_t$ adalah nilai turunan eror terhadap parameter, dan $\mu$ adalah nilai momentum. Bayangkan kita sedang menuruni gunung, ketika kemiringan tanahnya curam, maka kita akan turun lebih cepat dibanding di tanah landai, kan? Dengan memperhatikan nilai turunan eror pada iterasi sebelumnya ($g_{t-1}$), optimizer akan memperhatikan kecuraman eror yang bisa mengantarkan kita ke titik optimal lebih cepat.
Weight decay
Weight decay bermanfaat sebagai pencegah overfitting. Weight decay menambahkan syarat, selain meminimalkan eror, optimizer juga harus meminimalkan nilai parameter modelnya (nilai bobot/biasnya, $\theta$). Rumus SGD akan menjadi seperti ini, dengan $\lambda$ adalah nilai parameter weight decay:
$$ \theta_t = \theta_{t-1} – \alpha (\mu g_{t-1} + g_t + \lambda \theta_{t-1}) $$
Dengan memiliki nilai bobot yang kecil, harapannya model akan menjadi tidak terlalu kompleks.
Learning rate scheduler
LR scheduler berfungsi untuk mengubah-ubah nilai learning rate pada saat proses training. Pada tutorial ini kita menggunakan metode Cosine Annealing yang membuat proses pengubahan learning rate layaknya fungsi cosine (bisa naik-turun). Pada kode di atas, untuk menginisiasi scheduler kita perlu menambahkan dua parameter, yakni optimizer mana yang akan diupdate learning ratenya, dan juga T_max, yakni seberapa lama learning rate akan naik/turun sebelum dia akhirnya berubah arahnya (Dari turun jadi naik, dan sebaliknya). Tutorial ini menggunakan T_max=epoch, yakni 40, sehingga learning rate akan terus turun hingga akhir pelatihan. Kalian bisa cek bacaan menarik tentang LR Scheduler di sini.

Hasil = 80.89%
Setelah kita train selama 40 epoch (source code training standard bisa cek di colab), akurasi 80.89% saya peroleh untuk model basic ini untuk data test. Not bad!
2. Batch Normalization
Optimasi yang pertama akan kita lakukan adalah kita akan menambahkan batch normalization pada model. Apa itu batch normalization?
Kita mungkin tahu normalization (normalisasi) adalah teknik untuk mengubah nilai atau skala pada data tanpa menghilangkan informasi antardatanya. Normalisasi biasanya kita lakukan di awal, sebelum data masuk ke model machine learning dengan tujuan memudahkan model untuk menemukan pola pada data. Nah, Batch normalization itu konsepnya sama seperti normalisasi biasanya hanya saja kita lakukan pada tengah-tengah layer. Detail penjelasannya bisa agak panjang, blog menarik untuk dibaca tentang batch norm ada di sini.
Penambahan
Penambahan terdapat pada arsitektur model sehingga tertulis seperti di bawah ini. Karena kita menggunakan CNN, kita gunakan BatchNorm2D yang akan menormalisasi pada setiap channel/feature map. Karenanya, untuk setiap inisiasi kita tambahkan parameter jumlah feature map yang diterima batch norm.
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = nn.Conv2d(3, 128, kernel_size=3, stride=1, padding=1)
self.conv2 = nn.Conv2d(128, 256, kernel_size=3, stride=1, padding=1)
self.conv3 = nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1)
self.pool = nn.MaxPool2d(kernel_size=2, stride=2)
self.do = nn.Dropout()
self.conv1_bn = nn.BatchNorm2d(128) # initiating batch norm
self.conv2_bn = nn.BatchNorm2d(256) # initiating batch norm
self.conv3_bn = nn.BatchNorm2d(256) # initiating batch norm
self.fc1 = nn.Linear(4096, 1024)
self.fco = nn.Linear(1024, 10)
def forward(self, x):
batch_size = x.shape[0]
x = self.pool(torch.relu(self.conv1_bn(self.conv1(x)))) # batch norm
x = self.pool(torch.relu(self.conv2_bn(self.conv2(x)))) # batch norm
x = self.pool(torch.relu(self.conv3_bn(self.conv3(x)))) # batch norm
x = x.reshape(batch_size, -1)
x = self.do(torch.relu(self.fc1(x)))
x = self.fco(x)
return x
Sama seperti normalisasi yang dilakukan di awal, biasanya kita mengubah rentang nilai data sehingga memiliki rata-rata 0 dan standar deviasi 1. Pada batch norm, setial layer batch norm akan menormalisasi sehingga memiliki rata-rata dan standar deviasi tertentu. Nilai ini ditentukan selama proses training. Karenanya perlu diingat:
- Batch norm tiap layer harus diinisasi sendiri-sendiri
- Pastikan gunakan mode
.train()dan.eval()agar batch norm tahu kita sedang training atau testing. (cek kode)
Hasil = 83.63%
Naik 3%! yey!
3. Residual Connection
Salah satu pengubahan arsitektur yang cukup sederhana adalah dengan menambahkan residual connection. Residual connection ini diusulkan dan menjadi dasar dari model yang cukup populer yakni ResNet. Idenya dengan “menghubungkan” output suatu layer dengan output dari beberapa layer sebelumnya. Proses “menghubungkan” ini biasanya dilakukan hanya dengan menggunakan penjumlahan kedua tensor.

Tujuan dari residual connection adalah untuk memudahkan ketika proses update model / backpropagatoin. Gradient yang dihitung saat backpropagation akan bisa melewati atau menskip beberapa layer. Sehingga bisa terhindar dari vanishing atau exploding gradient.
Penambahan
penambahan terdapat pada arsitektur model sehingga tertulis seperti ini:
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = nn.Conv2d(3, 128, kernel_size=3, stride=1, padding=1)
self.conv2 = nn.Conv2d(128, 256, kernel_size=3, stride=1, padding=1)
self.conv3 = nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1)
self.pool = nn.MaxPool2d(kernel_size=2, stride=2)
self.do = nn.Dropout()
self.conv1_bn = nn.BatchNorm2d(128)
self.conv2_bn = nn.BatchNorm2d(256)
self.conv3_bn = nn.BatchNorm2d(256)
self.fc1 = nn.Linear(4096, 1024)
self.fco = nn.Linear(1024, 10)
def forward(self, x):
batch_size = x.shape[0]
x = self.pool(torch.relu(self.conv1_bn(self.conv1(x))))
x = self.pool(torch.relu(self.conv2_bn(self.conv2(x))))
x = self.pool(x + torch.relu(self.conv3_bn(self.conv3(x)))) # residual connection
x = x.reshape(batch_size, -1)
x = self.do(torch.relu(self.fc1(x)))
x = self.fco(x)
return x
residual connection hanya bisa dilakukan (setidaknya dengan mudah) pada layer yang memiliki output ukluran yang sama. Sehingga kita hanya gunakan pada satu layer, yakni pada proses konvolusi pada layer conv3 dengan menambahkan input sebelum proses konvolusi sebagai residual.
Hasil = 83.38%
Hmm, kok turun sedikit? bisa jadi memang tidak ngefek? tapi kalau kita cek akurasi pada data train, akurasi data train menurut agak banyak jadi sekitar 97% (sebelumnya 99%), kalau ini berarti model kita jadi “sedikit” tidak overfitting sepertinya ini pertanda bagus, kita lanjutkan dulu.
4. Data Augmentation
Data augmentation adalah trik yang digunakan dengan cara membuat data latih yang lebih variatif. Kita akan memodifikasi data latih CIFAR10 dan harapannya bisa membuat model lebih memahami pola pada gambar. Metode ini sering digunakan khususnya ketika dataset yang kita miliki jumlahnya sedikit atau kurang variatif.
Penambahan
penambahan terdapat pada proses transormasi data sehingga tertulis seperti ini:
train_transform = transforms.Compose(
[transforms.RandomCrop(32, padding=4), # data augmentation
transforms.RandomHorizontalFlip(), # data augmentation
transforms.ToTensor(),
transforms.Normalize((0.4914, 0.4822, 0.4465), (0.247, 0.243, 0.261))]) # normalization
Kita hanya menambahkan augmentasi pada saat train, jadi yang kita ubah hanya train_transform. Pada kode di atas kita menambahkan dua metode augmentasi. Yang pertama, adalah random crop dengan padding, ini seperti kita tambahkan bingkai 4 pixel mengelilingi gambar lalu secara acak kita akan buat kotak berukuran 32×32 untuk dicrop. Hasilnya nanti gambar akan seakan tergeser (cek ilustrasi di bawah). Metode augmentasi yang kedua adalah secara acak beberapa gambar akan di flip horizontal.

Hasil = 83.8%
Akurasinya sudah agak naik sedikit, dan akurasi data train turun banyak jadi sekitar 82% ini berarti model kita jadi “lebih” tidak overfitting lagi. Ini pertanda bagus 🙂
5. Tambah Layer
Terakhir, kita akan coba menambah kedalaman model kita dengan menambah satu layer konvolusi. FYI, model-model besar seperti ResNet, AlexNet, dkk yang paling sederhana biasanya terdiri dari >10 layer (bahkan hingga ratusan layer). Sejauh ini, model kita cuma terdiri dari 5 layer (atau 8 jika menghitung layer pooling), jadi tidak ada salahnya kita coba.
Penambahan
Penambahan terdapat pada arsitektur model, sehingga berakhir tertulis seperti di bawah ini. Karena penambahan layer tersebut, kita bisa menambah proses residual connection juga.
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = nn.Conv2d(3, 128, kernel_size=3, stride=1, padding=1)
self.conv2 = nn.Conv2d(128, 256, kernel_size=3, stride=1, padding=1)
self.conv3 = nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1)
self.conv4 = nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1) # added layer
self.pool = nn.MaxPool2d(kernel_size=2, stride=2)
self.do = nn.Dropout()
self.conv1_bn = nn.BatchNorm2d(128)
self.conv2_bn = nn.BatchNorm2d(256)
self.conv3_bn = nn.BatchNorm2d(256)
self.conv4_bn = nn.BatchNorm2d(256)
self.fc1 = nn.Linear(1024, 1024)
self.fco = nn.Linear(1024, 10)
def forward(self, x):
batch_size = x.shape[0]
x = self.pool(torch.relu(self.conv1_bn(self.conv1(x))))
x = self.pool(torch.relu(self.conv2_bn(self.conv2(x))))
x = self.pool(x + torch.relu(self.conv3_bn(self.conv3(x))))
x = self.pool(x + torch.relu(self.conv4_bn(self.conv4(x)))) # new residual connection
x = x.reshape(batch_size, -1)
x = self.do(torch.relu(self.fc1(x)))
x = self.fco(x)
return x
Hasil = 86.48%
Yeah, mission complete!
Rangkuman Hasil Akhir
Dari menggabungkan percobaan-percobaan di atas, kita berhasil memperoleh akurasi >85% dengan kurang dari 40 epoch di CIFAR10. Ini hasil yang cukup bagus dan kita jadi bisa belajar banyak beragam strategi dalam meningkatkan akurasi deep learning pada dataset CIFAR10.
By the way, menggunakan bentuk terakhir, jika kita tambahkan waktu train hingga 200 epoch, kita bisa mencapai akurasi pada CIFAR10 hingga >90% loh! Tantangan lain yang kita juga bisa coba adalah menggunakan dataset yang mirip tapi agak lebih besar, yakni CIFAR100. Selamat mencoba!
Source code tutorial ini tersedia dalam bentuk Google Colab di sini (akan terus dilengkapi komentar dan catatan pada notebook tersebut)
Nice, well structured 😀
Saya masih dalam proses eksplorasi tentang CNN dan Computer Vision. Sering baca-baca paper yang menggunakan datase CIFAR10, tapi kok saat saya coba sendiri pakai arsitektur CNN, akurasinya gk setinggi seperti paper-paper tsb, paling-paling mentok <80% akurasinya :"
Sejauh ini biasanya cuma saya akali pakai trik sederhana seperti: momentum, weight decay, dan lr scheduler. Namun setelah baca artikel ini, saya punya gambaran lebih terstruktur metode-metode apa yang bisa digunakan untuk improve model CNN saya 🙂
Thanks for sharing
alhamdulillah senang jika artikelnya bermanfaat, terima kasih. Silakan jika ada pengalaman-pengalamannya yang bisa dibagi