- Published on
Day 10: PyTorch Fundamentals
- Authors

- Name
- Trần Mạnh Thắng
- @TranManhThang96
Mục Tiêu
Sau bài này, bạn cần làm được các việc sau:
- Tạo và debug
Tensorbằngshape,dtype,device,requires_grad. - Hiểu autograd ở mức đủ để viết training loop đúng:
forward -> loss -> backward -> optimizer.step. - Viết model bằng
nn.Module, đặt computation trongforward()và dùngstate_dictđể lưu artifact. - Tạo
DatasetvàDataLoadercho mini-batch training. - Quản lý CPU/GPU device một cách nhất quán cho model, input, label và checkpoint.
- Rebuild MLP XOR của Day 9 bằng PyTorch, dùng
BCEWithLogitsLossthay vì tự viết backprop bằng NumPy. - Biết khi nào PyTorch phù hợp production và cần thêm điều kiện gì.
Cách Học Bài Này
- Đọc document.md để nắm mental model, API và trade-off.
- Làm lần lượt exercise.md, đặc biệt bài rebuild MLP XOR.
- So sánh code PyTorch với MLP NumPy ở Day 9: phần nào được framework xử lý, phần nào vẫn là trách nhiệm của engineer.
- Tự trả lời checklist cuối bài trước khi sang Day 11.
Bức Tranh Tổng Quan
PyTorch thay thế phần khó nhất của Day 9: bạn không còn tự tính đạo hàm và tự cập nhật từng weight bằng NumPy. Thay vào đó:
Tensor + autograd -> tự track phép toán và tính gradient
nn.Module -> đóng gói architecture và parameters
Dataset/DataLoader -> đóng gói data access và mini-batch
Optimizer -> cập nhật parameters từ gradient
state_dict -> artifact có thể lưu, load, deploy
Nhưng PyTorch không tự giải quyết mọi vấn đề production. Bạn vẫn phải kiểm soát shape contract, dtype, device, preprocessing, seed, data split, metric, checkpoint, logging, monitoring và rollback.
Best Solution Theo Context
Với bài Day 10, best solution là dùng PyTorch thuần, không dùng trainer framework. Lý do: mục tiêu là hiểu cơ chế nền tảng trước khi sang optimizer/scheduler ở Day 11 và Hugging Face ở các bài sau.
| Context | Lựa chọn nên dùng | Vì sao |
|---|---|---|
| Học backprop ở mức concept | NumPy như Day 9 | Thấy rõ phép toán và gradient |
| Training deep learning thật | PyTorch | Có autograd, GPU, module, optimizer, ecosystem |
| Model nhỏ, dev local | CPU fallback | Đơn giản, ít lỗi môi trường |
| Matrix compute lớn hoặc batch inference | GPU | Tận dụng parallel compute |
| Binary classification | BCEWithLogitsLoss | Ổn định số học hơn sigmoid + BCELoss |
| Inference/evaluation | model.eval() + torch.inference_mode() | Đúng behavior và giảm memory |
Dùng Được Trong Production Không?
Có, PyTorch dùng được trong production cho cả training và inference. Tuy nhiên code demo trong bài chỉ là baseline học tập. Để dùng production, tối thiểu cần:
- Artifact rõ ràng:
state_dict, config architecture, preprocessing config, label mapping, threshold và metric. - Reproducibility: seed, package version, dataset snapshot, data split, training config.
- Validation: kiểm tra
shape,dtype, missing value, range của feature và device mismatch. - Runtime mode đúng: training dùng
model.train(), evaluation/inference dùngmodel.eval()kèmtorch.no_grad()hoặctorch.inference_mode(). - Observability: log loss, metric, latency, error rate, prediction distribution, drift.
- Reliability: device fallback CPU/GPU, xử lý checkpoint lỗi, rollback model version, không load checkpoint từ nguồn không tin cậy.
- Performance test: benchmark p50/p95/p99 latency, throughput, VRAM/RAM, data loading bottleneck.
Checklist Hoàn Thành
- Giải thích được
Tensorkhác NumPy array ở điểm nào. - Biết vì sao
loss.backward()tạo gradient và vì sao gradient bị accumulate. - Biết vì sao cần
optimizer.zero_grad(set_to_none=True)trước mỗi update. - Viết được
nn.Modulecó__init__vàforward. - Tạo được
Dataset/DataLoadertrả về batch(features, labels). - Chạy được MLP XOR bằng PyTorch với
BCEWithLogitsLoss. - Save/load được
state_dict. - Trả lời được production readiness của code mình viết.
Tài liệu
1. PyTorch Giải Quyết Gì Sau Day 9?
Ở Day 9, bạn tự viết MLP bằng NumPy:
forward -> tính loss -> tự đạo hàm -> tự update W/b
Cách đó tốt để học, nhưng không phù hợp training deep learning thật vì dễ sai gradient, khó dùng GPU, khó mở rộng architecture và khó lưu/load artifact chuẩn.
PyTorch giữ lại mental model đó nhưng thay phần thủ công bằng các primitive production hơn:
| Day 9 với NumPy | Day 10 với PyTorch | Ý nghĩa |
|---|---|---|
np.ndarray | torch.Tensor | Dữ liệu n chiều, có thể chạy CPU/GPU |
| Tự viết backprop | Autograd | Tự tính gradient từ computational graph |
Tự quản lý W1, b1, W2, b2 | nn.Module và nn.Parameter | Đóng gói weights thành model |
| Tự chia batch | Dataset và DataLoader | Data pipeline chuẩn |
| Tự update weight | torch.optim | Optimizer chuẩn như SGD, AdamW |
| Tự save mảng NumPy | state_dict | Artifact chuẩn của PyTorch |
Best solution trong bài này: viết training loop PyTorch thuần. Chưa dùng abstraction cao hơn vì Senior SE cần hiểu loop nền trước khi debug model thật.
2. Tensor, dtype, shape, device
Tensor là core data structure của PyTorch. Nó giống NumPy array ở chỗ biểu diễn dữ liệu n chiều, nhưng có thêm:
device: tensor nằm trên CPU, CUDA GPU hoặc backend khác.requires_grad: có cần autograd track phép toán không..grad: nơi gradient được accumulate saubackward().- Tích hợp với
torch.nn, optimizer và GPU runtime.
Ví dụ inspect tensor:
import torch
x = torch.tensor(
[[0.0, 1.0], [1.0, 0.0]],
dtype=torch.float32,
)
print("shape:", x.shape) # torch.Size([2, 2])
print("dtype:", x.dtype) # torch.float32
print("device:", x.device) # cpu
print("ndim:", x.ndim)
print("numel:", x.numel())
Shape Là Contract
Với binary classifier MLP:
X shape = (batch_size, input_dim)
logits shape = (batch_size, 1)
y shape = (batch_size, 1)
Nếu logits là (32, 1) nhưng label là (32,), một số operation có thể broadcast âm thầm hoặc loss báo lỗi khó đọc. Trong code gần production, hãy validate shape ở boundary: khi tạo dataset, trước khi tính loss, hoặc trong test.
Dtype Là Contract Tính Toán
Các dtype hay gặp:
| Dtype | Dùng khi | Lưu ý |
|---|---|---|
torch.float32 | Default cho training neural network | Cân bằng tốc độ, memory, độ chính xác |
torch.float64 | Numerical analysis cần chính xác cao | Chậm hơn, tốn RAM/VRAM hơn |
torch.int64 / torch.long | Class index cho CrossEntropyLoss | Không dùng cho BCEWithLogitsLoss target |
torch.bool | Mask | Không dùng trực tiếp làm feature numeric |
torch.float16 / bfloat16 | Mixed precision | Cần hiểu GPU support và stability |
Với BCEWithLogitsLoss, input là logits dạng float và target nên là float cùng shape, giá trị 0 hoặc 1.
Device Là Runtime Placement
Model và tensor input phải nằm cùng device:
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
x = x.to(device)
model = model.to(device)
Lỗi phổ biến:
Expected all tensors to be on the same device, but found at least two devices...
Nguyên nhân thường là model đã .to("cuda") nhưng batch từ DataLoader vẫn ở CPU, hoặc checkpoint được load về CPU rồi trộn với tensor GPU.
Best practice:
- Chọn
devicemột lần ở đầu program. - Move model sang device ngay sau khi khởi tạo.
- Move từng batch sang device trong training/evaluation loop.
- Khi cần log hoặc convert sang NumPy, đưa tensor về CPU bằng
.detach().cpu().
3. Autograd
Autograd build computational graph trong lúc bạn chạy forward pass. Khi gọi loss.backward(), PyTorch tính gradient của loss theo các leaf tensor có requires_grad=True, thường là parameters của model.
Ví dụ nhỏ:
import torch
w = torch.tensor([2.0], requires_grad=True)
x = torch.tensor([3.0])
y = w * x
loss = (y - 10.0) ** 2
loss.backward()
print(w.grad) # dloss/dw
Điều quan trọng: gradient được accumulate. Nếu bạn không reset gradient, batch sau sẽ cộng dồn gradient với batch trước.
Training step chuẩn:
optimizer.zero_grad(set_to_none=True)
logits = model(xb)
loss = loss_fn(logits, yb)
loss.backward()
optimizer.step()
Vì sao set_to_none=True?
- Giảm một số thao tác ghi zero vào memory.
- Có thể tiết kiệm memory và nhanh hơn trong nhiều workload.
- Giúp phát hiện parameter nào không nhận gradient vì
.gradvẫn làNone.
Trade-off: nếu code cũ giả định .grad luôn là tensor zero, cần sửa logic đó.
Khi Không Cần Gradient
Evaluation và inference không cần lưu graph:
model.eval()
with torch.inference_mode():
logits = model(xb)
probs = torch.sigmoid(logits)
torch.no_grad() và torch.inference_mode() đều tắt gradient tracking. inference_mode() tối ưu mạnh hơn cho inference thuần, nhưng ít linh hoạt hơn nếu bạn còn cần tensor tham gia autograd sau đó. Trong bài này, evaluation/predict dùng inference_mode(). Khi viết code debug cần linh hoạt, no_grad() vẫn là lựa chọn an toàn.
model.eval() không thay thế no_grad() hoặc inference_mode(). eval() đổi behavior của layer như Dropout/BatchNorm; gradient mode kiểm soát autograd memory.
4. nn.Module Và forward()
nn.Module là base class cho model/layer. Subclass thường có:
__init__: khai báo layer, parameter, buffer.forward: mô tả computation từ input sang output.state_dict: dictionary chứa parameters và buffers.
Ví dụ:
from torch import nn
class XORMLP(nn.Module):
def __init__(self, hidden_dim: int = 8) -> None:
super().__init__()
self.net = nn.Sequential(
nn.Linear(2, hidden_dim),
nn.Tanh(),
nn.Linear(hidden_dim, 1),
)
def forward(self, x):
if x.ndim != 2 or x.shape[1] != 2:
raise ValueError(f"Expected input shape (batch, 2), got {tuple(x.shape)}")
return self.net(x)
Gọi model bằng model(x), không gọi trực tiếp model.forward(x) trong training code. model(x) đi qua hook và cơ chế nội bộ của nn.Module.
model.train() Và model.eval()
model.train() # training mode
model.eval() # evaluation mode
Với MLP XOR không có Dropout/BatchNorm, output có thể giống nhau giữa train/eval. Nhưng trong model thật, quên eval() có thể làm metric và inference sai.
Production rule:
- Training loop: gọi
model.train()đầu epoch. - Validation/test/inference: gọi
model.eval()và tắt gradient. - Sau khi load checkpoint để serve: gọi
model.eval().
5. Dataset Và DataLoader
Dataset mô tả cách lấy một sample. DataLoader biến dataset thành iterable mini-batch.
from torch.utils.data import Dataset, DataLoader
class XORDataset(Dataset):
def __init__(self):
self.features = ...
self.labels = ...
def __len__(self):
return len(self.features)
def __getitem__(self, idx):
return self.features[idx], self.labels[idx]
loader = DataLoader(
XORDataset(),
batch_size=32,
shuffle=True,
num_workers=0,
)
Các option quan trọng:
| Option | Ý nghĩa | Trade-off |
|---|---|---|
batch_size | Số sample mỗi batch | Lớn hơn thường tăng throughput nhưng tốn memory |
shuffle | Xáo trộn mỗi epoch | Nên bật cho train, tắt cho validation/test |
num_workers | Số process load data | Tăng throughput nhưng phức tạp debug hơn |
pin_memory | Copy tensor vào pinned memory cho CUDA | Có ích khi dùng GPU, không cần cho CPU |
drop_last | Bỏ batch cuối nếu thiếu sample | Hữu ích cho BatchNorm/distributed training |
collate_fn | Cách ghép sample thành batch | Cần cho sequence dài ngắn khác nhau |
Best practice:
- Dataset không nên tự move tensor sang GPU. Để training loop quyết định device.
- Dataset nên trả về tensor có shape/dtype ổn định.
shuffle=Truechỉ dùng cho training set.- Với dữ liệu lớn, tách preprocessing offline hoặc cache để
DataLoaderkhông thành bottleneck.
6. CPU/GPU Device Management
Pattern cơ bản:
def select_device() -> torch.device:
if torch.cuda.is_available():
return torch.device("cuda")
return torch.device("cpu")
device = select_device()
model = XORMLP().to(device)
for xb, yb in loader:
xb = xb.to(device)
yb = yb.to(device)
GPU không tự động nhanh hơn trong mọi trường hợp. Nếu model rất nhỏ hoặc batch rất nhỏ, cost copy CPU -> GPU có thể lớn hơn lợi ích compute.
Production considerations:
- Log device đang dùng ở startup.
- Có fallback khi CUDA không available.
- Tránh
.to(device)từng sample; move theo batch. - Tránh gọi
.item()quá nhiều trong GPU loop vì có thể ép đồng bộ CPU/GPU. - Theo dõi VRAM, GPU utilization, dataloader wait time.
7. Training Loop Tối Thiểu Nhưng Đúng
Skeleton cho binary classification:
from torch import nn
loss_fn = nn.BCEWithLogitsLoss()
optimizer = torch.optim.AdamW(model.parameters(), lr=3e-2, weight_decay=1e-4)
model.train()
for xb, yb in train_loader:
xb = xb.to(device)
yb = yb.to(device)
optimizer.zero_grad(set_to_none=True)
logits = model(xb)
loss = loss_fn(logits, yb)
loss.backward()
optimizer.step()
Tại sao không đặt Sigmoid trong model khi dùng BCEWithLogitsLoss?
BCEWithLogitsLossnhận raw logits.- Nó kết hợp sigmoid và binary cross entropy theo cách ổn định số học hơn.
- Khi cần probability để metric/inference, dùng
torch.sigmoid(logits)sau model.
8. Save/Load Bằng state_dict
Không nên serialize cả object model nếu không cần. Cách chuẩn là lưu state_dict kèm config.
checkpoint = {
"model_state_dict": model.state_dict(),
"optimizer_state_dict": optimizer.state_dict(),
"config": {
"input_dim": 2,
"hidden_dim": 8,
"output_dim": 1,
},
"metrics": {"val_accuracy": 1.0},
}
torch.save(checkpoint, "artifacts/xor_mlp.pt")
Load:
checkpoint = torch.load("artifacts/xor_mlp.pt", map_location=device)
model = XORMLP(hidden_dim=checkpoint["config"]["hidden_dim"]).to(device)
model.load_state_dict(checkpoint["model_state_dict"])
model.eval()
Production note:
- Chỉ load checkpoint từ nguồn tin cậy.
- Version config, preprocessing và code model cùng artifact.
- Với PyTorch mới, cân nhắc
weights_only=Truekhi phù hợp để giảm rủi ro untrusted pickle.
9. NumPy Vs PyTorch
| Tiêu chí | NumPy | PyTorch |
|---|---|---|
| Core data | ndarray | Tensor |
| Autograd | Không có native autograd | Có autograd |
| GPU | Không phải default workflow | First-class CUDA support |
| Neural network layers | Tự viết hoặc dùng lib khác | torch.nn |
| Optimizer | Tự viết | torch.optim |
| Data pipeline | Tự viết batching | Dataset/DataLoader |
| Production DL | Không phải lựa chọn chính | Rất phổ biến |
| Học math | Rất tốt | Tốt nhưng che bớt chi tiết gradient |
Best solution:
- Dùng NumPy để học linear algebra, loss, manual gradient.
- Dùng PyTorch khi training neural network thật, cần GPU, checkpoint, ecosystem và khả năng mở rộng.
10. Performance Và Trade-Off
| Quyết định | Lợi ích | Chi phí/rủi ro | Gợi ý |
|---|---|---|---|
| CPU | Dễ chạy, ít phụ thuộc | Chậm với model lớn | Tốt cho dev, test, inference nhỏ |
| GPU | Throughput cao cho matrix compute | VRAM, setup, transfer cost | Dùng khi batch/model đủ lớn |
| Batch lớn | Tận dụng vectorization | Tốn memory, có thể ảnh hưởng generalization | Tăng dần đến khi gần giới hạn memory |
num_workers > 0 | Load data song song | Debug khó hơn, overhead process | Bắt đầu 0, tăng khi data loading nghẽn |
float32 | Default ổn định | Tốn hơn mixed precision | Dùng trước khi tối ưu |
| Mixed precision | Nhanh hơn, ít VRAM hơn | Có rủi ro numerical issue | Để Day sau khi loop đã đúng |
inference_mode() | Ít overhead hơn inference | Ít linh hoạt hơn no_grad() | Dùng cho inference thuần |
Performance rule: đo trước khi tối ưu. Với model nhỏ như XOR, GPU có thể chậm hơn CPU vì overhead dominate.
11. Lỗi Phổ Biến
- Quên
optimizer.zero_grad(...), làm gradient cộng dồn sai. - Dùng
Sigmoidtrong model rồi lại dùngBCEWithLogitsLoss. - Label dtype là
torch.longtrong bài toán BCE thay vì float. - Label shape
(batch,)nhưng logits shape(batch, 1). - Model ở GPU nhưng batch ở CPU.
- Quên
model.eval()khi validation/inference. - Quên
torch.no_grad()hoặctorch.inference_mode()khi inference, làm tốn memory. - Save cả object model thay vì
state_dict, làm artifact phụ thuộc code path nhiều hơn. - Convert tensor GPU trực tiếp sang NumPy thay vì
.detach().cpu().numpy().
12. Kết Luận Production
PyTorch fundamentals trong bài này dùng được làm nền production. Code demo có thể chạy local và làm template nhỏ, nhưng chưa đủ production nếu thiếu config management, test, logging, checkpoint policy, data validation, model registry, serving layer và monitoring.
Câu trả lời ngắn: dùng được trong production nếu bạn quản lý đầy đủ artifact, reproducibility, input contract, runtime mode, device fallback, performance benchmark và observability. Không nên copy nguyên training script demo vào production service mà không bổ sung các lớp kiểm soát đó.
Bài tập
Chuẩn Bị
Cài PyTorch theo môi trường của bạn. Với máy CPU/dev local, cách tối thiểu thường là:
python3 -m pip install torch numpy
Kiểm tra:
python3 - <<'PY'
import torch
print(torch.__version__)
print("cuda:", torch.cuda.is_available())
PY
Nếu dùng CUDA, hãy cài theo hướng dẫn chính thức phù hợp driver/CUDA runtime của máy. Không hard-code "cuda" trong code production; luôn có fallback.
Bài 1: Inspect Tensor
Mục tiêu: quen với shape, dtype, device, requires_grad.
import torch
x = torch.tensor([[0.0, 1.0], [1.0, 0.0]], dtype=torch.float32)
w = torch.randn((2, 1), dtype=torch.float32, requires_grad=True)
print("x:", x)
print("x.shape:", x.shape)
print("x.dtype:", x.dtype)
print("x.device:", x.device)
print("w.requires_grad:", w.requires_grad)
Việc cần làm:
- Đổi
dtypecủaxsangtorch.float64và quan sát output. - Nếu máy có GPU, move
xsang CUDA bằng.to("cuda"). - Tạo lỗi device mismatch bằng cách để
xở CPU nhưngwở CUDA, sau đó sửa lại.
Bài 2: Autograd Tối Giản
Mục tiêu: hiểu loss.backward() và gradient accumulation.
import torch
w = torch.tensor([2.0], requires_grad=True)
x = torch.tensor([3.0])
target = torch.tensor([10.0])
for step in range(3):
y = w * x
loss = (y - target).pow(2).mean()
loss.backward()
print(step, "loss=", loss.item(), "grad=", w.grad.item())
Bạn sẽ thấy gradient bị cộng dồn. Sửa loop bằng cách thêm:
w.grad = None
trước mỗi loss.backward(). Khi dùng optimizer, cách chuẩn là:
optimizer.zero_grad(set_to_none=True)
Bài 3: Rebuild MLP XOR Từ Day 9 Bằng PyTorch
Tạo file train_xor_pytorch.py nếu muốn chạy riêng. Code dưới đây cố tình có cấu trúc gần production hơn toy script: có config, seed, device fallback, train/eval mode, DataLoader, BCEWithLogitsLoss, state_dict và checkpoint.
from __future__ import annotations
from dataclasses import asdict, dataclass
from pathlib import Path
import random
import numpy as np
import torch
from torch import nn
from torch.utils.data import DataLoader, Dataset, random_split
@dataclass(frozen=True)
class TrainConfig:
seed: int = 42
repeats: int = 512
noise_std: float = 0.04
hidden_dim: int = 8
batch_size: int = 32
epochs: int = 250
learning_rate: float = 0.03
weight_decay: float = 1e-4
val_ratio: float = 0.2
checkpoint_path: str = "artifacts/xor_mlp.pt"
def set_seed(seed: int) -> None:
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
# Determinism giúp demo dễ lặp lại hơn, nhưng có thể giảm performance.
torch.backends.cudnn.benchmark = False
torch.backends.cudnn.deterministic = True
def select_device() -> torch.device:
if torch.cuda.is_available():
return torch.device("cuda")
if hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
return torch.device("mps")
return torch.device("cpu")
class XORDataset(Dataset):
def __init__(self, repeats: int, noise_std: float, seed: int) -> None:
base_x = torch.tensor(
[[0.0, 0.0], [0.0, 1.0], [1.0, 0.0], [1.0, 1.0]],
dtype=torch.float32,
)
base_y = torch.tensor([[0.0], [1.0], [1.0], [0.0]], dtype=torch.float32)
self.features = base_x.repeat((repeats, 1))
self.labels = base_y.repeat((repeats, 1))
if noise_std > 0:
generator = torch.Generator().manual_seed(seed)
noise = torch.randn(self.features.shape, generator=generator) * noise_std
self.features = torch.clamp(self.features + noise, 0.0, 1.0)
if self.features.shape[0] != self.labels.shape[0]:
raise ValueError("features and labels must have the same number of rows")
def __len__(self) -> int:
return self.features.shape[0]
def __getitem__(self, idx: int) -> tuple[torch.Tensor, torch.Tensor]:
return self.features[idx], self.labels[idx]
class XORMLP(nn.Module):
def __init__(self, hidden_dim: int) -> None:
super().__init__()
self.net = nn.Sequential(
nn.Linear(2, hidden_dim),
nn.Tanh(),
nn.Linear(hidden_dim, 1),
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
if x.ndim != 2 or x.shape[1] != 2:
raise ValueError(f"Expected input shape (batch, 2), got {tuple(x.shape)}")
return self.net(x)
def binary_accuracy_from_logits(logits: torch.Tensor, targets: torch.Tensor) -> float:
probs = torch.sigmoid(logits)
preds = (probs >= 0.5).to(dtype=targets.dtype)
return (preds == targets).float().mean().item()
def move_batch(
batch: tuple[torch.Tensor, torch.Tensor],
device: torch.device,
) -> tuple[torch.Tensor, torch.Tensor]:
features, labels = batch
non_blocking = device.type == "cuda"
return (
features.to(device, non_blocking=non_blocking),
labels.to(device, non_blocking=non_blocking),
)
def train_one_epoch(
model: nn.Module,
loader: DataLoader,
loss_fn: nn.Module,
optimizer: torch.optim.Optimizer,
device: torch.device,
) -> dict[str, float]:
model.train()
total_loss = 0.0
total_accuracy = 0.0
total_rows = 0
for batch in loader:
xb, yb = move_batch(batch, device)
optimizer.zero_grad(set_to_none=True)
logits = model(xb)
loss = loss_fn(logits, yb)
loss.backward()
optimizer.step()
batch_size = xb.shape[0]
total_loss += loss.item() * batch_size
total_accuracy += binary_accuracy_from_logits(logits.detach(), yb) * batch_size
total_rows += batch_size
return {
"loss": total_loss / total_rows,
"accuracy": total_accuracy / total_rows,
}
@torch.inference_mode()
def evaluate(
model: nn.Module,
loader: DataLoader,
loss_fn: nn.Module,
device: torch.device,
) -> dict[str, float]:
model.eval()
total_loss = 0.0
total_accuracy = 0.0
total_rows = 0
for batch in loader:
xb, yb = move_batch(batch, device)
logits = model(xb)
loss = loss_fn(logits, yb)
batch_size = xb.shape[0]
total_loss += loss.item() * batch_size
total_accuracy += binary_accuracy_from_logits(logits, yb) * batch_size
total_rows += batch_size
return {
"loss": total_loss / total_rows,
"accuracy": total_accuracy / total_rows,
}
@torch.inference_mode()
def predict_clean_xor(model: nn.Module, device: torch.device) -> tuple[torch.Tensor, torch.Tensor]:
model.eval()
clean_x = torch.tensor(
[[0.0, 0.0], [0.0, 1.0], [1.0, 0.0], [1.0, 1.0]],
dtype=torch.float32,
device=device,
)
logits = model(clean_x)
probs = torch.sigmoid(logits)
preds = (probs >= 0.5).to(torch.int64)
return probs.cpu(), preds.cpu()
def make_loaders(config: TrainConfig, device: torch.device) -> tuple[DataLoader, DataLoader]:
dataset = XORDataset(
repeats=config.repeats,
noise_std=config.noise_std,
seed=config.seed,
)
val_size = int(len(dataset) * config.val_ratio)
train_size = len(dataset) - val_size
split_generator = torch.Generator().manual_seed(config.seed)
train_dataset, val_dataset = random_split(
dataset,
[train_size, val_size],
generator=split_generator,
)
pin_memory = device.type == "cuda"
loader_generator = torch.Generator().manual_seed(config.seed)
train_loader = DataLoader(
train_dataset,
batch_size=config.batch_size,
shuffle=True,
num_workers=0,
pin_memory=pin_memory,
generator=loader_generator,
)
val_loader = DataLoader(
val_dataset,
batch_size=config.batch_size,
shuffle=False,
num_workers=0,
pin_memory=pin_memory,
)
return train_loader, val_loader
def save_checkpoint(
config: TrainConfig,
model: nn.Module,
optimizer: torch.optim.Optimizer,
metrics: dict[str, float],
) -> None:
path = Path(config.checkpoint_path)
path.parent.mkdir(parents=True, exist_ok=True)
torch.save(
{
"model_state_dict": model.state_dict(),
"optimizer_state_dict": optimizer.state_dict(),
"config": asdict(config),
"metrics": metrics,
},
path,
)
def load_model_for_inference(path: str, device: torch.device) -> XORMLP:
checkpoint = torch.load(path, map_location=device, weights_only=True)
model_config = checkpoint["config"]
model = XORMLP(hidden_dim=model_config["hidden_dim"]).to(device)
model.load_state_dict(checkpoint["model_state_dict"])
model.eval()
return model
def main() -> None:
config = TrainConfig()
set_seed(config.seed)
device = select_device()
print("device:", device)
train_loader, val_loader = make_loaders(config, device)
model = XORMLP(hidden_dim=config.hidden_dim).to(device)
loss_fn = nn.BCEWithLogitsLoss()
optimizer = torch.optim.AdamW(
model.parameters(),
lr=config.learning_rate,
weight_decay=config.weight_decay,
)
best_val_accuracy = 0.0
best_metrics: dict[str, float] = {}
for epoch in range(1, config.epochs + 1):
train_metrics = train_one_epoch(model, train_loader, loss_fn, optimizer, device)
val_metrics = evaluate(model, val_loader, loss_fn, device)
if val_metrics["accuracy"] >= best_val_accuracy:
best_val_accuracy = val_metrics["accuracy"]
best_metrics = {
"epoch": float(epoch),
"train_loss": train_metrics["loss"],
"train_accuracy": train_metrics["accuracy"],
"val_loss": val_metrics["loss"],
"val_accuracy": val_metrics["accuracy"],
}
save_checkpoint(config, model, optimizer, best_metrics)
if epoch == 1 or epoch % 25 == 0:
print(
f"epoch={epoch:03d} "
f"train_loss={train_metrics['loss']:.4f} "
f"train_acc={train_metrics['accuracy']:.3f} "
f"val_loss={val_metrics['loss']:.4f} "
f"val_acc={val_metrics['accuracy']:.3f}"
)
print("best_metrics:", best_metrics)
loaded_model = load_model_for_inference(config.checkpoint_path, device)
probs, preds = predict_clean_xor(loaded_model, device)
print("Clean XOR probabilities:")
print(torch.round(probs * 10000) / 10000)
print("Clean XOR predictions:")
print(preds)
print("Expected:")
print(torch.tensor([[0], [1], [1], [0]], dtype=torch.int64))
if __name__ == "__main__":
main()
Kỳ vọng:
- Loss giảm dần sau vài chục epoch.
- Validation accuracy thường đạt gần
1.0. - Clean XOR predictions là
[[0], [1], [1], [0]]. - Checkpoint nằm ở
artifacts/xor_mlp.pt.
Nếu model không học:
- Kiểm tra
BCEWithLogitsLossđang nhận logits, không nhận probability đã sigmoid. - Kiểm tra label là
float32và shape(batch, 1). - Thử tăng
hidden_dimlên 16. - Thử giảm
noise_stdvề 0.0. - Kiểm tra learning rate quá lớn khiến loss dao động.
Bài 4: So Sánh Với NumPy Day 9
Điền bảng sau sau khi chạy hai phiên bản:
| Câu hỏi | NumPy Day 9 | PyTorch Day 10 |
|---|---|---|
| Weight nằm ở đâu? | ||
| Ai tính gradient? | ||
| Code update weight nằm ở đâu? | ||
| Có GPU dễ không? | ||
| Save/load artifact thế nào? | ||
| Lỗi shape dễ phát hiện ở đâu? |
Gợi ý trả lời:
- NumPy giúp thấy rõ math nhưng bạn tự chịu trách nhiệm gradient.
- PyTorch ngắn hơn ở phần gradient/update nhưng vẫn cần bạn kiểm soát data contract.
- PyTorch không loại bỏ nhu cầu test, logging, validation và monitoring.
Bài 5: Thay Đổi Có Kiểm Soát
Chạy lại script với từng thay đổi, mỗi lần chỉ đổi một biến:
hidden_dim: 2, 4, 8, 16.learning_rate: 0.003, 0.03, 0.3.batch_size: 4, 32, 128.noise_std: 0.0, 0.04, 0.15.- Device: CPU vs CUDA nếu có GPU.
Ghi lại:
- Loss cuối cùng.
- Validation accuracy tốt nhất.
- Clean XOR predictions.
- Thời gian chạy tương đối.
- Có lỗi memory/device/dtype/shape không.
Bài 6: Checklist Production Readiness
Trả lời trước khi coi code là gần production:
- Input contract đã rõ
shape,dtype, range chưa? - Train/validation/test split có deterministic và không leakage không?
- Có seed và config lưu cùng checkpoint không?
- Checkpoint có
state_dict, config và metrics không? - Inference có
model.eval()vàtorch.inference_mode()không? - Có CPU/GPU fallback không?
- Có logging metric theo epoch không?
- Có benchmark latency/throughput không?
- Có kiểm soát nguồn checkpoint khi load không?
- Có version preprocessing và threshold không?
Kết luận: script trong bài dùng tốt cho học tập, prototype và baseline nhỏ. Để vào production, cần bổ sung test tự động, data validation, config file, experiment tracking, model registry, serving API, monitoring và rollback.