Deep Learning 7 — Thực hành với PyTorch

13 thg 7, 2026 7 lượt xem
#ai
#deep-learning
#pytorch
#tensor
#autograd
#gpu

Deep Learning 7 — Thực hành với PyTorch

Sáu bài trước của series đã dựng xong phần lý thuyết: nền tảng mạng nơ-ron, cách một mạng học qua backpropagation và gradient descent, rồi các kiến trúc chuyên biệt. Nhưng lý thuyết chỉ sống dậy khi bạn gõ được một vòng lặp huấn luyện chạy thật. Bài này là cây cầu từ công thức sang code — và chúng ta sẽ đi cùng PyTorch, framework deep learning phổ biến nhất trong nghiên cứu và ngày càng chiếm ưu thế cả trong production.

Nếu bạn còn nhớ bài training: học một mạng nghĩa là lặp đi lặp lại bốn bước — đưa dữ liệu vào (forward), đo sai số (loss), tính đạo hàm của loss theo từng tham số (backward), rồi nhích tham số theo hướng giảm loss (update). Cả bài này thực chất là biến bốn bước đó thành vài dòng Python chạy được trên GPU.

1. Vì sao cần một framework?

Về nguyên tắc bạn có thể viết một mạng nơ-ron bằng NumPy thuần. Nhưng bạn sẽ phải tự tay đạo hàm từng lớp — và với một mạng vài chục lớp, việc tính gradient bằng tay vừa cực nhọc vừa dễ sai. Framework deep learning giải quyết đúng ba nỗi đau này:

  • Tự động tính gradient (autograd) — bạn chỉ viết phần forward, framework tự dựng chuỗi phép tính và tự tính toàn bộ đạo hàm ngược. Đây là tính năng cốt lõi.
  • Tối ưu GPU — các phép nhân ma trận khổng lồ được đẩy xuống GPU, nhanh hơn CPU hàng chục tới hàng trăm lần cho khối lượng tính toán của DL.
  • Khối dựng sẵn — các lớp (Linear, Conv, LSTM), hàm loss, thuật toán tối ưu (Adam, SGD), bộ nạp dữ liệu... đều có sẵn, được kiểm thử kỹ.

PyTorch nổi bật vì nó rất pythonic: viết mạng cảm giác như viết Python bình thường, không phải học một ngôn ngữ đồ thị riêng. Đó là lý do phần lớn bài báo nghiên cứu và mã nguồn mở hiện nay dùng PyTorch.

2. Tensor — viên gạch nền

Đơn vị dữ liệu của PyTorch là tensor. Bạn có thể coi nó như ndarray của NumPy nhưng có thêm hai siêu năng lực: chạy được trên GPU, và ghi nhớ được lịch sử phép tính để phục vụ autograd.

import torch

# tạo tensor (minh hoạ)
x = torch.tensor([[1.0, 2.0], [3.0, 4.0]])   # từ list
z = torch.zeros(2, 3)                          # ma trận 0
r = torch.randn(2, 3)                          # phân phối chuẩn

# thao tác giống NumPy
print(x.shape)        # torch.Size([2, 2])
print(x @ x)          # nhân ma trận
print(x.mean())       # trung bình

# chuyển qua lại với NumPy (chia sẻ bộ nhớ trên CPU)
import numpy as np
n = x.numpy()
t = torch.from_numpy(np.array([1, 2, 3]))

Điểm khác NumPy quan trọng nhất là khái niệm device. Một tensor sống ở cpu hoặc cuda (GPU). Mọi phép tính chỉ chạy được khi các tensor tham gia nằm cùng device — quên chuyển một cái lên GPU là lỗi phổ biến nhất của người mới:

# (minh hoạ)
device = "cuda" if torch.cuda.is_available() else "cpu"
x = x.to(device)          # đẩy tensor lên GPU nếu có

3. Autograd — trái tim của PyTorch

Đây là phần khiến framework khác hẳn NumPy. Khi bạn đánh dấu một tensor bằng requires_grad=True, PyTorch âm thầm ghi lại mọi phép tính bạn thực hiện trên nó thành một computation graph (đồ thị tính toán). Khi bạn gọi .backward() trên kết quả cuối (thường là loss), nó chạy ngược đồ thị và điền gradient vào thuộc tính .grad của từng tensor — chính xác là bước backward mà bài training & backprop mô tả bằng công thức.

# (minh hoạ)
w = torch.tensor(3.0, requires_grad=True)
b = torch.tensor(1.0, requires_grad=True)
x = torch.tensor(2.0)

y = w * x + b          # forward: y = 3*2 + 1 = 7
loss = (y - 10) ** 2   # giả sử mục tiêu là 10

loss.backward()        # tự tính gradient
print(w.grad)          # d(loss)/dw = 2*(y-10)*x = 2*(-3)*2 = -12
print(b.grad)          # d(loss)/db = 2*(y-10)   = -6

Bạn không viết một dòng đạo hàm nào — PyTorch tự lo. Đồ thị của PyTorch là động (dynamic): nó được dựng lại ở mỗi lần forward, nghĩa là bạn có thể dùng if, vòng lặp Python bình thường để đổi cấu trúc mạng theo dữ liệu. Đây là lý do PyTorch dễ debug và được giới nghiên cứu ưa chuộng.

4. nn.Module — cách định nghĩa model

Bạn hiếm khi tự quản lý từng tensor tham số. Thay vào đó, bạn gói model trong một lớp kế thừa nn.Module với hai phần: __init__ khai báo các lớp con, và forward mô tả dữ liệu chảy qua chúng như thế nào.

# (minh hoạ)
import torch.nn as nn
import torch.nn.functional as F

class MLP(nn.Module):
    def __init__(self, in_dim, hidden, n_classes):
        super().__init__()
        self.fc1 = nn.Linear(in_dim, hidden)   # lớp fully-connected
        self.fc2 = nn.Linear(hidden, n_classes)

    def forward(self, x):
        x = F.relu(self.fc1(x))   # kích hoạt ReLU
        return self.fc2(x)         # trả logits (chưa softmax)

model = MLP(in_dim=20, hidden=64, n_classes=3)

Vài điểm cần nắm:

  • Layers dựng sẵn: nn.Linear (fully-connected), nn.Conv2d (tích chập cho ảnh), nn.LSTM/nn.GRU (chuỗi), nn.Embedding, nn.Dropout, nn.BatchNorm... Bạn ghép chúng như xếp Lego.
  • Tham số tự đăng ký: mỗi layer chứa các tensor requires_grad=True. Gọi model.parameters() để lấy hết chúng đưa cho optimizer — không cần tự liệt kê.
  • nn.functional (F): phiên bản "hàm thuần" của các phép không có trạng thái như relu, softmax. Layer có trọng số thì dùng nn.Module; phép thuần thì dùng F.

Gọi model(x) sẽ tự động chạy forward(x).

5. Dataset & DataLoader — nạp dữ liệu đúng cách

Bạn không đưa cả tập dữ liệu vào mạng một lần. Bạn chia thành các batch (lô nhỏ). PyTorch tách việc này thành hai lớp:

  • Dataset: trả lời hai câu hỏi — "có bao nhiêu mẫu" (__len__) và "mẫu thứ i là gì" (__getitem__).
  • DataLoader: bọc quanh Dataset để tự động gom mẫu thành batch, xáo trộn (shuffle), và nạp song song bằng nhiều tiến trình.
# (minh hoạ)
from torch.utils.data import Dataset, DataLoader

class TabularDataset(Dataset):
    def __init__(self, X, y):
        self.X = torch.tensor(X, dtype=torch.float32)
        self.y = torch.tensor(y, dtype=torch.long)

    def __len__(self):
        return len(self.X)

    def __getitem__(self, i):
        return self.X[i], self.y[i]

train_ds = TabularDataset(X_train, y_train)
train_loader = DataLoader(
    train_ds,
    batch_size=32,
    shuffle=True,      # xáo trộn mỗi epoch — quan trọng cho train
    num_workers=4,     # nạp dữ liệu song song trên nhiều tiến trình
    pin_memory=True,   # tăng tốc chuyển CPU -> GPU
)

Hai tham số hiệu năng đáng nhớ: num_workers cho phép nạp và tiền xử lý dữ liệu song song trong khi GPU đang tính, tránh để GPU "đói" dữ liệu (một heuristic thường gặp là bắt đầu từ 0 rồi tăng dần); pin_memory=True giữ vùng nhớ khoá trên CPU để chuyển sang GPU nhanh hơn. Tập validation/test thì đặt shuffle=False.

6. Vòng lặp huấn luyện chuẩn

Đây là phần cốt lõi bạn sẽ viết đi viết lại. Cấu trúc kinh điển: lặp qua nhiều epoch, mỗi epoch lặp qua từng batch, và trong mỗi batch thực hiện đúng bốn bước của quá trình học.

Trình tự các lệnh có một logic chặt chẽ và không được đảo:

# (minh hoạ)
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
criterion = nn.CrossEntropyLoss()

for epoch in range(num_epochs):
    model.train()                          # bật chế độ train (dropout, batchnorm hoạt động)
    for xb, yb in train_loader:
        xb, yb = xb.to(device), yb.to(device)   # đưa dữ liệu lên đúng device

        optimizer.zero_grad()              # 1) xoá gradient tích luỹ từ batch trước
        output = model(xb)                 # 2) forward
        loss = criterion(output, yb)       # 3) tính loss
        loss.backward()                    # 4) backward: tính gradient
        optimizer.step()                   # 5) cập nhật trọng số

Vì sao cần optimizer.zero_grad()? PyTorch cộng dồn gradient qua các lần backward() (một thiết kế hữu ích cho một số kỹ thuật nâng cao). Nếu quên xoá, gradient của batch này sẽ chồng lên batch trước và việc học sai hoàn toàn. Đây là lỗi thầm lặng khét tiếng của người mới.

Đánh giá thì khác train. Khi validate hoặc inference, bạn cần hai thay đổi:

# (minh hoạ)
model.eval()                    # tắt dropout, cố định batchnorm
correct = 0
with torch.no_grad():           # không dựng computation graph -> nhanh & tiết kiệm bộ nhớ
    for xb, yb in val_loader:
        xb, yb = xb.to(device), yb.to(device)
        preds = model(xb).argmax(dim=1)
        correct += (preds == yb).sum().item()
accuracy = correct / len(val_ds)
  • model.eval() báo cho các lớp như Dropout và BatchNorm biết đang ở chế độ suy luận (Dropout tắt, BatchNorm dùng thống kê đã học). Quên bước này khiến kết quả đánh giá dao động vô lý.
  • torch.no_grad() bảo PyTorch đừng dựng đồ thị autograd — vì lúc này ta không cần gradient — giúp nhanh hơn và tốn ít bộ nhớ hơn.

7. Loss & Optimizer

Hai mảnh còn lại của vòng lặp:

  • Hàm loss đo sai số. Cho phân loại nhiều lớp, dùng nn.CrossEntropyLoss — lưu ý nó đã bao gồm softmax bên trong, nên model chỉ cần trả logits thô (đừng softmax lần nữa). Cho hồi quy, dùng nn.MSELoss. Phân loại nhị phân thường dùng nn.BCEWithLogitsLoss.
  • Optimizer quyết định cách nhích trọng số theo gradient. torch.optim.Adam là lựa chọn mặc định tốt cho hầu hết bài toán vì tự thích nghi bước học; torch.optim.SGD (thường kèm momentum) đơn giản và đôi khi tổng quát hoá tốt hơn. Tham số lr (learning rate) là siêu tham số quan trọng nhất cần chỉnh.

Optimizer được khởi tạo với model.parameters() — nhờ đó nó biết cần cập nhật những tensor nào.

8. Dùng GPU

GPU tăng tốc DL vì nó có hàng nghìn nhân xử lý song song, cực hợp với các phép nhân ma trận và tích chập vốn là bản chất của mạng nơ-ron. Cách bật GPU trong PyTorch rất gọn — bạn cần chuyển cả model lẫn dữ liệu lên cùng một device:

# (minh hoạ)
device = "cuda" if torch.cuda.is_available() else "cpu"
model = model.to(device)        # chuyển toàn bộ trọng số model lên GPU
# ... trong vòng lặp:
xb, yb = xb.to(device), yb.to(device)

Nguyên tắc: viết code phụ thuộc biến device thay vì hard-code "cuda", để cùng một script chạy được cả trên máy có và không có GPU.

Hai kỹ thuật nâng cao đáng biết tên (sẽ đào sâu ở nơi khác):

  • Mixed precision: dùng số thực nửa độ chính xác (float16/bfloat16) cho phần lớn phép tính, giúp nhanh hơn và tốn ít bộ nhớ GPU hơn, thông qua torch.cuda.amp. Rất phổ biến khi train model lớn.
  • Nhiều GPU: DistributedDataParallel chia batch cho nhiều GPU (thậm chí nhiều máy) để rút ngắn thời gian train.

9. Lưu và nạp model

Cách khuyến nghị là lưu state_dict — một dictionary chứa toàn bộ trọng số — thay vì lưu cả object model. state_dict nhẹ, ổn định và không phụ thuộc cấu trúc file code.

# (minh hoạ)
# lưu
torch.save(model.state_dict(), "model.pt")

# nạp: phải tạo lại kiến trúc model trước rồi mới nạp trọng số
model = MLP(in_dim=20, hidden=64, n_classes=3)
model.load_state_dict(torch.load("model.pt"))
model.eval()

Khi train dài, người ta lưu checkpoint gồm cả trọng số model, trạng thái optimizer và số epoch hiện tại, để có thể dừng và tiếp tục train mà không mất tiến độ:

# (minh hoạ)
torch.save({
    "epoch": epoch,
    "model_state": model.state_dict(),
    "optim_state": optimizer.state_dict(),
}, "checkpoint.pt")

10. Use case thực tế — classifier nhỏ end-to-end

Hãy ráp mọi mảnh trên thành một chương trình hoàn chỉnh: phân loại một tập dữ liệu bảng có 20 đặc trưng thành 3 lớp. Đây chính là bộ khung bạn sẽ sao chép cho hầu hết dự án phân loại.

# (minh hoạ)
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import Dataset, DataLoader

device = "cuda" if torch.cuda.is_available() else "cpu"

# --- 1. Dataset ---
class TabularDataset(Dataset):
    def __init__(self, X, y):
        self.X = torch.tensor(X, dtype=torch.float32)
        self.y = torch.tensor(y, dtype=torch.long)
    def __len__(self):
        return len(self.X)
    def __getitem__(self, i):
        return self.X[i], self.y[i]

train_loader = DataLoader(TabularDataset(X_train, y_train),
                          batch_size=32, shuffle=True)
val_loader   = DataLoader(TabularDataset(X_val, y_val),
                          batch_size=64, shuffle=False)

# --- 2. Model ---
class MLP(nn.Module):
    def __init__(self, in_dim, hidden, n_classes):
        super().__init__()
        self.fc1 = nn.Linear(in_dim, hidden)
        self.drop = nn.Dropout(0.2)
        self.fc2 = nn.Linear(hidden, n_classes)
    def forward(self, x):
        x = F.relu(self.fc1(x))
        x = self.drop(x)
        return self.fc2(x)

model = MLP(20, 64, 3).to(device)

# --- 3. Loss + Optimizer ---
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)

# --- 4. Train ---
for epoch in range(20):
    model.train()
    for xb, yb in train_loader:
        xb, yb = xb.to(device), yb.to(device)
        optimizer.zero_grad()
        loss = criterion(model(xb), yb)
        loss.backward()
        optimizer.step()

    # --- 5. Validate ---
    model.eval()
    correct = 0
    with torch.no_grad():
        for xb, yb in val_loader:
            xb, yb = xb.to(device), yb.to(device)
            correct += (model(xb).argmax(1) == yb).sum().item()
    print(f"epoch {epoch}: val_acc = {correct / len(val_loader.dataset):.3f}")

# --- 6. Lưu ---
torch.save(model.state_dict(), "classifier.pt")

Đọc lại đoạn này bạn sẽ thấy mọi mục ở trên hiện diện: Dataset/DataLoader gom batch, nn.Module định nghĩa model, bốn bước train, eval() + no_grad() khi validate, và state_dict khi lưu. Từ khung này, để chuyển sang bài toán ảnh bạn chỉ cần đổi Dataset (đọc ảnh) và model (dùng Conv2d), phần còn lại gần như giữ nguyên.

11. So sánh PyTorch vs TensorFlow/Keras

Đây là câu hỏi ai cũng gặp khi mới vào. Cả hai đều đủ mạnh cho mọi bài toán; khác biệt nằm ở triết lý và hệ sinh thái:

Tiêu chíPyTorchTensorFlow / Keras
Phong cáchRất pythonic, đồ thị động, dễ debugKeras trừu tượng hoá cao, dễ bắt đầu
Điểm mạnhNghiên cứu, prototype nhanh, tuỳ biến sâuPipeline production trưởng thành, mobile/edge
Triển khaiTorchServe, ExecuTorch (đang lớn nhanh)TF Serving, TF Lite, TF.js (lâu đời)
Cộng đồngChiếm đa số mã nghiên cứu & mô hình mởMạnh trong doanh nghiệp lớn

Diễn giải: PyTorch thắng ở tính linh hoạt và tốc độ thử nghiệm — đồ thị động cho phép viết logic bằng Python thuần và debug như code thường, nên phần lớn nghiên cứu mới ra đời bằng PyTorch. Keras/TensorFlow ghi điểm ở sự dễ khởi đầu (API cao cấp che phần lớn chi tiết) và một chuỗi công cụ production/edge đã chín (TF Serving cho serving, TF Lite cho di động). Xu hướng gần đây: PyTorch không còn thua kém về production và tiếp tục mở rộng thị phần, nên nếu chọn một framework để học đầu tiên hôm nay, PyTorch là lựa chọn an toàn.

Một cái tên nữa nên biết: PyTorch Lightning. Nó bọc quanh PyTorch để dẹp phần boilerplate lặp đi lặp lại (vòng lặp epoch/batch, chuyển device, checkpoint, log, train đa GPU) — bạn chỉ cần điền phần logic riêng của mình. Lightning giữ nguyên sức mạnh PyTorch nhưng gọn hơn nhiều khi dự án lớn dần.

12. Từ train tới dùng

Đến đây bạn đã có một model được huấn luyện và lưu ra file. Nhưng như bài MLOps đã nhấn mạnh, một model nằm trong file .pt chưa tạo ra giá trị nào — nó phải phục vụ dự đoán cho một hệ thống thật. Việc đóng gói, tối ưu tốc độ inference (TorchScript, ONNX, lượng tử hoá), dựng API phục vụ và giám sát model trong môi trường thật là chủ đề của bài tiếp theo về triển khai deep learning.

Ghi nhớ

  • Framework để có autograd + GPU + khối dựng sẵn. Bạn viết forward, PyTorch tự lo backward.
  • Tensor = ndarray + GPU + autograd. Luôn để ý device; model và dữ liệu phải cùng một nơi.
  • requires_grad.backward().grad. Đồ thị động dựng lại mỗi lần forward, nên dễ debug.
  • Model = nn.Module với __init__ (khai báo layer) và forward (luồng dữ liệu). model.parameters() gom hết tham số cho optimizer.
  • Dữ liệu = Dataset + DataLoader. DataLoader lo batch, shuffle, và nạp song song (num_workers, pin_memory).
  • Vòng train, thuộc lòng thứ tự: zero_grad()forwardlossbackward()step(). Quên zero_grad() là hỏng.
  • Khi đánh giá: model.eval() + torch.no_grad(). Khi train: model.train().
  • Loss & optimizer: CrossEntropyLoss (nhận logits, đã có softmax) cho phân loại, Adam là mặc định tốt.
  • Lưu state_dict, không lưu cả object; dùng checkpoint để train tiếp được.
  • PyTorch cho nghiên cứu & linh hoạt; TF/Keras mạnh ở production/edge. PyTorch Lightning giảm boilerplate.

Bài trước: các kiến trúc deep learning. Bài tiếp theo: Triển khai mô hình deep learning.

Bài viết liên quan

Phân biệt AI/ML/DL, các kiểu học máy, quy trình ML end-to-end và thuật ngữ nền tảng.

13 thg 7, 2026 15

Chất lượng dữ liệu, xử lý thiếu/ngoại lai, mã hoá, chuẩn hoá, tạo đặc trưng và tránh data leakage.

13 thg 7, 2026 13

Mô hình ngôn ngữ lớn hoạt động ra sao, token, context window, tham số sinh, và kỹ thuật viết prompt.

13 thg 7, 2026 13

Retrieval-Augmented Generation, vector database, tool calling, agent và MCP để xây ứng dụng LLM thực tế.

13 thg 7, 2026 11