Convolution, filter, stride, dan pooling kini sudah kita kuasai satu per satu. Pertanyaan penutup rangkaian dasar ini: apa sebenarnya yang mengalir di antara layer-layer CNN? Jawabannya adalah feature map — satu bidang respons untuk setiap filter, dan tumpukan feature map inilah yang membangun hierarki pengenalan visual. Pada chapter ini kita menjalankan nn.Conv2d sungguhan pada dataset digits, membaca statistik tiap channel keluarannya, lalu melacak pertumbuhan receptive field yang membuat layer dalam “melihat” lebih luas daripada kernel kecilnya.

Satu Filter, Satu Feature Map #

Feature map adalah keluaran satu filter terhadap seluruh gambar: angka besar berarti pola yang dicari filter tersebut hadir di posisi itu, angka kecil berarti tidak. Karena satu layer convolution memproses banyak filter sekaligus, keluarannya berbentuk tensor (N, out_channels, H', W') — jumlah channel keluaran persis sama dengan jumlah filter, dan setiap channel adalah feature map tersendiri. Ukuran spasial H' x W' ditentukan oleh kombinasi stride, padding, dan dilation dari chapter sebelumnya.

Berkas feature_map_demo.py menjalankan satu layer nn.Conv2d dengan 6 filter pada 8 citra digits, lalu menumpuk dua layer tambahan di atasnya:

python
"""Feature map: banyak filter menghasilkan banyak channel keluaran."""

import torch
import torch.nn as nn
from sklearn.datasets import load_digits

torch.manual_seed(42)

digits = load_digits()
batch = torch.tensor(digits.images[:8], dtype=torch.float32).reshape(8, 1, 8, 8)

# Satu layer Conv2d: 1 channel masuk, 6 filter keluar
conv = nn.Conv2d(in_channels=1, out_channels=6, kernel_size=3, padding=1)
with torch.no_grad():
    fitur = conv(batch)

print("input batch :", tuple(batch.shape))
print("feature map :", tuple(fitur.shape))
print("6 filter -> 6 channel keluaran, ukuran spasial dijaga padding=1")
print()

print("statistik per channel (8 contoh, channel c):")
for c in range(6):
    fc = fitur[:, c]
    print(f"  channel {c}: mean {fc.mean():7.3f}  std {fc.std():6.3f}  "
          f"min {fc.min():7.3f}  max {fc.max():7.3f}")
print()

print("feature map contoh pertama, channel 0 (8x8):")
fm0 = torch.round(fitur[0, 0] * 100) / 100
print(fm0)
print()

# Tumpukan layer: channel bertambah, receptive field ikut bertambah
conv2 = nn.Conv2d(6, 12, kernel_size=3, padding=1)
conv3 = nn.Conv2d(12, 24, kernel_size=3, padding=1)
with torch.no_grad():
    fitur2 = conv2(fitur)
    fitur3 = conv3(fitur2)
print("setelah layer ke-2:", tuple(fitur2.shape))
print("setelah layer ke-3:", tuple(fitur3.shape))
print()

# Receptive field: 1 + jumlah (kernel - 1) untuk stride 1
print("pertumbuhan receptive field (semua kernel 3x3, stride 1):")
rf = 1
for layer_ke in (1, 2, 3):
    rf += 3 - 1
    print(f"  setelah layer ke-{layer_ke}: receptive field = {rf}x{rf}")
print()
print("satu kernel 7x7        : 7*7  = 49 bobot per channel, RF 7x7")
print("tiga kernel 3x3 bertumpuk: 3*9 = 27 bobot per channel, RF 7x7")

Output yang Dihasilkan:

text
input batch : (8, 1, 8, 8)
feature map : (8, 6, 8, 8)
6 filter -> 6 channel keluaran, ukuran spasial dijaga padding=1

statistik per channel (8 contoh, channel c):
  channel 0: mean   4.791  std  4.462  min  -2.176  max  17.167
  channel 1: mean   3.817  std  3.810  min  -2.833  max  16.589
  channel 2: mean  -2.776  std  3.109  min -11.410  max   2.278
  channel 3: mean  -0.138  std  2.505  min -10.839  max   7.601
  channel 4: mean   3.927  std  3.806  min  -0.586  max  13.979
  channel 5: mean  -1.979  std  2.676  min -10.535  max   3.266

feature map contoh pertama, channel 0 (8x8):
tensor([[ 0.1000,  4.2500,  7.5600,  5.0500,  7.4200,  5.5600, -1.0500, -0.7200],
        [ 0.9800,  5.5700,  3.5600,  5.7800, 13.6000,  9.4700,  4.3600,  0.3300],
        [ 1.4700,  4.1800,  4.1800,  9.2800,  9.2200, 10.0600,  8.3500,  2.5200],
        [ 1.6000,  3.6000,  5.9600,  6.8500,  2.9300,  6.5800,  7.0800,  3.2900],
        [ 1.3000,  4.4500,  6.8900,  4.1100,  3.8000,  5.8100,  5.9400,  3.4500],
        [ 0.5600,  5.8100,  7.8900,  7.2200,  4.8000,  2.5900,  5.8200,  4.2800],
        [-0.0800,  2.9000,  9.0800, 11.9300,  0.8900,  3.6900,  8.7700,  1.8800],
        [-0.0600, -0.0400,  4.5200,  5.8300,  6.4500,  9.0300,  3.1500,  0.1000]])

setelah layer ke-2: (8, 12, 8, 8)
setelah layer ke-3: (8, 24, 8, 8)

pertumbuhan receptive field (semua kernel 3x3, stride 1):
  setelah layer ke-1: receptive field = 3x3
  setelah layer ke-2: receptive field = 5x5
  setelah layer ke-3: receptive field = 7x7

satu kernel 7x7        : 7*7  = 49 bobot per channel, RF 7x7
tiga kernel 3x3 bertumpuk: 3*9 = 27 bobot per channel, RF 7x7

Mari kita telaah kode di atas baris per baris:

  1. Baris 9-10: delapan citra digits dijadikan tensor (8, 1, 8, 8) — format NCHW batch 8, channel 1, gambar 8x8, mengikuti konvensi chapter Representasi Gambar .
  2. Baris 13 (nn.Conv2d): argumen out_channels=6 berarti 6 filter dipelajari; bobot layer berbentuk (6, 1, 3, 3) — 6 filter, masing-masing menyapu 1 channel masukan dengan kernel 3x3. padding=1 menjaga resolusi 8x8 tetap.
  3. Baris 14-15: forward pass dibungkus torch.no_grad() karena kita hanya mengamati, bukan melatih — kebiasaan yang dijelaskan pada chapter Membangun Model dengan nn.Module .
  4. Baris 17-19: keluaran berbentuk (8, 6, 8, 8) — 8 contoh, 6 feature map berukuran 8x8. Angka 6 pada dimensi kedua persis mengikuti out_channels.
  5. Baris 22-26: statistik per channel berbeda nyata: channel 2 bernada negatif (mean -2.776, maksimum hanya 2.278) sedangkan channel 0 bernada positif (maksimum 17.167). Keragaman ini tanda tiap filter merespons pola berbeda — satu filter bisa menyukai garis terang, filter lain menyukai daerah gelap.
  6. Baris 29-31: satu feature map dicetak penuh sebagai matriks 8x8 — angka tinggi memusat di wilayah tertentu, itulah “peta” lokasi pola.
  7. Baris 35-41: dua layer tambahan menaikkan channel dari 6 menjadi 12 lalu 24; tiap layer baru mengolah seluruh channel layer sebelumnya sekaligus, bukan satu-satu.
  8. Baris 46-52: receptive field dihitung bertambah 2 tiap layer 3x3 stride 1, dan perbandingan penutup menunjukkan tiga kernel 3x3 bertumpuk (27 bobot per channel) menjangkau area yang sama dengan satu kernel 7x7 (49 bobot per channel).

Hierarki Fitur Visual #

Menumpuk layer bukan sekadar memperdalam jaringan; setiap tingkat mengubah jenis pola yang bisa dideteksi, karena filter layer ke-n mengamati feature map — bukan piksel mentah — dari layer ke-n-1:

KedalamanReceptive fieldPola khas yang terdeteksi
Layer pertama3x3 pikseltepi, bercak, gradien
Layer tengahbelasan pikseltekstur, sudut gabungan, motif berulang
Layer akhirpuluhan pikselbagian objek: lingkaran digit, lengkung
Classifierseluruh gambarkelas objek utuh

Pertumbuhan receptive field tersebut berjalan seperti ini:

Satu neuron pada layer ketiga tidak melihat 7x7 piksel mentah sekaligus — neuron tersebut membaca kombinasi 12 feature map layer kedua yang masing-masing sudah merangkum 5x5 piksel. Efek komposisinya sama dengan jendela 7x7 pada gambar asli, itulah makna “receptive field tumbuh”.

Peta besar mekanisme CNN kini lengkap: gambar menjadi tensor, convolution mengekstraksi pola, stride-padding-dilation mengatur bentuk, pooling meringkas, dan feature map bertumpuk menjadi hierarki fitur. Seluruh komponen ini tinggal dirangkai menjadi arsitektur utuh — tugas chapter Arsitektur CNN beserta penelusuran evolusinya dari LeNet hingga EfficientNet. Katalog resmi nn.Conv2d dan ratusan layer lain bisa dirujuk di docs.pytorch.org/docs/stable/nn.html , dengan pendalaman visual di CS231n .