Langsung ke konten
KamusNgoding
Menengah Deep-learning 5 menit baca

Memahami Backpropagation di Neural Network

#backpropagation #neural network #deep learning #intermediate

Memahami Backpropagation di Neural Network

Pendahuluan

Pernahkah kamu bertanya-tanya bagaimana sebuah neural network bisa “belajar” dari data? Di balik kemampuan itu ada satu algoritma krusial yang bekerja keras setiap kali training berjalan: backpropagation.

Backpropagation adalah jantung dari proses pembelajaran supervised pada neural network. Tanpa algoritma ini, network hanya akan menebak secara acak tanpa pernah membaik. Jika kamu sudah familiar dengan konsep Apa itu Machine Learning? Penjelasan Lengkap untuk Pemula, artikel ini akan membawamu selangkah lebih dalam ke mekanisme yang membuat model neural network bisa dioptimasi.

Di artikel ini kita akan membedah backpropagation dari intuisi, matematika, sampai implementasi kode yang bisa langsung kamu jalankan.


Konsep Dasar Neural Network dan Gradient Descent

Sebelum masuk ke backpropagation, kita perlu paham dua fondasi utamanya.

Neural Network terdiri dari layer-layer neuron. Setiap neuron menerima input, mengalikannya dengan bobot (weight), menambahkan bias, lalu melewatkan hasilnya melalui fungsi aktivasi.

Gradient Descent adalah teknik optimasi yang digunakan untuk meminimalkan loss function — ukuran seberapa jauh prediksi model dari nilai sebenarnya. Caranya dengan menggerakkan weight ke arah yang menurunkan loss.

import numpy as np

# Contoh sederhana: satu neuron dengan satu input
def forward(x, w, b):
    return w * x + b

def loss_mse(y_pred, y_true):
    return 0.5 * (y_pred - y_true) ** 2

# Misal: prediksi = 3, target = 5
y_pred = 3.0
y_true = 5.0
print(f"Loss: {loss_mse(y_pred, y_true)}")  # Loss: 2.0

Gradient descent membutuhkan turunan (gradient) dari loss terhadap setiap weight. Di sinilah backpropagation berperan.


Intuisi dan Mekanisme Backpropagation

Bayangkan kamu sedang bermain telepon berantai. Pesan kesalahan dikirim dari orang terakhir (output) mundur ke orang pertama (input), dan setiap orang menyesuaikan cara mereka berbicara berdasarkan seberapa besar kesalahan yang mereka sebabkan.

Itulah inti backpropagation:

  1. Forward pass — input mengalir maju, menghasilkan prediksi
  2. Hitung loss — bandingkan prediksi dengan label sebenarnya
  3. Backward pass — error mengalir mundur, setiap weight mendapat gradiennya
  4. Update weight — kurangi weight proporsional dengan gradiennya (gradient descent)

Proses ini berulang ribuan hingga jutaan kali hingga loss konvergen ke nilai minimum.


Matematika di Balik Backpropagation (Secara Bertahap)

Kunci matematika backpropagation adalah chain rule dari kalkulus:

$$\frac{\partial L}{\partial w} = \frac{\partial L}{\partial \hat{y}} \cdot \frac{\partial \hat{y}}{\partial z} \cdot \frac{\partial z}{\partial w}$$

Di mana:

  • L = loss
  • ŷ = output setelah aktivasi
  • z = pre-activation (w·x + b)
  • w = weight yang ingin diupdate

Mari kita implementasikan dari nol dengan Python murni (tanpa library ML):

import numpy as np

# Fungsi aktivasi sigmoid dan turunannya
def sigmoid(z):
    return 1 / (1 + np.exp(-z))

def sigmoid_derivative(z):
    s = sigmoid(z)
    return s * (1 - s)

# Loss: Binary Cross-Entropy
def bce_loss(y_pred, y_true):
    eps = 1e-9  # hindari log(0)
    return -np.mean(y_true * np.log(y_pred + eps) + (1 - y_true) * np.log(1 - y_pred + eps))

# ========================
# Neural Network 2-layer sederhana: Input(2) -> Hidden(3) -> Output(1)
# ========================
np.random.seed(42)

# Inisialisasi weight
W1 = np.random.randn(2, 3) * 0.1   # input→hidden
b1 = np.zeros((1, 3))
W2 = np.random.randn(3, 1) * 0.1   # hidden→output
b2 = np.zeros((1, 1))

# Data contoh: XOR problem
X = np.array([[0,0],[0,1],[1,0],[1,1]])
y = np.array([[0],[1],[1],[0]])

learning_rate = 0.5

for epoch in range(10000):
    # ---- FORWARD PASS ----
    z1 = X @ W1 + b1           # (4,3)
    a1 = sigmoid(z1)            # aktivasi hidden
    z2 = a1 @ W2 + b2           # (4,1)
    a2 = sigmoid(z2)            # output (prediksi)

    # ---- HITUNG LOSS ----
    loss = bce_loss(a2, y)

    # ---- BACKWARD PASS ----
    # Gradient loss terhadap a2
    dL_da2 = (a2 - y) / len(y)

    # Gradient melalui sigmoid di layer output
    dL_dz2 = dL_da2 * sigmoid_derivative(z2)   # chain rule

    # Gradient W2 dan b2
    dW2 = a1.T @ dL_dz2
    db2 = np.sum(dL_dz2, axis=0, keepdims=True)

    # Propagasi ke layer 1
    dL_da1 = dL_dz2 @ W2.T
    dL_dz1 = dL_da1 * sigmoid_derivative(z1)

    # Gradient W1 dan b1
    dW1 = X.T @ dL_dz1
    db1 = np.sum(dL_dz1, axis=0, keepdims=True)

    # ---- UPDATE WEIGHT (Gradient Descent) ----
    W2 -= learning_rate * dW2
    b2 -= learning_rate * db2
    W1 -= learning_rate * dW1
    b1 -= learning_rate * db1

    if epoch % 2000 == 0:
        print(f"Epoch {epoch:5d} | Loss: {loss:.4f}")

print("\nPrediksi akhir (mendekati 0/1):")
print(np.round(a2, 3))

Output yang diharapkan:

Epoch     0 | Loss: 0.6931
Epoch  2000 | Loss: 0.1823
Epoch  4000 | Loss: 0.0521
Epoch  6000 | Loss: 0.0302
Epoch  8000 | Loss: 0.0213

Prediksi akhir (mendekati 0/1):
[[0.028]
 [0.972]
 [0.972]
 [0.028]]

Perhatikan bagaimana setiap gradient dihitung dari layer output mundur ke layer pertama — inilah esensi “backward propagation of errors”.


Contoh Kasus Nyata

Sekarang kita lihat implementasi yang lebih praktis menggunakan PyTorch, framework yang umum dipakai di industri:

import torch
import torch.nn as nn
import torch.optim as optim

# Data XOR dengan PyTorch tensor
X = torch.tensor([[0,0],[0,1],[1,0],[1,1]], dtype=torch.float32)
y = torch.tensor([[0],[1],[1],[0]], dtype=torch.float32)

# Definisi model
class SimpleNet(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc1 = nn.Linear(2, 4)
        self.fc2 = nn.Linear(4, 1)
        self.sigmoid = nn.Sigmoid()

    def forward(self, x):
        x = torch.relu(self.fc1(x))   # ReLU di hidden layer
        x = self.sigmoid(self.fc2(x)) # Sigmoid di output
        return x

model = SimpleNet()
criterion = nn.BCELoss()
optimizer = optim.Adam(model.parameters(), lr=0.01)

# Training loop — PyTorch menangani backprop otomatis
for epoch in range(5000):
    optimizer.zero_grad()       # reset gradient sebelumnya
    y_pred = model(X)
    loss = criterion(y_pred, y)
    loss.backward()             # ← backpropagation terjadi di sini!
    optimizer.step()            # update weight

    if epoch % 1000 == 0:
        print(f"Epoch {epoch:5d} | Loss: {loss.item():.4f}")

print("\nPrediksi:")
with torch.no_grad():
    print(torch.round(model(X), decimals=2))

Perlu dicatat bahwa kompleksitas komputasi backpropagation bergantung pada kedalaman dan lebar network. Memahami konsep ini berkaitan erat dengan Mengenal Big O Notation: Cara Mengukur Efisiensi Algoritma — semakin dalam network, semakin banyak operasi matrix yang harus dihitung.

Bayangkan kamu ingin membangun sistem rekomendasi seperti yang digunakan platform e-commerce besar — sistem itu menggunakan network jauh lebih besar, tapi prinsip backpropagation-nya sama persis dengan yang kita implementasikan di atas.


Troubleshooting: Error yang Sering Muncul

Gradient Vanishing: Loss Tidak Berubah di Epoch Awal

Penyebab: Fungsi aktivasi sigmoid/tanh menekan gradient mendekati nol saat nilai sangat besar atau sangat kecil. Di network dalam, gradient yang sudah kecil semakin mengecil saat dipropagasi ke belakang.

Solusi:

import torch
import torch.nn as nn

# Ganti sigmoid dengan ReLU di hidden layer
class BetterNet(nn.Module):
    def __init__(self):
        super().__init__()
        self.layers = nn.Sequential(
            nn.Linear(128, 256),
            nn.ReLU(),           # ← ReLU tidak squash gradient
            nn.Linear(256, 256),
            nn.ReLU(),
            nn.Linear(256, 10),
        )
    def forward(self, x):
        return self.layers(x)

# Gunakan weight initialization yang tepat (He initialization)
def init_weights(m):
    if isinstance(m, nn.Linear):
        nn.init.kaiming_uniform_(m.weight, nonlinearity='relu')
        nn.init.zeros_(m.bias)

model = BetterNet()
model.apply(init_weights)
print("Model siap digunakan dengan He initialization.")

Loss Menjadi NaN Setelah Beberapa Epoch

Penyebab: Learning rate terlalu besar menyebabkan gradient explode — nilai weight melompat ekstrem hingga overflow menjadi NaN.

Solusi:

import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.utils as utils

# Buat model dan data dummy untuk demonstrasi
model = nn.Sequential(nn.Linear(4, 8), nn.ReLU(), nn.Linear(8, 1))
criterion = nn.MSELoss()
X = torch.randn(32, 4)
y = torch.randn(32, 1)

optimizer = optim.Adam(model.parameters(), lr=0.001)  # turunkan lr
epochs = 100

for epoch in range(epochs):
    optimizer.zero_grad()
    output = model(X)
    loss = criterion(output, y)
    loss.backward()

    # Gradient clipping: batasi maksimum norm gradient
    utils.clip_grad_norm_(model.parameters(), max_norm=1.0)

    optimizer.step()

    # Deteksi NaN lebih awal
    if torch.isnan(loss):
        print(f"NaN terdeteksi di epoch {epoch}, hentikan training!")
        break

    if epoch % 20 == 0:
        print(f"Epoch {epoch:3d} | Loss: {loss.item():.4f}")

RuntimeError: Trying to backward through the graph a second time

Penyebab: PyTorch secara default menghapus computational graph setelah satu kali .backward(). Memanggil .backward() dua kali tanpa retain_graph=True akan menyebabkan error ini.

Solusi:

import torch
import torch.nn as nn
import torch.optim as optim

model = nn.Linear(3, 1)
criterion = nn.MSELoss()
optimizer = optim.SGD(model.parameters(), lr=0.01)
X = torch.randn(10, 3)
y = torch.randn(10, 1)

epochs = 5
for epoch in range(epochs):
    optimizer.zero_grad()   # ← WAJIB ada di setiap iterasi
    output = model(X)
    loss = criterion(output, y)
    loss.backward()         # hanya dipanggil sekali per iterasi
    optimizer.step()
    print(f"Epoch {epoch} | Loss: {loss.item():.4f}")

# Jika memang perlu backward dua kali (jarang terjadi):
# loss_utama.backward(retain_graph=True)   # pertama, graph dipertahankan
# loss_aux.backward()                       # kedua, graph dihapus

Pertanyaan yang Sering Diajukan

Apa perbedaan backpropagation dengan gradient descent?

Keduanya berbeda tapi saling melengkapi. Gradient descent adalah algoritma optimasi yang menentukan cara mengupdate weight menggunakan gradient. Backpropagation adalah metode menghitung gradient itu sendiri secara efisien menggunakan chain rule. Gradient descent tidak bisa berjalan tanpa gradient, dan backpropagation menyediakan gradient tersebut.

Mengapa kita perlu fungsi aktivasi non-linear?

Tanpa aktivasi non-linear, tumpukan layer linear berapa pun dalamnya tetap setara dengan satu layer linear. Fungsi aktivasi seperti ReLU, sigmoid, atau tanh memungkinkan network mempelajari pola non-linear yang kompleks — seperti membedakan gambar kucing dari anjing atau mendeteksi sentimen dari teks.

Bagaimana cara kerja loss.backward() di PyTorch?

PyTorch menggunakan autograd — setiap operasi tensor direkam dalam computational graph. Saat .backward() dipanggil, PyTorch menelusuri graph tersebut dari node loss mundur ke setiap leaf tensor (weight), menghitung gradient dengan chain rule secara otomatis. Kamu tidak perlu menghitung turunan secara manual seperti di implementasi from-scratch kita sebelumnya.

Apakah backpropagation selalu konvergen ke solusi optimal?

Tidak selalu. Backpropagation dengan gradient descent bisa terjebak di local minimum atau saddle point, terutama untuk loss landscape yang kompleks. Teknik seperti momentum, Adam optimizer, learning rate scheduling, dan batch normalization membantu mengatasi ini. Dalam praktiknya, untuk model besar, bahkan local minimum yang “cukup baik” sudah memberikan performa luar biasa.

Berapa banyak epoch yang dibutuhkan untuk training?

Tidak ada angka pasti — tergantung kompleksitas data, arsitektur model, dan learning rate. Gunakan teknik early stopping: hentikan training saat validation loss tidak membaik selama N epoch berturut-turut, bukan setelah jumlah epoch tetap.

import torch
import torch.nn as nn
import torch.optim as optim

# Simulasi early stopping
model = nn.Sequential(nn.Linear(4, 8), nn.ReLU(), nn.Linear(8, 1))
optimizer = optim.Adam(model.parameters(), lr=0.01)
criterion = nn.MSELoss()

X_train = torch.randn(64, 4)
y_train = torch.randn(64, 1)
X_val = torch.randn(16, 4)
y_val = torch.randn(16, 1)

best_loss = float('inf')
patience = 10
patience_counter = 0
max_epochs = 200

for epoch in range(max_epochs):
    # Training step
    model.train()
    optimizer.zero_grad()
    loss = criterion(model(X_train), y_train)
    loss.backward()
    optimizer.step()

    # Validasi
    model.eval()
    with torch.no_grad():
        val_loss = criterion(model(X_val), y_val).item()

    if val_loss < best_loss:
        best_loss = val_loss
        patience_counter = 0
        torch.save(model.state_dict(), 'best_model.pt')
    else:
        patience_counter += 1
        if patience_counter >= patience:
            print(f"Early stopping di epoch {epoch} | Best val loss: {best_loss:.4f}")
            break

    if epoch % 50 == 0:
        print(f"Epoch {epoch:3d} | Train Loss: {loss.item():.4f} | Val Loss: {val_loss:.4f}")

Kesimpulan

Backpropagation adalah fondasi dari hampir semua model deep learning modern. Dengan memahami alur forward pass → hitung loss → backward pass → update weight, kamu kini punya gambaran jelas tentang bagaimana neural network benar-benar “belajar”. Kita juga sudah melihat implementasi dari nol dengan NumPy untuk memahami matematika di baliknya, serta versi praktis dengan PyTorch yang digunakan di industri.

Langkah selanjutnya, coba eksplorasi variasi optimizer seperti Adam dan RMSprop, serta teknik regularisasi seperti Dropout dan Batch Normalization untuk membuat training lebih stabil.

Selamat belajar dan terus bereksperimen — semakin banyak model yang kamu latih, semakin dalam intuisimu terhadap backpropagation! Jika ada pertanyaan atau kamu ingin menggali lebih jauh, KamusNgoding masih banyak artikel seru yang menanti untuk dijelajahi.

Artikel Terkait