Idenya
Modul sebelumnya memotong tensor secara manual untuk membuat mini-batch. Cara itu bisa untuk 300 baris di memori. Proyek sungguhan punya data yang tidak muat di memori (ribuan gambar), butuh pemrosesan per contoh (membaca file, augmentasi gambar), dan sebaiknya dimuat secara paralel sementara GPU melakukan training. PyTorch membaginya menjadi dua bagian:
Dataset: tahu ada berapa contoh dan cara mengambil contoh nomori;DataLoader: mengambil contoh dari Dataset, mengacaknya, mengelompokkannya menjadi batch, dan (kalau mau) memuatnya secara paralel.
Dataset hanyalah dua method
Di modul sebelumnya, training loop memotong batch dari tensor secara manual. Itu bisa untuk data kecil, tapi merepotkan dan tidak bisa dipakai untuk data yang tidak muat di memori. Jawaban PyTorch adalah membungkus datanya dalam sebuah Dataset: class yang tahu ada berapa contoh dan bagaimana mengambil satu contoh:
import numpy as np
import pandas as pd
import torch
from torch.utils.data import Dataset, DataLoader
class ChurnDataset(Dataset):
def __init__(self, frame, features, target):
self.X = torch.tensor(frame[features].to_numpy(), dtype=torch.float32)
self.y = torch.tensor(frame[target].to_numpy(), dtype=torch.float32)
def __len__(self):
return len(self.y)
def __getitem__(self, i):
return self.X[i], self.y[i]
churn = pd.read_csv("churn.csv")
FEATURES = ["tenure_months", "orders_90d", "days_since_last_order", "support_tickets", "app_user"]
dataset = ChurnDataset(churn, FEATURES, "churned")
print(len(dataset))
x0, y0 = dataset[0]
print(x0, y0)
400
tensor([12., 7., 47., 1., 0.]) tensor(0.)
__len__mengembalikan jumlah contoh.__getitem__(i)mengembalikan contohi, biasanya sebagai(features, target).
Itulah seluruh kontraknya (special method Python, dari modul class). Untuk data yang sudah berupa tensor, TensorDataset(X, y) melakukan hal yang sama dalam satu baris.
DataLoader membuat batch
Dataset memberikan satu contoh setiap kalinya. Pelatihan membutuhkan batch berisi contoh-contoh yang sudah diacak. DataLoader duduk di atas Dataset dan melakukan persis itu:
torch.manual_seed(0)
loader = DataLoader(dataset, batch_size=64, shuffle=True)
print(len(loader))
X_batch, y_batch = next(iter(loader))
print(X_batch.shape, y_batch.shape)
print(sum(len(yb) for _, yb in loader))
7
torch.Size([64, 5]) torch.Size([64])
400
batch_size=64: setiap iterasi menghasilkan 64 contoh yang ditumpuk menjadi tensor (fitur(64, 5), target(64,)).shuffle=True: urutan acak baru di setiap epoch. Gunakan untuk training; jangan untuk validasi dan test, di mana urutan tidak penting tetapi reprodusibilitas penting.len(loader)adalah jumlah batch per epoch (400 / 64 → 7, yang terakhir lebih kecil).num_workers=4(tidak ditampilkan) memuat batch di proses latar belakang: penting untuk gambar, tidak perlu untuk tabel kecil.
Membagi sebuah Dataset
random_split membagi Dataset menjadi bagian training dan validasi:
from torch.utils.data import random_split
generator = torch.Generator().manual_seed(42)
train_set, val_set = random_split(dataset, [300, 100], generator=generator)
train_loader = DataLoader(train_set, batch_size=32, shuffle=True)
val_loader = DataLoader(val_set, batch_size=100)
print(len(train_set), len(val_set), len(train_loader), len(val_loader))
300 100 10 1
generator yang diberi seed membuat pembagiannya bisa direproduksi, seperti random_state di scikit-learn. (Untuk pembagian terstratifikasi atau preprocessing yang di-fit hanya pada baris training, membagi DataFrame dengan scikit-learn lebih dulu lalu membuat dua Dataset sering kali lebih sederhana.)
Training loop yang lebih rapi
Dengan loader, loop dari modul sebelumnya menjadi:
from torch import nn
mean = dataset.X[train_set.indices].mean(dim=0)
std = dataset.X[train_set.indices].std(dim=0)
torch.manual_seed(0)
model = nn.Sequential(nn.Linear(5, 16), nn.ReLU(), nn.Linear(16, 1))
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
loss_fn = nn.BCEWithLogitsLoss()
for epoch in range(1, 31):
model.train()
for X_b, y_b in train_loader:
optimizer.zero_grad()
loss = loss_fn(model((X_b - mean) / std).squeeze(1), y_b)
loss.backward()
optimizer.step()
model.eval()
correct = 0
with torch.no_grad():
for X_b, y_b in val_loader:
probs = torch.sigmoid(model((X_b - mean) / std).squeeze(1))
correct += ((probs >= 0.5).float() == y_b).sum().item()
if epoch in (1, 10, 30):
print(f"epoch {epoch:2d} validation accuracy {correct / len(val_set):.2f}")
epoch 1 validation accuracy 0.73
epoch 10 validation accuracy 0.86
epoch 30 validation accuracy 0.84
Isi loop tidak lagi tahu bagaimana batch dibuat; loader yang menanganinya. Loop yang sama akan berfungsi tanpa diubah untuk dataset berisi jutaan gambar di disk.
Perhatikan detailnya: squeeze(1) mengubah output model (batch, 1) menjadi (batch,) agar cocok dengan target, dan statistik penskalaan berasal dari baris training saja.
Dataset menjawab "ada berapa?" dan "berikan nomor i"; DataLoader mengubahnya menjadi batch yang diacak. Training loop melakukan iterasi atas loader: for X_b, y_b in train_loader:.
Mengapa ini penting untuk kode yang bersih
Dataset memisahkan akses data dari logika training. Kamu bisa mengetes Dataset secara terpisah (apakah dataset[0] mengembalikan shape yang benar?), menukar Dataset berbasis CSV dengan yang berbasis database tanpa menyentuh loop, dan memakai ulang loop yang sama di berbagai proyek. Ini pemisahan yang sama seperti load_data() vs analyse() di modul pipeline pandas.
Rangkuman
Dataset: implementasikan__len__dan__getitem__(i);TensorDataset(X, y)untuk tensor di memori.DataLoader(dataset, batch_size, shuffle=True)menghasilkan batch; tanpa pengacakan untuk validasi/test.len(loader)= jumlah batch per epoch;num_workersmemuat secara paralel.random_split(dataset, [n_train, n_val], generator=...)untuk pembagian yang bisa direproduksi.- Training loop melakukan iterasi
for X_b, y_b in loader:; fit preprocessing hanya pada baris training.