Chapter sebelumnya memperlihatkan jaringan 2-2-1 yang menyelesaikan XOR, tetapi kodenya sengaja belum kita bedah — fokus kita saat itu adalah representasi terpelajari di lapisan tersembunyi. Sekarang saatnya membuka kotaknya: apa sebenarnya “neuron” yang dihitung, mengapa susunan bobot berbentuk matriks, dan bagaimana tumpukan lapisan membentuk satu fungsi besar. Kita akan membangun semuanya dengan NumPy — neuron tunggal, lapisan, lalu kelas FeedforwardNet lengkap dengan forward pass — dan menutup dengan teorema yang sering dikutip namun jarang dipahami batasnya: universal approximation. Setelah chapter ini, istilah “jaringan saraf” seharusnya tidak lagi terdengar seperti benda asing; seluruhnya hanya perkalian matriks dan penjumlahan.
Neuron Artificial: Jumlah Tertimbang Plus Bias #
Neuron artificial adalah fungsi yang sangat sederhana: terima beberapa masukan, kalikan masing-masing dengan bobot, jumlahkan, tambahkan bias, lalu lewatkan hasilnya ke fungsi aktivasi. Dalam notasi ringkas:
z = w1*x1 + w2*x2 + ... + wn*xn + b (jumlah tertimbang + bias)
a = g(z) (fungsi aktivasi)Bobot w mengatur seberapa penting tiap masukan, sedangkan bias menggeser titik potong — dengan bias, neuron tidak terpaksa “berpendirian” tepat di titik nol. Fungsi aktivasi g menentukan keluaran akhir; contoh paling tua adalah aktivasi ambang (keluaran 1 bila z > 0, selain itu 0) yang dipakai perceptron pada tahun 1950-an. Bentuk linear z sendiri sudah kita kenal dari Chapter Regresi Linier Berganda — satu neuron dengan aktivasi identik memang identik dengan regresi linier. Mari kita lihat perhitungannya secara eksplisit, lalu bandingkan dengan bentuk matriks:
# Neuron artificial: jumlah tertimbang masukan + bias, lalu fungsi aktivasi.
# Perhitungan perulangan untuk tiap contoh dibandingkan dengan bentuk matriks
# X @ w + b, lalu satu neuron diperluas menjadi satu lapisan tiga neuron.
import numpy as np
# Satu neuron dengan dua masukan.
w = np.array([0.7, -0.5]) # bobot per masukan
b = -0.1 # bias
# Empat contoh data, dua fitur per contoh.
X = np.array(
[
[1.0, 0.0],
[0.0, 1.0],
[1.0, 1.0],
[0.0, 0.0],
]
)
def aktivasi_ambang(z):
"""Aktivasi ambang: keluaran 1 jika z > 0, selain itu 0."""
return 1 if z > 0 else 0
print("Perhitungan per contoh (perulangan eksplisit):")
z_perulangan = []
for i in range(len(X)):
z = 0.0
for j in range(len(w)):
z += X[i, j] * w[j]
z += b
z_perulangan.append(z)
print(f"contoh {i}: z = {z:.2f} -> keluaran {aktivasi_ambang(z)}")
# Bentuk matriks: seluruh contoh dihitung sekaligus.
Z = X @ w + b
keluaran = (Z > 0).astype(int)
print("\nBentuk matriks X @ w + b:")
print(f"Z = {np.round(Z, 2)}")
print(f"keluaran = {keluaran}")
print(f"sama dengan perulangan: {np.allclose(z_perulangan, Z)}")
# Lapisan berisi tiga neuron: bobot menjadi matriks (2 masukan x 3 neuron),
# tiap kolom adalah bobot milik satu neuron, vektor bias berisi satu nilai
# per neuron.
W = np.array(
[
[0.7, -0.5, 0.3],
[-0.2, 0.4, 0.9],
]
)
b_lapisan = np.array([-0.1, 0.0, 0.2])
Z_lapisan = X @ W + b_lapisan
print("\nSatu lapisan tiga neuron, Z = X @ W + b_lapisan:")
print(f"bentuk Z = {Z_lapisan.shape} (4 contoh x 3 neuron)")
print(np.round(Z_lapisan, 3))Output yang Dihasilkan:
Perhitungan per contoh (perulangan eksplisit):
contoh 0: z = 0.60 -> keluaran 1
contoh 1: z = -0.60 -> keluaran 0
contoh 2: z = 0.10 -> keluaran 1
contoh 3: z = -0.10 -> keluaran 0
Bentuk matriks X @ w + b:
Z = [ 0.6 -0.6 0.1 -0.1]
keluaran = [1 0 1 0]
sama dengan perulangan: True
Satu lapisan tiga neuron, Z = X @ W + b_lapisan:
bentuk Z = (4, 3) (4 contoh x 3 neuron)
[[ 0.6 -0.5 0.5]
[-0.3 0.4 1.1]
[ 0.4 -0.1 1.4]
[-0.1 0. 0.2]]Mari kita bedah hasilnya langkah demi langkah:
- Baris 27–35 (perulangan eksplisit): untuk tiap contoh, kita menjumlahkan
x1*w1 + x2*w2 + bsecara manual — contoh 0 menghasilkan1,0 × 0,7 + 0,0 × (−0,5) − 0,1 = 0,60, lalu aktivasi ambang mengubahnya menjadi keluaran 1. - Baris 37–43 (bentuk matriks):
X @ w + bmenghitung keempat contoh sekaligus dan hasilnyaTruepada pemeriksaannp.allclose— perulangan dan perkalian matriks memberikan angka yang persis sama, tetapi versi matriks jauh lebih ringkas dan cepat karena NumPy mendelegasikannya ke pustaka BLAS. - Baris 45–58 (satu lapisan tiga neuron): bobot menjadi matriks
Wberbentuk(2, 3). Kolom pertama[0.7, -0.2]adalah bobot neuron pertama, kolom kedua milik neuron kedua, dan seterusnya. Satu operasiX @ W + b_lapisankini menghasilkan matriks(4, 3)— empat contoh kali tiga neuron, semua dihitung bersamaan.
Perhatikan juga bagaimana vektor b_lapisan berukuran (3,) “dilebarkan” otomatis ke empat baris hasil. Itu adalah broadcasting NumPy, dan inilah alasan bias selalu ditulis sebagai vektor per-neuron, bukan matriks per-contoh.
Feedforward Network sebagai Komposisi Fungsi #
Satu lapisan menghitung transformasi linear Z = A @ W + b; fungsi aktivasi g mengubahnya menjadi A_baru = g(Z). Feedforward network adalah tumpukan transformasi tersebut: keluaran lapisan menjadi masukan lapisan berikutnya, tanpa sirkuit ulang (itulah makna feedforward, lawan dari recurrent). Secara matematis, jaringan dengan dua lapisan tersembunyi adalah komposisi fungsi:
f(x) = g3( W3 @ g2( W2 @ g1( W1 @ x + b1 ) + b2 ) + b3 )Setiap lapisan hanya melakukan operasi sederhana, tetapi komposisinya membentuk fungsi yang jauh lebih kaya — persis tema “kedalaman lewat komposisi” pada chapter sebelumnya. Berikut implementasi lengkapnya sebagai kelas Python:
# Feedforward network murni NumPy: kelas dengan forward pass saja.
# Belum ada pelatihan -- bobot diundi dari rng berseed sehingga hasil selalu
# sama; pelatihan menyusul pada chapter "Loss Function dan Backpropagation".
import numpy as np
rng = np.random.default_rng(42)
def relu(z):
return np.maximum(0.0, z)
def sigmoid(z):
return 1.0 / (1.0 + np.exp(-z))
class FeedforwardNet:
"""Jaringan maju: lapisan tersembunyi ReLU, keluaran sigmoid."""
def __init__(self, ukuran_lapisan, rng):
self.bobot = []
self.bias = []
for n_masuk, n_keluar in zip(ukuran_lapisan[:-1], ukuran_lapisan[1:]):
# Inisialisasi bersekala 1/sqrt(n_masuk) agar aktivasi stabil.
skala = 1.0 / np.sqrt(n_masuk)
self.bobot.append(rng.normal(0.0, skala, size=(n_masuk, n_keluar)))
self.bias.append(np.zeros(n_keluar))
def forward(self, X):
"""Propagasi maju: komposisi fungsi lapisan demi lapisan."""
A = X
n_lapisan = len(self.bobot)
for i in range(n_lapisan):
Z = A @ self.bobot[i] + self.bias[i]
lapisan_terakhir = i == n_lapisan - 1
A = sigmoid(Z) if lapisan_terakhir else relu(Z)
return A
def jumlah_parameter(self):
return sum(W.size + bias.size for W, bias in zip(self.bobot, self.bias))
# Arsitektur: 4 masukan -> 4 neuron ReLU -> 3 neuron ReLU -> 1 sigmoid.
ukuran = [4, 4, 3, 1]
jaringan = FeedforwardNet(ukuran, rng)
# Enam contoh data sintetis dengan empat fitur tiap contoh.
X = rng.uniform(-1.0, 1.0, size=(6, 4))
print("Jumlah parameter tiap lapisan:")
for i, (W, b) in enumerate(zip(jaringan.bobot, jaringan.bias)):
peran = f"W{i + 1} {W.shape} + b{i + 1} {b.shape}"
print(f" lapisan {i + 1}: {peran} = {W.size + b.size} parameter")
print(f"total parameter = {jaringan.jumlah_parameter()}")
print("\nBentuk aktivasi saat data mengalir ke depan:")
A = X
print(f" masukan : {A.shape}")
for i, (W, b) in enumerate(zip(jaringan.bobot, jaringan.bias)):
A = A @ W + b
terakhir = i == len(jaringan.bobot) - 1
A = sigmoid(A) if terakhir else relu(A)
nama = "keluaran " if terakhir else f"tersembunyi {i + 1} "
print(f" {nama.strip():<12}: {A.shape}")
prediksi = jaringan.forward(X)
print("\nPrediksi jaringan (hanya maju, belum dilatih):")
for i in range(len(X)):
print(f" contoh {i}: prediksi = {prediksi[i, 0]:.4f}")Output yang Dihasilkan:
Jumlah parameter tiap lapisan:
lapisan 1: W1 (4, 4) + b1 (4,) = 20 parameter
lapisan 2: W2 (4, 3) + b2 (3,) = 15 parameter
lapisan 3: W3 (3, 1) + b3 (1,) = 4 parameter
total parameter = 39
Bentuk aktivasi saat data mengalir ke depan:
masukan : (6, 4)
tersembunyi 1: (6, 4)
tersembunyi 2: (6, 3)
keluaran : (6, 1)
Prediksi jaringan (hanya maju, belum dilatih):
contoh 0: prediksi = 0.5834
contoh 1: prediksi = 0.5360
contoh 2: prediksi = 0.5239
contoh 3: prediksi = 0.5040
contoh 4: prediksi = 0.5000
contoh 5: prediksi = 0.5000Beberapa pengamatan penting dari output di atas:
- Baris 21–28 (
__init__): bobot tiap lapisan diundi darirng.normalbersekala1/sqrt(n_masuk)— skala kecil ini menjaga nilaiZtidak membengkak di lapisan-lapisan berikutnya. Generatorrngberseed 42 menjamin hasil identik di setiap eksekusi. - Baris 40–41 (
jumlah_parameter): jaringan mungil 4-4-3-1 ini sudah menyimpan 39 parameter (20 + 15 + 4). Angka ini penting diingat: jumlah parameter yang besar adalah alasan neural network butuh data banyak dan regularisasi — tema Chapter Regularisasi Neural Network. - Bentuk aktivasi: data enam contoh mengalir
(6, 4) -> (6, 4) -> (6, 3) -> (6, 1). Jumlah baris (contoh) tidak pernah berubah; hanya kolom (dimensi representasi) yang bertransformasi. - Prediksi ~0,5: keluaran sigmoid berhenti di sekitar 0,52–0,58 karena bobot masih acak — jaringan belum belajar apa pun. Dua contoh terakhir tepat 0,5000 karena seluruh jalur ReLU yang dilalui bernilai nol sehingga
Zkeluaran juga nol dansigmoid(0) = 0,5.
Teorema Universal Approximation: Apa yang Dijanjikan dan Apa yang Tidak #
Salah satu argumen paling terkenal tentang jaringan saraf adalah universal approximation theorem (Cybenko 1989; Hornik 1991). Bunyinya kira-kira: satu jaringan dengan satu lapisan tersembunyi berisi cukup banyak neuron dapat mendekati sebarang fungsi kontinu pada interval kompak seakurat yang kita mau. Teorema ini sering dikutip sebagai alasan “jaringan saraf bisa apa saja”, tetapi tiga hal jarang disebut bersamanya:
- Teorema hanya menjanjikan keberadaan bobot yang mendekati fungsi target — bukan bahwa gradient descent pasti menemukannya.
- Jumlah neuron yang dibutuhkan bisa sangat besar; untuk beberapa keluarga fungsi, jaringan lebar satu lapisan membutuhkan neuron secara eksponensial, sedangkan jaringan dalam (banyak lapisan) cukup membutuhkannya secara linear. Inilah argumen matematika yang kuat untuk kedalaman.
- Fungsi target harus bisa dihampiri pada data yang kita miliki — pada data sampel terbatas, kapasitas besar justru menjadi pintu overfitting.
Kesimpulan praktisnya jujur dan sederhana: teorema tersebut menegaskan bahwa arsitektur ini cukup ekspresif, tetapi pekerjaan memilih kedalaman, melatih bobot, dan mencegah overfitting tetap sepenuhnya milik kita.
Seluruh komputasi pada chapter ini menyembunyikan satu asumsi besar yang belum kita uji: fungsi aktivasi g. Kita memakai ReLU di lapisan tersembunyi dan sigmoid di keluaran tanpa membahas alasannya. Padahal pilihan tersebut menentukan apakah gradien bisa mengalir saat pelatihan — sigmoid yang “jenuh” bisa membuat jaringan dalam berhenti belajar. Mari kita bedah keluarga fungsi aktivasi beserta turunannya pada Chapter Fungsi Aktivasi.
Referensi Resmi #
- numpy.matmul — NumPy — operasi perkalian matriks yang menjadi inti seluruh forward pass.
- Broadcasting — NumPy — mekanisme pelebaran vektor bias ke seluruh contoh.
- Neural network supervised — scikit-learn User Guide — implementasi MLP siap pakai sebagai pembanding.