Gradient Descent

Minimisasi iteratif dan peran learning rate.

Daftar isi

Gradient descent adalah algoritma minimisasi iteratif yang berulang kali melangkah melawan gradien menuju titik terendah suatu fungsi. Topik ini membahas aturan pembaruan, peran learning rate sebagai penentu panjang langkah, implementasi menggunakan NumPy, serta perilaku lintasan pada permukaan mangkuk. Setelah topik ini selesai, pembaruan parameter selama pelatihan model dapat dibaca dari satu rumus yang sama, dan pengaruh learning rate dapat diprediksi sebelum kode dijalankan.

Langkah menurun mengikuti negatif gradien#

Gradien menunjuk arah kenaikan paling curam, sehingga arah penurunan paling curam adalah negatif gradiennya. Gradient descent memanfaatkan fakta itu melalui aturan pembaruan

xk+1=xk−η ∇f(xk), \mathbf{x}_{k+1} = \mathbf{x}_k - \eta \, \nabla f(\mathbf{x}_k),

yaitu titik saat ini digeser sejauh η \eta kali gradiennya ke arah yang menurun. Notasi x \mathbf{x} mewakili seluruh parameter yang dioptimasi, baik dua variabel maupun satu juta bobot, dan rumusnya tidak berubah. Karena setiap langkah diambil pada arah penurunan paling curam, nilai f f pada kasus yang rapi turun terus menuju titik terendah.

Algoritma ini iteratif, artinya hasilnya diperoleh dari pengulangan langkah kecil dan bukan dari satu rumus penyelesaian langsung. Pengulangan dihentikan ketika gradien sudah cukup kecil, jumlah maksimum iterasi tercapai, atau perbaikan nilai fungsi sudah tidak berarti. Titik berhenti yang wajar adalah titik dengan ∇f=0 \nabla f = \mathbf{0} , karena tidak ada arah yang menurun dari titik itu. Untuk permukaan mangkuk, titik itu bertepatan dengan minimum sebenarnya.

Learning rate menentukan panjang langkah#

Faktor η \eta disebut learning rate, yaitu kelipatan yang menentukan panjang langkah setiap pembaruan. Learning rate yang kecil menghasilkan langkah pendek, sehingga konvergen dengan lambat, sedangkan learning rate yang terlalu besar membuat titik melompati lembah dan berosilasi, bahkan menjauhi minimum pada nilai yang lebih ekstrem. Untuk fungsi kuadrat dengan kelengkungan terbesar L L , osilasi mulai terjadi ketika η \eta melampaui 1/L 1/L . Gambar berikut membandingkan lintasan pada f(x)=x2 f(x) = x^2 dari titik awal yang sama untuk dua learning rate yang berbeda.

python Contoh: membandingkan dua learning rate pada f(x) = x^2
import numpy as np
import matplotlib.pyplot as plt


def turun(x0, eta, n):
    """Jejak gradient descent pada f(x) = x^2 dari titik awal x0."""
    jejak = [x0]
    x = x0
    for _ in range(n):
        x = x - eta * 2.0 * x
        jejak.append(x)
    return np.array(jejak)


x = np.linspace(-6.0, 6.0, 200)

fig, axes = plt.subplots(1, 2, figsize=(6, 3))
for ax, eta, judul in zip(axes, (0.1, 1.1), ("learning rate = 0.1", "learning rate = 1.1")):
    jejak = turun(2.0, eta, 5)
    ax.plot(x, x**2, color="#1a5fb4", label="$f(x) = x^2$")
    ax.plot(jejak, jejak**2, "o-", color="#c01c28", markersize=4, linewidth=1)
    ax.set_xlabel("x")
    ax.set_title(judul)
    ax.axhline(0.0, color="#000000", linewidth=0.8)
axes[0].set_ylabel("f(x)")
fig.tight_layout()
fig.savefig("langkah-learning-rate.svg")
Lintasan gradient descent pada f(x) = x kuadrat dengan learning rate 0,1 dan 1,1 dari titik awal yang sama

Panel kiri menggunakan learning rate 0.1, sehingga setiap langkah memangkas posisi menjadi 0.8 kalinya dan lintasannya menurun rapi menuju minimum. Panel kanan menggunakan learning rate 1.1, sehingga setiap langkah melewati minimum dengan jarak yang makin jauh dan lintasannya melebar keluar. Kedua panel berasal dari fungsi yang sama, titik awal yang sama, dan aturan pembaruan yang sama, dan hanya learning rate-nya yang berbeda. Perbedaan hasil sebesar itu menjelaskan mengapa learning rate dianggap hyperparameter yang paling berpengaruh pada pelatihan model.

Implementasi dengan NumPy#

Implementasi gradient descent hanya membutuhkan fungsi, gradiennya, titik awal, learning rate, dan pengulangan. Berkas berikut menurunkan fungsi mangkuk g(x,y)=(x−2)2+(y+1)2+4 g(x, y) = (x-2)^2 + (y+1)^2 + 4 yang titik terendahnya sudah diketahui, yaitu (2,−1) (2, -1) dengan nilai g=4 g = 4 . Dengan titik awal dan jumlah iterasi yang sama, dua learning rate dicoba, dan nilai yang dicetak dibulatkan agar jejaknya mudah dibaca.

python gradient_descent.py
import numpy as np


def g(v):
    """Fungsi mangkuk dengan titik terendah di (2, -1) bernilai 4."""
    return (v[0] - 2.0)**2 + (v[1] + 1.0)**2 + 4.0


def grad_g(v):
    """Gradien analitik g, yaitu (2(x - 2), 2(y + 1))."""
    return np.array([2.0 * (v[0] - 2.0), 2.0 * (v[1] + 1.0)])


for eta in (0.15, 1.2):
    x = np.array([0.0, 0.0])
    print(f"--- learning rate {eta} ---")
    for k in range(11):
        if k % 2 == 0:
            print(f"iterasi {k:2d} : x = {np.round(x, 4)}, g(x) = {np.round(g(x), 4)}")
        x = x - eta * grad_g(x)
text Keluaran
--- learning rate 0.15 ---
iterasi  0 : x = [0. 0.], g(x) = 9.0
iterasi  2 : x = [ 1.02 -0.51], g(x) = 5.2005
iterasi  4 : x = [ 1.5198 -0.7599], g(x) = 4.2882
iterasi  6 : x = [ 1.7647 -0.8824], g(x) = 4.0692
iterasi  8 : x = [ 1.8847 -0.9424], g(x) = 4.0166
iterasi 10 : x = [ 1.9435 -0.9718], g(x) = 4.004
--- learning rate 1.2 ---
iterasi  0 : x = [0. 0.], g(x) = 9.0
iterasi  2 : x = [-1.92  0.96], g(x) = 23.208
iterasi  4 : x = [-5.6832  2.8416], g(x) = 77.7895
iterasi  6 : x = [-13.0591   6.5295], g(x) = 287.4696
iterasi  8 : x = [-27.5158  13.7579], g(x) = 1092.9767
iterasi 10 : x = [-55.8509  27.9255], g(x) = 4187.4128

Dengan learning rate 0.15, titik bergerak menuju (2,−1) (2, -1) dan nilai g g turun dari 9.0 hingga 4.004 yang semakin dekat ke nilai terendahnya 4. Dengan learning rate 1.2, titik justru menjauh, dan nilai g g membengkak dari 9.0 menjadi lebih dari empat ribu hanya dalam sepuluh iterasi. Kedua jalannya menggunakan gradien yang sama, sehingga perbedaan hasilnya sepenuhnya berasal dari panjang langkah. Jejak seperti ini menjadi cara cepat mendiagnosis learning rate sebelum algoritma diterapkan pada fungsi yang minimumnya belum diketahui.

Demo interaktif: menyetel learning rate#

Demo berikut menjalankan gradient descent pada peta kontur mangkuk f(x,y)=x2+y2 f(x, y) = x^2 + y^2 . Atur slider learning rate, tekan tombol Satu langkah berkali-kali untuk melihat setiap pembaruan satu per satu, atau tekan 20 langkah untuk menjalankannya sekaligus. Dengan learning rate 0.2, lintasannya menuju pusat secara rapi. Dengan 1.0, titiknya berosilasi pada lingkaran yang sama, dan dengan 1.2, titiknya terpental keluar peta. Tekan Ulang untuk kembali ke titik awal setelah setiap percobaan.

Kode sumber demo
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="utf-8">
<title>Gradient descent pada peta kontur f(x, y) = x&#178; + y&#178;</title>
<style>
  body { font-family: sans-serif; margin: 12px; color: #241f31; }
  .baris { display: flex; align-items: center; gap: 10px; margin: 8px 0; flex-wrap: wrap; }
  input[type="range"] { width: 170px; }
  button { padding: 5px 10px; cursor: pointer; }
  #peta { background: #f6f5f4; border-radius: 6px; }
  #status { margin-top: 8px; padding: 10px 12px; border-radius: 6px; background: #f6f5f4; }
</style>
</head>
<body>
  <div class="baris">
    <label for="eta">learning rate = <span id="veta">0.2</span></label>
    <input id="eta" type="range" min="0.05" max="1.2" step="0.05" value="0.2">
    <button id="satu" type="button">Satu langkah</button>
    <button id="banyak" type="button">20 langkah</button>
    <button id="ulang" type="button">Ulang</button>
  </div>
  <canvas id="peta" width="420" height="300"></canvas>
  <div id="status"></div>
<script>
  var skala = 34;          // piksel per satuan
  var cx = 210, cy = 150;  // letak titik asal pada kanvas
  var titik = [3.0, 3.0];
  var jejak = [[3.0, 3.0]];

  function f(x, y) { return x * x + y * y; }

  function gambar() {
    var kanvas = document.getElementById("peta");
    var ctx = kanvas.getContext("2d");
    ctx.clearRect(0, 0, kanvas.width, kanvas.height);

    // sumbu x dan y
    ctx.strokeStyle = "#c9c5c1";
    ctx.beginPath();
    ctx.moveTo(0, cy); ctx.lineTo(kanvas.width, cy);
    ctx.moveTo(cx, 0); ctx.lineTo(cx, kanvas.height);
    ctx.stroke();

    // garis kontur pada level f = 1, 4, 9, 16 (jari-jari 1, 2, 3, 4)
    var jari = [1, 2, 3, 4];
    for (var k = 0; k < jari.length; k++) {
      ctx.beginPath();
      ctx.arc(cx, cy, jari[k] * skala, 0, 2 * Math.PI);
      ctx.strokeStyle = "#9a9996";
      ctx.stroke();
      ctx.fillStyle = "#77767b";
      ctx.fillText("f = " + (jari[k] * jari[k]), cx + jari[k] * skala - 36, cy - 4);
    }

    // jejak lintasan yang sudah dilalui
    ctx.strokeStyle = "#c01c28";
    ctx.beginPath();
    for (var i = 0; i < jejak.length; i++) {
      var px = cx + jejak[i][0] * skala;
      var py = cy - jejak[i][1] * skala;
      if (i === 0) { ctx.moveTo(px, py); } else { ctx.lineTo(px, py); }
    }
    ctx.stroke();

    // titik saat ini
    var lx = cx + titik[0] * skala, ly = cy - titik[1] * skala;
    var luar = lx < 0 || lx > kanvas.width || ly < 0 || ly > kanvas.height;
    if (!luar) {
      ctx.beginPath();
      ctx.arc(lx, ly, 5, 0, 2 * Math.PI);
      ctx.fillStyle = "#c01c28";
      ctx.fill();
    }

    var info = "iterasi " + (jejak.length - 1) + " : titik (" + titik[0].toFixed(2) +
               ", " + titik[1].toFixed(2) + "), f = " + f(titik[0], titik[1]).toFixed(2);
    if (luar) { info += " (titik keluar peta)"; }
    document.getElementById("status").textContent = info;
  }

  function satuLangkah() {
    var eta = parseFloat(document.getElementById("eta").value);
    // gradien f(x, y) = x^2 + y^2 adalah (2x, 2y), sehingga tiap pembaruan:
    // titik = titik - eta * (2 * titik)
    titik = [titik[0] * (1 - 2 * eta), titik[1] * (1 - 2 * eta)];
    jejak.push([titik[0], titik[1]]);
    gambar();
  }

  document.getElementById("satu").addEventListener("click", satuLangkah);
  document.getElementById("banyak").addEventListener("click", function () {
    for (var i = 0; i < 20; i++) { satuLangkah(); }
  });
  document.getElementById("ulang").addEventListener("click", function () {
    titik = [3.0, 3.0];
    jejak = [[3.0, 3.0]];
    gambar();
  });
  document.getElementById("eta").addEventListener("input", function () {
    document.getElementById("veta").textContent =
      parseFloat(document.getElementById("eta").value).toFixed(2);
  });
  gambar();
</script>
</body>
</html>

Di mana hal ini ditemui dalam kode#

Pelatihan hampir semua model machine learning menggunakan versi gradient descent sebagai penggeraknya. Backpropagation menyediakan gradien loss, kemudian optimizer mengambil alih untuk memperbarui bobot pada setiap iterasi. Varian stochastic gradient descent menghitung gradien dari sebagian kecil data setiap langkah, sedangkan optimizer seperti momentum dan Adam menyesuaikan arah maupun panjang langkah berdasarkan riwayat gradien. Semua varian itu tetap berpegang pada rumus dasar yang sama, yaitu pengurangan kelipatan gradien dari parameter.

Learning rate muncul sebagai hyperparameter yang paling sering disetel, mulai dari nilai tetap hingga penjadwal yang menurunkannya seiring iterasi. Apabila loss pelatihan membesar atau menghasilkan nan, learning rate yang terlalu besar menjadi penyebab utama, dan langkah pertama pemecahan masalahnya adalah mengecilkan nilainya. Sebaliknya, loss yang turun terlalu lambat menandakan langkah yang terlalu pendek. Dua gejala itu dijelaskan langsung oleh lintasan pada gambar dan demo di atas.

Kesalahan umum#

Tiga kesalahan muncul berulang ketika gradient descent pertama kali diterapkan pada masalah nyata. Ketiganya dapat dikenali dari perilaku jejaknya dan diperbaiki dengan penyetelan sederhana.

  • Loss naik atau menghasilkan nan setelah beberapa iterasi. Penyebabnya adalah learning rate yang terlalu besar sehingga setiap langkah melompati lembah, dan perbaikannya adalah mengecilkan learning rate, misalnya sepuluh kali lebih kecil.
  • Loss nyaris tidak turun setelah ratusan iterasi. Penyebabnya adalah langkah yang terlalu pendek, dan perbaikannya adalah memperbesar learning rate secara bertahap sambil memantau jejaknya.
  • Loss bertambah terus karena algoritma mendaki alih-alih menurun. Penyebabnya adalah tanda pembaruan yang terbalik, dan perbaikannya adalah memastikan pembaruan berbentuk pengurangan η∇f \eta \nabla f dari parameter.

Ringkasan#

  • Gradient descent memperbarui parameter dengan xk+1=xk−η ∇f(xk) \mathbf{x}_{k+1} = \mathbf{x}_k - \eta \, \nabla f(\mathbf{x}_k) , yaitu langkah melawan gradien secara berulang.
  • Learning rate η \eta menentukan panjang langkah, dengan nilai kecil yang konvergen lambat dan nilai besar yang berosilasi atau menjauhi minimum.
  • Implementasinya hanya membutuhkan fungsi, gradien, titik awal, learning rate, dan pengulangan, dengan jejak yang dapat diverifikasi pada fungsi yang minimumnya diketahui.
  • Optimizer pada pelatihan model merupakan varian dari aturan yang sama, dan learning rate menjadi hyperparameter pertama yang disetel ketika loss berperilaku tidak wajar.

Referensi#