Representasi gambar sebagai tensor (H, W, C) sudah kita kuasai; kini tiba di operasi inti CNN. Convolution layer tidak melihat gambar sekaligus, melainkan menggeser sebuah jendela kecil ke seluruh posisi gambar dan merangkum setiap potongan menjadi satu angka. Pada chapter ini kita membangun operasi tersebut dari nol dengan loop NumPy pada array 6x6 yang bisa dicek dengan hitungan tangan, lalu memverifikasi bahwa hasilnya identik dengan F.conv2d milik PyTorch — bukti bahwa mekanisme di balik layer framework bukan kotak gelap.

Mekanisme: Jendela Geser, Kali, Jumlah #

Satu langkah convolution bekerja dalam empat aksi berurutan pada satu posisi jendela:

Jendela 3x3 pada input 6x6 hanya punya 4x4 posisi menempel (stride 1, tanpa padding), sehingga keluarannya berukuran 4x4. Jumlah posisi inilah yang dihitung rumus ukuran keluaran pada bagian akhir chapter.

Implementasi NumPy Murni #

Berkas konvolusi_numpy.py mendefinisikan fungsi konvolusi2d dengan dua loop eksplisit, menjalankannya pada input 6x6 dan kernel 3x3, lalu membandingkan hasilnya dengan torch.nn.functional.conv2d:

python
"""Konvolusi 2D dari nol dengan NumPy, lalu verifikasi hasilnya dengan PyTorch."""

import numpy as np
import torch
import torch.nn.functional as F

np.random.seed(42)
torch.manual_seed(42)


def konvolusi2d(x, kernel, bias=0.0):
    """Konvolusi 2D stride 1 tanpa padding: jendela geser, kali, jumlah."""
    H, W = x.shape
    Fh, Fw = kernel.shape
    H_out = H - Fh + 1
    W_out = W - Fw + 1
    out = np.zeros((H_out, W_out))
    for i in range(H_out):
        for j in range(W_out):
            jendela = x[i:i + Fh, j:j + Fw]
            out[i, j] = np.sum(jendela * kernel) + bias
    return out


# Input 6x6 dan kernel 3x3 yang sama persis dengan hitungan manual
x = np.arange(1, 37).reshape(6, 6)
kernel = np.array([[0, 1, 0],
                   [1, 2, 1],
                   [0, 1, 0]], dtype=np.float64)

print("input x (6x6):")
print(x)
print()
print("kernel (3x3):")
print(kernel.astype(int))
print()

# Konvolusi tanpa bias
out = konvolusi2d(x, kernel, bias=0.0)
print("hasil konvolusi tanpa bias (4x4):")
print(out.astype(int))
print()

# Konvolusi dengan bias 1.0
out_bias = konvolusi2d(x, kernel, bias=1.0)
print("hasil konvolusi dengan bias 1.0 (4x4):")
print(out_bias.astype(int))
print()

# Verifikasi ukuran keluaran dengan rumus: (W - F + 2P)/S + 1
W_in, K, P, S = 6, 3, 0, 1
rumus = (W_in - K + 2 * P) // S + 1
print(f"rumus (W - F + 2P)/S + 1 = ({W_in} - {K} + {2*P})/{S} + 1 = {rumus}")
print(f"ukuran keluaran NumPy    = {out.shape[0]} x {out.shape[1]}")
print()

# Verifikasi angka dengan torch.nn.functional.conv2d
x_t = torch.tensor(x, dtype=torch.float32).reshape(1, 1, 6, 6)
k_t = torch.tensor(kernel, dtype=torch.float32).reshape(1, 1, 3, 3)
b_t = torch.tensor([1.0])
out_t = F.conv2d(x_t, k_t, bias=b_t)
print("F.conv2d shape :", tuple(out_t.shape))
print("F.conv2d hasil (bias 1.0):")
print(out_t.reshape(4, 4).numpy().astype(int))
print()
sama = np.array_equal(out_t.reshape(4, 4).numpy(), out_bias)
print("NumPy == torch :", sama)

Output yang Dihasilkan:

text
input x (6x6):
[[ 1  2  3  4  5  6]
 [ 7  8  9 10 11 12]
 [13 14 15 16 17 18]
 [19 20 21 22 23 24]
 [25 26 27 28 29 30]
 [31 32 33 34 35 36]]

kernel (3x3):
[[0 1 0]
 [1 2 1]
 [0 1 0]]

hasil konvolusi tanpa bias (4x4):
[[ 48  54  60  66]
 [ 84  90  96 102]
 [120 126 132 138]
 [156 162 168 174]]

hasil konvolusi dengan bias 1.0 (4x4):
[[ 49  55  61  67]
 [ 85  91  97 103]
 [121 127 133 139]
 [157 163 169 175]]

rumus (W - F + 2P)/S + 1 = (6 - 3 + 0)/1 + 1 = 4
ukuran keluaran NumPy    = 4 x 4

F.conv2d shape : (1, 1, 4, 4)
F.conv2d hasil (bias 1.0):
[[ 49  55  61  67]
 [ 85  91  97 103]
 [121 127 133 139]
 [157 163 169 175]]

NumPy == torch : True

Mari kita telaah kode di atas baris per baris:

  1. Baris 11-22 (def konvolusi2d): seluruh mekanisme convolution tinggal 12 baris. Baris 15-16 menghitung ukuran keluaran H - Fh + 1 untuk stride 1 tanpa padding; baris 18-19 menggeser posisi jendela; baris 20 memotong patch; baris 21 melakukan inti operasi — np.sum(jendela * kernel) mengalikan elementwise 9 pasang angka lalu menjumlahkannya, sebelum bias ditambahkan.
  2. Baris 26-29: input np.arange(1, 37).reshape(6, 6) berisi angka berurutan 1-36 supaya setiap sel mudah dilacak, dan kernel [[0,1,0],[1,2,1],[0,1,0]] memberi bobot besar pada piksel tengah.
  3. Baris 39 dan 45: fungsi dipanggil dua kali — tanpa bias dan dengan bias 1.0. Selisih kedua keluaran tepat bernilai 1 di setiap sel karena bias ditambahkan sekali per posisi jendela.
  4. Baris 51-54: rumus ukuran keluaran (W - F + 2P)/S + 1 dihitung untuk W=6, F=3, P=0, S=1 menghasilkan 4, cocok dengan bentuk keluaran NumPy 4 x 4.
  5. Baris 58-61: PyTorch menuntut tensor 4 dimensi (N, C, H, W) — reshape(1, 1, 6, 6) menambah dimensi batch dan channel; kernel ikut dibentuk (1, 1, 3, 3); F.conv2d menjalankan operasi yang sama secara vektor.
  6. Baris 66: np.array_equal membandingkan seluruh angka kedua hasil — keluaran True membuktikan implementasi loop NumPy dan implementasi PyTorch menghitung hal yang persis sama.

Uji Hitungan Manual #

Angka pertama keluaran bisa kita hitung dengan tangan. Jendela pertama menempati baris-kolom (0:3, 0:3) sehingga patchnya berisi [[1,2,3],[7,8,9],[13,14,15]]. Kernel hanya menimbang lima posisi: tetangga atas 2, kiri 7, tengah 8 x 2, kanan 9, dan bawah 14. Jumlahnya 2 + 7 + 16 + 9 + 14 = 48 — persis sel out[0, 0] pada keluaran tanpa bias, dan menjadi 49 setelah bias 1.0 ditambahkan. Seluruh 15 sel lainnya dihasilkan mekanisme yang sama pada posisi jendela berikutnya.

KomponenBentukPeran
Input x(6, 6)gambar grayscale contoh
Kernel(3, 3)bobot yang digeser ke semua posisi
Patch/jendela(3, 3)potongan input per posisi
Biasskalardigeser sekali per posisi jendela
Feature map(4, 4)rangkuman respons tiap posisi

Rumus Ukuran Keluaran #

Dengan lebar input W, ukuran kernel F, padding P, dan stride S, ukuran keluaran convolution adalah (W - F + 2P)/S + 1 — dibulatkan ke bawah bila hasilnya pecahan. Contoh kita: (6 - 3 + 0)/1 + 1 = 4. Pembulatan ke bawah punya konsekuensi nyata: pada input 7x7 dengan stride 2, rumus memberi (7 - 3)/2 + 1 = 3, artinya jendela terakhir berhenti satu piksel sebelum tepi dan kolom paling kanan tidak pernah diamati penuh. PyTorch membiarkan piksel yang tersisa itu terlewat daripada memotong jendela — perilaku yang perlu diingat saat merakit arsitektur. Variasi P dan S (beserta dilation) dibahas tuntas pada chapter Stride, Padding, dan Dilation .

Satu pertanyaan masih menggantung: dari mana angka-angka dalam kernel berasal? Pada contoh ini kita memasangnya manual — bab selanjutnya menunjukkan kernel klasik buatan manusia sekaligus alasan CNN modern membiarkan kernel dipelajari dari data pada chapter Filter dan Kernel . Dokumentasi resmi Conv2d lengkap dengan seluruh parameternya tersedia di docs.pytorch.org/docs/stable/generated/torch.nn.Conv2d.html .