Chapter sebelumnya menunjukkan bahwa fully connected layer kehilangan struktur spasial gambar. Sebelum kita membongkar mekanisme convolution, kita perlu memahami bentuk data yang diprosesnya: gambar hanyalah kumpulan angka pada tensor multi-dimensi. Kesalahan kecil dalam membaca dimensi, channel, atau rentang nilai adalah sumber bug paling umum pada kode pengolahan gambar. Pada chapter ini kita membedah representasi gambar langkah demi langkah dengan array sintetis kecil dan dataset digits sebagai contoh nyata.

Gambar adalah Tensor (H, W, C) #

Gambar grayscale disimpan sebagai matriks dua dimensi (H, W): tinggi kali lebar, satu angka intensitas per piksel. Gambar berwarna menyimpan tiga matriks sekaligus — merah, hijau, biru — sehingga bentuknya menjadi (H, W, C) dengan C = 3; setiap matriks disebut satu channel. Nilai 0 berarti intensitas minimum (gelap) dan nilai maksimum berarti intensitas penuh.

Berkas representasi_gambar.py membangun gambar sintetis 8x8 berupa pola silang, lalu menyusun versi RGB-nya, mengakses piksel individual, dan mengonversi skala nilainya:

python
"""Gambar sebagai tensor: dimensi, channel, rentang nilai, dan konversi skala."""

import numpy as np
import torch
from sklearn.datasets import load_digits

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

# ===== Gambar sintetis 8x8: pola silang pada grayscale =====
silang = np.zeros((8, 8), dtype=np.uint8)
silang[3, :] = 255      # garis horizontal penuh di baris 3
silang[:, 4] = 255      # garis vertikal penuh di kolom 4
print("shape grayscale :", silang.shape)
print("nilai piksel    : min", silang.min(), "max", silang.max())
print(silang)

# ===== Versi RGB: tiga channel, pola berbeda per channel =====
rgb = np.zeros((8, 8, 3), dtype=np.uint8)
rgb[..., 0] = silang           # channel merah: pola silang
rgb[6, 1:3, 1] = 255           # channel hijau: dua piksel kecil
print()
print("shape RGB       :", rgb.shape)
print("piksel (3, 4) pusat silang :", rgb[3, 4])
print("piksel (6, 1) titik hijau  :", rgb[6, 1])
print("piksel (0, 0) latar polos  :", rgb[0, 0])

# ===== Konversi rentang 0-255 menjadi 0-1 =====
silang01 = silang.astype(np.float32) / 255.0
print()
print("sebelum konversi: silang[3, 4] =", silang[3, 4], "| silang[0, 0] =", silang[0, 0])
print("sesudah konversi: silang01[3, 4] =", silang01[3, 4], "| silang01[0, 0] =", silang01[0, 0])

# ===== NHWC -> NCHW di PyTorch =====
batch_nhwc = torch.from_numpy(rgb).float().unsqueeze(0)      # (1, 8, 8, 3)
batch_nchw = batch_nhwc.permute(0, 3, 1, 2)                  # (1, 3, 8, 8)
print()
print("NHWC (aturan TF) :", tuple(batch_nhwc.shape))
print("NCHW (aturan torch):", tuple(batch_nchw.shape))
print("channel merah di NCHW diakses lewat batch_nchw[0, 0]")

# ===== Contoh gambar nyata: sklearn digits (grayscale 8x8) =====
digits = load_digits()
X = digits.images
print()
print("digits shape    :", X.shape, "| dtype:", X.dtype)
print("nilai piksel    : min", X.min(), "max", X.max())
print("citra pertama (angka", digits.target[0], "):")
print(np.round(X[0]).astype(int))
patch = X[0, 2:5, 2:5]
print("patch lokal 3x3 (baris 2-4, kolom 2-4):")
print(patch)

Output yang Dihasilkan:

text
shape grayscale : (8, 8)
nilai piksel    : min 0 max 255
[[  0   0   0   0 255   0   0   0]
 [  0   0   0   0 255   0   0   0]
 [  0   0   0   0 255   0   0   0]
 [255 255 255 255 255 255 255 255]
 [  0   0   0   0 255   0   0   0]
 [  0   0   0   0 255   0   0   0]
 [  0   0   0   0 255   0   0   0]
 [  0   0   0   0 255   0   0   0]]

shape RGB       : (8, 8, 3)
piksel (3, 4) pusat silang : [255   0   0]
piksel (6, 1) titik hijau  : [  0 255   0]
piksel (0, 0) latar polos  : [0 0 0]

sebelum konversi: silang[3, 4] = 255 | silang[0, 0] = 0
sesudah konversi: silang01[3, 4] = 1.0 | silang01[0, 0] = 0.0

NHWC (aturan TF) : (1, 8, 8, 3)
NCHW (aturan torch): (1, 3, 8, 8)
channel merah di NCHW diakses lewat batch_nchw[0, 0]

digits shape    : (1797, 8, 8) | dtype: float64
nilai piksel    : min 0.0 max 16.0
citra pertama (angka 0 ):
[[ 0  0  5 13  9  1  0  0]
 [ 0  0 13 15 10 15  5  0]
 [ 0  3 15  2  0 11  8  0]
 [ 0  4 12  0  0  8  8  0]
 [ 0  5  8  0  0  9  8  0]
 [ 0  4 11  0  1 12  7  0]
 [ 0  2 14  5 10 12  0  0]
 [ 0  0  6 13 10  0  0  0]]
patch lokal 3x3 (baris 2-4, kolom 2-4):
[[15.  2.  0.]
 [12.  0.  0.]
 [ 8.  0.  0.]]

Mari kita telaah kode di atas baris per baris:

  1. Baris 11-13 (silang): np.zeros((8, 8), dtype=np.uint8) membuat kanvas hitam 8x8; baris ke-3 dan kolom ke-4 diisi 255 sehingga terbentuk pola silang putih. Tipe uint8 (0-255) adalah format standar berkas gambar.
  2. Baris 19-21 (rgb): array (8, 8, 3) — sumbu terakhir adalah channel. Notasi rgb[..., 0] mengisi seluruh channel merah dengan pola silang, sedangkan rgb[6, 1:3, 1] menyalakan dua piksel hijau pada baris 6, kolom 1-2.
  3. Baris 24-26: akses piksel memakai indeks (baris, kolom): pusat silang rgb[3, 4] bernilai [255, 0, 0] (merah penuh), titik hijau [0, 255, 0], dan latar [0, 0, 0].
  4. Baris 29-32: pembagian dengan 255.0 memetakan rentang 0-255 ke 0-1 sekaligus mengubah tipe menjadi float32 — format yang ramah bagi pelatihan model.
  5. Baris 35-36: unsqueeze(0) menambah dimensi batch di depan (NHWC: (1, 8, 8, 3)), lalu permute(0, 3, 1, 2) memindahkan channel ke posisi kedua (NCHW: (1, 3, 8, 8)).
  6. Baris 43-49 (load_digits): dataset digits scikit-learn memuat 1.797 citra grayscale 8x8 — bentuk (1797, 8, 8), nilai piksel 0-16 karena dataset ini merekam intensitas 16 tingkat, bukan 256.
  7. Baris 50-52 (patch): slicing X[0, 2:5, 2:5] mengambil kotak 3x3 piksel — inilah patch lokal, unit terkecil yang diamati oleh satu neuron convolution pada chapter berikutnya.

Rentang Nilai dan Konversi Skala #

Dari segi penyimpanan, gambar mentah lazim memakai uint8 — satu byte per piksel, hemat memori namun terbatas pada 256 tingkat. Model deep learning justru bekerja paling stabil pada bilangan pecahan float32 berskala kecil, itulah mengapa konversi 0-255 menjadi 0-1 pada baris 29-32 skrip di atas menjadi langkah pra-pemrosesan standar. Pembagian sederhana tersebut sekaligus menjaga gradien tetap seimbang: aktivasi bernilai ratusan membuat learning rate yang sama tampak terlalu agresif bagi sebagian layer.

NCHW versus NHWC #

Urutan dimensi channel berbeda antar kerangka kerja, dan perbedaan ini kerap membuat bentuk tensor tidak cocok saat berpindah tools. Satu istilah lagi yang perlu dikuasai: N pada awal kedua format adalah dimensi batch — banyaknya gambar yang diproses sekaligus. Satu gambar RGB 8x8 berbentuk (3, 8, 8) di PyTorch, namun 32 gambar yang dikemas menjadi satu batch berbentuk (32, 3, 8, 8); dimensi ini yang dihitung DataLoader saat menyuapi model.

Tabel berikut merangkum kedua konvensi tersebut:

AspekPyTorch (NCHW)TensorFlow/Keras (NHWC)
Urutan dimensibatch, channel, tinggi, lebarbatch, tinggi, lebar, channel
Batch 8 gambar RGB 8x8(8, 3, 8, 8)(8, 8, 8, 3)
Akses channel pertamat[:, 0]t[..., 0]
Konversi silangpermute(0, 3, 1, 2)tf.transpose(x, (0, 3, 1, 2))

Skrip di atas sengaja menunjukkan konversi NHWC ke NCHW karena channel pada data mentah biasanya berada di posisi terakhir (mengikuti format berkas gambar), sedangkan layer convolution PyTorch menuntut channel di posisi kedua. Detail pengindeksan tensor dibahas pada chapter Indexing dan Slicing , dan dokumentasi lengkap tensor PyTorch ada di docs.pytorch.org/docs/stable/generated/torch.Tensor.html .

Kita kini punya gambar sebagai tensor (H, W, C) dengan skala yang terkendali. Pertanyaan berikutnya adalah bagaimana persisnya operasi convolution membaca patch-patch lokal tersebut menjadi satu angka keluaran — mekanisme lengkapnya kita bedah dengan implementasi NumPy murni pada chapter Convolution Layer .