nn.Module

Pola kelas model dengan nn.Module dan peran method forward().

Daftar isi

Semua arsitektur jaringan saraf di PyTorch, dari yang terdiri atas dua layer sampai model berskala besar, dibangun di atas kelas yang sama, yaitu nn.Module. Topik ini membahas pola kelas yang digunakan untuk mendefinisikan model: layer yang dideklarasikan di __init__, aliran data yang dirangkai di method forward(), dan cara objek model dipanggil seperti fungsi. Sebagai contoh, sebuah model klasifikasi kecil dengan dua layer ditulis lengkap, dijalankan, lalu aliran bentuk (shape) tensornya ditelusuri pada setiap tahap.

Pola kelas model#

Model di PyTorch didefinisikan sebagai subclass dari nn.Module, dan pola kelasnya terdiri atas tiga bagian yang selalu sama. Pertama, method __init__ memanggil super().__init__(), kemudian mendefinisikan layer-layer sebagai atribut. Kedua, method forward() merangkai aliran data dari masukan sampai keluaran. Ketiga, objek model dipanggil seperti fungsi biasa, tanpa perlu mendefinisikan __call__ sendiri.

Definisi layer di __init__ bukan sekadar kerapian. Ketika sebuah layer ditetapkan sebagai atribut, nn.Module mendaftarkannya bersama seluruh parameternya. Berkat pendaftaran itu, parameter dapat ditemukan oleh model.parameters(), ikut dipindahkan ketika model.to(device) dipanggil, dan ikut tersimpan pada state_dict. Mekanisme pendaftaran ini ditelusuri lebih rinci pada topik Parameter.

Contoh yang digunakan adalah model klasifikasi untuk catatan sensor suhu. Setiap sampel memiliki empat fitur pengukuran dan dikelompokkan ke salah satu dari tiga kategori suhu. Modelnya terdiri atas dua layer Linear yang dipisahkan oleh satu ReLU, susunan yang lazim untuk data tabular. Pola yang sama berlaku untuk model yang lebih besar: sebuah kelas model juga boleh memiliki kelas model lain sebagai atribut, dan pendaftarannya bekerja bertingkat sampai submodule terdalam.

File berikut memuat definisi lengkapnya. Method forward() menerima tensor masukan x, meneruskannya ke fc1, mengaktifkan hasilnya dengan ReLU, lalu mengembalikan keluaran fc2.

python model.py
import torch
import torch.nn as nn

torch.manual_seed(0)

class KlasifikasiSuhu(nn.Module):
    """Klasifikasi tiga kategori suhu dari empat fitur sensor."""

    def __init__(self):
        super().__init__()
        self.fc1 = nn.Linear(4, 8)
        self.aktivasi = nn.ReLU()
        self.fc2 = nn.Linear(8, 3)

    def forward(self, x):
        h = self.fc1(x)
        h = self.aktivasi(h)
        logits = self.fc2(h)
        return logits

model = KlasifikasiSuhu()

x = torch.randn(5, 4)          # (N, in_features) = (5, 4)
logits = model(x)              # pemanggilan seperti fungsi: __call__ meneruskan ke forward
print(logits.shape)            # (N, out_features) = (5, 3)
text Keluaran
torch.Size([5, 3])

Keluaran torch.Size([5, 3]) menyatakan tensor berbentuk (5, 3), yaitu lima sampel yang masing-masing menghasilkan tiga nilai. Masukan dibuat dengan torch.randn(5, 4), sehingga bentuknya (5, 4) dan cocok dengan in_features layer pertama. Nilai keluaran semacam ini disebut logits, yaitu nilai mentah model sebelum diubah menjadi probabilitas.

Model dipanggil seperti fungsi#

Kelas KlasifikasiSuhu tidak pernah mendefinisikan method __call__, tetapi model(x) tetap berjalan. Penjelasannya ada pada nn.Module: kelas dasar itu mendefinisikan __call__ yang meneruskan pemanggilan ke forward(). Dengan demikian, model(x) dan model.forward(x) menghasilkan nilai yang sama persis, sebagaimana dibuktikan potongan kode berikut.

python Perbandingan model(x) dengan model.forward(x)
import torch
import torch.nn as nn

torch.manual_seed(0)

class KlasifikasiSuhu(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc1 = nn.Linear(4, 8)
        self.aktivasi = nn.ReLU()
        self.fc2 = nn.Linear(8, 3)

    def forward(self, x):
        return self.fc2(self.aktivasi(self.fc1(x)))

model = KlasifikasiSuhu()
x = torch.randn(5, 4)

lewat_call = model(x)             # jalur normal: __call__ meneruskan ke forward
lewat_forward = model.forward(x)  # pemanggilan forward secara langsung

print(torch.equal(lewat_call, lewat_forward))
text Keluaran
True

torch.equal mengembalikan True, sehingga kedua jalur pemanggilan memberikan tensor yang identik. Meskipun begitu, konvensi PyTorch adalah memanggil model dengan model(x). Alasannya, __call__ juga menjalankan hook, yaitu fungsi yang dieksekusi otomatis pada titik tertentu, misalnya untuk mencatat nilai aktivasi saat debugging. Pemanggilan forward() secara langsung melewati hook tersebut.

Aliran data dan bentuk tensor#

Diagram berikut memperlihatkan aliran tensor di dalam model(x) beserta bentuknya pada setiap tahap.

x (N, 4)h (N, 8)h (N, 8)logits (N, 3)fc1Linear(4, 8)ReLUfc2Linear(8, 3)
Aliran tensor dalam model dua layer beserta bentuknya

Layer fc1 mengubah dimensi terakhir dari 4 menjadi 8, sehingga bentuknya berubah dari (N, 4) menjadi (N, 8). ReLU bekerja per elemen dan tidak mengubah bentuk sama sekali. Layer fc2 mengubah dimensi terakhir dari 8 menjadi 3, menghasilkan logits berbentuk (N, 3).

Kesalahan umum#

Kesalahan pertama adalah lupa memanggil super().__init__() di awal __init__. Pendaftaran layer membutuhkan bagian internal nn.Module, dan bagian itu baru ada setelah inisialisasi kelas dasar dijalankan. Akibatnya, penetapan layer pertama memicu error. Kode berikut menangkap errornya agar pesan aslinya terlihat.

python Demonstrasi error ketika super().__init__() dilewati
import torch
import torch.nn as nn

class TanpaSuper(nn.Module):
    def __init__(self):
        self.fc = nn.Linear(4, 3)   # lupa memanggil super().__init__()

try:
    TanpaSuper()
except AttributeError as e:
    print("AttributeError:", str(e).splitlines()[0])
text Keluaran
AttributeError: cannot assign module before Module.__init__() call

Kesalahan kedua adalah memanggil model.forward(x) secara langsung. Nilai keluarannya sama, tetapi hook yang terpasang tidak dijalankan. Gunakan model(x) pada seluruh kode, baik saat pelatihan maupun saat inferensi.

Kesalahan ketiga adalah membuat layer di dalam forward(). Setiap pemanggilan akan membuat layer baru dengan bobot baru yang diacak, sehingga model tidak pernah memiliki bobot yang konsisten. Layer selalu didefinisikan di __init__, sedangkan forward() hanya merangkai layer yang sudah ada.

Ringkasan#

  • Model didefinisikan sebagai subclass nn.Module: layer dideklarasikan sebagai atribut di __init__ setelah super().__init__(), dan forward() merangkai aliran datanya.
  • model(x) berjalan karena __call__ dari nn.Module meneruskan pemanggilan ke forward(), sekaligus menjalankan hook yang terpasang.
  • Linear mengubah dimensi terakhir dari in_features ke out_features, sedangkan ReLU bekerja per elemen tanpa mengubah bentuk.

Referensi#