Klasifikasi Gambar dengan Transfer Learning: Tutorial Komprehensif
Daftar Isi
Pendahuluan
Transfer learning adalah teknik machine learning di mana model yang telah dilatih pada dataset besar digunakan kembali untuk tugas yang berbeda namun terkait. Dalam computer vision, teknik ini biasanya melibatkan pengambilan model yang telah dilatih sebelumnya pada ImageNet (1,4 juta gambar, 1000 kelas) dan mengadaptasinya untuk masalah klasifikasi spesifik Anda. Pendekatan ini secara dramatis mengurangi kebutuhan data dan komputasi untuk mencapai akurasi tinggi.
Tutorial ini mencakup alur kerja lengkap: memilih model pre-trained, menyiapkan data, fine-tuning dengan PyTorch, mengevaluasi performa, dan melakukan deployment model akhir.
Prasyarat
pip install torch torchvision
pip install timm # PyTorch Image Models - koleksi model yang luas
pip install albumentations # Augmentasi lanjutan
pip install scikit-learn # Metrik evaluasi
pip install matplotlib seaborn # Visualisasi
pip install onnx onnxruntime # Ekspor dan deployment
Persyaratan sistem:
- Python 3.8 atau lebih tinggi
- GPU dengan minimal 6 GB VRAM (untuk pelatihan), CPU cukup untuk inferensi
- Pemahaman dasar tentang jaringan saraf dan PyTorch
import torch
import torchvision
import timm
print(f"PyTorch: {torch.version}")
print(f"Torchvision: {torchvision.version}")
print(f"CUDA: {torch.cuda.isavailable()}")
print(f"Timm: {timm.version}")
print(f"Model timm tersedia: {len(timm.listmodels())}")
Memahami Transfer Learning
Transfer learning berhasil karena layer-layer awal pada CNN mempelajari fitur universal (tepi, tekstur, pola) yang berlaku untuk hampir semua tugas vision. Hanya layer-layer akhir yang menjadi spesifik terhadap tugas tertentu.
Ada dua strategi utama:
# Strategi 1: Ekstraksi fitur
def buatekstraktorfitur(namamodel, jumlahkelas):
"""Bekukan semua layer kecuali classification head."""
model = timm.createmodel(namamodel, pretrained=True, numclasses=jumlahkelas)
# Bekukan semua parameter
for param in model.parameters():
param.requiresgrad = False
# Buka kunci classification head
if hasattr(model, 'classifier'):
for param in model.classifier.parameters():
param.requiresgrad = True
elif hasattr(model, 'fc'):
for param in model.fc.parameters():
param.requiresgrad = True
elif hasattr(model, 'head'):
for param in model.head.parameters():
param.requiresgrad = True
return model
Strategi 2: Fine-tuning penuh dengan learning rate diskriminatif
def buatmodelfinetuning(namamodel, jumlahkelas):
"""Semua layer dapat dilatih, tetapi dengan learning rate berbeda."""
model = timm.createmodel(namamodel, pretrained=True, numclasses=jumlahkelas)
# Semua parameter dapat dilatih secara default
return model
Memilih Model Pre-trained
Perbandingan Arsitektur Populer
import timm
def bandingkanmodel():
"""Membandingkan model populer berdasarkan ukuran, kecepatan, dan akurasi."""
infomodel = {
"resnet50": {
"params": "25.6M",
"top1imagenet": 80.4,
"kecepatan": "cepat",
"kasuspenggunaan": "Serbaguna, baseline yang baik"
},
"efficientnetb3": {
"params": "12.2M",
"top1imagenet": 82.0,
"kecepatan": "sedang",
"kasuspenggunaan": "Tradeoff akurasi-efisiensi terbaik"
},
"efficientnetv2s": {
"params": "21.5M",
"top1imagenet": 83.9,
"kecepatan": "sedang",
"kasuspenggunaan": "EfficientNet yang ditingkatkan, pelatihan lebih cepat"
},
"vitbasepatch16224": {
"params": "86.6M",
"top1imagenet": 84.5,
"kecepatan": "lambat",
"kasuspenggunaan": "Vision Transformer, terbaik dengan dataset besar"
},
"convnexttiny": {
"params": "28.6M",
"top1imagenet": 82.1,
"kecepatan": "sedang",
"kasuspenggunaan": "CNN modern yang menyamai performa ViT"
},
}
for nama, info in infomodel.items():
print(f"{nama:30s} | Params: {info['params']:>8s} | "
f"Top-1: {info['top1imagenet']:.1f}% | "
f"Kecepatan: {info['kecepatan']:>6s} | {info['kasuspenggunaan']}")
return infomodel
bandingkanmodel()
Memuat Model dengan timm
import timm
def muatmodelpretrained(namamodel, jumlahkelas, pretrained=True):
"""
Memuat model pre-trained dan mengadaptasinya untuk jumlah kelas Anda.
timm secara otomatis mengganti classification head.
"""
model = timm.createmodel(
namamodel,
pretrained=pretrained,
numclasses=jumlahkelas,
)
# Dapatkan konfigurasi preprocessing spesifik model
dataconfig = timm.data.resolvemodeldataconfig(model)
print(f"Model: {namamodel}")
print(f"Ukuran input: {dataconfig['inputsize']}")
print(f"Mean: {dataconfig['mean']}")
print(f"Std: {dataconfig['std']}")
print(f"Parameter: {sum(p.numel() for p in model.parameters()):,}")
return model, dataconfig
model, config = muatmodelpretrained("efficientnetb3", jumlahkelas=10)
Pemuatan dan Persiapan Data
Struktur Direktori
dataset/
train/
kelasa/
img001.jpg
img002.jpg
kelasb/
img003.jpg
img004.jpg
val/
kelasa/
img005.jpg
kelasb/
img006.jpg
Membuat Data Loader
import torch
from torch.utils.data import DataLoader
from torchvision import datasets
import albumentations as A
from albumentations.pytorch import ToTensorV2
import numpy as np
class DatasetAlbumentations(torch.utils.data.Dataset):
"""Membungkus dataset torchvision dengan transformasi Albumentations."""
def init(self, rootdir, transform=None):
self.dataset = datasets.ImageFolder(rootdir)
self.transform = transform
self.classes = self.dataset.classes
def len(self):
return len(self.dataset)
def getitem(self, idx):
image, label = self.dataset[idx]
image = np.array(image)
if self.transform:
augmented = self.transform(image=image)
image = augmented["image"]
return image, label
def buatdataloader(dirdata, ukuraninput=224, batchsize=32, numworkers=4):
"""Membuat data loader pelatihan dan validasi dengan augmentasi yang tepat."""
transformtrain = A.Compose([
A.RandomResizedCrop(height=ukuraninput, width=ukuraninput, scale=(0.8, 1.0)),
A.HorizontalFlip(p=0.5),
A.VerticalFlip(p=0.1),
A.Rotate(limit=15, p=0.3),
A.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, hue=0.1, p=0.5),
A.GaussNoise(varlimit=(10, 50), p=0.2),
A.CoarseDropout(maxholes=8, maxheight=16, maxwidth=16, p=0.2),
A.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)),
ToTensorV2(),
])
transformval = A.Compose([
A.Resize(height=int(ukuraninput 1.14), width=int(ukuraninput 1.14)),
A.CenterCrop(height=ukuraninput, width=ukuraninput),
A.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)),
ToTensorV2(),
])
datasettrain = DatasetAlbumentations(f"{dirdata}/train", transform=transformtrain)
datasetval = DatasetAlbumentations(f"{dirdata}/val", transform=transformval)
loadertrain = DataLoader(
datasettrain, batchsize=batchsize, shuffle=True,
numworkers=numworkers, pinmemory=True, droplast=True,
)
loaderval = DataLoader(
datasetval, batchsize=batchsize, shuffle=False,
numworkers=numworkers, pinmemory=True,
)
print(f"Sampel pelatihan: {len(datasettrain)}")
print(f"Sampel validasi: {len(datasetval)}")
print(f"Kelas: {datasettrain.classes}")
return loadertrain, loaderval, datasettrain.classes
loadertrain, loaderval, kelas = buatdataloader("./dataset", ukuraninput=300, batchsize=32)
Fine-tuning dengan PyTorch
Mengatur Konfigurasi Pelatihan
import torch.nn as nn
import torch.optim as optim
from torch.optim.lrscheduler import CosineAnnealingLR
def setuppelatihan(model, jumlahepoch=30, lr=1e-4, weightdecay=1e-4):
"""Mengkonfigurasi optimizer, fungsi loss, dan scheduler."""
# Learning rate diskriminatif: lebih rendah untuk layer pre-trained,
# lebih tinggi untuk classification head yang baru
paramhead = []
parambackbone = []
for nama, param in model.namedparameters():
if not param.requiresgrad:
continue
if "classifier" in nama or "fc" in nama or "head" in nama:
paramhead.append(param)
else:
parambackbone.append(param)
paramgroups = [
{"params": parambackbone, "lr": lr 0.1}, # LR 10x lebih rendah untuk backbone
{"params": paramhead, "lr": lr}, # LR penuh untuk head
]
optimizer = optim.AdamW(paramgroups, weightdecay=weightdecay)
scheduler = CosineAnnealingLR(
optimizer,
Tmax=jumlahepoch,
etamin=lr 0.01,
)
criterion = nn.CrossEntropyLoss(labelsmoothing=0.1)
return optimizer, scheduler, criterion
optimizer, scheduler, criterion = setuppelatihan(model, jumlahepoch=30, lr=1e-4)
Implementasi Training Loop
import time
from collections import defaultdict
class Pelatih:
"""Pipeline pelatihan lengkap untuk klasifikasi gambar."""
def init(self, model, optimizer, scheduler, criterion, device="cuda"):
self.model = model.to(device)
self.optimizer = optimizer
self.scheduler = scheduler
self.criterion = criterion
self.device = device
self.riwayat = defaultdict(list)
self.akurasivalterbaik = 0.0
def latihsatuepoch(self, trainloader):
"""Melatih selama satu epoch."""
self.model.train()
totalloss = 0.0
benar = 0
total = 0
for batchidx, (gambar, label) in enumerate(trainloader):
gambar = gambar.to(self.device)
label = label.to(self.device)
# Forward pass
output = self.model(gambar)
loss = self.criterion(output, label)
# Backward pass
self.optimizer.zerograd()
loss.backward()
# Gradient clipping untuk stabilitas
torch.nn.utils.clipgradnorm(self.model.parameters(), maxnorm=1.0)
self.optimizer.step()
# Lacak metrik
totalloss += loss.item() gambar.size(0)
, prediksi = output.max(1)
total += label.size(0)
benar += prediksi.eq(label).sum().item()
epochloss = totalloss / total
epochacc = 100.0 benar / total
return epochloss, epochacc
@torch.nograd()
def validasi(self, valloader):
"""Mengevaluasi pada set validasi."""
self.model.eval()
totalloss = 0.0
benar = 0
total = 0
for gambar, label in valloader:
gambar = gambar.to(self.device)
label = label.to(self.device)
output = self.model(gambar)
loss = self.criterion(output, label)
totalloss += loss.item() gambar.size(0)
, prediksi = output.max(1)
total += label.size(0)
benar += prediksi.eq(label).sum().item()
epochloss = totalloss / total
epochacc = 100.0 benar / total
return epochloss, epochacc
def latih(self, trainloader, valloader, jumlahepoch, savepath="modelterbaik.pth"):
"""Training loop lengkap dengan validasi dan penyimpanan model."""
print(f"{'Epoch':>6} | {'Loss Train':>10} | {'Akur Train':>10} | "
f"{'Loss Val':>10} | {'Akur Val':>10} | {'LR':>10} | {'Waktu':>6}")
print("-" 78)
for epoch in range(jumlahepoch):
waktumulai = time.time()
trainloss, trainacc = self.latihsatuepoch(trainloader)
valloss, valacc = self.validasi(valloader)
self.scheduler.step()
lrsekarang = self.optimizer.paramgroups[0]["lr"]
durasi = time.time() - waktumulai
# Catat riwayat
self.riwayat["trainloss"].append(trainloss)
self.riwayat["trainacc"].append(trainacc)
self.riwayat["valloss"].append(valloss)
self.riwayat["valacc"].append(valacc)
print(f"{epoch + 1:>6} | {trainloss:>10.4f} | {trainacc:>9.2f}% | "
f"{valloss:>10.4f} | {valacc:>9.2f}% | {lrsekarang:>10.6f} | "
f"{durasi:>5.1f}s")
# Simpan model terbaik
if valacc > self.akurasivalterbaik:
self.akurasivalterbaik = valacc
torch.save({
"epoch": epoch,
"modelstatedict": self.model.statedict(),
"optimizerstatedict": self.optimizer.statedict(),
"valacc": valacc,
"valloss": valloss,
}, savepath)
print(f" -> Model terbaik disimpan (akurasival: {valacc:.2f}%)")
print(f"\nAkurasi validasi terbaik: {self.akurasivalterbaik:.2f}%")
return self.riwayat
Jalankan pelatihan
pelatih = Pelatih(model, optimizer, scheduler, criterion)
riwayat = pelatih.latih(loadertrain, loaderval, jumlahepoch=30)
Visualisasi Riwayat Pelatihan
import matplotlib.pyplot as plt
def plotriwayatpelatihan(riwayat):
"""Membuat plot metrik pelatihan dan validasi."""
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))
# Plot loss
ax1.plot(riwayat["trainloss"], label="Loss Pelatihan")
ax1.plot(riwayat["valloss"], label="Loss Validasi")
ax1.setxlabel("Epoch")
ax1.setylabel("Loss")
ax1.settitle("Loss per Epoch")
ax1.legend()
ax1.grid(True)
# Plot akurasi
ax2.plot(riwayat["trainacc"], label="Akurasi Pelatihan")
ax2.plot(riwayat["valacc"], label="Akurasi Validasi")
ax2.setxlabel("Epoch")
ax2.setylabel("Akurasi (%)")
ax2.settitle("Akurasi per Epoch")
ax2.legend()
ax2.grid(True)
plt.tightlayout()
plt.savefig("riwayatpelatihan.png", dpi=150)
plt.close()
plotriwayatpelatihan(riwayat)
Evaluasi dan Metrik
from sklearn.metrics import classificationreport, confusionmatrix
import seaborn as sns
import numpy as np
@torch.nograd()
def evaluasimodel(model, valloader, namakelas, device="cuda"):
"""Menghasilkan metrik evaluasi yang komprehensif."""
model.eval()
semuaprediksi = []
semualabel = []
semuaprobabilitas = []
for gambar, label in valloader:
gambar = gambar.to(device)
output = model(gambar)
probabilitas = torch.softmax(output, dim=1)
, prediksi = output.max(1)
semuaprediksi.extend(prediksi.cpu().numpy())
semualabel.extend(label.numpy())
semuaprobabilitas.extend(probabilitas.cpu().numpy())
semuaprediksi = np.array(semuaprediksi)
semualabel = np.array(semualabel)
# Laporan klasifikasi
print("Laporan Klasifikasi:")
print(classificationreport(semualabel, semuaprediksi,
targetnames=namakelas, digits=3))
# Confusion matrix
cm = confusionmatrix(semualabel, semuaprediksi)
fig, ax = plt.subplots(figsize=(10, 8))
sns.heatmap(cm, annot=True, fmt="d", cmap="Blues",
xticklabels=namakelas, yticklabels=namakelas, ax=ax)
ax.setxlabel("Prediksi")
ax.setylabel("Aktual")
ax.settitle("Confusion Matrix")
plt.tightlayout()
plt.savefig("confusionmatrix.png", dpi=150)
plt.close()
akurasi = (semuaprediksi == semualabel).mean() 100
print(f"\nAkurasi Keseluruhan: {akurasi:.2f}%")
return akurasi
evaluasimodel(model, loaderval, kelas)
Ekspor dan Deployment Model
Ekspor ke ONNX
def eksporkeonnx(model, ukuraninput=224, outputpath="model.onnx"):
"""Mengekspor model ke format ONNX untuk deployment lintas platform."""
model.eval()
model.cpu()
dummy
input = torch.randn(1, 3, ukuraninput, ukuraninput)
torch.onnx.export(
model,
dummyinput,
outputpath,
exportparams=True,
opsetversion=17,
doconstantfolding=True,
inputnames=["input"],
outputnames=["output"],
dynamicaxes={
"input": {0: "batchsize"},
"output": {0: "batchsize"},
},
)
print(f"Model diekspor ke {outputpath}")
eksporkeonnx(model, ukuraninput=300)
Inferensi dengan ONNX Runtime
import onnxruntime as ort
import cv2
import numpy as np
class KlasifierONNX:
"""Menjalankan inferensi menggunakan ONNX Runtime."""
def init(self, modelpath, namakelas, ukuraninput=224):
self.session = ort.InferenceSession(
modelpath,
providers=["CUDAExecutionProvider", "CPUExecutionProvider"]
)
self.namakelas = namakelas
self.ukuraninput = ukuraninput
self.namainput = self.session.getinputs()[0].name
def preprocess(self, imagepath):
"""Memproses gambar untuk inferensi."""
gambar = cv2.imread(imagepath)
gambar = cv2.cvtColor(gambar, cv2.COLORBGR2RGB)
gambar = cv2.resize(gambar, (self.ukuraninput, self.ukuraninput))
gambar = gambar.astype(np.float32) / 255.0
mean = np.array([0.485, 0.456, 0.406])
std = np.array([0.229, 0.224, 0.225])
gambar = (gambar - mean) / std
gambar = gambar.transpose(2, 0, 1) # HWC -> CHW
gambar = np.expanddims(gambar, axis=0).astype(np.float32)
return gambar
def prediksi(self, imagepath, topk=5):
"""Menjalankan inferensi dan mengembalikan prediksi top-k."""
inputtensor = self.preprocess(imagepath)
output = self.session.run(None, {self.namainput: inputtensor})
logits = output[0][0]
# Terapkan softmax
explogits = np.exp(logits - np.max(logits))
probabilitas = explogits / explogits.sum()
indekstop = np.argsort(probabilitas)[::-1][:topk]
hasil = [
{"kelas": self.namakelas[i], "probabilitas": float(probabilitas[i])}
for i in indekstop
]
return hasil
Penggunaan
klasifier = KlasifierONNX("model.onnx", namakelas=kelas, ukuraninput=300)
prediksi = klasifier.prediksi("gambartest.jpg")
for pred in prediksi:
print(f"{pred['kelas']}: {pred['probabilitas']:.4f}")
Deployment dengan FastAPI
from fastapi import FastAPI, File, UploadFile
import io
from PIL import Image
app = FastAPI()
klasifier = KlasifierONNX("model.onnx", namakelas=kelas, ukuraninput=300)
@app.post("/klasifikasi")
async def klasifikasigambar(file: UploadFile = File(...)):
"""Mengklasifikasikan gambar yang diunggah."""
konten = await file.read()
gambar = np.array(Image.open(io.BytesIO(konten)).convert("RGB"))
temppath = "/tmp/tempklasifikasi.jpg"
cv2.imwrite(temppath, cv2.cvtColor(gambar, cv2.COLORRGB2BGR))
prediksi = klasifier.prediksi(temppath, topk=5)
return {"prediksi": prediksi}
Praktik Terbaik
torch.amp.autocast dan GradScaler untuk mengurangi penggunaan memori dan mempercepat pelatihan sebesar 30-50% pada GPU modern.Kesimpulan
Transfer learning adalah pendekatan paling praktis untuk klasifikasi gambar di sebagian besar aplikasi dunia nyata. Dengan memanfaatkan model pre-trained, Anda dapat mencapai akurasi yang sangat baik dengan data dan komputasi minimal. Alur kerja yang dibahas dalam tutorial ini -- pemilihan model, persiapan data, fine-tuning dengan learning rate diskriminatif, evaluasi yang ketat, dan ekspor ONNX -- merepresentasikan pipeline yang siap produksi dan dapat diterapkan untuk tugas klasifikasi apapun.
Poin-poin kunci: mulai dari yang sederhana (ekstraksi fitur), ukur dengan cermat (validasi yang tepat), dan iterasi secara bertahap (pembukaan kunci layer bertahap, penyesuaian hyperparameter). Dengan library timm yang menyediakan akses ke ratusan arsitektur state-of-the-art, hambatan utama bukan lagi ketersediaan model tetapi kualitas data dan metodologi pelatihan.