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:
"""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:
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:
- 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. - Baris 14-16 (data sintetis):
Xberisi 200 baris dua fitur dari distribusi normal; label dihitung dari jumlah kedua fitur sehingga hubungan fitur-label sederhana namun tidak sepele. - 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. - Baris 24-32 (map): fungsi
standardisasiditerapkan 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. - Baris 34-36 (urutan asli): dua belas label pertama dicatat sebelum pengacakan —
[1, 1, 0, 1, ...]— sebagai pembanding. - 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. - 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=Truepadabatchakan membuangnya bila ukuran seragam dibutuhkan. - 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. - Baris 53-56 (buffer kecil):
range(20)diacak denganbuffer_size=5menghasilkan[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
:
| Peran | tf.data | PyTorch |
|---|---|---|
| Membungkus data | from_tensor_slices((X, y)) | TensorDataset(X, y) |
| Data non-trivial | from_generator, TFRecordDataset | class 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.