Membekukan seluruh backbone melindungi bobot pretrained, tetapi juga membatasi model pada fitur ImageNet sebagaimana adanya. Topik ini membahas praktik fine-tuning yang sehat, yaitu cara membuka kembali backbone secara terkendali: head dilatih lebih dahulu, backbone diperbarui dengan langkah kecil, dan learning rate dibedakan per kelompok parameter. Bahasan ditutup dengan pedoman memilih antara ekstraksi fitur dan fine-tuning menurut ukuran serta kemiripan data.
Urutan yang lazim: head dahulu, backbone kemudian#
Head baru berisi inisialisasi acak, sehingga gradien yang dipancarkannya pada langkah-langkah pertama masih keliru arah. Apabila backbone langsung dilatih bersama head pada tahap itu, bobot pretrained tergeser oleh gradien yang keliru tersebut sebelum head sempat belajar. Kerusakan semacam ini tidak terlihat pada loss pelatihan yang tetap turun, karena loss itu dihitung pada data yang sedang diproses, dan baru muncul sebagai memburuknya loss validasi. Karena itu, urutan dua tahap lazim digunakan, yaitu melatih head saja selama backbone dibekukan, kemudian membuka backbone dengan learning rate kecil setelah head konvergen.
Kedua tahap itu digambarkan pada diagram berikut:
Diagram tersebut menetapkan dua keputusan learning rate. Head yang berangkat dari inisialisasi acak memerlukan langkah besar, dalam contoh ini 0.02, agar mencapai daerah yang wajar dalam beberapa epoch. Backbone yang sudah berada dekat solusi yang baik cukup dilangkahi dengan 0.001, sepuluh kali lebih kecil, sehingga fitur ImageNet terangkat sedikit demi sedikit dan tidak rusak. Pemisahan learning rate antara kelompok parameter semacam ini memiliki nama khusus dan menjadi bahasan bagian berikutnya.
Learning rate diskriminatif#
Learning rate diskriminatif (discriminative learning rate) adalah kebijakan memberikan learning rate berbeda untuk kelompok parameter yang berbeda, biasanya head diberi learning rate lebih besar dan backbone lebih kecil. Kebijakan ini bertentangan dengan kebiasaan memberi satu learning rate untuk seluruh model, dan justru perbedaan itulah yang sesuai dengan kondisi transfer learning. PyTorch mendukungnya melalui param group, yaitu kelompok parameter di dalam optimizer yang masing-masing membawa pengaturannya sendiri.
Sebuah optimizer menerima daftar dictionary sebagai argumen pertamanya, dan setiap dictionary berisi kunci params beserta kunci pengaturan seperti lr:
import torch
import torch.nn as nn
from torchvision.models import resnet18, ResNet18_Weights
model = resnet18(weights=ResNet18_Weights.IMAGENET1K_V1)
model.fc = nn.Linear(512, 4)
backbone = [p for n, p in model.named_parameters() if not n.startswith("fc")]
optimizer = torch.optim.SGD([
{"params": backbone, "lr": 0.0001},
{"params": model.fc.parameters(), "lr": 0.001},
], momentum=0.9)
for i, grup in enumerate(optimizer.param_groups):
jumlah = sum(p.numel() for p in grup["params"])
print(f"grup {i}: lr = {grup['lr']}, {len(grup['params'])} tensor, {jumlah} parameter")grup 0: lr = 0.0001, 60 tensor, 11176512 parameter
grup 1: lr = 0.001, 2 tensor, 2052 parameterKeluaran memperlihatkan pembagian yang diinginkan. Grup pertama menampung enam puluh tensor backbone berisi 11176512 parameter dengan learning rate 0.0001, sedangkan grup kedua menampung dua tensor head berisi 2052 parameter dengan learning rate 0.001. Nilai lr di dalam dictionary menimpa pengaturan optimizer, dan pilihan lain seperti momentum maupun weight_decay juga dapat dibedakan per grup. Bentuk ini diterima seluruh optimizer pada torch.optim, dan rasio sepuluh kali antara head dan backbone merupakan titik awal yang lazim, kemudian disesuaikan dengan mengamati kurva loss validasi.
Demonstrasi dua tahap#
Skrip berikut merangkai kedua tahap pada data sintetis yang sama dengan topik Pembekuan dan penggantian head, yaitu 64 citra 64x64 yang kelasnya ditandai oleh kuadran blok terang. Tahap pertama melatih head saja selama empat epoch dengan learning rate 0.02, dan statistik batch normalization tetap dipertahankan pada mode eval dengan alasan yang sama seperti pada topik tersebut. Tahap kedua membuka seluruh parameter, membuat optimizer baru berisi dua param group, lalu melatih dua epoch:
import torch
import torch.nn as nn
from torchvision.models import resnet18, ResNet18_Weights
torch.manual_seed(0)
def data_sintetis(jumlah, ukuran=64):
# kelas ditandai kuadran tempat blok terang berada, latar diberi derau
x = torch.rand(jumlah, 3, ukuran, ukuran) * 0.15 + 0.35
y = torch.zeros(jumlah, dtype=torch.long)
setengah = ukuran // 2
urut = torch.randperm(jumlah)
for i, j in enumerate(urut):
kelas = i % 4
y[j] = kelas
awal_h = 6 if kelas < 2 else setengah + 2
awal_w = 6 if kelas % 2 == 0 else setengah + 2
panjang = setengah // 2 - 2
x[j, :, awal_h:awal_h + panjang, awal_w:awal_w + panjang] += 0.55
return x, y
def latih(model, optimizer, epoch):
for e in range(epoch):
model.train()
for layer in model.modules():
# statistik batch normalization dipertahankan karena data sedikit
if isinstance(layer, nn.BatchNorm2d):
layer.eval()
total = 0.0
for i in range(0, 64, 16):
xb, yb = x_latih[i:i + 16], y_latih[i:i + 16]
optimizer.zero_grad()
loss = loss_fn(model(xb), yb)
loss.backward()
optimizer.step()
total += loss.item() * len(xb)
print(f"epoch {e + 1}: loss = {total / 64:.4f}")
def akurasi(model):
model.eval()
with torch.no_grad():
benar = (model(x_val).argmax(dim=1) == y_val).sum().item()
print(f"akurasi validasi : {benar}/32")
x_latih, y_latih = data_sintetis(64)
x_val, y_val = data_sintetis(32)
loss_fn = nn.CrossEntropyLoss()
model = resnet18(weights=ResNet18_Weights.IMAGENET1K_V1)
model.requires_grad_(False)
model.fc = nn.Linear(512, 4)
print("== Tahap 1: head saja, backbone dibekukan ==")
optimizer = torch.optim.SGD(model.fc.parameters(), lr=0.02)
latih(model, optimizer, 4)
akurasi(model)
print("== Tahap 2: seluruh jaringan, learning rate diskriminatif ==")
for p in model.parameters():
p.requires_grad = True
optimizer = torch.optim.SGD([
{"params": [p for n, p in model.named_parameters() if not n.startswith("fc")],
"lr": 0.001}, # backbone: langkah kecil
{"params": model.fc.parameters(), "lr": 0.01}, # head: langkah lebih besar
], momentum=0.9)
latih(model, optimizer, 2)
akurasi(model)== Tahap 1: head saja, backbone dibekukan ==
epoch 1: loss = 2.6960
epoch 2: loss = 2.2183
epoch 3: loss = 0.9061
epoch 4: loss = 0.4938
akurasi validasi : 32/32
== Tahap 2: seluruh jaringan, learning rate diskriminatif ==
epoch 1: loss = 0.1960
epoch 2: loss = 0.0342
akurasi validasi : 32/32Tahap pertama menurunkan loss dari 2.6960 menjadi 0.4938 dan sudah mencapai akurasi validasi penuh, karena fitur ImageNet yang dibekukan memang memadai untuk pola sederhana tersebut. Tahap kedua menekan loss lebih jauh menjadi 0.0342, yaitu menaikkan keyakinan model terhadap prediksinya tanpa mengubah keputusannya. Pada tugas nyata yang berbeda dari ImageNet, kontribusi tahap kedua jauh lebih besar daripada pada demonstrasi ini, sedangkan biayanya tetap terkendali karena backbone yang digunakan berukuran kecil dan epoch-nya sedikit.
Memilih strategi menurut data#
Dua pertanyaan menentukan strategi yang lazim dipilih, yaitu berapa banyak data tersedia dan seberapa mirip data itu dengan dataset yang melatih bobot pretrained. Jawaban atas keduanya mengarahkan pilihan antara ekstraksi fitur, fine-tuning penuh, dan bentuk di antara keduanya.
| Data tersedia | Kemiripan dengan dataset pretrained | Strategi yang lazim |
|---|---|---|
| sedikit | mirip | ekstraksi fitur, cukup melatih head baru |
| sedikit | berbeda | ekstraksi fitur dahulu, lalu fine-tune sebagian blok akhir |
| banyak | mirip | fine-tuning seluruh jaringan dengan learning rate kecil |
| banyak | berbeda | fine-tuning seluruh jaringan, dengan epoch yang lebih banyak |
Tabel tersebut adalah pedoman, bukan aturan mutlak. Data yang sedikit membuat jaringan besar cepat menghafal, sehingga loss validasi yang berbalik naik adalah sinyal untuk membekukan lebih banyak parameter atau berhenti lebih awal. Pada data yang banyak dan mirip, layer batch normalization boleh kembali ke mode train karena statistik batch-nya stabil, sedangkan pada data yang sedikit, praktik mempertahankannya pada mode eval lebih aman. Membandingkan hasil ekstraksi fitur dengan hasil fine-tuning pada data validasi yang sama juga merupakan cara murah untuk menilai apakah tahap fine-tuning memang memberi manfaat.
Kesalahan umum#
Membuka backbone tanpa memasukkannya kembali ke optimizer. Keanggotaan param group ditetapkan saat optimizer dibuat, sehingga optimizer yang dibuat ketika backbone masih beku hanya berisi parameter head. Menetapkan requires_grad = True setelahnya tidak menambahkan apa pun ke optimizer, dan pelatihan pun berjalan seolah hanya head yang ada. Program berikut memperlihatkan kondisi tersebut:
import torch
import torch.nn as nn
from torchvision.models import resnet18, ResNet18_Weights
model = resnet18(weights=ResNet18_Weights.IMAGENET1K_V1)
model.requires_grad_(False)
model.fc = nn.Linear(512, 4)
optimizer = torch.optim.SGD(
(p for p in model.parameters() if p.requires_grad), lr=0.01
)
print("tensor di optimizer :", len(optimizer.param_groups[0]["params"]))
for p in model.parameters():
p.requires_grad = True
print("setelah requires_grad = True :", len(optimizer.param_groups[0]["params"]))
print("tensor pada model :", len(list(model.parameters())))tensor di optimizer : 2
setelah requires_grad = True : 2
tensor pada model : 62Keluaran menunjukkan bahwa optimizer tetap berisi dua tensor meskipun model sudah memiliki 62 tensor yang dapat dilatih. Perbaikannya adalah membuat optimizer baru setelah seluruh parameter dibuka, atau menambahkan parameter yang tertinggal melalui optimizer.add_param_group().
Menggunakan learning rate yang sama besar untuk backbone dan head. Learning rate yang pas untuk head baru sering kali terlalu besar bagi bobot pretrained yang sudah matang, sehingga fitur ImageNet rusak sambil loss pelatihan tetap turun. Gejalanya muncul di data validasi, yaitu loss validasi memburuk sejak tahap fine-tuning dimulai. Perbaikannya adalah menggunakan param group dengan learning rate backbone yang sepuluh kali lebih kecil, atau memuat ulang bobot pretrained dan mengulang tahap tersebut dengan langkah yang lebih hati-hati.
Ringkasan#
- Urutan yang sehat adalah melatih head terlebih dahulu sementara backbone dibekukan, lalu membuka backbone dengan learning rate kecil setelah head konvergen.
- Learning rate diskriminatif diimplementasikan melalui param group berupa daftar dictionary berisi
paramsdanlr, dan diterima seluruh optimizertorch.optim. - Rasio sepuluh kali antara learning rate head dan backbone adalah titik awal yang lazim, kemudian disesuaikan melalui loss validasi.
- Optimizer perlu dibuat ulang setelah parameter dibuka, karena keanggotaan param group ditetapkan saat optimizer dibuat.
Referensi#
- torch.optim — PyTorch — Dokumentasi optimizer yang menjelaskan param group dan pilihan per kelompok parameter.
- Finetuning Torchvision Models — PyTorch — Pola resmi melatih head dahulu, kemudian fine-tuning dengan learning rate kecil.
- Transfer Learning for Computer Vision Tutorial — PyTorch — Fine-tuning ResNet18 pretrained dengan SGD learning rate 0.001.