Dropout, weight decay, dan early stopping menekan overfitting dari sisi model serta prosedur latih. Data augmentation menyerang dari sisi lain: data. Alih-alih menyulit model, kita memperbanyak data latih dengan menyalin tiap sampel lalu menerapkan transformasi yang tidak mengubah labelnya — gambar kucing yang diputar 10 derajat tetaplah gambar kucing. Pendekatan ini nyaris gratis dibanding mengumpulkan dan menganotasi data baru, dan pada banyak domain menjadi penyumbang kenaikan performa terbesar. Chapter ini membangun transformasi dasar dengan NumPy murni, lalu membuktikan efeknya pada eksperimen generalisasi kecil yang berjalan dalam sepersekian detik.

Konsep: Menambah Data tanpa Mengumpulkan Data #

Transformasi augmentasi harus memenuhi satu syarat: hasil transformasi tetap mewakili kelas yang sama. Pada gambar, rotasi kecil, pencerminan, pemotongan acak, dan penambahan derau memenuhi syarat tersebut karena label “kucing” tidak melekat pada sudut kamera. Model yang dilatih pada banyak varian posisi dan pencahayaan tidak bisa lagi mengandalkan satu kebetulan piksel — model dipaksa belajar pola yang stabil, dan itulah generalisasi.

Pilihan transformasi bergantung pada domain data. Tabel berikut merangkum transformasi yang lazim dipakai:

DomainTransformasi umumCatatan
Gambarflip, rotasi kecil, crop acak, zoom, noise gaussian, penyesuaian brightnesspaling matang; pipeline lengkap di Computer Vision
Tekspenggantian sinonim, back-translation (terjemahkan bolak-balik)hati-hati: sinonim bisa menggeser makna; lebih sering dipakai pada data latih besar
Audiopergeseran pitch, pengubahan tempo, penambahan derau latarlabel pembicara atau kata tetap bertahan
Tabularinterpolasi antar sampel sekelas, pembalikan nilai tanda pada fitur tertentupaling terbatas; mengubah satu fitur bisa mengubah makna baris

Alur augmentasi berjalan hanya pada jalur data latih, sebagaimana diagram berikut:

Validasi dan test berada di luar kotak transformasi dengan sengaja: keduanya harus menirukan data dunia nyata yang datang apa adanya.

Transformasi Dasar dengan NumPy Murni #

Berkas augmentasi_numpy.py membangun semuanya dari nol tanpa pustaka gambar: sebuah “gambar” sintetis 8×8 berisi kuadrat 3×3, tiga transformasi dasar (flip horizontal, geser melingkar, noise gaussian), lalu pencetakan statistik sebelum dan sesudah:

python
"""Data augmentation dengan NumPy murni: flip, geser, dan noise gaussian.

Bagian 1: statistik sebuah "gambar" sintetis 8x8 sebelum dan sesudah tiap
transformasi. Bagian 2: eksperimen generalisasi — nearest-centroid dilatih
pada 12 sampel bersih vs 12 sampel + 48 hasil augmentasi, keduanya dievaluasi
pada test set yang sama sekali tidak diaugmentasi.
"""

import numpy as np

rng = np.random.default_rng(42)

UKURAN = 8


# ---------- 1. Transformasi dasar pada satu gambar ----------
def buat_gambar(baris, kolom):
    """Gambar UKURAN x UKURAN berlatar 0 dengan kuadrat 3x3 bernilai 1.0."""
    img = np.zeros((UKURAN, UKURAN))
    img[baris:baris + 3, kolom:kolom + 3] = 1.0
    return img


def flip_horizontal(img):
    """Balik kiri-kanan: kolom c dipetakan ke kolom (lebar-1-c)."""
    return img[:, ::-1]


def geser_kolom(img, geser):
    """Geser isi gambar secara melingkar sepanjang sumbu kolom."""
    return np.roll(img, geser, axis=1)


def noise_gaussian(img, sigma):
    """Tambahkan derau gaussian lalu jepit kembali ke rentang [0, 1]."""
    return np.clip(img + rng.normal(0.0, sigma, img.shape), 0.0, 1.0)


def tampilkan(nama, img):
    """Cetak isi gambar sebagai grid ringkas plus empat statistik."""
    print(f"{nama}  (mean {img.mean():.3f} | std {img.std():.3f} | "
          f"min {img.min():.2f} | max {img.max():.2f})")
    for baris in img:
        print("   " + " ".join(f"{v:.1f}" for v in baris))


print("=== Bagian 1: statistik sebelum/sesudah transformasi ===\n")
asli = buat_gambar(1, 2)
tampilkan("asli                :", asli)
print()
tampilkan("flip horizontal     :", flip_horizontal(asli))
print()
tampilkan("noise gaussian 0.15 :", noise_gaussian(asli, 0.15))


# ---------- 2. Eksperimen generalisasi ----------
def buat_dataset(jumlah_per_kelas, kolom_min, kolom_maks, sigma, generator):
    """Kelas 0: kuadrat di baris atas; kelas 1: kuadrat di baris bawah."""
    X, y = [], []
    for kelas, baris in ((0, 1), (1, 5)):
        for _ in range(jumlah_per_kelas):
            kolom = int(generator.integers(kolom_min, kolom_maks))
            img = buat_gambar(baris, kolom)
            img = np.clip(img + generator.normal(0.0, sigma, img.shape), 0.0, 1.0)
            X.append(img.ravel())
            y.append(kelas)
    return np.array(X), np.array(y)


def augmentasi(X, y, jumlah_varian, sigma, generator):
    """Flip acak + geser acak + noise ringan; label disalin apa adanya."""
    X_baru, y_baru = [], []
    for x, label in zip(X, y):
        img = x.reshape(UKURAN, UKURAN)
        for _ in range(jumlah_varian):
            varian = img
            if generator.random() < 0.5:
                varian = flip_horizontal(varian)
            varian = geser_kolom(varian, int(generator.integers(-2, 3)))
            varian = np.clip(
                varian + generator.normal(0.0, sigma, varian.shape), 0.0, 1.0
            )
            X_baru.append(varian.ravel())
            y_baru.append(label)
    return np.array(X_baru), np.array(y_baru)


def latih_centroid(X, y):
    """Nearest-centroid: satu titik pusat per kelas."""
    return np.stack([X[y == k].mean(axis=0) for k in (0, 1)])


def akurasi(X, y, centroid):
    """Prediksi = centroid terdekat (jarak kuadrat, ||x|| sama untuk semua)."""
    jarak = -2.0 * X @ centroid.T + (centroid ** 2).sum(axis=1)
    return float((jarak.argmin(axis=1) == y).mean())


print("\n=== Bagian 2: eksperimen generalisasi ===\n")

# Data latih bersih: kuadrat hanya muncul pada kolom 1-2
X_latih, y_latih = buat_dataset(6, 1, 3, 0.05, rng)
# Test set: kuadrat muncul di kolom 3-5 — posisi yang tak pernah dilihat
# saat latih, dan test set sama sekali tidak diaugmentasi
X_test, y_test = buat_dataset(12, 3, 6, 0.10, rng)

X_aug, y_aug = augmentasi(X_latih, y_latih, 4, 0.10, rng)

X_gab = np.vstack([X_latih, X_aug])
y_gab = np.concatenate([y_latih, y_aug])

akurasi_bersih = akurasi(X_test, y_test, latih_centroid(X_latih, y_latih))
akurasi_aug = akurasi(X_test, y_test, latih_centroid(X_gab, y_gab))

print(f"data latih bersih : {len(X_latih):2d} sampel, kuadrat di kolom 1-2")
print(f"data latih augmen : {len(X_gab):2d} sampel "
      f"(12 asli + {len(X_aug)} hasil augmentasi)")
print(f"data test         : {len(X_test):2d} sampel, kuadrat di kolom 3-5, "
      f"TIDAK diaugmentasi")
print(f"\nakurasi test dengan latih bersih : {akurasi_bersih:.3f}")
print(f"akurasi test dengan latih augmen : {akurasi_aug:.3f}")
print(f"kenaikan                         : {akurasi_aug - akurasi_bersih:+.3f}")

Output yang Dihasilkan:

text
=== Bagian 1: statistik sebelum/sesudah transformasi ===

asli                :  (mean 0.141 | std 0.348 | min 0.00 | max 1.00)
   0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
   0.0 0.0 1.0 1.0 1.0 0.0 0.0 0.0
   0.0 0.0 1.0 1.0 1.0 0.0 0.0 0.0
   0.0 0.0 1.0 1.0 1.0 0.0 0.0 0.0
   0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
   0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
   0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
   0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0

flip horizontal     :  (mean 0.141 | std 0.348 | min 0.00 | max 1.00)
   0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
   0.0 0.0 0.0 1.0 1.0 1.0 0.0 0.0
   0.0 0.0 0.0 1.0 1.0 1.0 0.0 0.0
   0.0 0.0 0.0 1.0 1.0 1.0 0.0 0.0
   0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
   0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
   0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
   0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0

noise gaussian 0.15 :  (mean 0.183 | std 0.335 | min 0.00 | max 1.00)
   0.0 0.0 0.1 0.1 0.0 0.0 0.0 0.0
   0.0 0.0 1.0 1.0 1.0 0.2 0.1 0.0
   0.1 0.0 1.0 1.0 1.0 0.0 0.2 0.0
   0.0 0.0 1.0 1.0 1.0 0.1 0.3 0.0
   0.0 0.0 0.1 0.2 0.0 0.0 0.0 0.1
   0.1 0.1 0.0 0.0 0.0 0.0 0.1 0.0
   0.1 0.0 0.0 0.1 0.0 0.0 0.0 0.0
   0.0 0.2 0.0 0.1 0.0 0.0 0.0 0.1

=== Bagian 2: eksperimen generalisasi ===

data latih bersih : 12 sampel, kuadrat di kolom 1-2
data latih augmen : 60 sampel (12 asli + 48 hasil augmentasi)
data test         : 24 sampel, kuadrat di kolom 3-5, TIDAK diaugmentasi

akurasi test dengan latih bersih : 0.792
akurasi test dengan latih augmen : 1.000
kenaikan                         : +0.208

Mari kita telaah kode di atas baris per baris:

  1. Baris 11 (rng = np.random.default_rng(42)): pembangkit acak NumPy ber-seed 42 — semua keputusan acak (noise, flip, geser) deterministik sehingga keluaran identik di setiap eksekusi.
  2. Baris 17-21 (buat_gambar): gambar 8×8 berlatar 0 dengan kuadrat 3×3 bernilai 1.0 pada posisi (baris, kolom); kuadrat tersebut berperan sebagai “objek” yang harus dikenali model.
  3. Baris 24-26 (flip_horizontal): img[:, ::-1] membalik urutan kolom — kolom c berpindah ke kolom lebar-1-c — sementara baris tidak tersentuh; statistik mean dan std tidak berubah karena flip hanya memindahkan piksel, seperti terlihat pada keluaran Bagian 1.
  4. Baris 29-31 (geser_kolom): np.roll menggeser isi gambar secara melingkar pada sumbu kolom — piksel yang keluar di sisi kanan masuk kembali di sisi kiri.
  5. Baris 34-36 (noise_gaussian): menambahkan derau rng.normal(0.0, sigma, img.shape) lalu np.clip menjepit hasil ke rentang [0, 1]; pada keluaran, mean naik 0.141 → 0.183 dan latar berisi nilai-nilai kecil.
  6. Baris 57-67 (buat_dataset): kelas 0 berisi kuadrat di baris atas (baris 1) dan kelas 1 di baris bawah (baris 5); posisi kolom diacak dalam rentang tertentu dan sigma mengatur derau alami tiap sampel.
  7. Baris 70-85 (augmentasi): setiap sampel memperoleh 4 varian — flip dengan peluang 0.5, geser acak −2 sampai +2 kolom, lalu noise sigma=0.10; label disalin apa adanya karena ketiga transformasi tidak mengubah posisi baris kuadrat, dan posisi baris itulah penentu kelas.
  8. Baris 88-96 (latih_centroid dan akurasi): nearest-centroid menyimpan satu titik pusat per kelas; jarak kuadrat dihitung dalam bentuk terbentangkan -2·x·c + ||c||² karena suku ||x||² sama untuk kedua kandidat sehingga boleh dihilangkan.
  9. Baris 101-110: data latih bersih berisi 12 sampel dengan kuadrat hanya di kolom 1-2, sedangkan test set memakai kolom 3-5; hasil augmentasi (48 varian) digabungkan dengan 12 sampel asli menjadi 60 sampel latih.
  10. Baris 112-122: dua centroid dilatih — satu dari 12 sampel bersih, satu dari 60 sampel gabungan — lalu dievaluasi pada test set yang sama.

Membaca Eksperimen: Kenaikan 0.792 ke 1.000 #

Dengan 12 sampel bersih, centroid tiap kelas hanya “melihat” kuadrat di kolom 1-2, sehingga pusat kelas condong ke kiri. Test set menempatkan kuadrat di kolom 3-5 — wilayah yang tak pernah terlihat saat latih — dan akurasi terpaut 0.792. Augmentasi menggeser dan membalik kuadrat ke berbagai kolom, sehingga centroid bergeser ke tengah dan berhenti bergantung pada posisi kolom kebetulan; akurasi test mencapai 1.000. Inilah mekanisme inti augmentasi: model dipaksa berpegang pada pola yang benar-benar menentukan label (baris atas atau bawah), bukan artefak penyajian data.

Implementasi di sini sengaja dilakukan luring (offline) dengan NumPy agar mekanismenya terlihat utuh. Pada proyek nyata, augmentasi gambar umumnya dijalankan serentak di dalam pipeline data — dibahas tuntas pada Chapter Image Augmentation di section Computer Vision, termasuk pemisahan subset sebelum transformasi untuk mencegah kebocoran data. Rujukan API NumPy yang dipakai tersedia di numpy.org/doc/stable/reference/generated/numpy.roll.html .

Ringkasan #

Data augmentation memperluas data latih lewat transformasi label-preserving: flip, geser, dan noise pada gambar, sinonim dan back-translation pada teks. Eksperimen NumPy murni menunjukkan mekanismenya secara terukur — 12 sampel menjadi 60, akurasi test melompat dari 0.792 ke 1.000, dengan test set yang tetap bersih. Transformasi harus sesuai domain: yang mengubah label justru merusak data latih. Namun augmentasi terbatas pada data milik kita sendiri; bab berikutnya melangkah lebih jauh — memakai ulang pengetahuan dari model yang dilatih orang lain pada dataset raksasa melalui transfer learning.