Mode training vs eval

Perbedaan model.train() dan model.eval() serta perilaku dropout dan batchnorm.

Daftar isi

Setiap objek nn.Module menyimpan satu penanda mode, dan model.train() beserta model.eval() adalah dua tombol yang mengubahnya. Mode training dan mode eval tidak mengubah struktur model, melainkan mengubah perilaku sebagian kecil layer, terutama dropout dan batch normalization, yang memang dirancang untuk bekerja dalam dua mode. Topik ini mendemonstrasikan perbedaan kedua mode dengan keluaran nyata, lalu meluruskan satu salah kaprah yang sering menular dari bahasa pemrograman lain, yaitu anggapan bahwa mode eval menghentikan pembelajaran.

Dua mode pada setiap nn.Module#

Pemanggilan model.train() menetapkan mode training, sedangkan model.eval() menetapkan mode eval pada seluruh submodul model tersebut, termasuk layer yang bersarang di dalamnya. Penanda mode inilah yang dibaca oleh layer tertentu saat forward dijalankan. Layer yang tidak bergantung pada mode, misalnya nn.Linear dan nn.ReLU, menghasilkan keluaran yang sama di kedua mode. Dengan kata lain, mode adalah kontrak antara pemilik model dan layer yang perilakunya bergantung pada konteks penggunaan, yaitu melatih atau menebak.

Dua layer yang paling sering digunakan dan paling sensitif terhadap mode adalah nn.Dropout dan nn.BatchNorm1d. Keduanya dibedah pada dua bagian berikut dengan model paling kecil yang mungkin, sehingga perbedaannya terlihat pada angka, bukan pada teori.

Perilaku dropout pada kedua mode#

Dropout, yaitu penyembunyian sebagian unit secara acak saat pelatihan, menghasilkan keluaran yang berbeda pada setiap panggilan di mode training dan keluaran yang tetap di mode eval. Demonstrasi berikut menggunakan satu masukan berisi delapan nilai satu, sehingga setiap perubahan terlihat jelas.

python
import torch
import torch.nn as nn

torch.manual_seed(0)

dropout = nn.Dropout(p=0.5)
x = torch.ones(1, 8)

dropout.train()
print("train, panggilan 1 :", dropout(x))
print("train, panggilan 2 :", dropout(x))

dropout.eval()
print("eval , panggilan 1 :", dropout(x))
print("eval , panggilan 2 :", dropout(x))
text Keluaran
train, panggilan 1 : tensor([[0., 0., 2., 0., 0., 0., 2., 2.]])
train, panggilan 2 : tensor([[0., 2., 2., 2., 2., 0., 2., 2.]])
eval , panggilan 1 : tensor([[1., 1., 1., 1., 1., 1., 1., 1.]])
eval , panggilan 2 : tensor([[1., 1., 1., 1., 1., 1., 1., 1.]])

Pada mode training, dropout mengundi masker acak baru pada setiap panggilan, dan setiap unit berpeluang p=0.5 untuk dijadikan nol. Unit yang lolos diskalakan dengan 1/(1−p)=21/(1-p) = 2, sehingga jumlah rata-rata aktivasi tetap terjaga meskipun separuh unit hilang. Dua panggilan pertama menghasilkan pola nol yang berbeda, dan perbedaan itulah yang membuat pelatihan dropout bersifat nondeterministik secara alami. Pada mode eval, masker tidak digambar sama sekali, sehingga keluaran identik dengan masukan di kedua panggilan berikutnya. Model yang digunakan untuk menebak tidak boleh bergantung pada undian, dan mode eval memastikan hal itu.

Perilaku batch normalization pada kedua mode#

Batch normalization, yaitu penormalan aktivasi per fitur, menggunakan dua sumber statistik yang berbeda di kedua mode. Di mode training, layer menghitung rata-rata dan varians dari batch yang sedang diproses, sekaligus memperbarui statistik berjalan (running statistics), yaitu rata-rata dan varians yang disimpan layer untuk digunakan nanti. Di mode eval, layer berhenti membaca batch dan menggunakan statistik berjalan yang tersimpan itu.

python
import torch
import torch.nn as nn

torch.manual_seed(0)

bn = nn.BatchNorm1d(num_features=4)
x = torch.randn(32, 4) * 3.0 + 5.0

bn.train()
keluaran_train = bn(x)
print("train, mean per fitur:", [round(v, 3) for v in keluaran_train.mean(dim=0).tolist()])
print("train, std per fitur :", [round(v, 3) for v in keluaran_train.std(dim=0, unbiased=False).tolist()])

bn.eval()
keluaran_eval = bn(x)
print("eval , mean per fitur:", [round(v, 3) for v in keluaran_eval.mean(dim=0).tolist()])
print("eval , std per fitur :", [round(v, 3) for v in keluaran_eval.std(dim=0, unbiased=False).tolist()])
print("running_mean tersimpan:", [round(v, 3) for v in bn.running_mean.tolist()])
text Keluaran
train, mean per fitur: [0.0, -0.0, 0.0, -0.0]
train, std per fitur : [1.0, 1.0, 1.0, 1.0]
eval , mean per fitur: [3.007, 2.97, 3.98, 3.716]
eval , std per fitur : [2.159, 2.454, 2.074, 2.19]
running_mean tersimpan: [0.44, 0.509, 0.563, 0.551]

Masukan dibuat berpusat di sekitar lima dengan sebaran sekitar tiga. Pada mode training, keluaran ternormalisasi sempurna, yaitu mean nol dan simpangan baku satu pada setiap fitur, karena normalisasi dihitung dari batch itu sendiri. Pada mode eval, keluaran jauh dari normal, karena layer menggunakan statistik berjalan yang baru diperbarui satu kali dan belum menstabilkan. Nilai running_mean sekitar 0.5 merupakan hasil pencampuran momentum bawaan 0.1, yakni 0.9 kali nilai lama ditambah 0.1 kali mean batch sekitar lima. Setelah banyak batch diproses, statistik berjalan akan menghampiri statistik data yang sesungguhnya, dan keluaran mode eval menjadi stabil. Batch ukuran satu juga tidak boleh diproses di mode training karena varians batchnya tidak terdefinisi, sedangkan mode eval memprosesnya tanpa masalah.

Mode tidak menghentikan pembaruan bobot#

Nama eval sering disalahpahami sebagai tombol berhenti. Kenyataannya, mode hanya mengubah perilaku layer tertentu, sedangkan gradien dan optimizer tetap aktif di kedua mode. Demonstrasi berikut mengaktifkan mode eval lalu memanggil backward() seperti biasa.

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

torch.manual_seed(0)

model = nn.Sequential(nn.Linear(4, 8), nn.ReLU(), nn.Linear(8, 3))
model.eval()

x = torch.randn(8, 4)
y = torch.randint(0, 3, (8,))

loss = F.cross_entropy(model(x), y)
loss.backward()
gradien_terisi = model[0].weight.grad.abs().sum().item() > 0
print("mode eval, backward tetap mengisi gradien:", gradien_terisi)
text Keluaran
mode eval, backward tetap mengisi gradien: True

Gradien tetap terisi penuh meskipun mode eval aktif, dan optimizer.step() yang dipanggil setelahnya tetap akan mengubah bobot. Yang membedakan langkah evaluasi dari langkah pelatihan bukan mode, melainkan tiga hal yang tidak dipanggil, yaitu backward(), step(), dan zero_grad(), ditambah pembungkus torch.no_grad() yang menonaktifkan perekaman graf demi efisiensi. Mode eval melengkapi paket itu, tetapi tidak pernah menggantikannya.

LayerMode trainingMode eval
nn.Dropout(p)mengundi masker acak baru setiap forward, unit yang lolos diskalakan 1/(1−p)1/(1-p)diteruskan tanpa perubahan
nn.BatchNorm1dmenormalkan dengan rata-rata dan varians batch saat itumenormalkan dengan statistik berjalan yang tersimpan

Kesalahan umum#

  1. Memanggil model.eval() lalu menilai model dengan data evaluasi, tetapi lupa kembali ke model.train() pada epoch berikutnya. Pelatihan berlanjut dengan dropout yang tidak aktif dan batchnorm yang menggunakan statistik berjalan, sehingga kualitasnya menurun tanpa pesan error. Perbaikannya: aktifkan model.train() di awal perulangan pelatihan dan model.eval() di awal perulangan evaluasi.
  2. Menganggap model.eval() menggantikan torch.no_grad(). Keduanya bekerja pada tingkat berbeda, yaitu mode mengubah perilaku layer, sedangkan no_grad() menghentikan perekaman graf komputasi. Evaluasi yang efisien menggunakan keduanya sekaligus.
  3. Mengukur model dengan batch berukuran satu di mode training. nn.BatchNorm1d menolaknya dengan error karena varians satu sampel tidak bermakna, sehingga batch terakhir yang tersisa satu sampel perlu dihindari atau diproses di mode eval.

Ringkasan#

  • model.train() dan model.eval() menetapkan penanda mode pada seluruh submodul, dan hanya layer sensitif konteks seperti dropout serta batchnorm yang meresponsnya.
  • Dropout mengundi masker acak dengan penskalaan 1/(1−p)1/(1-p) di mode training dan meneruskan masukan apa adanya di mode eval, sedangkan batchnorm menggunakan statistik batch di mode training dan statistik berjalan di mode eval.
  • Mode tidak menyentuh gradien maupun optimizer, sehingga penghentian pembelajaran tetap ditentukan oleh tidak dipanggilnya backward() dan step() serta penggunaan torch.no_grad().

Referensi#