Inti LoRA ada pada satu gagasan: perubahan bobot selama penyesuaian tidak perlu berupa matriks penuh, cukup hasil kali dua matriks tipis B @ A. Bagian ini menerapkan gagasan tersebut pada model bahasa mini — pra-latih dulu model dasar pada subset umum selama 150 epoch, bekukan seluruh 27257 bobotnya, lalu tempel adapter rank 4 pada lstm.gab dan kepala sehingga hanya 1764 parameter baru yang dilatih pada subset kopi. Rumus dan alasan matematisnya dibahas tuntas pada chapter LoRA dan QLoRA ; fokus kita di sini adalah implementasinya.

Adapter sebagai Pembungkus Lapisan #

Kelas LoRALinear di src/latih_lora.py membungkus nn.Linear yang sudah beku:

python src/latih_lora.py — kelas LoRALinear
class LoRALinear(torch.nn.Module):
    """Bungkus nn.Linear beku dengan adapter rank rendah delta W = B @ A."""

    def __init__(self, dasar: torch.nn.Linear, r: int = 4, alpha: float = 8.0):
        super().__init__()
        self.dasar = dasar                      # bobot beku W
        for p in self.dasar.parameters():
            p.requires_grad_(False)
        self.A = torch.nn.Parameter(
            torch.randn(r, dasar.in_features) * 0.01)     # gaussian kecil
        self.B = torch.nn.Parameter(
            torch.zeros(dasar.out_features, r))           # nol -> delta awal 0
        self.skala = alpha / r

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        delta = x @ self.A.transpose(0, 1) @ self.B.transpose(0, 1)
        return self.dasar(x) + self.skala * delta

Empat keputusan desain pada kelas ini perlu dibedah:

  1. requires_grad_(False): seluruh bobot lapisan dasar ditandai beku, sehingga autograd tidak menghitung gradient-nya dan optimizer tidak memperbaruinya.
  2. A diinisialisasi gaussian kecil, B nol: hasil kali B @ A tepat nol saat awal, jadi keluaran model identik sebelum dan sesudah adapter dipasang — dijamin pula oleh pengujian tests/test_lora.py.
  3. self.skala = alpha / r: penyesuaian magnitudo delta terhadap rank; dengan alpha 8 dan rank 4, skala bernilai 2.
  4. forward menghitung jalur ganda: keluaran lapisan dasar ditambah skala * delta, sehingga adapter hanya menambahkan koreksi, bukan menggantikan perilaku model.

Fungsi pasang_lora kemudian mengganti dua atribut model dengan versi ber-adapter dan menyalakan requires_grad hanya untuk A dan B. Siklus latih bersama dari model_bahasa.py sudah memfilter parameter: torch.optim.Adam([p for p in model.parameters() if p.requires_grad], lr=lr).

Eksekusi dan Angka Nyata #

Jalankan dari folder proyek:

bash
python3 src/latih_lora.py

Output yang Dihasilkan:

text
== 1. pra-latih pada subset umum ==
jendela latih      : 46
loss ep1           : 3.2306
loss ep150          : 1.0986

== 2. adapter LoRA terpasang (r=4, alpha=8) ==
norm delta W lstm  : 0.0000
norm delta W kepala: 0.0000

== 3. fine-tuning pada subset kopi (hanya A dan B) ==
jendela latih      : 17
loss ep1           : 2.5984
loss ep60          : 1.7209

== 4. anggaran parameter ==
total parameter model dasar    : 27257
parameter full fine-tune       : 27257 (100,0%)
parameter LoRA (A dan B saja)  : 1764 (6.5% dari dasar)
norm delta W lstm setelah latih: 4.2079
norm delta W kepala setelah    : 1.6270
bobot dasar lstm tidak berubah : True

Empat pengamatan bisa ditarik dari keluaran tersebut. Norm delta W bernilai tepat 0.0000 saat adapter dipasang — bukti inisialisasi B nol bekerja. Setelah 60 epoch, norm delta W lstm naik ke 4.2079, artinya adapter benar-benar belajar koreksi berbobot. Anggaran parameter sangat timpang: 1764 dari 27257 (6,5 persen), dengan rincian 1408 pada lstm.gab dan 356 pada kepala. Terakhir, baris bobot dasar lstm tidak berubah : True membuktikan seluruh penyesuaian hanya terjadi di A dan B.

Pembanding Full Fine-Tuning #

Skrip src/latih_full.py mengulang skenario yang sama tanpa adapter — seluruh parameter ikut diperbarui:

text
== 2. full fine-tuning pada subset kopi ==
loss ep1   : 2.5984
loss ep60   : 1.2884
bobot dasar lstm berubah : True

== 3. perbandingan anggaran parameter ==
total parameter model         : 27257
dilatih full fine-tune        : 27257 (100,0%)
dilatih LoRA r=4 (latih_lora) : 1764 (6.5%)

Loss akhir full fine-tuning (1.2884) memang lebih rendah daripada LoRA (1.7209) pada data latih kopi — wajar karena full fine-tuning menggerakkan 15 kali lebih banyak parameter. Namun biayanya juga lebih besar: state optimizer Adam harus menyimpan momentum untuk seluruh 27257 bobot, sementara jalur LoRA hanya untuk 1764. Pada LLM sungguhan dengan miliaran parameter, rasio biaya inilah yang membuat LoRA menjadi pilihan utama, sebagaimana dirangkum pada chapter Parameter-Efficient Fine-Tuning .

Loss yang lebih rendah pada data latih belum berarti model lebih baik secara umum. Bagian berikutnya menguji kedua jalur pada data validasi kopi sekaligus subset umum untuk melihat kemampuan mana yang bertahan dan mana yang tergeser.