VGG membuktikan bahwa lebih dalam biasanya lebih baik — sampai batas tertentu. Tahun 2015, tim Microsoft Research menemukan fenomena yang mengejutkan: jaringan 56 layer bisa kalah dari jaringan 20 layer yang persis sama strukturnya. Masalahnya bukan overfitting karena performa pada data latih juga ikut turun. Fenomena ini disebut degradasi: semakin dalam jaringan plain, semakin sulit proses pelatihan mengoptimalkan layer-layer awal. Kita sudah melihat akar masalahnya di chapter Exploding dan Vanishing Gradient — gradien menyusut drastis sebelum sampai ke layer paling awal.
Residual Network (ResNet) menghadirkan solusi yang hampir memalukan karena sederhananya: tambahkan identitas masukan langsung ke keluaran tiap blok. Alih-alih memaksa blok mempelajari pemetaan penuh H(x), blok cukup mempelajari residual F(x) = H(x) − x, dan keluaran blok menjadi y = F(x) + x. Jika pemetaan optimal dekat dengan identitas, jaringan cukup mendorong bobot menuju nol — jauh lebih mudah daripada merekonstruksi fungsi identitas dari rangkaian layer non-linear.
Verifikasi Mekanisme: Identitas dan Jalan Pintas Gradien #
Berkas residual_demo.py membuktikan dua klaim inti ResNet dengan angka nyata. Bagian A membuktikan bahwa blok residual bisa menghasilkan fungsi identitas secara eksak: ketika convolution kedua dinolkan, F(x) = 0 dan keluaran blok tepat sama dengan masukan. Bagian B mengukur norm gradien bobot layer pertama pada jaringan plain versus residual dengan kedalaman yang sama — memperlihatkan skip connection menjaga gradien tetap hidup saat kedalaman tumbuh:
"""Residual block: verifikasi skip connection + demo gradien layer awal.
Bagian A memverifikasi identitas: ResBlock dengan bobot conv terakhir
dinolkan menghasilkan F(x) = 0 sehingga output blok = x + 0 = x.
Bagian B membandingkan norm gradien bobot layer pertama (paling awal)
pada jaringan plain versus jaringan residual dengan kedalaman
5, 10, 20, dan 30 layer. Loss yang sama dihitung, backward dijalankan,
lalu norm gradien layer pertama tiap jaringan dicetak.
"""
import torch
import torch.nn as nn
import torch.nn.functional as F
torch.manual_seed(42)
# ---------- A. ResBlock dan verifikasi identitas ----------
class ResBlock(nn.Module):
"""Blok residual: y = x + conv2(ReLU(conv1(x))), channel tetap."""
def __init__(self, channel):
super().__init__()
self.conv1 = nn.Conv2d(channel, channel, kernel_size=3, padding=1)
self.conv2 = nn.Conv2d(channel, channel, kernel_size=3, padding=1)
def forward(self, x):
return x + self.conv2(F.relu(self.conv1(x)))
blok = ResBlock(channel=8)
nn.init.zeros_(blok.conv2.weight) # F(x) tepat nol di titik awal
nn.init.zeros_(blok.conv2.bias)
x = torch.randn(2, 8, 8, 8)
with torch.no_grad():
y = blok(x)
print("verifikasi ResBlock saat F(x) = 0:")
print(f" bentuk input : {tuple(x.shape)}")
print(f" bentuk output : {tuple(y.shape)}")
print(f" y sama dengan x (allclose): {torch.allclose(y, x)}\n")
# ---------- B. Norm gradien layer pertama: plain vs residual ----------
LEBAR = 16
class JaringanPlain(nn.Module):
"""Susunan linear + tanh tanpa skip connection."""
def __init__(self, kedalaman):
super().__init__()
self.layers = nn.ModuleList(
[nn.Linear(LEBAR, LEBAR) for _ in range(kedalaman)]
)
for lapis in self.layers:
nn.init.normal_(lapis.weight, std=0.2)
nn.init.zeros_(lapis.bias)
def forward(self, x):
for lapis in self.layers:
x = torch.tanh(lapis(x))
return x
class JaringanResidual(nn.Module):
"""Susunan linear + tanh dengan skip: x = x + tanh(W x)."""
def __init__(self, kedalaman):
super().__init__()
self.layers = nn.ModuleList(
[nn.Linear(LEBAR, LEBAR) for _ in range(kedalaman)]
)
for lapis in self.layers:
nn.init.normal_(lapis.weight, std=0.2)
nn.init.zeros_(lapis.bias)
def forward(self, x):
for lapis in self.layers:
x = x + torch.tanh(lapis(x))
return x
masukan = torch.randn(4, LEBAR)
target = torch.randn(4, LEBAR)
kriteria = nn.MSELoss()
print("norm gradien bobot layer PERTAMA setelah backward:")
print(f" {'kedalaman':>9s} {'plain':>12s} {'residual':>12s} {'rasio':>10s}")
for kedalaman in [5, 10, 20, 30]:
norma = {}
for nama, kelas in [("plain", JaringanPlain), ("residual", JaringanResidual)]:
torch.manual_seed(42) # inisialisasi identik antar varian
model = kelas(kedalaman)
loss = kriteria(model(masukan), target)
loss.backward()
norma[nama] = model.layers[0].weight.grad.norm().item()
rasio = norma["residual"] / norma["plain"]
print(f" {kedalaman:>9d} {norma['plain']:>12.3e} "
f"{norma['residual']:>12.3e} {rasio:>10.0f}x")
print("\nkedalaman 30: gradien layer pertama jaringan plain tinggal")
print("sekitar 2e-04, sedangkan jaringan residual masih sehat di 2e+01")
print("--> skip connection menjadi jalan pintas bagi gradien.")Output yang Dihasilkan:
verifikasi ResBlock saat F(x) = 0:
bentuk input : (2, 8, 8, 8)
bentuk output : (2, 8, 8, 8)
y sama dengan x (allclose): True
norm gradien bobot layer PERTAMA setelah backward:
kedalaman plain residual rasio
5 2.445e-01 2.727e+00 11x
10 1.106e-01 5.928e+00 54x
20 9.849e-03 1.310e+01 1331x
30 2.451e-04 2.062e+01 84149x
kedalaman 30: gradien layer pertama jaringan plain tinggal
sekitar 2e-04, sedangkan jaringan residual masih sehat di 2e+01
--> skip connection menjadi jalan pintas bagi gradien.Mari kita telaah kode di atas baris per baris:
- Baris 19-30 (
ResBlock): blok residual versi mini —y = x + conv2(ReLU(conv1(x))). Tanda tambah adalah skip connection: masukanxdilompati melewati kedua convolution lalu dijumlahkan. Padding 1 pada kedua convolution menjaga resolusi dan jumlah channel tetap, syarat agar penjumlahahan sah. - Baris 32-34: bobot
conv2sengaja dinolkan. DenganF(x) = 0, keluaran blok menjadix + 0 = x— bukti bahwa fungsi identitas adalah titik yang mudah dicapai blok residual. - Baris 36-40: verifikasi berjalan tanpa gradien (
torch.no_grad()) dan hasilallclosebernilaiTrue— masukan dan keluaran identik secara numerik. - Baris 43-68 (
JaringanPlainvsJaringanResidual): dua jaringan pembanding dengan bahan yang sama (layerLinear+ aktivasitanh, inisialisasistd=0.2). Satu-satunya perbedaan ada pada barisforward: varian plain menimpaxdi tiap layer, varian residual menjumlahkannyax = x + tanh(Wx). - Baris 84-95: untuk tiap kedalaman (5, 10, 20, 30), kedua varian dibangun dengan seed yang sama sehingga bobot awal identik, loss MSE dihitung dari target yang sama,
backward()dijalankan, lalu norm gradien bobot layer pertama dicetak. Rasio kedua norm menunjukkan seberapa besar manfaat skip connection.
Membaca Hasil: Gradien yang Tetap Hidup #
Tabel hasil bagian B adalah bukti numerik paling penting di chapter ini. Pada kedalaman 5, gradien layer pertama jaringan plain masih sehat (0,245) dan residual hanya 11x lebih besar. Namun seiring kedalaman bertambah, gradien plain runtuh secara eksponensial: 0,11 pada 10 layer, 0,0098 pada 20 layer, dan tinggal 0,000245 pada 30 layer. Jaringan residual menunjukkan arah yang berlawanan — norm gradien justru membesar (2,7 hingga 20,6), dan rasio kedua varian mencapai 84.149x pada kedalaman 30.
Mengapa bisa demikian? Skip connection memberi gradien jalur tambahan yang bebas dari perkalian turunan berantai. Ketika backward melewati penjumlahan y = x + F(x), turunan terhadap x menerima dua kontribusi: satu dari jalur F(x) (yang bisa menyusut) dan satu dari jalur identitas yang selalu bernilai tepat 1. Gradien bisa menyusut di jalur F, tetapi kontribusi 1 dari jalur identitas tidak pernah hilang — inilah “jalan pintas” yang disebut keluaran program.
Diagram di atas memperlihatkan satu blok residual. Saat ResNet disusun dari blok-blok seperti ini, setiap identitas membentuk tangga gradien yang tidak pernah putus dari layer akhir sampai layer pertama.
ResNet dalam Praktik #
ResNet asli (152 layer) memenangkan kompetisi ImageNet 2015 dengan tingkat galat top-5 sekitar 3,57 persen — lebih baik dari tingkat galat manusia sekitar 5,1 persen. Struktur bloknya hampir sama dengan ResBlock di atas, dengan dua penyesuaian: blok bottleneck (1x1 → 3x3 → 1x1) menurunkan biaya komputasi pada channel yang lebar, dan ketika resolusi berubah, jalur skip diganti proyeksi Conv2d 1x1 agar dimensi cocok. Kedua teknik sudah kita kenal dari chapter VGG dan Inception
.
Makalah aslinya layak dibaca lengkap: Deep Residual Learning for Image Recognition
. Implementasi siap pakai tersedia di halaman model torchvision
— dari resnet18 hingga resnet152 — dan akan kita pakai sebagai backbone transfer learning pada chapter Transfer Learning untuk Gambar
.
ResNet menjawab “seberapa dalam”. Pertanyaan berikutnya justru berlawanan arah: bagaimana membuat jaringan yang efisien — akurasi tinggi dengan parameter dan komputasi sekecil mungkin? EfficientNet menjawabnya dengan formula compound scaling — pokok chapter berikutnya .