Chương 4·Bài 4.1

Sử dụng model pretrained

Model pretrained là model người khác đã huấn luyện sẵn trên dữ liệu lớn và công bố trọng số, ví dụ ResNet huấn luyện trên ImageNet. Muốn dùng được cần hai thứ: trọng số, và đúng cách xử lý đầu vào lúc model được huấn luyện (kích thước ảnh, cách chuẩn hóa màu). Xử lý sai thì code vẫn chạy nhưng kết quả sai. Bài này nạp một model, chạy dự đoán trên một batch ảnh và kiểm kết quả, trước khi nghĩ tới huấn luyện thêm.

Mô hình có sẵn gồm những gì?

Lập trình thi đấu có kiểu lời giải “đánh bảng”: chương trình chỉ tra một bảng đã tính sẵn ở nhà, còn phần khó nằm trong bảng. Thiếu bảng, code vẫn biên dịch nhưng không trả lời đúng được câu nào. Với mạng neural, kiến trúc là phần code, mô tả các phép toán; trọng số là cái bảng, tức kết quả đã học. nn.Linear(2, 3) vừa tạo ra ba phép cộng có trọng số từ hai đầu vào, nhưng những trọng số ngẫu nhiên ấy chưa biết phân loại gì.

Model pretrained là mô hình có trọng số đã được học từ một nhiệm vụ trước đó. Nhiệm vụ trước có thể là phân loại ảnh, dự đoán token bị che hoặc ghép ảnh với mô tả. Nó không nhất thiết trùng với nhiệm vụ của ta. Không có pretrained thì trọng số bắt đầu ngẫu nhiên như bài 3.3; có thì bắt đầu từ chỗ đã học.

Vì sao trọng số học cho nhiệm vụ khác lại có ích? Để phân biệt hơn một triệu ảnh thuộc 1000 lớp của ImageNet, mạng buộc phải học những đặc trưng mà ảnh nào cũng có: cạnh, kết cấu, hình dạng, bộ phận của vật thể. Các tầng sâu dần ghép đặc trưng đơn giản thành đặc trưng phức tạp, và phần lớn chúng không gắn với một lớp cụ thể nào. ImageNet không có lớp “thi đấu” hay “bài giảng”, nhưng ảnh câu lạc bộ vẫn có cạnh, kết cấu, bàn ghế, màn hình, máy chiếu. Chỉ tầng cuối, gọi là head, gắn chặt với 1000 lớp cũ.

Các tầng đầu học đặc trưng chung dùng lại được; chỉ head cuối gắn với 1000 lớp cũ.cạnhkết cấuhình dạngbộ phậnhead1000 lớp ImageNetdùng lại được cho ảnh câu lạc bộphải thaytầng nông → tầng sâu
Các tầng đầu học đặc trưng chung dùng lại được; chỉ head cuối gắn với 1000 lớp cũ.

Một gói có thể dùng lại cần nhiều thứ đi cùng nhau:

  • Trọng số: kết quả đã học.
  • Cấu hình: số lớp và kích thước các tầng.
  • Processor: cách xử lý đầu vào như resize, thứ tự kênh màu, chuẩn hóa, hoặc tokenizer với chữ.
  • Bảng nhãn: chỉ số 0, 1, 2… nghĩa là gì.
  • Revision: đúng phiên bản đã tải.

Nếu dùng trọng số của phiên bản này với tokenizer của phiên bản khác, chương trình có thể vẫn chạy nhưng đầu vào đã đổi ý nghĩa.

Với ảnh, đầu vào thường có shape (B, C, H, W): BB là số ảnh trong batch, CC là số kênh, HH và WW là chiều cao và rộng. Với chữ, tokenizer thường tạo input_ids và attention_mask có shape (B, L), trong đó LL là độ dài đã padding. Cùng là float32 không có nghĩa cùng thang đo.

Processor của các model ảnh huấn luyện trên ImageNet thường chia pixel cho 255, rồi chuẩn hóa từng kênh bằng mean (0.485,0.456,0.406)(0.485, 0.456, 0.406) và std (0.229,0.224,0.225)(0.229, 0.224, 0.225) của tập ấy. Có processor nhận pixel từ 0 đến 255 và tự chia, có processor nhận ảnh đã chia sẵn. Làm một bước hai lần, hoặc bỏ sót một lần, đều đổi dữ liệu mà không gây lỗi nào.

Trong torchvision, transforms.ToTensor() đổi ảnh PIL (H,W,3)(H, W, 3) giá trị 0–255 thành tensor float (3,H,W)(3, H, W) trong [0,1][0, 1]; Normalize làm việc trên tensor nên đứng sau nó, như Compose([Resize(...), ToTensor(), Normalize(mean, std)]).

Đọc bằng cv2 thì đỏ và lam bị đảo; chia 255 hai lần thì ảnh gần như một màu.RGB đúngđọc bằng cv2 (BGR)chia 255 hai lầnmọi giá trị ≤ 1/255shape, dtype đều đúng: không có lỗi nào báo ra
Đọc bằng cv2 thì đỏ và lam bị đảo; chia 255 hai lần thì ảnh gần như một màu.
Ảnh mèo qua đúng tiền xử lý của ResNet-18: ba lớp mèo đứng đầu.

Hỏi nhanh

Có kiến trúc ResNet, và trọng số được khởi tạo ngẫu nhiên. Mô hình này đã là một model pretrained chưa?

Hỏi nhanh

Processor của checkpoint nhận pixel từ 0 đến 255, tự chia cho 255, rồi trừ mean và chia std. Code của đội lại đã chia cho 255 trước khi gọi processor. Chuyện gì xảy ra?

Thử cơ chế bằng một gói nhỏ chạy offline

Trước khi đụng tới một checkpoint cỡ hàng chục đến hàng trăm MiB, ta đi hết đường dữ liệu trên một gói đủ nhỏ để tính tay. Mỗi mẫu có hai đặc trưng thay cho một ảnh, mạng là nn.Linear(2, 3) với trọng số đặt sẵn, đầu ra là ba nhãn hoạt động. Gói này đóng vai checkpoint mà một nhóm khác đã công bố.

Các bước lưu, nạp, tiền xử lý và suy luận giống hệt với model thật, chỉ khác kích thước; vì mọi số đều tính tay được, sai ở bước nào sẽ lộ ngay ở bước ấy.

state_dict là ánh xạ từ tên tham số hoặc buffer đến tensor. Ta lưu thêm các số của processor và bảng nhãn trong cùng gói. BytesIO đóng vai một tệp trong bộ nhớ; với tệp thật, chỉ cần thay bằng đường dẫn .pt.

Python
import io
import numpy as np
import torch
from torch import nn

torch.set_num_threads(1)
torch.manual_seed(0)
source = nn.Linear(2, 3)
with torch.no_grad():
    source.weight.copy_(torch.tensor([[1., -1.], [-1., 1.], [.5, .5]]))
    source.bias.copy_(torch.tensor([0., 0., .25]))

package = {
    "state_dict": source.state_dict(),
    "mean": torch.tensor([10., 10.]),
    "scale": torch.tensor([2., 2.]),
    "labels": ["hoat_dong_A", "hoat_dong_B", "chua_ro"],
}
buffer = io.BytesIO()
torch.save(package, buffer)
buffer.seek(0)
loaded = torch.load(buffer, map_location="cpu", weights_only=True)

model = nn.Linear(2, 3)
model.load_state_dict(loaded["state_dict"], strict=True)
model.eval()
print("shape W:", tuple(model.weight.shape))
print("nhan:", loaded["labels"])

Ba tham số khi nạp:

  • strict=True phát hiện tên hoặc shape tham số không khớp, thay vì âm thầm bỏ một tầng.
  • map_location="cpu" mở tensor đã được lưu ở thiết bị khác lên CPU.
  • weights_only=True, mặc định từ PyTorch 2.6, chỉ mở tensor và kiểu dữ liệu đơn giản như số, chuỗi, list, dict; nó không chạy code tùy ý có thể nằm trong file pickle. Vì vậy gói nên chứa state_dict cùng các số và chuỗi, không chứa cả đối tượng model.

Nạp được cũng chưa có nghĩa là đúng tệp: nguồn và hash vẫn phải kiểm, như ở mục cuối bài.

Hỏi nhanh

Checkpoint được lưu từ một model bọc trong DataParallel, nên các khóa là module.weight và module.bias, trong khi model mới chờ weight và bias. Đội gọi load_state_dict(sd, strict=False). Kết quả là gì?

Ta viết processor thành hàm riêng. Với x=(12,8)\mathbf{x}=(12,8), trừ (10,10)(10,10) rồi chia (2,2)(2,2) cho z=(1,−1)\mathbf{z}=(1,-1). Mạng tính

ℓ=Wz+b=(2,−2,0.25).\boldsymbol{\ell} = W\mathbf{z}+\mathbf{b}=(2,-2,0.25).

ℓ\boldsymbol{\ell} là vector logits, chưa phải xác suất. Softmax không đổi thứ tự các logits, nên argmax trước hay sau softmax như nhau; softmax chỉ cần khi muốn xác suất, chẳng hạn để nộp xác suất hoặc đặt ngưỡng. Logit lớn nhất nằm ở chỉ số 0, và bảng nhãn đổi chỉ số ấy thành tên.

Một mẫu qua ba phần của gói: processor, trọng số, bảng nhãn.x = (12, 8)z = (1, −1)processorℓ = (2, −2, 0.25)state_dicthoat_dong_Alabels(x − mean) / scaleW z + bargmax = 0thiếu hay lệch một phần nào, chương trình vẫn chạy mà trả lời sai
Một mẫu qua ba phần của gói: processor, trọng số, bảng nhãn.
Python
def process(raw):
    raw_tensor = torch.as_tensor(raw, dtype=torch.float32)
    return (raw_tensor - loaded["mean"]) / loaded["scale"]

raw = np.array([[12., 8.], [8., 12.], [10., 10.], [14., 6.], [9., 11.]])
with torch.inference_mode():
    logits = model(process(raw))
    predicted = logits.argmax(dim=1)
print(logits.numpy().round(2))
print([loaded["labels"][i] for i in predicted.tolist()])

Hàng đầu đúng bằng (2,−2,0.25)(2,-2,0.25) như tính tay. Hàng thứ ba có hai đặc trưng bằng trung bình nên thành (0,0)(0,0) sau processor; chỉ còn bias, và lớp chua_ro thắng. Với checkpoint thật không ai tính tay được toàn bộ logits, nhưng cách kiểm vẫn như vậy: shape phải là (số mẫu, số lớp), vài mẫu đã biết đáp án phải ra đúng, và một đường tính độc lập phải khớp (mục sau).

Hỏi nhanh

logits có shape (5, 3). Muốn mỗi mẫu có một nhãn, ta dùng gì?

Batch phải giữ nguyên ý nghĩa và thứ tự

Một batch là nhiều mẫu được tính chung để tận dụng phép toán ma trận. Nó không phải một mẫu mới được tạo bằng cách trộn các ảnh. Batch size đổi lượng công việc mỗi lần gọi, còn thứ tự đầu ra phải khớp với thứ tự đầu vào.

Năm mẫu chia batch 2, 2, 1; kết quả nối lại theo đúng thứ tự id.input101102103104105batch 1batch 2batch 3kết quả101102103104105batch 2, 2, 1 nối lại đúng thứ tự; giữ id theo từng hàng
Năm mẫu chia batch 2, 2, 1; kết quả nối lại theo đúng thứ tự id.

Ở chế độ suy luận, mạng tuyến tính này cho cùng đáp án với mọi cách chia batch. Ta kiểm với cả kết quả chạy một lần và một bản NumPy độc lập; kiểm thứ hai giúp bắt lỗi chuyển vị ma trận, không chỉ lỗi ghép batch.

Python
def infer_batches(raw, batch_size):
    rows = []
    with torch.inference_mode():
        for start in range(0, len(raw), batch_size):
            rows.append(model(process(raw[start:start + batch_size])))
    return torch.cat(rows, dim=0)

batched = infer_batches(raw, batch_size=2)
z_numpy = (raw - np.array([10., 10.])) / np.array([2., 2.])
W_numpy = np.array([[1., -1.], [-1., 1.], [.5, .5]])
expected = z_numpy @ W_numpy.T + np.array([0., 0., .25])
print("batch khop:", torch.allclose(batched, logits))
print("NumPy khop:", np.allclose(batched.numpy(), expected))
print("so mau:", len(batched))

Ba dòng cần lần lượt xác nhận cách chia batch, phép toán và số mẫu. Với dữ liệu thật, còn phải giữ id cùng mỗi hàng. Nếu DataLoader shuffle rồi ghi kết quả theo thứ tự CSV ban đầu, mọi tensor đều đúng shape mà file nộp vẫn sai.

model.eval() và torch.inference_mode() giải quyết hai việc:

  • eval() đặt các tầng như Dropout và BatchNorm vào chế độ đánh giá; nó không tắt autograd.
  • inference_mode() bỏ theo dõi gradient để suy luận gọn hơn, giống no_grad() nhưng chặt hơn; nó không tự gọi eval().

Tensor tạo trong inference_mode() không dùng được cho phép tính cần backward; nếu sau đó muốn học một head trên embedding (bài 4.2), lưu embedding thành mảng NumPy hoặc .clone() ra tensor thường trước.

Mạng ảnh pretrained thường có BatchNorm, và đó là thêm một lý do để gọi eval(). Ở chế độ train, BatchNorm chuẩn hóa mỗi mẫu bằng mean và variance của chính batch chứa nó, nên đầu ra của một ảnh phụ thuộc những ảnh đi cùng. Ở chế độ eval, nó dùng running statistics đã lưu trong checkpoint, và mọi cách chia batch lại cho cùng kết quả như mạng tuyến tính ở trên.

Hỏi nhanh

CNN pretrained có BatchNorm. Đội quên gọi eval(), nhưng có inference_mode(). Chạy cùng tập test với batch 32 rồi với batch 8, hai file nộp khác nhau ở vài dòng. Vì sao?

Hỏi nhanh

Model đã gọi eval(), nhưng vòng dự đoán không bọc trong inference_mode() hay no_grad(). Tham số vẫn có requires_grad=True, còn input thì không. Câu nào đúng?

Ước lượng bộ nhớ trước khi chạy

Nếu mô hình có PP tham số, mỗi tham số chiếm ss byte, bộ nhớ thô của trọng số là PsPs. float32 dùng 4 byte, float16 dùng 2 byte. MiB là 2202^{20} byte; đừng lẫn với MB là 10610^6 byte. Một mô hình 100 triệu tham số có phần trọng số float32 khoảng 381.47 MiB, hoặc float16 khoảng 190.73 MiB.

Kích thước ảnh cũng đổi ra số được. Ảnh 1920×10801920 \times 1080 có 1920 điểm ảnh theo chiều ngang, 1080 theo chiều dọc. Ảnh RGB 32×3232 \times 32 có 32⋅32⋅3=307232 \cdot 32 \cdot 3 = 3072 số; ảnh 100×100100 \times 100 lưu 1 byte mỗi kênh là 30 000 byte. Số điểm ảnh tăng theo bình phương cạnh: thu 4000×30004000 \times 3000 về 400×300400 \times 300 bớt 100 lần số pixel, giảm 640×640640 \times 640 về 320×320320 \times 320 bớt khoảng 4 lần khối tính của mạng tích chập.

Python
P = 100_000_000
print("FP32 MiB:", round(P * 4 / 2**20, 2))
print("FP16 MiB:", round(P * 2 / 2**20, 2))
input_bytes = 16 * 3 * 224 * 224 * 4
print("input batch MiB:", round(input_bytes / 2**20, 2))
actual_bytes = sum(p.numel() * p.element_size() for p in model.parameters())
print("mang tuyen tinh bytes:", actual_bytes)

Mạng nhỏ có 3⋅2+3=93\cdot2+3=9 tham số, mỗi số 4 byte, nên 36 byte. Input batch 16 ảnh RGB cỡ 224×224224\times224 là một phần khác: 16⋅3⋅224⋅224⋅416\cdot3\cdot224\cdot224\cdot4 byte, khoảng 9.19 MiB. Khi chạy còn có activation trung gian, buffer, vùng làm việc của thư viện và bộ nhớ dự trữ.

Input và phần lớn activation của mạng tích chập tăng tỉ lệ với B⋅H⋅WB\cdot H\cdot W, nên gấp đôi cạnh ảnh tốn ngang gấp bốn batch. Khi training còn có gradient và trạng thái optimizer. Vì vậy “trọng số vừa bộ nhớ” chưa đủ để kết luận “batch này chạy được”. Cách chắc chắn là đo một batch nhỏ trên đúng thiết bị rồi tăng dần.

Gấp đôi batch và gấp đôi cạnh ảnh thì input gấp 8 lần.trọng số 100 triệu, FP32381.47 MiBtrọng số 100 triệu, FP16190.73 MiBinput 16 × 3 × 224²9.19 MiBinput 32 × 3 × 448²73.5 MiBchưa tính activation, gradient, optimizer và bộ nhớ dự trữ
Gấp đôi batch và gấp đôi cạnh ảnh thì input gấp 8 lần.

Hỏi nhanh

Một batch 16 ảnh RGB 224×224224\times 224 kiểu float32 chiếm 9.19 MiB. Đổi sang batch 32 và ảnh 448×448448\times 448 thì tensor input chiếm bao nhiêu?

Dùng model pretrained thật

Với bộ tìm kiếm của câu lạc bộ, việc đầu tiên là đọc model card: model học trên dữ liệu gì, nhận input nào, xuất ra nhãn nào, license cho phép dùng ra sao. Model chỉ học tiếng Anh sẽ yếu với chú thích tiếng Việt. Head phân loại ImageNet chỉ chọn trong 1000 lớp cũ, không tự biến thành head “sinh hoạt”, “thi đấu”, “bài giảng” của câu lạc bộ. Phần dùng lại được là các tầng bên dưới: bài 4.2 lấy đặc trưng từ đó, bài 4.3 học head mới.

Hỏi nhanh

Đội đưa ảnh câu lạc bộ vào một ResNet học trên ImageNet, còn nguyên head, rồi lấy argmax trên 1000 logits. Kết quả là gì?

Script của chương làm việc này theo hai lượt:

  1. Lượt có mạng (download_assets.py --download) tải đúng một revision, tức một commit 40 ký tự hex chứ không phải nhánh main có thể đổi, rồi ghi model ID, license đã đọc, kích thước và SHA-256 của từng tệp vào ASSET.json.
  2. Lượt offline kiểm lại mọi hash, đặt HF_HUB_OFFLINE=1, nạp model cùng tokenizer hoặc processor từ đường dẫn local với local_files_only=True, chạy dữ liệu nhỏ của mình và ghi môi trường cùng kết quả vào RESULT.json.

Đọc kết quả theo thứ tự: hash khớp thì tệp đúng; shape và ID khớp thì đường dữ liệu đúng; chỉ khi đó điểm validation mới nói về chất lượng của model trên bài.

Hỏi nhanh

Hai tuần sau, đội tải lại cùng model ID từ nhánh main, chạy năm ảnh kiểm ở chế độ eval, và thấy logits khác lần trước ngay từ chữ số đầu. Nên kiểm gì đầu tiên?

Khi đi thi

  • Đọc quy định về model, Internet và thư viện trước khi chuẩn bị.
  • Đầu giờ, nạp model từ đường dẫn local, chạy một batch và tạo một file nộp thử.
  • Resize, mean/std hay tokenizer phải giống hệt giữa validation và test; lưu chúng cạnh trọng số.

Tóm tắt

  • Model pretrained gồm trọng số đã học, kiến trúc, processor và bảng nhãn; thiếu hay lệch một phần thì code vẫn chạy nhưng kết quả sai.
  • Suy luận: gọi eval(), tắt gradient, giữ id qua batch rồi đối chiếu shape và vài kết quả đã biết.
  • Bộ nhớ trọng số là PsPs byte; tổng bộ nhớ khi chạy phải đo.
  • Model thật cần đúng revision, license, processor, hash và thử chạy offline; chất lượng trên bài mới chỉ biết được qua validation.

Kiểm tra cuối bài

Câu 1

Câu lạc bộ có 300 ảnh gán nhãn “sinh hoạt”, “thi đấu”, “bài giảng”, và ImageNet không có lớp nào như vậy. Vì sao backbone học trên ImageNet vẫn thường tốt hơn khởi tạo ngẫu nhiên?

Phỏng theoNOAI 2026 Singapore · Vòng sơ loại · Câu 213

Câu 2

ResNet-50 nhận input (B, 3, 224, 224). Phần đầu, gồm conv stride 2 rồi max-pool stride 2, đưa ảnh về 56×5656\times 56. layer1 giữ 56×5656\times 56 và cho ra 256 kênh. Khối đầu của layer2 có conv 3×33\times 3 stride 2 trên 128 kênh, rồi conv 1×11\times 1 mở ra 512 kênh. Đầu ra của layer2 có shape gì?

Phỏng theoUSA-NA-AIO 2026 · Vòng 1 · Part 8.2

Câu 3

Với model = resnet50(weights=None), biểu thức nào cho đúng số tham số học được?

n1 = len(list(model.parameters()))
n2 = sum(t.numel() for t in model.state_dict().values())
n3 = sum(p.numel() for p in model.parameters())
n4 = sum(p.numel() * p.element_size() for p in model.parameters())

Phỏng theoUSA-NA-AIO 2026 · Vòng 1 · Part 8.1

Câu 4

Processor ImageNet chia pixel cho 255, rồi với kênh R thì trừ mean 0.485 và chia std 0.229. Một pixel kênh R bằng 128 trở thành bao nhiêu, làm tròn 3 chữ số?

Câu 5

Config của checkpoint ghi id2label = {0: "cat", 1: "dog", 2: "bird"}. Logits của một ảnh là (−1,2,0.5)(-1, 2, 0.5). Đội lại tự tạo danh sách nhãn bằng sorted(set(train_labels)), tức ["bird", "cat", "dog"], để ghi file nộp. Nhãn đúng và nhãn đội ghi lần lượt là gì?

Câu 6

Đội chuyển model đã eval sang float16 trên GPU để suy luận nhanh hơn, giữ input_ids ở kiểu số nguyên. Nên kỳ vọng gì, và kiểm gì?

Phỏng theoNOAI 2026 Singapore · Vòng sơ loại · Câu 300

Câu 7

ResNet-50 có 25 557 032 tham số. Ở float16, phần trọng số chiếm khoảng bao nhiêu?

Câu 8

Trọng số chiếm 2 GiB trên GPU 4 GiB. Suy luận với batch 1 thì chạy được, batch 128 thì báo out-of-memory. Giải thích nào phù hợp nhất?

Câu 9

Hai hàm suy luận PyTorch, một chạy cả tập, một chia batch, cùng gọi một hàm process tự viết, và cho logits bằng nhau. Viết lại công thức từ đầu bằng NumPy thì thêm được gì?

Câu 10

Trên Colab, đội lưu torch.save(model, "m.pt"), tức lưu cả đối tượng model. Máy chấm dùng PyTorch mới, và torch.load("m.pt") báo lỗi “Weights only load failed”. Lần sau nên lưu thế nào cho bền?

Phỏng theoNOAI 2026 Singapore · Vòng sơ loại · Câu 134

Đọc thêm

Nguồn ảnh: ảnh hoa là ảnh mẫu flower.jpg kèm scikit-learn (vultilion, Flickr, CC BY 2.0); ảnh mèo là ảnh mẫu chelsea kèm scikit-image (Stefan van der Walt, CC0).

Tải gói recipe của chương — gồm các module phụ và hướng dẫn chuẩn bị asset.