Fine-tuning trong thực tế: từ baseline đến thí nghiệm đáng tin cậy
Ở bài trước, chúng ta đã trả lời câu hỏi nền tảng: transfer learning là gì, tại sao nó hoạt động và khi nào nên dùng. Bài viết này sẽ đối diện với câu hỏi thực tế và gai góc hơn nhiều: làm thế nào để fine-tune một mô hình mà kết quả thu được thực sự đáng tin cậy, thay vì chỉ là vài dòng code chạy qua mà không thể bảo vệ trước hội đồng hay đồng nghiệp?
Nếu chỉ giữ lại một câu: Một quy trình fine-tuning tốt không bắt đầu từ lệnh
model.train(), mà bắt đầu từ việc khóa phương pháp chia dữ liệu, thiết lập một baseline tử tế và đo lường sự tiến bộ bằng nhiều tiêu chí khách quan thay vì đuổi theo một con số accuracy ảo.
Chúng ta sẽ mượn một bài toán phân loại ảnh với tập dữ liệu nhỏ bằng PyTorch để dựng lại toàn bộ quy trình thực nghiệm. Mục tiêu không phải là tinh chỉnh cho riêng một bộ dữ liệu đồ chơi, mà là trang bị một phương pháp luận có thể dùng lại cho CNN, cho Vision Transformer và sau này là cả các mô hình ngôn ngữ lớn (LLM).
Tình huống thực tế: phân loại ảnh với tập dữ liệu nhỏ
Hãy tưởng tượng bạn đối mặt với một tập dữ liệu ảnh đặc thù chỉ có 5 lớp, mỗi lớp vỏn vẹn vài trăm mẫu. Nhiệm vụ đặt ra là xây dựng một mô hình có độ chính xác cao nhất trong thời gian và tài nguyên hữu hạn. Một kỹ sư có phương pháp sẽ không nhảy ngay vào việc điều chỉnh siêu tham số bừa bãi, mà sẽ thiết lập 4 mốc chiến lược rõ ràng:
- S0 — Học từ đầu (Train from scratch): Giữ nguyên kiến trúc nhưng khởi tạo ngẫu nhiên toàn bộ trọng số;
- S1 — Đóng băng backbone (Linear / Head probing): Tải mô hình đã học trước (pretrained), khóa toàn bộ thân mạng chính và chỉ huấn luyện bộ phân loại mới;
- S2 — Tinh chỉnh một phần (Partial fine-tuning): Mở khóa một vài khối cuối của backbone với tốc độ học (learning rate) rất nhỏ;
- S3 — Tinh chỉnh toàn bộ (Full fine-tuning): Mở khóa toàn bộ mô hình với learning rate cực thấp sau khi phần đầu ra đã hội tụ.
Nếu tài nguyên GPU hạn chế hoặc kích thước mô hình quá lớn, ta có thêm nhánh S4 — PEFT (Parameter-Efficient Fine-Tuning, ví dụ như LoRA) để tối ưu chi phí.
Quy tắc bất di bất dịch của thực nghiệm khoa học: tất cả các chiến lược trên phải được đo lường trên cùng một phép chia dữ liệu, cùng pipeline tiền xử lý, cùng độ đo và cùng quy ước dừng sớm. Nếu mỗi chiến lược dùng một random seed khác nhau, tập test khác nhau hay cách augment khác nhau, mọi kết luận rút ra đều là vô nghĩa.
Bước 0: Khóa quy trình chia dữ liệu trước khi đụng vào mô hình
Đây là bước mà những người mới làm học sâu thường mắc sai lầm nghiêm trọng nhất: vội vàng viết vòng lặp huấn luyện khi dữ liệu chưa được cách ly đúng chuẩn.
Chia dữ liệu theo đúng đơn vị độc lập
Nếu từng bức ảnh trong tập dữ liệu hoàn toàn độc lập với nhau, việc chia ngẫu nhiên có bảo toàn tỷ lệ lớp (stratified split) là đủ. Nhưng trong thực tế, dữ liệu hiếm khi độc lập hoàn toàn:
- Nhiều lát cắt ảnh chụp từ cùng một bệnh nhân;
- Nhiều khung hình cắt ra từ cùng một video clip;
- Nhiều ảnh chụp cùng một tán cây ở các góc độ khác nhau;
- Nhiều bản ghi âm của cùng một người nói.
Khi đó, bắt buộc phải dùng chia theo nhóm (Group split) dựa trên danh tính nguồn gốc. Nếu ảnh của cùng một bệnh nhân xuất hiện ở cả tập train và tập test, mô hình chỉ đơn giản là đang “nhớ mặt bệnh nhân” thay vì học dấu hiệu bệnh lý. Điểm test cao chót vót trong phòng thí nghiệm sẽ sụp đổ hoàn toàn khi triển khai thực tế.
Augmentation chỉ được áp dụng sau khi phân tách dữ liệu
Một sai lầm kinh điển khác là tăng cường dữ liệu (data augmentation) cho toàn bộ thư mục ảnh trước rồi mới chia train/val/test:
Nếu tăng cường ảnh trước khi chia, hai phiên bản xoay/lật của cùng một bức ảnh gốc có thể rơi vào cả tập train lẫn tập test. Đây là hình thức rò rỉ dữ liệu (data leakage) trực tiếp và nghiêm trọng.
Sử dụng đúng pipeline tiền xử lý đi kèm pretrained checkpoint
Mỗi pretrained checkpoint không chỉ là một file trọng số .pt, mà nó đi kèm một quy ước khắt khe về kích thước ảnh, thuật toán nội suy (interpolation), giá trị trung bình (mean) và độ lệch chuẩn (std) chuẩn hóa kênh màu. Trong torchvision, cách chuẩn nhất là truy xuất trực tiếp phép biến đổi từ chính đối tượng weights:
from torchvision.models import efficientnet_b0, EfficientNet_B0_Weights
# Lấy weights mặc định tốt nhất của kiến trúc
weights = EfficientNet_B0_Weights.DEFAULT
# Trích xuất đúng pipeline tiền xử lý chuẩn mực của checkpoint
preprocess = weights.transforms()
Cảnh báo thực chiến: Nếu bạn chuẩn hóa sai lệch dải màu (ví dụ checkpoint yêu cầu dải nhưng bạn đưa vào , hoặc sai thứ tự RGB/BGR), phân phối đầu vào của mô hình sẽ bị vỡ vụn ngay từ tầng đầu tiên. Khi đó bạn sẽ vội kết luận rằng “pretrained model không hợp với bài toán”, trong khi lỗi thực chất nằm ở khâu chuẩn bị ảnh.
Mốc S0: Huấn luyện từ đầu để biết transfer learning thực sự giúp được bao nhiêu
Không phải dự án thương mại nào cũng bắt bạn phải train from scratch. Nhưng trong nghiên cứu hoặc khi cần chứng minh giá trị của transfer learning, mốc S0 là ngọn hải đăng đối chứng không thể thiếu:
from torchvision.models import efficientnet_b0
# Khởi tạo mô hình ngẫu nhiên hoàn toàn, không nạp weights
model_scratch = efficientnet_b0(weights=None)
Khi huấn luyện trên một tập dữ liệu nhỏ từ đầu, bạn sẽ thấy rõ các đặc trưng tiêu biểu:
- Tốc độ hội tụ rất chậm: Mô hình phải dò dẫm học lại từ các bộ lọc cạnh, góc, vân bề mặt cơ bản;
- Dao động lớn: Đường cong validation thường giật cục và có phương sai rất lớn giữa các lần đổi random seed;
- Overfitting nghiêm trọng: Vì số tham số quá lớn so với số mẫu dữ liệu, mô hình nhanh chóng ghi nhớ tập train;
- Đòi hỏi regularization nặng: Cần áp dụng weight decay cao, dropout và các kỹ thuật augment mạnh mẽ để kéo mô hình không bị quá khớp.
Nếu mốc train from scratch lại cho kết quả vượt trội hơn hẳn một pretrained model, bạn cần dừng lại để kiểm tra: Liệu miền nguồn có bị chuyển giao tiêu cực (negative transfer)? Hay đường ống tiền xử lý đang phá vỡ biểu diễn của pretrained model?
Mốc S1: Đóng băng backbone — kiểm tra chất lượng biểu diễn có sẵn
Đây là mốc thực nghiệm đầu tiên bạn nên chạy khi bắt đầu dự án transfer learning. Nó cực kỳ rẻ về mặt tính toán và có tính giải thích rất cao.
Tải checkpoint và thay thế classifier head
import torch.nn as nn
from torchvision.models import efficientnet_b0, EfficientNet_B0_Weights
num_classes = 5
weights = EfficientNet_B0_Weights.DEFAULT
model = efficientnet_b0(weights=weights)
# Đóng băng toàn bộ các tầng trích xuất đặc trưng (backbone)
for p in model.features.parameters():
p.requires_grad = False
# Thay thế tầng Linear cuối cùng theo số lớp bài toán mới
in_features = model.classifier[-1].in_features
model.classifier[-1] = nn.Linear(in_features, num_classes)
Sau thao tác này, ta chia mô hình thành hai phần riêng biệt:
model.features: Giữ nguyên biểu diễn đã học từ hàng triệu ảnh của ImageNet;model.classifier: Học cách ánh xạ không gian vector đặc trưng này sang 5 lớp mới.

Hình: trích từ Deep Learning Cơ Bản, Chương 11, Hình 11.5.
Kiểm tra số lượng tham số có thể huấn luyện
Đừng bao giờ chỉ nhìn vào tổng số tham số của mô hình. Trong nhật ký thí nghiệm, bạn cần ghi lại chính xác số tham số thực sự nhận đạo hàm:
def count_params(model):
total = sum(p.numel() for p in model.parameters())
trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
return total, trainable
total, trainable = count_params(model)
print(f"Tổng tham số: {total:,} | Tham số huấn luyện: {trainable:,} ({trainable/total:.2%})")
Với EfficientNet-B0, phần classifier chỉ chiếm chưa đầy 1% tổng số tham số. Quá trình huấn luyện sẽ cực kỳ nhanh và nhẹ nhàng về VRAM.
Giá trị thực nghiệm của S1
Nếu chỉ đóng băng thân mạng và huấn luyện một lớp Linear đơn giản mà Macro-F1 đã đạt 90%, điều đó mang lại hai thông điệp vô giá:
- Không gian biểu diễn của mô hình nguồn tương thích rất tốt với bài toán đích;
- Bạn có thể không cần mạo hiểm mở khóa backbone, tiết kiệm được hàng giờ tính toán và giảm thiểu nguy cơ quá khớp.
Đừng vội fine-tune toàn bộ chỉ vì “trong tutorial người ta làm thế”. Một mô hình đơn giản đạt chuẩn luôn được ưu tiên hơn một mô hình cồng kềnh khó kiểm soát.
Bẫy ngầm: requires_grad=False chưa chắc đã đóng băng hoàn toàn BatchNorm
Đây là cái bẫy kỹ thuật khiến vô số kỹ sư đau đầu khi chuyển từ lý thuyết sang code PyTorch thực tế.
Trong PyTorch, câu lệnh param.requires_grad = False chỉ ngăn không cho autograd tính đạo hàm và optimizer không cập nhật trọng số . Tuy nhiên, khi bạn gọi:
model.train()
Tất cả các lớp trong mô hình, bao gồm cả Batch Normalization (BatchNorm), đều bị chuyển sang chế độ huấn luyện. Khi ở chế độ này, BatchNorm liên tục tính toán giá trị trung bình và phương sai của từng mini-batch hiện tại để cập nhật vào hai biến thống kê chạy (running mean và running var).
Nếu tập dữ liệu của bạn nhỏ hoặc batch size bé, thống kê của mini-batch sẽ rất ồn ào. Hậu quả là: dù trọng số không đổi, nhưng running mean và running var của backbone bị phá hỏng hoàn toàn, khiến chất lượng suy luận sụp đổ sau vài epoch.
Để đóng băng một cách tuyệt đối an toàn trong PyTorch:
model.train()
# Ép riêng phần backbone quay về chế độ eval (giữ cố định running stats của BatchNorm)
model.features.eval()
Lúc này, chỉ có model.classifier nhận gradient và chạy ở chế độ train, còn model.features hoạt động như một hàm trích xuất đặc trưng tất định, bảo toàn trọn vẹn tri thức từ ImageNet.
Mốc S2: Tinh chỉnh một phần — mở dần các khối cuối với tốc độ học nhỏ
Khi mốc S1 đã hội tụ nhưng chưa đạt mức hiệu năng kỳ vọng, đó là lúc ta đặt câu hỏi: Liệu các đặc trưng mức cao (các bộ lọc hình khối trừu tượng ở cuối mạng) có cần được uốn nắn lại theo miền dữ liệu mới không?
Ví dụ, mở khóa 2 block cuối cùng của EfficientNet-B0:
# Mở khóa gradient cho 2 block cuối
for p in model.features[-2:].parameters():
p.requires_grad = True
Sau khi mở khóa, bắt buộc phải khởi tạo lại optimizer hoặc cập nhật param_groups để đưa các tham số mới vào danh sách tối ưu, đồng thời áp dụng kỹ thuật phân tầng tốc độ học (Differential Learning Rates):
from torch.optim import AdamW
optimizer = AdamW(
[
{"params": model.features[-2:].parameters(), "lr": 1e-5}, # LR nhỏ cho backbone
{"params": model.classifier.parameters(), "lr": 3e-4}, # LR lớn cho head mới
],
weight_decay=1e-4,
)
Tại sao cần phân tầng tốc độ học?
Classifier head là một tầng hoàn toàn mới, được khởi tạo ngẫu nhiên nên cần một bước nhảy đủ lớn để tìm vùng cực tiểu phù hợp. Ngược lại, các block cuối của backbone đã ở trong một thung lũng nghiệm rất tốt, chúng chỉ cần một bước tinh chỉnh cực nhỏ để thích nghi với các hoa văn đặc thù của miền đích:
Nếu bạn dùng chung một tốc độ học lớn cho toàn bộ mô hình, gradient mạnh từ một classifier chưa ổn định sẽ dội ngược về và phá hủy các đặc trưng tinh xảo đã học trước đó — hiện tượng này gọi là phá vỡ biểu diễn (representation disruption).
Mốc S3: Tinh chỉnh toàn bộ — chỉ khi dữ liệu đủ lớn và biện minh được
Tinh chỉnh toàn bộ mô hình (Full fine-tuning) chỉ nên được cân nhắc khi:
- Tập dữ liệu đích đủ lớn (hàng nghìn mẫu trở lên trên mỗi lớp);
- Khoảng cách miền (domain gap) giữa dữ liệu nguồn và đích quá lớn;
- Mốc S2 bị underfitting rõ rệt;
- Bạn có đủ tài nguyên tính toán và hệ thống triển khai chấp nhận lưu trữ toàn bộ checkpoint độc lập.
# Mở toàn bộ tham số
for p in model.parameters():
p.requires_grad = True
Chiến lược này thường được triển khai theo hai giai đoạn (two-stage training):
- Giai đoạn 1: Đóng băng backbone, chỉ huấn luyện classifier head cho đến khi hàm mất mát ổn định;
- Giai đoạn 2: Mở khóa toàn bộ mô hình và huấn luyện với tốc độ học toàn cục rất thấp (ví dụ đến ), thường kết hợp với scheduler có bước khởi động ấm (warmup) và giảm dần theo cosine.
Thiết kế ma trận thực nghiệm: đừng chỉ nhìn một con số Accuracy
Trong một buổi báo cáo kỹ thuật hay một bài báo nghiên cứu, bảng kết quả không thể chỉ có một cột Accuracy duy nhất. Bạn cần ghi nhận một ma trận đa chiều:
| Chiến lược | Pretrained Checkpoint | Trainable Params | Val Macro-F1 | Test Macro-F1 | Test Acc | Thời gian train | Bộ nhớ VRAM |
|---|---|---|---|---|---|---|---|
| S0 — Scratch | Không | 4.01M (100%) | 0.721 | 0.715 | 73.2% | 45m | 1.8 GB |
| S1 — Frozen | EfficientNet-B0 (DEFAULT) | 6.4K (0.16%) | 0.884 | 0.879 | 89.1% | 8m | 1.2 GB |
| S2 — Partial | EfficientNet-B0 (DEFAULT) | 1.15M (28.7%) | 0.926 | 0.921 | 93.4% | 18m | 2.1 GB |
| S3 — Full | EfficientNet-B0 (DEFAULT) | 4.01M (100%) | 0.918 | 0.912 | 92.5% | 40m | 3.6 GB |
Nhìn vào bảng trên, ta thấy ngay một phát hiện thú vị: S2 (Partial) đạt kết quả tốt nhất, trong khi S3 (Full) bị giảm nhẹ chất lượng và tiêu tốn gấp đôi thời gian cùng bộ nhớ. Điều này xảy ra rất phổ biến khi tập dữ liệu đích nhỏ: việc mở toàn bộ mô hình dẫn đến hiện tượng quá khớp nhẹ.
Cẩn trọng với chênh lệch nhỏ giữa các random seed
Nếu chiến lược A đạt F1 0.912 và chiến lược B đạt 0.915 ở đúng một lần chạy duy nhất, bạn chưa có bất kỳ cơ sở khoa học nào để tuyên bố B tốt hơn A. Sự chênh lệch 0.003 đó hoàn toàn có thể đến từ:
- Cách khởi tạo ngẫu nhiên của classifier head;
- Thứ tự xáo trộn mẫu (batch shuffling);
- Các phép biến đổi augment ngẫu nhiên.
Một thực nghiệm nghiêm túc đòi hỏi:
- Chạy lặp lại ít nhất 3 đến 5 seed ngẫu nhiên độc lập;
- Báo cáo kết quả dưới dạng ;
- Giữ cố định mọi tham số cấu hình khác khi so sánh.
Đọc đồ thị learning curves để chẩn đoán trạng thái mô hình
Đồ thị đường cong học tập (learning curves) phản ánh nhịp tim của quá trình huấn luyện. Hãy học cách đọc các mẫu hình kinh điển để có hành động chính xác:
Mẫu A — Cả Train loss và Val loss đều cao (Underfitting)
- Nguyên nhân: Tốc độ học quá nhỏ, số epoch chưa đủ, backbone bị đóng băng quá sâu trong khi miền đích quá xa lạ, hoặc đường ống tiền xử lý đang làm méo ảnh.
- Hành động: Kiểm tra lại ảnh hiển thị trực quan sau khi đi qua transform; nếu ảnh đúng, tăng nhẹ learning rate hoặc mở thêm block.
Mẫu B — Train loss giảm sâu nhưng Val loss tăng vọt (Overfitting)
- Nguyên nhân: Dữ liệu quá ít nhưng mô hình mở khóa quá nhiều tham số; learning rate của backbone quá lớn; nhãn bị nhiễu.
- Hành động: Thu hẹp phạm vi unfreeze (quay về S1 hoặc giảm số block ở S2); bổ sung weight decay; tăng cường độ biến đổi ảnh; kích hoạt Early Stopping.
Mẫu C — S1 hoạt động rất tốt, nhưng vừa unfreeze sang S2 là Val loss nhảy vọt
- Nguyên nhân: Đây là triệu chứng kinh điển của việc phá hỏng đặc trưng đã học. Thường do đặt quá cao, hoặc quên giữ
features.eval()khiến BatchNorm cập nhật theo batch nhỏ ồn ào. - Hành động: Giảm xuống 10 đến 100 lần so với ; kiểm tra lại trạng thái của BatchNorm.
Mẫu D — Mọi chiến lược từ S0 đến S3 đều thất bại
- Nguyên nhân: Đừng tốn công đổi sang một kiến trúc phức tạp hơn. Vấn đề nằm ở dữ liệu: ánh xạ nhãn sai, ảnh bị hỏng/trùng lặp, hoặc định nghĩa các lớp quá nhập nhằng khiến con người cũng không thể phân biệt được.
- Hành động: Mở thư mục ảnh ra và soi từng mẫu dữ liệu. Kiểm tra ma trận nhầm lẫn để xem mô hình đang bế tắc ở cặp lớp nào.
Thực hành với PyTorch: viết code fine-tuning an toàn
Một đoạn script fine-tuning chuyên nghiệp cần tuân thủ các nguyên tắc thiết kế phòng thủ:
import torch
from torch.optim import AdamW
from torch.optim.lr_scheduler import CosineAnnealingLR
def build_finetune_pipeline(model, num_classes, lr_head=3e-4, lr_backbone=1e-5):
# 1. Khóa toàn bộ backbone
for p in model.features.parameters():
p.requires_grad = False
# 2. Mở khóa 2 block cuối cùng
for p in model.features[-2:].parameters():
p.requires_grad = True
# 3. Lọc danh sách tham số theo từng nhóm riêng biệt
backbone_params = [p for p in model.features[-2:].parameters() if p.requires_grad]
head_params = [p for p in model.classifier.parameters() if p.requires_grad]
# 4. Thiết lập Optimizer với differential learning rates
optimizer = AdamW([
{"params": backbone_params, "lr": lr_backbone},
{"params": head_params, "lr": lr_head},
], weight_decay=1e-4)
scheduler = CosineAnnealingLR(optimizer, T_max=20, eta_min=1e-6)
return optimizer, scheduler
def train_one_epoch(model, loader, criterion, optimizer, device):
model.train()
# Giữ cố định running stats của toàn bộ BatchNorm trong backbone
model.features.eval()
total_loss = 0.0
for images, targets in loader:
images, targets = images.to(device), targets.to(device)
optimizer.zero_grad()
outputs = model(images)
loss = criterion(outputs, targets)
loss.backward()
# Gradient clipping để chống bùng nổ gradient khi mở tầng sâu
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()
total_loss += loss.item() * images.size(0)
return total_loss / len(loader.dataset)
Cây quyết định chọn chiến lược thích nghi
Khi bắt đầu một bài toán học máy mới, bạn có thể tham khảo sơ đồ ra quyết định sau để chọn chiến lược phù hợp nhất:
Từ CNN sang Vision Transformer và LLM: nguyên lý bất biến
Khi bạn chuyển từ các mô hình thị giác cổ điển (ResNet, EfficientNet) sang Vision Transformer (ViT) hay các mô hình ngôn ngữ lớn (LLM), từ vựng chuyên môn có thể thay đổi, nhưng nguyên lý thích nghi tri thức vẫn giữ nguyên bản chất:
Tiếp tục tiền huấn luyện (Continued Pretraining)
Mô hình nền tiếp tục học mục tiêu tự giám sát (như Masked Autoencoding ở ViT hoặc Next Token Prediction ở LLM) trên kho dữ liệu thô chuyên ngành (văn bản y tế, luật, tài chính, ảnh viễn thám). Mục đích là để mô hình làm quen với phân phối từ vựng và tri thức mới mà không làm thay đổi cấu trúc nhiệm vụ.
Tinh chỉnh có giám sát (Supervised Fine-Tuning - SFT)
Mô hình học từ các cặp dữ liệu có nhãn chỉ dẫn–phản hồi (Instruction–Response). Đây chính là phiên bản mở rộng của việc thay thế classifier head và huấn luyện có giám sát trong thị giác máy tính.
LoRA và PEFT (Parameter-Efficient Fine-Tuning)
Thay vì cập nhật toàn bộ ma trận trọng số , LoRA đóng băng và học một ma trận bù cập nhật hạng thấp:
với và hạng . Điều này tương đồng với tinh thần của S1 và S2: cố định phần lớn tri thức cốt lõi và chỉ cập nhật một phần cực nhỏ tham số để thích nghi tác vụ mới.
Giới hạn của Fine-tuning: không dùng để nhồi nhét dữ liệu biến động
Fine-tuning xuất sắc trong việc uốn nắn phong cách, định dạng đầu ra, và tinh chỉnh ranh giới quyết định. Tuy nhiên, nếu bạn muốn mô hình cập nhật tỷ giá ngoại tệ hay giá cổ phiếu thay đổi từng giờ, fine-tuning là lựa chọn sai lầm. Bạn cần cơ chế Truy xuất tăng cường (RAG - Retrieval-Augmented Generation) để truy vấn dữ liệu sống tại thời điểm suy luận.
Những lỗi kinh điển trong bài tập và dự án thực tế
- Chỉ báo cáo duy nhất một mô hình tốt nhất: Không cung cấp baseline đối chứng, khiến người đọc không thể biết sự tiến bộ đến từ pretrained weights, từ phép tăng cường dữ liệu hay từ may mắn ngẫu nhiên;
- Dùng tập test để chọn epoch tốt nhất: Đây là lỗi rò rỉ thông tin nghiêm trọng. Quyết định lưu checkpoint và dừng sớm (Early Stopping) chỉ được phép dựa trên tập Validation;
- Đóng băng trọng số nhưng để BatchNorm tự do: Khiến các thông số thống kê chạy bị phá hủy âm thầm;
- Mở toàn bộ mô hình ngay từ epoch đầu tiên: Dẫn đến gradient bùng nổ từ head ngẫu nhiên làm hỏng các tầng biểu diễn sâu;
- Dùng augmentation khác nhau giữa các chiến lược so sánh: Khiến mọi kết luận so sánh giữa S1 và S2 bị sai lệch bởi một biến số ngoại lai;
- Chỉ nhìn vào một thước đo duy nhất là Accuracy: Trên các tập dữ liệu mất cân bằng lớp (ví dụ lớp bệnh chỉ chiếm 2%), một mô hình dự đoán toàn bộ là “khỏe mạnh” vẫn đạt Accuracy 98% nhưng giá trị sử dụng bằng 0. Hãy dùng Macro-F1 và Confusion Matrix.
Mẫu bảng báo cáo kết quả thực nghiệm chuẩn mực
Khi viết báo cáo hoặc công bố khoa học, một bảng tổng hợp kết quả đạt chuẩn kỹ thuật cần bao gồm đầy đủ các thông tin sau:
| Mã TN | Chiến lược | Backbone | Trainable Params | Val Macro-F1 | Test Macro-F1 | Test Accuracy | Ghi chú & Cấu hình |
|---|---|---|---|---|---|---|---|
| E0 | S0 (Scratch) | EfficientNet-B0 | 4.01M (100%) | Baseline đối chứng, 5 seeds | |||
| E1 | S1 (Linear probe) | EfficientNet-B0 | 6.4K (0.16%) | Freeze features, BN eval | |||
| E2 | S2 (Partial) | EfficientNet-B0 | 1.15M (28.7%) | Unfreeze last 2 blocks, diff LR | |||
| E3 | S3 (Full) | EfficientNet-B0 | 4.01M (100%) | 2-stage training, LR toàn cục 1e-5 |
Kèm theo các bằng chứng trực quan bắt buộc:
- Đồ thị đường cong học tập (Loss và Macro-F1 theo epoch);
- Ma trận nhầm lẫn (Confusion Matrix) chi tiết trên tập test;
- Phân tích trực quan 5-10 trường hợp mô hình phân loại sai nặng nhất để tìm nguyên nhân từ dữ liệu.
Checklist kỹ sư trước khi chốt checkpoint sản phẩm
- Phép chia dữ liệu (Train/Val/Test) đã được thực hiện trước khi áp dụng bất kỳ phép tăng cường nào.
- Đã kiểm tra không có rò rỉ dữ liệu theo nhóm đối tượng (bệnh nhân, video, cây trồng).
- Phép tiền xử lý (Resize, Normalization mean/std) khớp chính xác với tài liệu của pretrained checkpoint.
- Có ít nhất một baseline đối chứng (S0 hoặc S1) để chứng minh giá trị của chiến lược nâng cao.
- Khi đóng băng backbone, đã xử lý BatchNorm có chủ đích (
features.eval()). - Classifier head mới đã được huấn luyện sơ bộ trước khi mở khóa các tầng sâu của backbone.
- Đã sử dụng tốc độ học phân tầng () khi thực hiện tinh chỉnh một phần.
- Checkpoint tốt nhất được lựa chọn hoàn toàn dựa trên validation metric, không dùng test metric.
- Tập test chỉ được nạp đúng một lần duy nhất để ghi nhận số liệu báo cáo cuối cùng.
- Đã ghi nhận đầy đủ chi phí: số tham số có thể huấn luyện, thời gian chạy và dung lượng VRAM tiêu tốn.
Nếu chỉ nhớ 10 ý
- Fine-tuning là một quy trình khoa học: Không bắt đầu bằng
model.train(), mà bắt đầu từ việc khóa phân tách dữ liệu và thiết lập baseline đối chứng. - Cách ly dữ liệu nghiêm ngặt: Chia dữ liệu theo nguồn gốc độc lập (Group split) và chỉ áp dụng data augmentation sau khi đã chia train/val/test.
- Tiền xử lý là một phần của mô hình: Sai lệch kích thước, mean hoặc std chuẩn hóa sẽ phá hủy hoàn toàn biểu diễn của pretrained checkpoint.
- Mốc S0 (Scratch) định giá transfer learning: Huấn luyện từ đầu giúp bạn biết chính xác mình đã tiết kiệm được bao nhiêu dữ liệu và thời gian nhờ tri thức sẵn có.
- Mốc S1 (Frozen) là phép thử biểu diễn rẻ nhất: Nếu đóng băng backbone và chỉ học một lớp Linear đã đạt kết quả xuất sắc, bạn có thể không cần mạo hiểm tinh chỉnh sâu.
- Coi chừng cái bẫy BatchNorm:
requires_grad=Falsekhông ngăn BatchNorm cập nhật running statistics khi gọimodel.train(). Phải épfeatures.eval(). - Phân tầng tốc độ học là bắt buộc khi unfreeze: Luôn giữ nhỏ hơn từ 10 đến 100 lần để tránh phá vỡ các đặc trưng tinh xảo.
- Mở toàn bộ (Full fine-tuning) không phải lúc nào cũng tốt nhất: Với tập dữ liệu nhỏ, tinh chỉnh một phần (Partial) thường cho kết quả tổng quát hóa cao hơn do ít bị quá khớp.
- Đừng bao giờ tin vào một run duy nhất: Khi chênh lệch giữa hai phương pháp dưới 1%, hãy chạy kiểm tra trên 3-5 random seed và so sánh .
- Độ đo phải phản ánh đúng thực tế: Trên dữ liệu mất cân bằng lớp, Macro-F1 và Confusion Matrix luôn đáng tin cậy hơn Accuracy đơn thuần.
Câu hỏi tự kiểm tra
- Phản biện kết quả: Trong một bài báo cáo, một bạn sinh viên tuyên bố: “Mô hình full fine-tuning đạt độ chính xác 91.5%, vượt trội so với partial fine-tuning đạt 91.2% ở lần chạy đầu tiên.” Hãy nêu 3 lý do tại sao kết luận này chưa đủ sức thuyết phục về mặt phương pháp luận thực nghiệm.
- Chẩn đoán sự cố BatchNorm: Tại sao trong PyTorch, việc chỉ đặt
param.requires_grad = Falsecho backbone vẫn có thể khiến chất lượng dự đoán của mô hình bị suy giảm nghiêm trọng khi huấn luyện với batch size nhỏ? Cách khắc phục triệt để bằng code là gì? - Cơ chế phân tầng tốc độ học: Giải thích về mặt toán học và trực giác hình học tại sao cần phải nhỏ hơn rất nhiều so với trong chiến lược S2?
- Phân tích rò rỉ dữ liệu: Một nhóm kỹ sư thực hiện data augmentation (xoay, lật, đổi màu) cho 1,000 bức ảnh chụp da liễu để thành 10,000 ảnh, sau đó dùng lệnh
train_test_splitcủa scikit-learn để chia 80/20. Mô hình đạt test accuracy 99.5% nhưng thất bại hoàn toàn khi thử nghiệm tại bệnh viện. Hãy chỉ ra chính xác hai lỗi rò rỉ dữ liệu nghiêm trọng trong quy trình này. - Thiết kế thí nghiệm bóc tách (Ablation Study): Hãy thiết kế một bảng kế hoạch thực nghiệm nhằm chứng minh độc lập tác động của 3 yếu tố: (a) Pretrained weights, (b) Data augmentation, và (c) Số lượng block được mở khóa. Quy tắc kiểm soát biến số ở đây là gì?
- Ứng dụng sang LLM: Giả sử bạn có một mô hình Llama-3 8B và muốn xây dựng một trợ lý hỗ trợ hỏi đáp chính sách nội bộ của công ty. Bạn sẽ dùng RAG, Continued Pretraining, Supervised Fine-Tuning hay LoRA? Hãy biện minh cho sự lựa chọn của mình.
Kết thúc Chương 12: Đòn bẩy tri thức và bước đệm sang kỷ nguyên Foundation Model
Chương 12 đã khép lại bức tranh toàn cảnh về cách thức khai thác tri thức tích lũy trong kỷ nguyên học sâu:
- Bài 16 đã xây dựng nền móng tư duy về tự giám sát (self-supervised learning), ranh giới chuyển giao (transferability) và bản đồ các kỹ thuật thích nghi từ đóng băng tuyến tính đến LoRA/PEFT;
- Bài 17 đã biến các nguyên lý đó thành kỷ luật thực nghiệm thực chiến: từ việc bảo vệ dữ liệu chống rò rỉ, thiết lập các mốc đối chứng khoa học S0–S3, cho đến cách phòng tránh các bẫy ngầm trong mã nguồn PyTorch.
Khi nắm vững phương pháp luận của Chương 12, bạn không còn nhìn các mô hình khổng lồ như những chiếc “hộp đen” huyền bí. Bạn đã sở hữu chiếc chìa khóa vạn năng để tiếp cận bất kỳ mô hình nền tảng nào: hiểu rõ nó được tôi luyện từ đâu, đánh giá được biểu diễn của nó mạnh yếu ở điểm nào, và biết cách biến nó thành công cụ đắc lực giải quyết bài toán đặc thù của riêng mình với chi phí tối thiểu.
Đó cũng chính là hành trang quan trọng nhất để chúng ta tự tin bước sang các chương tiếp theo: nơi các mô hình ngôn ngữ lớn (LLM) và kiến trúc đa phương thức (Multimodal Foundation Models) tái định hình toàn bộ ranh giới của trí tuệ nhân tạo.
Tài liệu tham khảo và đọc thêm
- François Chollet (2020), Transfer learning & fine-tuning, Keras Developer Guides: https://keras.io/guides/transfer_learning/
- Hugging Face PEFT Team, Parameter-Efficient Fine-Tuning (PEFT) Methods Overview: https://huggingface.co/docs/peft/main/methods/overview
- Tim Dettmers, Artidoro Pagnoni, Ari Holtzman, Luke Zettlemoyer (2023), QLoRA: Efficient Finetuning of Quantized LLMs, arXiv: https://arxiv.org/abs/2305.14314
- Edward J. Hu et al. (2021), LoRA: Low-Rank Adaptation of Large Language Models, arXiv: https://arxiv.org/abs/2106.09685
- PyTorch Documentation, Torchvision Models and Pre-trained Weights: https://pytorch.org/vision/stable/models.html
- Jason Yosinski, Jeff Clune, Yoshua Bengio, Hod Lipson (2014), How transferable are features in deep neural networks?, NeurIPS 2014: https://arxiv.org/abs/1411.1792