Mục lục
- Mục tiêu bài học
- Vì sao PyTorch không có .fit()
- Khái niệm: epoch, iteration, batch
- 5 bước cốt lõi mỗi batch
- Pattern chuẩn của training loop
- model.train() vs model.eval()
- Validation loop và torch.no_grad()
- Tracking metrics
- Save checkpoint
- Resume training
- Logging: tqdm, TensorBoard, wandb
- Gradient accumulation
- Gradient clipping
- Mixed precision training
- PyTorch Lightning
- Pitfall thường gặp
- Code Python: MLP MNIST end-to-end
- Bài tập
- Tóm tắt
Mục tiêu bài học
Sau bài học, bạn sẽ:
- Viết được training loop chuẩn cho mọi model PyTorch — 5 bước cốt lõi mỗi batch.
- Phân biệt và switch đúng
model.train()/model.eval()mỗi khi đổi giữa train và validation/test. - Viết validation loop với
torch.no_grad()để tiết kiệm VRAM và tăng tốc. - Track train/val loss, accuracy qua các epoch; plot learning curve để detect overfit.
- Save/resume checkpoint chứa cả model và optimizer state.
- Biết khi nào cần gradient accumulation (VRAM thấp), gradient clipping (RNN/Transformer), mixed precision (GPU mới).
- Nhận diện các pitfall: quên
model.eval(), quênzero_grad(), inlossthay vìloss.item().
Bài này nối tiếp Bài 17 — Optimizer SGD và Adam (cái mà loop sẽ gọi .step()) và là tiền đề cho Bài 19 — Dataset và DataLoader (cái cung cấp batch cho loop).
Vì sao PyTorch không có .fit()
Keras có model.fit(X, y, epochs=10) chạy gọn một dòng. PyTorch không cung cấp API tương đương ở core — bạn phải viết training loop. Lý do gắn với triết lý thiết kế:
- Full control: kiểm soát từng bước forward, backward, update; không bị ràng buộc bởi callback API.
- Dễ debug:
print,pdb,breakpoint()đặt được ở bất cứ đâu trong loop. - Dễ customize: multi-task loss, GAN với hai optimizer, RL với rollout, BPTT cho RNN — không loop nào trong số đó nhét vừa
.fit()generic.
Giá phải trả: viết nhiều dòng hơn cho task đơn giản. PyTorch Lightning (mục 15) là wrapper giảm boilerplate khi pattern của bạn đã ổn định.
Toàn bộ ecosystem PyTorch (Hugging Face Trainer, fastai, Catalyst, Composer) đều xây trên cùng pattern training loop ở mục 5 — hiểu pattern này là điều kiện cần để hiểu mọi framework cao hơn.
Khái niệm: epoch, iteration, batch
- Batch: một nhóm sample được xử lý cùng lúc. Kích thước batch thường 32, 64, 128, 256 — phụ thuộc VRAM.
- Iteration / step: một lần forward + backward + update trên một batch. Đôi khi gọi là "training step" hoặc "global step".
- Epoch: một pass qua toàn bộ training set. Sau mỗi epoch, mọi sample đã được model nhìn thấy đúng một lần (nếu shuffle nhưng không drop_last).
Quan hệ:
iterations_per_epoch = ceil(len(dataset) / batch_size)
total_iterations = num_epochs * iterations_per_epoch
Ví dụ: train set 60 000 sample, batch size 128 → 469 iteration/epoch (ceil của 60 000 / 128). Train 10 epoch → 4 690 iteration tổng cộng.
Lưu ý: scheduler có thể bước theo iteration (cosine LR per step) hoặc theo epoch (step LR per epoch). Đọc docs scheduler kỹ trước khi dùng.
5 bước cốt lõi mỗi batch
Mọi training loop PyTorch dù phức tạp tới đâu cũng quy về 5 bước này trong mỗi batch:
- Move data sang device:
X, y = X.to(device), y.to(device). Bắt buộc khi model trên GPU. - Forward:
y_pred = model(X). Gọi__call__củann.Moduleđể chạy đúng hook (vd dropout, batchnorm). - Loss:
loss = criterion(y_pred, y). Output phải là scalar để backward được. - Zero grad + Backward:
optimizer.zero_grad()rồiloss.backward(). Reset gradient cũ (vì autograd cộng dồn — B15) trước khi tính gradient mới. - Step:
optimizer.step(). Cập nhật parameter theo gradient vừa tính, dùng rule của SGD/Adam (B17).
Thứ tự bắt buộc: zero_grad → backward → step. Đảo thứ tự (vd step trước backward) là bug câm — không lỗi runtime nhưng model không học.
Một số source viết optimizer.zero_grad() ở đầu loop trước forward — cũng được, miễn nằm trước backward(). Vị trí trước backward() trực quan hơn về mặt logic.
Pattern chuẩn của training loop
Skeleton tối thiểu của một training loop có validation:
model.to(device)
for epoch in range(num_epochs):
# ----- TRAIN -----
model.train()
for batch_idx, (X, y) in enumerate(train_loader):
X, y = X.to(device), y.to(device)
# Forward
y_pred = model(X)
loss = criterion(y_pred, y)
# Backward
optimizer.zero_grad()
loss.backward()
optimizer.step()
# ----- VALIDATION sau mỗi epoch -----
model.eval()
val_loss = 0.0
with torch.no_grad():
for X, y in val_loader:
X, y = X.to(device), y.to(device)
y_pred = model(X)
val_loss += criterion(y_pred, y).item() * X.size(0)
val_loss /= len(val_loader.dataset)
print(f"Epoch {epoch}: train_loss={loss.item():.4f}, val_loss={val_loss:.4f}")
Mọi tính năng "thêm" — checkpoint, scheduler, gradient clipping, mixed precision, logger — đều cắm vào skeleton này. Học vững skeleton trước, kỹ thuật phụ tự nhiên ráp được sau.
Lưu ý cách tính val_loss trung bình: nhân loss.item() với X.size(0) (số sample của batch) rồi chia tổng số sample. Nếu reduction của criterion là 'mean' (mặc định cho CrossEntropyLoss, MSELoss), batch cuối có thể nhỏ hơn — chia thẳng cho len(val_loader) sẽ sai.
model.train() vs model.eval()
Một số layer hành xử khác giữa train và eval. model.train() / model.eval() set flag self.training cho mọi submodule, các layer đó đọc flag và đổi behavior:
- Dropout — train: random mask một phần neuron (zero out) và scale phần còn lại; eval: identity (không drop).
- BatchNorm / LayerNorm — train: dùng batch statistics (mean/var của batch hiện tại) và update running statistics; eval: dùng running statistics đã tích luỹ trong train.
Pattern:
model.train() # trước vòng train
for X, y in train_loader: ...
model.eval() # trước vòng eval/test
with torch.no_grad():
for X, y in val_loader: ...
Quên switch là pitfall #1 của người mới:
- Quên
model.eval()khi validate → dropout vẫn active → val accuracy thấp giả tạo, không tái lập được khi infer. - Quên
model.train()trở lại sau validate → BatchNorm dùng running stats để train tiếp → training instability.
Quy ước an toàn: luôn gọi model.train() ở đầu mỗi epoch ngay cả khi không có validation, để chắc.
Validation loop và torch.no_grad()
Hai mục đích của validation sau mỗi epoch:
- Detect overfit: nếu train loss giảm nhưng val loss bắt đầu tăng, model đang nhớ vẹt train set. Đây là tín hiệu để thêm regularization (B22) hoặc early stop (B24).
- Chọn best model: save checkpoint mỗi khi val metric cải thiện, dùng làm model cuối cùng — không nhất thiết là epoch cuối.
with torch.no_grad(): bắt buộc trong eval (B15):
model.eval()
total_loss, correct, n = 0.0, 0, 0
with torch.no_grad():
for X, y in val_loader:
X, y = X.to(device), y.to(device)
logits = model(X)
loss = criterion(logits, y)
total_loss += loss.item() * X.size(0)
preds = logits.argmax(dim=1)
correct += (preds == y).sum().item()
n += X.size(0)
avg_loss = total_loss / n
acc = correct / n
Lợi ích cụ thể của no_grad:
- Tiết kiệm VRAM: không lưu activation cho backward → có thể eval với batch size lớn hơn train.
- Nhanh hơn: không build computation graph, không tracking.
Cho production inference, dùng @torch.inference_mode() — strict hơn no_grad, cấm cả việc tạo tensor có thể thành leaf của graph khác.
Tracking metrics
Hai loại metric cần track:
- Loss: train loss và val loss qua epoch — quan trọng nhất, dùng để chẩn đoán overfit / underfit.
- Metric task: accuracy, F1 cho classification (B25 Series 2); MSE, R² cho regression (B17-18 Series 2).
Tính accuracy đúng cách (cộng dồn theo sample, không theo batch):
correct = 0
n = 0
for X, y in loader:
logits = model(X.to(device))
preds = logits.argmax(dim=1).cpu()
correct += (preds == y).sum().item()
n += y.size(0)
acc = correct / n
Lưu lại để plot learning curve:
history = {"train_loss": [], "val_loss": [], "val_acc": []}
for epoch in range(num_epochs):
train_loss = train_one_epoch(model, train_loader, criterion, optimizer, device)
val_loss, val_acc = evaluate(model, val_loader, criterion, device)
history["train_loss"].append(train_loss)
history["val_loss"].append(val_loss)
history["val_acc"].append(val_acc)
import matplotlib.pyplot as plt
plt.plot(history["train_loss"], label="train")
plt.plot(history["val_loss"], label="val")
plt.legend(); plt.xlabel("epoch"); plt.ylabel("loss"); plt.show()
Plot dù chỉ vài epoch cũng đáng làm — đường val tách lên trong khi train xuống là tín hiệu overfit rõ ràng nhất.
Save checkpoint
Checkpoint = snapshot đủ thông tin để resume train hoặc deploy. Khuyến cáo save cả model state, optimizer state, epoch, và metric:
checkpoint = {
"epoch": epoch,
"model_state_dict": model.state_dict(),
"optimizer_state_dict": optimizer.state_dict(),
"loss": loss.item(),
"val_acc": val_acc,
}
torch.save(checkpoint, "checkpoint.pth")
Vì sao save state_dict chứ không phải object model trực tiếp? state_dict là dict thuần (key tên parameter → tensor), portable giữa các version PyTorch và không gắn với class definition. Save torch.save(model, ...) pickle nguyên object — load lại bắt buộc cùng class ở cùng path.
Hai chiến lược save phổ biến:
- Save mỗi N epoch: phòng máy crash, log progress. Vd
epoch % 5 == 0. - Save best model: chỉ ghi đè
best_model.pthkhi val metric cải thiện. Đây là model dùng cho inference cuối cùng.
best_val_acc = 0.0
for epoch in range(num_epochs):
...
if val_acc > best_val_acc:
best_val_acc = val_acc
torch.save({
"epoch": epoch,
"model_state_dict": model.state_dict(),
"val_acc": val_acc,
}, "best_model.pth")
Lưu ý kích thước: checkpoint model 100M parameter ở fp32 chiếm 400 MB. Save mỗi epoch nhanh tràn disk — giới hạn N gần nhất hoặc dùng compression.
Resume training
Load checkpoint và resume từ epoch tiếp theo:
model = MyNet().to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
checkpoint = torch.load("checkpoint.pth", map_location=device)
model.load_state_dict(checkpoint["model_state_dict"])
optimizer.load_state_dict(checkpoint["optimizer_state_dict"])
start_epoch = checkpoint["epoch"] + 1
for epoch in range(start_epoch, num_epochs):
...
Vì sao phải load cả optimizer state? Adam (B17) lưu running average của gradient và bình phương gradient. Bắt đầu lại từ zero là làm cold-start optimizer — vài step đầu update lệch. Resume đúng phải khôi phục cả momentum / second moment.
Pitfall thường gặp khi resume:
- Quên
map_location: checkpoint save từ GPU, load trên CPU sẽ lỗi nếu không truyềnmap_location='cpu'(hoặcdeviceđích). - Mismatch architecture: model định nghĩa khác với khi save →
load_state_dictbáo missing/unexpected key. Dùngstrict=Falseđể bỏ qua nhưng cẩn thận với layer thiếu. - Quên đổi LR scheduler state: nếu dùng scheduler, save và load thêm
scheduler.state_dict(). - Random state: nếu cần reproduce chính xác, save thêm
torch.get_rng_state(),numpy.random.get_state()— phần lớn project không cần mức này.
Logging: tqdm, TensorBoard, wandb
Ba mức logging phổ biến:
1) print mỗi N batch hoặc mỗi epoch — đơn giản nhất, phù hợp lúc prototype:
if batch_idx % 100 == 0:
print(f"Epoch {epoch} [{batch_idx}/{len(train_loader)}] loss={loss.item():.4f}")
2) tqdm cho progress bar:
from tqdm import tqdm
for X, y in tqdm(train_loader, desc=f"Epoch {epoch}"):
...
# Hoặc update postfix với loss hiện tại
pbar = tqdm(train_loader, desc=f"Epoch {epoch}")
for X, y in pbar:
loss = ...
pbar.set_postfix(loss=loss.item())
3) TensorBoard — built-in của PyTorch, log scalar/image/histogram, xem trên browser:
from torch.utils.tensorboard import SummaryWriter
writer = SummaryWriter("runs/exp1")
for epoch in range(num_epochs):
train_loss = ...
val_loss, val_acc = ...
writer.add_scalar("Loss/train", train_loss, epoch)
writer.add_scalar("Loss/val", val_loss, epoch)
writer.add_scalar("Accuracy/val", val_acc, epoch)
writer.close()
# Chạy: tensorboard --logdir=runs
4) Weights & Biases (wandb) — experiment tracking online, compare nhiều run, hyperparam sweep:
import wandb
wandb.init(project="mnist-mlp", config={"lr": 1e-3, "batch_size": 128})
for epoch in range(num_epochs):
...
wandb.log({"train_loss": train_loss, "val_loss": val_loss, "val_acc": val_acc})
Chọn tool tuỳ scale: prototype dùng print + tqdm; project nhiều experiment dùng wandb hoặc TensorBoard.
Gradient accumulation
Khi VRAM không đủ cho batch size mong muốn, chia thành accum_steps mini-batch, cộng dồn gradient, rồi step một lần:
accum_steps = 4
optimizer.zero_grad()
for i, (X, y) in enumerate(train_loader):
X, y = X.to(device), y.to(device)
loss = criterion(model(X), y) / accum_steps # chia trước
loss.backward() # cộng dồn vào .grad
if (i + 1) % accum_steps == 0:
optimizer.step()
optimizer.zero_grad()
Hiệu quả tương đương batch lớn gấp accum_steps lần (về mặt gradient signal), nhưng chậm hơn batch lớn thực sự do mất song song trong forward.
Vì sao chia loss cho accum_steps? Để giữ scale tương đương trung bình một batch lớn. Nếu criterion dùng reduction='sum', không chia. Nếu dùng reduction='mean' (mặc định), phải chia — mỗi mini-batch đã là mean, cộng accum_steps lần là sum rồi.
Use case: train LLM/Transformer lớn trên GPU consumer; train ImageNet trên GPU 8 GB.
Gradient clipping
Khi gradient nổ (exploding gradient — phổ biến ở RNN và Transformer dài), update parameter một bước quá lớn → NaN, train hỏng. Gradient clipping cắt gradient về một ngưỡng trước khi step():
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()
clip_grad_norm_ scale toàn bộ gradient sao cho L2 norm của vector gradient gộp không vượt max_norm:
\[ g \leftarrow g \cdot \frac{\text{max\_norm}}{\max(\|g\|_2, \text{max\_norm})} \]
Khi \( \|g\|_2 \le \text{max\_norm} \), không đổi. Khi nổ, scale xuống. Hướng gradient giữ nguyên, chỉ giảm độ lớn.
Biến thể khác: clip_grad_value_ clip element-wise. Ít dùng hơn vì làm méo hướng.
Giá trị max_norm tiêu chuẩn:
- Transformer / LLM: thường 1.0.
- RNN / LSTM: 0.25–5.0 tuỳ paper.
- MLP / CNN nhỏ: thường không cần clip.
Clipping phải đặt sau backward() và trước step() — sau step() là vô tác dụng vì update đã xong.
Mixed precision training
Mixed precision = forward và một phần backward chạy ở fp16 (hoặc bf16), update parameter ở fp32. Trên GPU có Tensor Core (Volta V100 trở lên), speedup 1.5–2x và tiết kiệm ~50% VRAM:
from torch.cuda.amp import autocast, GradScaler
scaler = GradScaler()
for X, y in train_loader:
X, y = X.to(device), y.to(device)
optimizer.zero_grad()
with autocast(device_type="cuda", dtype=torch.float16):
y_pred = model(X)
loss = criterion(y_pred, y)
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()
Vai trò của GradScaler: gradient ở fp16 dễ underflow (giá trị quá nhỏ thành 0). Scaler nhân loss với một hằng số lớn trước backward → gradient cũng lớn lên cùng tỉ lệ, không underflow → trước khi step, unscale lại. Auto-adjust scale theo NaN/Inf phát hiện.
Lưu ý:
- Chỉ
autocastphần forward + loss, không bọc backward. - API mới
torch.amp.autocast(device_type='cuda')general hơn (hỗ trợ CPU bf16);torch.cuda.amplà alias cũ. - bf16 (Ampere A100, Hopper H100 trở lên) range lớn hơn fp16, ít cần GradScaler.
Bài này chỉ giới thiệu ở mức preview; chi tiết mixed precision sẽ ở B23.
PyTorch Lightning
PyTorch Lightning là wrapper high-level: bạn chỉ viết logic (forward, training_step, validation_step, optimizer config), framework lo phần lặp (loop, device move, distributed, checkpoint, logging, AMP):
import pytorch_lightning as pl
class LitMLP(pl.LightningModule):
def __init__(self):
super().__init__()
self.net = MLP()
self.criterion = nn.CrossEntropyLoss()
def forward(self, x):
return self.net(x)
def training_step(self, batch, batch_idx):
X, y = batch
loss = self.criterion(self(X), y)
self.log("train_loss", loss)
return loss
def validation_step(self, batch, batch_idx):
X, y = batch
loss = self.criterion(self(X), y)
self.log("val_loss", loss)
def configure_optimizers(self):
return torch.optim.Adam(self.parameters(), lr=1e-3)
trainer = pl.Trainer(max_epochs=10, accelerator="gpu")
trainer.fit(model=LitMLP(), train_dataloaders=train_loader, val_dataloaders=val_loader)
Lợi:
- Giảm boilerplate khi pattern đã ổn định.
- Multi-GPU, DDP, TPU chỉ cần đổi argument trong
Trainer, không sửa code training. - Tích hợp sẵn checkpoint, early stopping, logger (TensorBoard, wandb).
Bất lợi:
- Một lớp abstraction nữa cần học; debug khó hơn vì loop ẩn.
- Custom logic phức tạp (GAN, RL) có khi viết raw PyTorch sạch hơn.
Khuyến cáo: nắm vững raw PyTorch loop (bài này) trước; dùng Lightning khi project lớn và pattern đã chuẩn hoá.
Pitfall thường gặp
- Quên
model.train()/model.eval(): dropout/batchnorm hành xử sai mode → val accuracy giả tạo, hoặc train instability sau eval. Triệu chứng: val accuracy thấp đột ngột so với train accuracy khi model có dropout. - Quên
optimizer.zero_grad(): gradient cộng dồn qua các step (B15) → update không khớp LR, loss oscillate hoặc tăng đột biến. - Forget
.to(device): hoặc data ở CPU mà model ở GPU →RuntimeError: Expected all tensors to be on the same device. Hoặc model trên GPU nhưngnn.Modulecon tạo bên trong forward không tự đẩy sang device → silent bug. - Print
lossthay vìloss.item():losslà tensor cógrad_fn; appendlossvào list giữ luôn computation graph trong RAM → VRAM tăng dần qua epoch tới OOM. Luôn dùng.item()(cho scalar) hoặc.detach()(cho tensor) khi lưu metric. - Tính accuracy bằng
preds == yrồi.mean()nhưng để gradient track: nên.detach()hoặc bọc trongno_grad. Bug câm. - Chia val_loss cho
len(val_loader)trong khi batch cuối nhỏ hơn → trung bình bị lệch. Chia cho tổng số sample mới đúng. - Quên
no_gradtrong validation: VRAM tăng vọt khi eval, có thể OOM trong khi train không OOM. - Save/load model trên device khác mà không
map_location: lỗi khi GPU → CPU. - Scheduler step sai chỗ: gọi
scheduler.step()trướcoptimizer.step()→ cảnh báo và LR sai cho step đầu. Theo docs: optimizer step trước, scheduler step sau.
Code Python: MLP MNIST end-to-end
Train MLP nhận dạng chữ số viết tay MNIST end-to-end: 28×28 → 256 → 10. Track train/val loss + val accuracy, save best checkpoint.
import torch
import torch.nn as nn
from torch.utils.data import DataLoader, random_split
from torchvision import datasets, transforms
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
torch.manual_seed(42)
# ----- DATA -----
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,)), # mean/std MNIST
])
full_train = datasets.MNIST("data", train=True, download=True, transform=transform)
test_set = datasets.MNIST("data", train=False, transform=transform)
train_set, val_set = random_split(full_train, [55000, 5000])
train_loader = DataLoader(train_set, batch_size=128, shuffle=True, num_workers=2)
val_loader = DataLoader(val_set, batch_size=256, shuffle=False, num_workers=2)
test_loader = DataLoader(test_set, batch_size=256, shuffle=False, num_workers=2)
# ----- MODEL -----
class MLP(nn.Module):
def __init__(self):
super().__init__()
self.net = nn.Sequential(
nn.Flatten(),
nn.Linear(28 * 28, 256),
nn.ReLU(),
nn.Dropout(0.2),
nn.Linear(256, 10),
)
def forward(self, x):
return self.net(x)
model = MLP().to(device)
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
# ----- HELPERS -----
def train_one_epoch(model, loader, criterion, optimizer, device):
model.train()
total_loss, n = 0.0, 0
for X, y in loader:
X, y = X.to(device), y.to(device)
optimizer.zero_grad()
logits = model(X)
loss = criterion(logits, y)
loss.backward()
optimizer.step()
total_loss += loss.item() * X.size(0)
n += X.size(0)
return total_loss / n
@torch.no_grad()
def evaluate(model, loader, criterion, device):
model.eval()
total_loss, correct, n = 0.0, 0, 0
for X, y in loader:
X, y = X.to(device), y.to(device)
logits = model(X)
loss = criterion(logits, y)
total_loss += loss.item() * X.size(0)
correct += (logits.argmax(dim=1) == y).sum().item()
n += X.size(0)
return total_loss / n, correct / n
# ----- LOOP -----
num_epochs = 10
best_val_acc = 0.0
history = {"train_loss": [], "val_loss": [], "val_acc": []}
for epoch in range(num_epochs):
train_loss = train_one_epoch(model, train_loader, criterion, optimizer, device)
val_loss, val_acc = evaluate(model, val_loader, criterion, device)
history["train_loss"].append(train_loss)
history["val_loss"].append(val_loss)
history["val_acc"].append(val_acc)
print(f"Epoch {epoch+1:2d}/{num_epochs} "
f"train_loss={train_loss:.4f} "
f"val_loss={val_loss:.4f} "
f"val_acc={val_acc:.4f}")
if val_acc > best_val_acc:
best_val_acc = val_acc
torch.save({
"epoch": epoch,
"model_state_dict": model.state_dict(),
"optimizer_state_dict": optimizer.state_dict(),
"val_acc": val_acc,
}, "best_mnist_mlp.pth")
print(f" ↳ saved best (val_acc={val_acc:.4f})")
# ----- TEST với best model -----
checkpoint = torch.load("best_mnist_mlp.pth", map_location=device)
model.load_state_dict(checkpoint["model_state_dict"])
test_loss, test_acc = evaluate(model, test_loader, criterion, device)
print(f"\nTest: loss={test_loss:.4f} acc={test_acc:.4f} "
f"(best epoch={checkpoint['epoch']})")
Kết quả điển hình sau 10 epoch: val_acc ≈ 0.978–0.982, test_acc cùng phạm vi. Với MLP 1 hidden layer 256 unit, MNIST là task đã near-saturated; CNN (B27 sau này) đẩy lên 0.99+.
Cấu trúc code:
- Tách
train_one_epochvàevaluatethành function — dễ test, dễ reuse cho test set. @torch.no_grad()như decorator trênevaluate— gọn hơnwithtrong body.model.train()/model.eval()đặt ngay đầu mỗi function — không thể quên.
Bài tập
- Load Iris dataset (
sklearn.datasets.load_iris), chia 70/15/15 thành train/val/test. Convert sangTensorDataset+DataLoadervới batch size 16. - Build MLP: 4 input → 16 hidden (ReLU) → 3 output. Optimizer Adam lr=1e-2, loss
CrossEntropyLoss. - Viết training loop 50 epoch với 5 bước cốt lõi đúng pattern bài này. Bắt buộc switch
model.train()/model.eval(). - Thêm validation loop sau mỗi epoch, log
train_loss,val_loss,val_acc. Plot learning curve. - Save best model theo
val_accvàobest_iris.pth(chứamodel_state_dictvàoptimizer_state_dict). - Sau khi train xong, viết script riêng: load
best_iris.pth, evaluate trên test set, in test accuracy. - Train tiếp thêm 30 epoch bằng cách resume từ
best_iris.pth: load model state, optimizer state, đặtstart_epochđúng. Verify val_acc tiếp tục cải thiện (hoặc plateau, không tệ đi). - Thêm gradient clipping
max_norm=1.0vào loop. So sánh loss curve trước và sau khi clip — Iris nhỏ nên thường không khác biệt, nhưng verify clip không phá train. - Optional: wrap loop bằng
tqdmvà log một metric (vdtrain_loss) lên TensorBoard. Mở TensorBoard xem curve.
Gợi ý ngắn
- Bài 1:
x = torch.tensor(iris.data, dtype=torch.float32); y = torch.tensor(iris.target, dtype=torch.long). Dùngrandom_splitcho ba phần. - Bài 2:
nn.Sequential(nn.Linear(4, 16), nn.ReLU(), nn.Linear(16, 3)). Iris nhỏ không cần dropout. - Bài 5: lưu cả
epochđể bài 7 resume đúng. - Bài 6: nhớ
model.load_state_dicttrước khievaluate;map_location=devicenếu khác môi trường. - Bài 7:
for epoch in range(start_epoch, start_epoch + 30). - Bài 8:
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)ngay sauloss.backward(), trướcoptimizer.step().
Tóm tắt
- PyTorch không có
.fit()built-in; phải tự viết training loop — đổi lại được full control, dễ debug, dễ customize. - 5 bước cốt lõi mỗi batch: to device → forward → loss → zero_grad + backward → step. Đúng thứ tự bắt buộc.
model.train()trước vòng train,model.eval()trước validation/test — Dropout và BatchNorm hành xử khác giữa hai mode.- Validation loop nằm trong
with torch.no_grad():— tiết kiệm VRAM, nhanh hơn. - Track loss + metric (accuracy/F1 cho classification, MSE/R² cho regression); plot learning curve để detect overfit.
- Save checkpoint =
state_dictcủa model + optimizer + epoch; save best theo val metric, không nhất thiết epoch cuối. - Resume: load state vào model và optimizer cùng nhau; nhớ
map_locationkhi khác device. - Logging:
print+tqdmcho prototype; TensorBoard hoặc wandb cho experiment tracking. - Gradient accumulation simulate batch lớn khi VRAM thấp; gradient clipping chống exploding (RNN/Transformer); mixed precision nhanh hơn và tiết kiệm VRAM trên GPU mới.
- PyTorch Lightning là wrapper giảm boilerplate — học sau khi nắm vững raw loop.
- Pitfall hàng đầu: quên train/eval switch, quên zero_grad, quên
.to(device), printlossthay vìloss.item().
- PyTorch Tutorial - Optimizing Model Parameters
- PyTorch Tutorial - Training with PyTorch
- PyTorch Tutorial - Saving and Loading Models
- PyTorch Docs - nn.Module.train / eval
- PyTorch Docs - torch.no_grad
- PyTorch Docs - torch.save
- PyTorch Docs - clip_grad_norm_
- PyTorch Docs - Automatic Mixed Precision (AMP)
- PyTorch Notes - AMP Examples (GradScaler)
- PyTorch Docs - torch.utils.tensorboard
- PyTorch Lightning - Introduction
- Pascanu et al. (2013) - On the difficulty of training Recurrent Neural Networks (gradient clipping)
- Micikevicius et al. (2018) - Mixed Precision Training
