Di balik hampir semua algoritma machine learning klasik terdapat operasi array: perkalian matriks, rata-rata, dan pengurangan yang bekerja per elemen. Itulah sebabnya scikit-learn dibangun di atas NumPy — dan itulah alasan kita bisa membongkar mekanisme pelatihan model tanpa pustaka model apa pun, hanya dengan array.

Sebagai studi kasus, kita memprediksi gaji (juta rupiah) dari pengalaman kerja (tahun) menggunakan sepuluh data fiktif. Empat langkah yang kita tempuh:

  1. Menstandardisasi fitur dengan z-score.
  2. Melatih regresi linear dengan gradient descent murni operasi array.
  3. Menghitung solusi tertutup dengan np.linalg.lstsq.
  4. Membandingkan hasilnya dengan LinearRegression scikit-learn.

Menyiapkan Data dan Standardisasi z-score #

Fitur dengan skala besar membuat gradient descent bergerak lambat dan berayun. Standardisasi z-score memusatkan fitur ke rata-rata 0 dengan deviasi standar 1, sehingga langkah pembaruan model menjadi stabil:

python
import numpy as np

pengalaman = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], dtype=np.float64)
gaji = np.array([5.2, 6.1, 6.8, 7.9, 8.6, 9.5, 10.2, 11.1, 12.0, 12.9])
print("rata-rata lama:", pengalaman.mean(), "deviasi lama:", pengalaman.std())
x = (pengalaman - pengalaman.mean()) / pengalaman.std()
print("rata-rata baru:", x.mean())
print("deviasi baru  :", x.std())
print(np.round(x, 4))

Output yang Dihasilkan:

text
rata-rata lama: 5.5 deviasi lama: 2.8722813232690143
rata-rata baru: -6.661338147750939e-17
deviasi baru  : 1.0
[-1.5667 -1.2185 -0.8704 -0.5222 -0.1741  0.1741  0.5222  0.8704  1.2185
  1.5667]

Mari kita bedah:

  1. Baris 6 ((pengalaman - mean) / std): dua operasi vectorization menjalankan rumus z-score (nilai - rata-rata) / deviasi untuk seluruh kolom sekaligus.
  2. Baris 7 (x.mean()): rata-rata baru tampak -6.66e-17, bukan tepat 0.0 — sisa presisi floating-point yang wajar. Uji dengan np.isclose(x.mean(), 0.0) yang mengembalikan True (lihat Chapter Floating Point Comparison).
  3. Baris 8-9: deviasi baru tepat 1.0, dan z-score tiap nilai tercetak teratur — fitur kini berada pada skala yang ramah untuk pelatihan.

Melatih Model dengan Gradient Descent #

Model kita sederhana: prediksi = w * x + b, dengan w (bobot) dan b (bias) sebagai dua angka yang harus ditemukan. Kualitas model diukur MSE — rata-rata kuadrat selisih prediksi dan nilai asli. Gradient descent mengulang dua langkah: hitung gradien MSE terhadap w dan b, lalu kurangi keduanya dengan learning_rate dikali gradien:

python
import numpy as np

pengalaman = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], dtype=np.float64)
gaji = np.array([5.2, 6.1, 6.8, 7.9, 8.6, 9.5, 10.2, 11.1, 12.0, 12.9])
x = (pengalaman - pengalaman.mean()) / pengalaman.std()
w, b, lr = 0.0, 0.0, 0.05
for epoch in range(1, 501):
    error = w * x + b - gaji
    w -= lr * 2 * np.mean(error * x)
    b -= lr * 2 * np.mean(error)
    if epoch in (1, 10, 50, 100, 500):
        mse = np.mean((w * x + b - gaji) ** 2)
        print(f"epoch {epoch:3d} -> MSE = {mse:.4f}")
print(f"model: gaji = {w:.4f} * x + {b:.4f}")

Output yang Dihasilkan:

text
epoch   1 -> MSE = 70.8574
epoch  10 -> MSE = 10.6397
epoch  50 -> MSE = 0.0075
epoch 100 -> MSE = 0.0052
epoch 500 -> MSE = 0.0052
model: gaji = 2.4353 * x + 9.0300

Penjelasan tiap baris kunci:

  1. Baris 8 (error = w * x + b - gaji): seluruh sepuluh selisih prediksi dihitung sekaligus lewat vectorization — inilah loop yang biasanya ditulis per baris data.
  2. Baris 9-10 (pembaruan w dan b): np.mean(error * x) adalah turunan MSE terhadap w, dan np.mean(error) turunannya terhadap b; kedua parameter bergerak melawan arah gradien sebesar lr per langkah.
  3. Baris 11-13: MSE dicetak pada beberapa epoch terpilih. Angkanya turun dari 70.86 menuju 0.0052 lalu mendatar — tanda model telah konvergen.
  4. Baris 14: model akhir gaji = 2.4353 * x + 9.03; b = 9.03 berarti prediksi untuk pengalaman rata-rata (z-score 0) adalah 9,03 juta rupiah, persis rata-rata gaji data kita.

Solusi Tertutup dengan np.linalg.lstsq #

Regresi linear sebenarnya punya jawaban analitik yang tidak butuh loop. Susun matriks desain berisi [x, 1] per baris, lalu serahkan ke np.linalg.lstsq — solver least squares yang lebih stabil daripada menyusun normal equation secara manual (dibahas penuh pada Chapter Least Squares):

python
import numpy as np

pengalaman = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], dtype=np.float64)
gaji = np.array([5.2, 6.1, 6.8, 7.9, 8.6, 9.5, 10.2, 11.1, 12.0, 12.9])
x = (pengalaman - pengalaman.mean()) / pengalaman.std()
A = np.column_stack([x, np.ones(x.size)])
koef, _, _, _ = np.linalg.lstsq(A, gaji, rcond=None)
print("bentuk A:", A.shape)
print("koefisien:", koef.round(4))
pred = A @ koef
print("MAE      :", round(np.mean(np.abs(pred - gaji)), 4))

Output yang Dihasilkan:

text
bentuk A: (10, 2)
koefisien: [2.4353 9.03  ]
MAE      : 0.0565

Yang perlu diperhatikan:

  1. Baris 6 (np.column_stack): kolom z-score dan kolom angka satu disusun berdampingan menjadi matriks (10, 2); kolom satu inilah yang dianggap b.
  2. Baris 7 (np.linalg.lstsq): mengembalikan koefisien yang meminimalkan jumlah kuadrat error; rcond=None menonaktifkan peringatan versi lama.
  3. Baris 9: hasil [2.4353, 9.03] cocok dengan gradient descent pada empat desimal — dua jalan berbeda menuju model yang sama.
  4. Baris 10-11: rata-rata selisih mutlak (MAE) hanya 0,0565 juta rupiah, sekitar 56 ribu rupiah per prediksi.

Membandingkan dengan scikit-learn #

LinearRegression dari scikit-learn menyelesaikan persoalan yang sama lewat solver yang serupa. Perbedaannya ada pada antarmuka, bukan pada angkanya:

python
import numpy as np
from sklearn.linear_model import LinearRegression

pengalaman = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], dtype=np.float64)
gaji = np.array([5.2, 6.1, 6.8, 7.9, 8.6, 9.5, 10.2, 11.1, 12.0, 12.9])
x = (pengalaman - pengalaman.mean()) / pengalaman.std()
model = LinearRegression().fit(x.reshape(-1, 1), gaji)
print("sklearn coef    :", round(model.coef_[0], 4))
print("sklearn intercept:", round(model.intercept_, 4))
A = np.column_stack([x, np.ones(x.size)])
koef, _, _, _ = np.linalg.lstsq(A, gaji, rcond=None)
print("identik dgn lstsq:", np.allclose(koef, [model.coef_[0], model.intercept_]))

Output yang Dihasilkan:

text
sklearn coef    : 2.4353
sklearn intercept: 9.03
identik dgn lstsq: True
  1. Baris 7 (fit(x.reshape(-1, 1), gaji)): scikit-learn menuntut fitur berbentuk (n_sampel, n_fitur), maka array satu dimensi di-reshape menjadi (10, 1).
  2. Baris 8-9: coef_ dan intercept_ adalah w dan b versi scikit-learn — nilainya sama dengan hasil NumPy kita.
  3. Baris 12 (np.allclose(...)): True mengonfirmasi kedua solver mencapai solusi yang identik hingga toleransi floating-point.
AspekVersi NumPyscikit-learn
Melatihloop pembaruan w, bmodel.fit(X, y)
Prediksiw * x + bmodel.predict(X)
Parameterarray biasa, bisa diinspeksiatribut coef_, intercept_
Cocok untukmemahami mekanismeproduksi dan eksperimen cepat

Praktik Baik dan Jebakan #

Pengalaman dari latihan ini menyangkut tiga hal:

  1. Standardisasi dulu, latih kemudian. Tanpa z-score, fitur berskala besar membuat langkah gradient descent harus sangat kecil agar tidak melompat melewati titik minimum.
  2. Perhatikan bentuk array saat memakai pustaka lain. NumPy senang dengan vektor satu dimensi; scikit-learn menuntut dua dimensi — pesan error reshape akan sering kita temui (lihat Chapter NumPy dan scikit-learn).
  3. Gunakan solver tertutup bila tersedia. Untuk regresi linear, lstsq memberi jawaban pasti tanpa memilih learning_rate atau jumlah epoch; gradient descent tetap berharga sebagai dasar memahami model yang tidak punya solusi tertutup.