Seluruh pelatihan pada Chapter sebelumnya memakai array NumPy yang termuat penuh di memori — aman untuk 200 sampel, tetapi tidak untuk dataset citra berukuran ratusan gigabyte. Masalahnya bukan sekadar ukuran: kita juga ingin pengacakan, pembagian batch, transformasi per elemen, dan penyiapan batch berikutnya selagi batch saat ini dilatih. API tf.data menyediakan semua itu melalui satu abstraksi — objek Dataset yang memperlakukan data sebagai deret elemen yang mengalir melewati rangkaian transformasi. Chapter ini membangun pipeline lengkap dari data sintetis 200 sampel, membedah efek tiap tahap pada bentuk batch, lalu membandingkannya dengan DataLoader PyTorch yang sudah kita kenal pada section sebelumnya.

Dataset sebagai Deret Elemen #

tf.data.Dataset.from_tensor_slices((X, y)) memotong sumbu pertama kedua array: dataset berisi 200 elemen, dan tiap elemen adalah pasangan (fitur, label) dengan bentuk (2,) dan skalar. Semua transformasi tf.data bekerja pada level elemen ini — map menerapkan fungsi ke tiap elemen, batch menggabungkan 32 elemen menjadi satu tensor dengan sumbu batch baru di depan. Model menerima (32, 2), persis bentuk yang selama ini kita susun manual dengan pengirisan array.

Urutan tahapan lazim sebuah pipeline adalah sebagai berikut:

Diagram di atas bukan sekadar dekorasi: urutannya menentukan perilaku. map ditempatkan sebelum batch agar transformasi berjalan per elemen; shuffle ditempatkan sebelum batch agar pengacakan menyentuh elemen, bukan blok batch yang sudah terkunci; prefetch di ujung agar CPU menyiapkan batch berikutnya selagi GPU atau CPU sibuk melatih batch saat ini.

Pipeline Lengkap 200 Sampel #

Berkas pipeline_tf_data.py membangun pipeline utuh dari data sintetis: 200 sampel dua fitur berlabel biner, distandardisasi lewat map, diacak, dibagi batch 32, lalu di-prefetch:

python
"""Pipeline tf.data lengkap: from_tensor_slices -> map -> shuffle -> batch -> prefetch."""

import os

os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2"

import numpy as np
import tensorflow as tf

np.random.seed(42)
tf.keras.utils.set_random_seed(42)

# ---------- 1. Data sintetis 200 sampel: 2 fitur + label biner ----------
n = 200
X = np.random.normal(size=(n, 2)).astype(np.float32)
y = (X[:, 0] + X[:, 1] > 0).astype(np.int32)

dataset = tf.data.Dataset.from_tensor_slices((X, y))
print("cardinality()      :", int(dataset.cardinality()))
fitur0, label0 = next(iter(dataset))
print("bentuk 1 elemen    :", fitur0.shape, "| label:", int(label0))

# ---------- 2. Transformasi per elemen dengan map ----------
rata2 = X.mean(axis=0)
simpangan = X.std(axis=0)


def standardisasi(fitur, label):
    return (fitur - rata2) / simpangan, label


dataset = dataset.map(standardisasi)

label_asli = [int(lbl) for _, lbl in dataset.take(12)]
print()
print("12 label urutan asli:", label_asli)

# ---------- 3. Pipeline: shuffle -> batch -> prefetch ----------
dataset = (
    dataset
    .shuffle(buffer_size=n, seed=42)
    .batch(32)
    .prefetch(tf.data.AUTOTUNE)
)

bentuk_batch = [tuple(batch_X.shape) for batch_X, _ in dataset]
print("bentuk tiap batch  :", bentuk_batch)

label_awal = next(iter(dataset))[1]
print("12 label batch #1  :", label_awal[:12].numpy().tolist())

# ---------- 4. buffer_size kecil: pengacakan hanya lokal ----------
demo = tf.data.Dataset.range(20).shuffle(buffer_size=5, seed=42)
print()
print("range(20).shuffle(buffer_size=5), 12 elemen pertama:")
print([int(x) for x in demo.take(12)])

Output yang Dihasilkan:

text
cardinality()      : 200
bentuk 1 elemen    : (2,) | label: 1

12 label urutan asli: [1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0]
bentuk tiap batch  : [(32, 2), (32, 2), (32, 2), (32, 2), (32, 2), (32, 2), (8, 2)]
12 label batch #1  : [1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0]

range(20).shuffle(buffer_size=5), 12 elemen pertama:
[0, 1, 6, 5, 7, 3, 9, 8, 12, 11, 10, 4]

Mari kita telaah kode di atas baris per baris:

  1. Baris 10-11 (seed ganda): np.random.seed(42) menjaga pembangkitan data, tf.keras.utils.set_random_seed(42) menjaga operasi acak TensorFlow — kombinasi yang membuat seluruh keluaran identik pada setiap eksekusi.
  2. Baris 14-16 (data sintetis): X berisi 200 baris dua fitur dari distribusi normal; label dihitung dari jumlah kedua fitur sehingga hubungan fitur-label sederhana namun tidak sepele.
  3. Baris 18-21 (from_tensor_slices): dataset berisi 200 elemen — dibuktikan cardinality(); elemen pertama berbentuk (2,) dengan satu label, karena sumbu pertama kedua array dipotong menjadi deret pasangan.
  4. Baris 24-32 (map): fungsi standardisasi diterapkan per elemen; rata-rata dan simpangan dihitung sekali di luar fungsi lalu ditangkap sebagai konstanta. Pada proyek nyata, kedua statistik tersebut dihitung dari data latih saja agar data uji tidak menyusup ke preprocessing.
  5. Baris 34-36 (urutan asli): dua belas label pertama dicatat sebelum pengacakan — [1, 1, 0, 1, ...] — sebagai pembanding.
  6. Baris 39-44 (pipeline): shuffle(buffer_size=n) mengacak seluruh 200 elemen karena buffer sama besar dengan dataset; batch(32) menggabungkan elemen menjadi tensor; prefetch(tf.data.AUTOTUNE) membiarkan TensorFlow memutuskan berapa batch disiapkan lebih awal.
  7. Baris 46-47 (efek batch pada bentuk): 200 sampel menghasilkan tujuh batch — enam batch penuh berbentuk (32, 2) dan satu batch sisa (8, 2). Batch terakhir yang lebih kecil ini normal; drop_remainder=True pada batch akan membuangnya bila ukuran seragam dibutuhkan.
  8. Baris 49-50 (bukti shuffle): label dua belas elemen pertama batch #1 adalah [1, 1, 1, 0, 1, ...] — urutan berbeda dari urutan asli, bukti pengacakan bekerja.
  9. Baris 53-56 (buffer kecil): range(20) diacak dengan buffer_size=5 menghasilkan [0, 1, 6, 5, 7, 3, ...] — setiap nilai hanya bergeser beberapa posisi dari tetangga asalnya karena jendela buffer cuma menampung lima elemen.

buffer_size dan Determinisme #

shuffle tidak mengacak seluruh dataset sekaligus; shuffle mengisi buffer berukuran buffer_size, mengambil satu elemen acak dari buffer, lalu mengisi ulang posisi kosong dengan elemen berikutnya. Konsekuensinya: buffer seukuran dataset berarti pengacakan penuh, sedangkan buffer kecil menghasilkan pengacakan lokal — elemen hanya berpindah maksimal sejauh buffer_size dari posisi asal, persis seperti keluaran demo [0, 1, 6, 5, 7, ...] di atas.

tf.data vs DataLoader PyTorch #

Kedua framework memecahkan masalah pasokan batch dengan abstraksi berbeda nama namun sejajar perannya. Padanan praktis DataLoader sudah kita jalankan pada Chapter Dataset dan DataLoader :

Perantf.dataPyTorch
Membungkus datafrom_tensor_slices((X, y))TensorDataset(X, y)
Data non-trivialfrom_generator, TFRecordDatasetclass Dataset dengan __getitem__
Transformasi.map(fungsi)transform di dalam class Dataset
Pengacakan.shuffle(buffer_size, seed)DataLoader(shuffle=True)
Pembagian batch.batch(32)DataLoader(batch_size=32)
Penyiapan paralel.prefetch(tf.data.AUTOTUNE)num_workers, pin_memory

Perbedaan filosofinya: tf.data merangkai transformasi sebagai pipeline deklaratif yang bisa dioptimasi dan didaur ulang lintas epoch, sedangkan PyTorch menyerahkan logika data ke class Python yang kita tulis sendiri. Dokumentasi lengkap seluruh transformasi tersedia di tensorflow.org/guide/data dan halaman API tf.data.Dataset .

Ringkasan #

Kita kini mengalirkan data lewat pipeline from_tensor_slices → map → shuffle → batch → prefetch, memahami efek tiap tahap pada bentuk (32, 2) maupun urutan label, serta menghindari dua jebakan buffer_size. Objek Dataset hasil pipeline ini siap diserahkan langsung ke model.fit() menggantikan array — cukup satu penggantian argumen. Chapter berikutnya memanfaatkannya sekaligus membedah model.fit() secara menyeluruh: validation_split, objek History, hingga callback EarlyStopping dan ModelCheckpoint.