Tóm tắt trong 60 giây
Bạn có hai bộ não nhân tạo cùng làm một việc: nhìn ảnh khuôn mặt rồi trả lời câu hỏi — “người này là ai?”, “có đeo kính không?”, “hai ảnh này cùng một người chứ?”.
- Bộ não ConvMAE-Base — bản gốc, đầy đủ.
- Bộ não Ghost+ConvMAE — bản đã được làm gọn bằng kỹ thuật Ghost.
Câu hỏi nghiên cứu: làm gọn thì tiết kiệm được bao nhiêu, và trả giá bằng bao nhiêu độ chính xác?
Demo là cái cân để trả lời câu đó cho người xem thấy tận mắt: bấm một nút, cùng một tấm ảnh chạy qua cả hai bộ não, hiện cạnh nhau dự đoán, thời gian chạy và bộ nhớ GPU. Có 5 bài kiểm tra (5 tab) và 3 chế độ chạy.
“Đây là công cụ đối chứng trực quan cho phần triển khai của đề tài: cùng một ảnh, cùng một máy, cùng một cách đo, hai kiến trúc chạy cạnh nhau — để người xem tự thấy đánh đổi giữa độ chính xác và chi phí, thay vì chỉ đọc bảng số trong báo cáo.”
Giải thích từ số 0
Máy “nhìn” ảnh như thế nào
Một tấm ảnh với máy tính chỉ là một bảng số. Ảnh màu 224×224 là 224 × 224 × 3 = 150.528 con số, mỗi số nói điểm ảnh đó sáng, đỏ, xanh bao nhiêu.
Máy không “thấy” khuôn mặt. Nó chỉ nhân và cộng trên bảng số đó rất nhiều lần, cho tới khi bảng số khổng lồ biến thành một danh sách ngắn — ví dụ 768 con số. Danh sách ngắn đó gọi là đặc trưng hay embedding.
Hãy tưởng tượng bạn mô tả một người bạn cho người khác. Bạn không đọc từng sợi tóc; bạn nói “cao, tóc xoăn, mắt to, có nốt ruồi ở cằm”. Bốn đặc điểm đó chính là embedding mà não bạn rút ra. Máy làm y hệt, chỉ khác là 768 đặc điểm và không đặt tên được cho chúng.
Backbone và head
| Phần | Tên gọi | Việc của nó | Ví dụ |
|---|---|---|---|
| Phần thân | backbone | biến 150.528 số thành 768 số đặc trưng | “cao, tóc xoăn, mắt to…” |
| Phần đầu | head | từ 768 số đó trả lời câu hỏi cụ thể | “→ đây là người số 42” |
Ghi chú Backbone là phần đắt tiền và dùng chung; head thì rẻ và thay được. Cùng một backbone lắp head khác nhau cho 5 bài toán khác nhau — đúng như 5 tab của demo.
Tại sao phải làm model nhẹ
Model càng lớn càng chính xác, nhưng chạy chậm (camera an ninh cần trả lời trong vài chục mili-giây), ngốn bộ nhớ (card laptop chỉ 4 GB, không phải 48 GB như máy chủ) và tốn điện. Nên có cả một ngành nghiên cứu tên inference-efficient: giữ độ chính xác gần như cũ nhưng chạy rẻ hơn. Đề tài nằm ở đó.
Ghost là gì
Trong mạng nơ-ron có một hiện tượng: nhiều bản đồ đặc trưng sinh ra rất giống nhau, gần như bản sao mờ của nhau. Sinh 100 bản đồ mà 50 cái na ná nhau thì phí một nửa công.
- Làm thật một nửa số bản đồ bằng phép tính đắt tiền (
primary_conv, conv 1×1). - Nửa còn lại không tính lại từ đầu — lấy nửa đã có rồi biến đổi bằng một phép rất rẻ (
cheap_conv, conv 3×3 depthwise). - Ghép hai nửa lại → vẫn đủ số bản đồ, chi phí giảm mạnh.
Nửa “làm rẻ” đó chính là các bóng ma — tên gọi đến từ đây. Bản GhostNetV2 dùng trong đề tài thêm DFC attention (hai conv dải dài 1×9 và 9×1 để mỗi điểm ảnh nhìn xa theo hàng và cột) và SE (bộ chấm điểm “kênh nào đang quan trọng” rồi nhân trọng số lên).
Hai mô hình khác nhau đúng ở đâu
Khung ConvViT ba tầng
Cả hai dùng chung một khung tên ConvViT. Ảnh càng đi sâu càng nhỏ lại nhưng càng dày thông tin:
| Tầng | Bản đồ | Số vị trí | Số chiều | Số khối | Loại khối |
|---|---|---|---|---|---|
| Stage 1 | 56 × 56 | 3.136 | 256 | 2 | convolution |
| Stage 2 | 28 × 28 | 784 | 384 | 2 | convolution |
| Stage 3 | 14 × 14 | 196 | 768 | 11 | Transformer |
Ý tưởng: tầng đầu dùng convolution — rẻ, giỏi bắt chi tiết cục bộ như cạnh và nếp nhăn; tầng cuối dùng Transformer — đắt, giỏi liên kết toàn ảnh (“mắt trái và cằm cùng thuộc một khuôn mặt”).
Ghost thay đúng cái gì
Ghi chú Ghost chỉ thay 4 khối ở Stage 1 và Stage 2 — đổi CBlock thành GhostV2BlockMasked. Stage 3 giữ nguyên hoàn toàn.
| Thành phần | CBlock (baseline) | GhostV2Block (ghost) |
|---|---|---|
| Nhánh chính | conv 1×1 (dim → dim) | conv 1×1 (dim → dim/2) |
| Nhánh rẻ | — | conv 3×3 depthwise trên dim/2 |
| Trộn không gian | conv 5×5 depthwise | DFC: conv 1×9 + 9×1 depthwise |
| Cổng chú ý | — | sigmoid(DFC) nhân vào |
| Chấm điểm kênh | — | SE (giảm 4 lần rồi phục hồi) |
| MLP | có (nở 4 lần rồi co lại) | không có |
Ước lượng tham số một khối ở số chiều d: CBlock ≈ d²+d²+8d² ≈ 10d², GhostBlock ≈ 0,5d²+d²+0,5d² ≈ 2d². Khối ghost rẻ khoảng 5 lần; mất nhiều nhất là MLP bị bỏ hẳn.
Con số thật — và nghịch lý phải giải thích được
| Tab | Baseline | Ghost | Chênh |
|---|---|---|---|
| CelebA | 84,6 M / 339 MB | 81,2 M / 325 MB | −3,4 M (−4,0 %) |
| SCface | 84,7 M | 81,3 M | −3,4 M |
| LFW | 85,9 M | 82,5 M | −3,4 M |
| ImageNet | 89,7 M | 86,3 M | −3,4 M |
| CASIA | 92,7 M | 89,3 M | −3,4 M |
Chênh lệch giữa các tab là do head khác nhau — CASIA có 10.571 lớp nên head to nhất.
“Ghost chỉ giảm 4% tham số mà mất tới 14 điểm phần trăm trên SCface — vậy đóng góp nằm ở đâu?”
Phép tính khớp chính xác với kiến trúc:
- Stage 1: 2 khối × 10 × 256² ≈ 1,3 M → ghost ≈ 0,26 M
- Stage 2: 2 khối × 10 × 384² ≈ 3,0 M → ghost ≈ 0,59 M
- Tiết kiệm ≈ 3,4 M — đúng bằng số đo được
- Stage 3: 11 khối × ~12 × 768² ≈ 78 M — Ghost không đụng tới
Nghĩa là Ghost chỉ tác động lên ~5% khối lượng tham số. Nhưng — mấu chốt — tham số không phải thứ tốn kém ở Stage 1 và 2:
“Stage 1 xử lý 3.136 vị trí, Stage 3 chỉ 196 — gấp 16 lần. Chi phí ở tầng đầu đến từ số vị trí phải xử lý, không phải số trọng số. Ghost tiết kiệm 4% tham số nhưng 21% bộ nhớ, vì nó cắt đúng ở hai tầng có độ phân giải cao.”
| Chỉ số | Baseline | Ghost | Chênh |
|---|---|---|---|
| VRAM (FP16) | 771 MB | 609 MB | −21 % |
| VRAM (FP32) | 1.518 MB | 1.196 MB | −21 % |
Nghịch lý thứ hai: nhanh hơn hay chậm hơn?
Ghi chú Hai nguồn số nói ngược nhau, phải biết trước:
| Nguồn | Chế độ | Baseline | Ghost | Kết luận |
|---|---|---|---|---|
| Báo cáo · A5000 · FP16 · batch 32 | thông lượng | 738 ảnh/s | 612 ảnh/s | Ghost chậm hơn 17 % |
| Demo · GTX 1650 · FP16 · batch 1 | độ trễ | 23,0 ms | 20,9 ms | Ghost nhanh hơn ~9 % |
Ghost thay các phép nhân ma trận dày — thứ GPU lớn chạy cực nhanh nhờ tensor core — bằng nhiều phép depthwise mỏng cộng SE và sigmoid. Depthwise có cường độ số học thấp: đọc/ghi bộ nhớ nhiều mà tính toán ít.
- Trên A5000 batch 32: GPU thừa sức tính, nút cổ chai là băng thông bộ nhớ → Ghost thua.
- Trên GTX 1650 batch 1: GPU yếu, không có gì để song song hoá, nút cổ chai là số phép tính → Ghost thắng nhẹ.
“Đóng góp chính của Ghost là bộ nhớ, không phải tốc độ. Tốc độ phụ thuộc chế độ chạy: có lợi ở batch nhỏ trên phần cứng yếu, bất lợi ở batch lớn trên phần cứng mạnh vì depthwise bị giới hạn băng thông. Cả hai đều là số thật, chỉ khác chế độ.”
Ba kiểu huấn luyện xuất hiện trong demo
Pre-train bằng MAE — “học bằng cách chơi trò che ảnh”
MAE = Masked AutoEncoder. Không cần nhãn. Lấy một ảnh bất kỳ, che ngẫu nhiên 75% diện tích, bắt mô hình vẽ lại phần bị che, vẽ sai thì sửa — lặp lại hàng triệu lần (ở đây 300 epoch).
Muốn vẽ lại được con mắt bị che, mô hình buộc phải “hiểu” mặt người trông ra sao. Đó là cách nó học đặc trưng mà không cần ai gán nhãn.
Trong GhostV2BlockMasked có tham số mask và ghi chú bias=False — chính là để phục vụ giai đoạn này: bias khác 0 sẽ làm rò rỉ thông tin từ vùng bị che sang vùng không che, khiến mô hình gian lận. Lúc chạy demo thì mask=None nên nhánh này không dùng tới.
Fine-tune — “học nghề cụ thể”
Lấy backbone đã pre-train, gắn head mới, rồi huấn luyện lại toàn bộ trên dữ liệu một bài toán cụ thể. Đây là cách cho kết quả tốt nhất, và là cách 4 tab khuôn mặt được tạo ra.
Số epoch — đặt theo dataset, không theo model
Số epoch tỉ lệ nghịch với kích thước tập: tập lớn như CASIA hội tụ ở 40 epoch, tập nhỏ như ARFace/SCface cần ~100 epoch. Mục tiêu là giữ tổng số bước tối ưu tương đương giữa các tập — tránh vừa quá khớp ở tập lớn vừa thiếu học ở tập nhỏ.
Ghi chú Epoch là tham số theo dataset chứ không theo model. Trên cùng một tập, cả bốn kiến trúc dùng đúng cùng số epoch và cùng một công thức — batch 1.024, blr 5e-4, layer-decay 0,65, wd 0,05, drop-path 0,1, warmup 5, cosine. Nhờ vậy chênh lệch độ chính xác giữa các arm phản ánh đúng thay đổi kiến trúc, không phải do lịch huấn luyện.
Ngoài ra fine-tune ở đây là phần trình diễn khả năng chuyển giao downstream — thước đo phân biệt chính vẫn là linear probing với backbone đóng băng — nên ngân sách epoch được chọn để hội tụ tiết kiệm, không nhằm đạt SOTA.
| Dataset | Epoch cấu hình | Best | Đạt best | Chững lại (≥99 % best) | % lịch |
|---|---|---|---|---|---|
| CASIA · Top-1 | 40 | 91,32 | ep ~38 | ep ~29 | 73 % |
| CelebA · mAP | 30 | 0,79 | ep ~23 | ep ~20 | ~67 % |
| LFW · AUC | 50 | 0,99 | ep ~45 | ep ~17 | 34 % |
| SCface · Top-1 (base) | 100 | 45,13 | ep ~69 | ep ~69 | ~70 % |
| SCface · Top-1 (ghost) | 100 | ~31,7 | ep ~54 | ep ~54 | ~55 % |
Đọc theo hình dạng đường học:
- CelebA — chững từ ep ~20 (~67 % lịch). Đạt ≥99 % mAP ở epoch 20, best ở epoch 23; phần còn lại chủ yếu là tinh chỉnh cân bằng biên.
- CASIA — chững ~ep 29/40 (73 % lịch). Sau epoch 29 mức cải thiện <1 %, best rơi vào ~ep 38 → lịch 40 là hợp lý, đuôi cuối là refinement biên.
- LFW — đạt ≥99 % AUC rất sớm (~ep 17) rồi có đuôi dài. AUC đã ~0,99 từ epoch 17, sau đó bò chậm tới best ~ep 45 để vắt nốt phần lẻ.
- SCface — không chững sớm, dùng gần trọn ngân sách. Đây là ca khó (ảnh độ phân giải thấp, xuyên độ phân giải): cải thiện đều tới tận best ~ep 69 (base) — chính xác lý do cấp 100 epoch cho tập nhỏ và khó này; cắt ngắn là mất điểm.
Linear probe — “bài kiểm tra công tâm”
Đóng băng toàn bộ backbone, chỉ huấn luyện một lớp tuyến tính phía trên: BatchNorm1d(768, affine=False) → Linear(768, 1000), 90 epoch.
Fine-tune có thể “chữa cháy” cho một backbone kém, vì mọi trọng số đều được sửa. Linear probe thì không — nó đo chất lượng nguyên bản của đặc trưng. Nếu đặc trưng tách lớp tốt, một đường thẳng là đủ; nếu không, không có gì cứu được.
Đây là thước đo trung thực nhất để so hai backbone, và cũng là chỗ Ghost thua rõ nhất: 64,06 % so với 58,60 %.
Năm tab, mỗi tab kiểm tra điều gì
| Tab | Kiểu bài | Dữ liệu | Thước đo | Base | Ghost |
|---|---|---|---|---|---|
| CelebA | đa nhãn thuộc tính | 39.829 ảnh test | mAP | 0,789 | 0,778 |
| CASIA | định danh tập đóng | 10.571 người | Top-1 | 91,49 % | 91,32 % |
| SCface | xuyên độ phân giải | 130 người | Top-1 | 45,13 % | 31,36 % |
| LFW | xác minh cặp | 5.749 người | ROC-AUC | 0,9921 | 0,9833 |
| ImageNet | linear probe | 50.000 ảnh val | Top-1 | 64,06 % | 58,60 % |
CelebA — nhận thuộc tính khuôn mặt
Mỗi ảnh, mô hình chấm 40 thuộc tính cùng lúc: “Đang cười”, “Đeo kính”, “Nam”, “Tóc vàng”… Đây là đa nhãn — một mặt có thể đúng nhiều thuộc tính cùng lúc.
Giao diện hiện mọi thuộc tính được chấm trên 50%, không phải cố định top-5, vì số thuộc tính đúng thay đổi theo từng mặt. Nếu ảnh thuộc CelebA thì có sẵn đáp án nên tab chấm điểm luôn: tỉ lệ đúng trên 40 câu, cộng F1 trên các nhãn dương.
Ghi chú Vì sao phải có F1? Vì phần lớn thuộc tính là “không” với gần như mọi khuôn mặt (ví dụ “Đội mũ”). Một mô hình lười trả lời “không” cho tất cả vẫn đạt accuracy rất cao. F1 trên nhãn dương mới lộ ra nó có thật sự nhận được gì không.
CASIA — “người này là ai” trong 10.571 người
Tập đóng: người trong ảnh chắc chắn nằm trong danh sách mà mô hình đã học.
CASIA đóng gói dạng RecordIO kèm file train.lst. Bản đầu tiên của script giải nén ghép bản ghi thứ N với dòng thứ N. Sai, vì train.idx có 501.196 bản ghi còn train.lst có 494.149 dòng, thứ tự khác nhau.
Hai bên khớp ở đầu rồi lệch dần: đến khoá 5.000 header nói nhãn 28 còn dòng 5.000 nói 27; đến khoá 341.263 là 6.332 so với 6.285. Hậu quả: mọi ảnh bị xếp nhầm sang người bên cạnh, Top-1 rơi từ ~91% xuống 1%.
Sửa: danh tính lấy từ nhãn trong IRHeader của chính bản ghi — nguồn duy nhất đáng tin.
Còn một chi tiết tinh vi: nhãn không liên tục. Nhãn 09282 chỉ có 2 ảnh, cả hai rơi vào tập val nên thư mục train/09282 không tồn tại. Vì thế lớp thứ i ứng với nhãn i khi i < 9282 và i+1 từ 9282 trở đi. Thứ tự lớp lưu sẵn trong assets/casia_classes.json, không tái tạo bằng cách sắp xếp tên thư mục.
SCface — bài khó nhất, và cái bẫy ba nhóm ảnh
Kịch bản thực tế: cảnh sát có ảnh thẻ chụp studio, cần nhận ra người đó trên camera an ninh mờ, xa, góc xấu. Mô hình chỉ được học ảnh thẻ nhưng bị kiểm tra trên ảnh camera. Đây là bài khó nhất, và là chỗ Ghost thua đậm nhất: 45,13 % → 31,36 %.
Ghi chú Thư mục surveillance_cameras_all/ trông như một tập, nhưng thực ra là ba loại ảnh hoàn toàn khác nhau:
| Nhóm | Là gì | Số ảnh |
|---|---|---|
cam1–cam5 × 3 khoảng cách | camera an ninh ánh sáng thường — đúng thiết lập của báo cáo | 1.950 |
cam6, cam7 | camera hồng ngoại (ban đêm) | 780 |
cam8 | ảnh thẻ chụp bằng hồng ngoại, dù nằm trong thư mục “surveillance” | 130 |
Mô hình chưa bao giờ học ảnh hồng ngoại. Gộp cả ba nhóm để chấm điểm thì con số tụt xuống thấp hơn báo cáo và trông như một lỗi thoái lui — trong khi không phải. Demo xử lý bằng cách dán nhãn ngay trên danh sách chọn ảnh: mục nào hồng ngoại thì ghi rõ IR / IR mugshot; mục không ghi gì là ảnh thường.
Chi tiết nhỏ nhưng từng gây lỗi: file cam8 đặt tên 001_cam8.jpg, không có phần khoảng cách, nên đoạn kiểm tra len(parts) >= 3 đã âm thầm bỏ sót toàn bộ 130 ảnh đó.
LFW — “hai ảnh này có phải cùng một người không”
Khác hẳn ba tab trên: mô hình không đoán tên. Nó biến mỗi khuôn mặt thành một vector 768 chiều rồi đo độ tương đồng cosine — gần +1 là cùng người, gần 0 hoặc âm là khác người.
Ghi chú Đây là tập mở. Không một người nào trong LFW là lớp mà mô hình đã học. Nghĩa là hệ thống xác minh được người chưa từng gặp — đúng như mở khoá điện thoại bằng khuôn mặt: máy không cần học bạn là ai trong 10.000 người, chỉ cần so bạn với ảnh đã đăng ký.
Ban đầu code dùng ngưỡng mặc định 0,5. Khả năng tách hai lớp rất tốt — ROC-AUC 0,9925 / 0,9809 — nhưng cặp đúng vẫn bị đọc thành “khác người”: tỉ lệ bắt đúng chỉ 49,5 % / 37,5 %. Hạ xuống 0,235 cho ~95 % / ~92 % độ chính xác trên cùng mẫu.
Bài học: AUC đo khả năng xếp hạng, không đo điểm cắt. Một hệ thống AUC 0,99 vẫn có thể trả lời sai gần hết nếu chọn sai ngưỡng.
ImageNet linear probe
- Ảnh val nằm trong thư mục tên wnid (ví dụ
n01440764), và thứ tự sắp xếp của các wnid chính là chỉ số lớp mô hình đã học. Nên tên thư mục = đáp án, tab tự chấm điểm được. - Vì thế script tải bản val-only ~6,7 GB thay vì bản thi đấu ~155 GB — bản kia còn giải nén val ra dạng phẳng, mất luôn nhãn.
- Head tab này là
Sequential(BatchNorm1d, Linear)nên không cómodel.head.weight; code nhận diện bằng khoáhead.1.*và dựng lại head trước khi nạp.
Hai chế độ chạy: PyTorch và TensorRT
PyTorch chạy mô hình theo cách tổng quát và linh hoạt: mỗi phép toán là một lệnh riêng, gọi lần lượt. Ví như nấu ăn theo công thức, đọc từng dòng rồi làm từng bước.
TensorRT là công cụ của NVIDIA. Nó nhận mô hình rồi biên dịch trước thành một “engine” tối ưu cho đúng con GPU đó: gộp phép toán thành một kernel, đo thử để chọn thuật toán nhanh nhất cho từng lớp, hạ độ chính xác xuống FP16 nếu cho phép. Ví như thuê đầu bếp học thuộc món đó, tối ưu từng động tác cho đúng căn bếp của bạn.
FP32 và FP16
FP32 — số thực 32 bit, chính xác cao, tốn bộ nhớ và băng thông. FP16 — 16 bit, một nửa bộ nhớ, GPU hiện đại có phần cứng riêng chạy rất nhanh, đổi lại kém chính xác hơn một chút.
“Giảm xuống FP16 có làm sai kết quả không?” — Có kiểm chứng. Bộ kiểm thử so từng dự đoán của engine với PyTorch trên cả 30 tổ hợp (5 bài × 2 mô hình × 3 backend): Top-1 khớp 100 %; chênh lệch xác suất tối đa 0,004 ở FP16 và 0,0000 ở FP32. FP16 làm xác suất lệch ở chữ số thập phân thứ ba, không đổi câu trả lời.
Engine không mang đi được
Ghi chú Một engine bị khoá chặt vào kiến trúc GPU + phiên bản TensorRT đã tạo ra nó. Chuyện thật đã gặp ngay trên cùng một máy: engines/ dựng bằng môi trường ảo ./.venv (TensorRT 10.16), nhưng Python hệ thống lại có TensorRT 11.2 — chạy app bằng Python hệ thống thì toàn bộ 20 engine hỏng cùng lúc:
Serialization assertion stdVersionRead == kSERIALIZATION_VERSION failed. Current Version: 243, Serialized Engine Version: 240
Và một cái bẫy nữa: hàm deserialize_cuda_engine không báo lỗi khi lệch phiên bản — nó ghi log rồi trả về None, khiến dòng sau chết với thông báo vô nghĩa. Code hiện đã kiểm tra None, báo đúng nguyên nhân, và in phiên bản interpreter/torch/tensorrt ngay lúc khởi động.
Demo đo thời gian và bộ nhớ như thế nào
Đây là phần kỹ thuật đáng tự hào nhất, và cũng là nơi hội đồng dễ vặn “sao tin được số đó”.
Warm-up — vì sao phải chạy nháp trước khi đo
Lần chạy đầu tiên luôn chậm bất thường: cuDNN phải thử vài thuật toán rồi chọn cái nhanh nhất, và bộ cấp phát bộ nhớ GPU phải xin thêm vùng nhớ mới.
Code chạy nháp _WARMUP = 5 lần. Con số 5 cũng là kết quả đo: với 2 lần, lần bấm đầu vẫn cao hơn mức ổn định +16 %; với 5 lần chỉ còn +2 %; 10 lần không tốt thêm.
Trung vị thay vì một lần đo
Một lần chạy batch-1 trên GPU laptop dao động 38–77 ms tuỳ tải nền — biên độ đó nuốt chửng khoảng cách ~10 % giữa hai mô hình. Nên bảng hiện trung vị của 7 lần chạy thật gần nhất, và không chạy thêm lượt nào ngoài lượt đã sinh ra dự đoán bạn đang xem.
Đo bằng CUDA event, không phải đồng hồ tường
GPU chạy bất đồng bộ: CPU ra lệnh xong là quay về ngay, GPU mới bắt đầu làm. Bấm đồng hồ quanh lời gọi sẽ đo nhầm thời gian ra lệnh, không phải thời gian làm. Code dùng CUDA event — hai mốc đặt trong hàng đợi của GPU.
VRAM tính theo từng mô hình, không theo tiến trình
Ghi chú Lấy thẳng max_memory_allocated() của cả tiến trình sẽ bao gồm mô hình kia đang nằm trong cache và cả CUDA context → cả hai mô hình đều ra ~700 MB giống hệt nhau, xoá sạch đúng cái khác biệt mà demo sinh ra để cho thấy. Nên demo báo: trọng số của chính mô hình đó + phần kích hoạt tăng thêm của đúng lượt chạy vừa rồi.
Với dòng TensorRT còn phải làm khác nữa: TensorRT giữ trọng số và vùng nháp bên ngoài bộ cấp phát của torch, nơi max_memory_allocated() không nhìn thấy. Đo theo kiểu PyTorch sẽ ra “trọng số của bản PyTorch đang nằm không” và “kích hoạt gần bằng 0” — sai hoàn toàn. Code đọc device_memory_size của engine và kích thước file engine thay thế.
Tab LFW: cộng chứ không lấy trung bình
Tab LFW cần embedding của hai khuôn mặt. PyTorch làm một lượt batch 2 → một con số cho cả cặp; TensorRT engine chỉ dựng cho batch 1 → phải chạy hai lượt batch 1.
Lấy trung bình hai lượt engine là đặt số cho một mặt cạnh số cho cả cặp → TensorRT trông nhanh gấp ~2 lần so với thực tế. Code cộng hai lượt: 66 ms (PyTorch) so với 47 ms (TensorRT FP16) cho cả cặp.
Toàn bộ số liệu
Độ chính xác
| Bài | Thước đo | Báo cáo Base | Báo cáo Ghost | Đo lại trên laptop (mẫu 400) |
|---|---|---|---|---|
| CelebA | mAP | 0,789 | 0,778 | 0,783 / 0,780 |
| CASIA | Top-1 | 91,49 % | 91,32 % | 91,3 % / 88,8 % |
| SCface (camera thường) | Top-1 | 45,13 % | 31,36 % | 45,9 % / 31,5 % |
| LFW | ROC-AUC | 0,9921 | 0,9833 | 0,995 / 0,986 |
| ImageNet linear probe | Top-1 | 64,06 % | 58,60 % | 66,5 % / 60,3 % |
Cột cuối là mẫu nhỏ nên có sai số lấy mẫu: 400 mẫu ở mức ~64 % có sai số chuẩn ~2,4 điểm. Bộ kiểm thử vì thế nới ngưỡng chấp nhận theo 2 lần sai số chuẩn của chính mẫu đang chạy.
Độ trễ — GTX 1650, batch 1, trung vị 50 lần bấm
| Bài | PyTorch (B / G) | TensorRT FP16 | TensorRT FP32 |
|---|---|---|---|
| CelebA | 38,8 / 35,9 ms | 22,9 / 21,3 ms | 26,7 / 21,7 ms |
| CASIA | 40,8 / 38,6 ms | 23,0 / 20,8 ms | 27,4 / 22,2 ms |
| SCface | 41,2 / 37,7 ms | 23,0 / 20,9 ms | 27,2 / 22,2 ms |
| ImageNet | 40,3 / 37,9 ms | 24,0 / 21,3 ms | 27,6 / 22,5 ms |
| LFW (cả cặp) | 66,8 / 59,8 ms | 47,4 / 42,5 ms | 53,8 / 44,6 ms |
Ghi chú Tỉ lệ Base/Ghost luôn nằm trong 1,05× – 1,26× trên mọi bài và mọi backend. Bộ kiểm thử đặt ngưỡng cảnh báo ở 1,6×: chênh nhiều hơn thế chắc chắn không phải do mô hình, mà do một nhánh âm thầm rơi về PyTorch.
Bộ nhớ
| Phép đo | Baseline | Ghost |
|---|---|---|
| Trọng số (tab CelebA) | 339 MB | 325 MB |
| Trọng số + kích hoạt, PyTorch batch 1 | ~374 MB | ~352 MB |
| Dòng TensorRT FP16 (engine + vùng nháp) | ~342 MB | ~339 MB |
| Báo cáo A5000, FP16, batch 32 | 771 MB | 609 MB |
Phần mềm được tổ chức thế nào
app.py Giao diện Gradio + toàn bộ logic suy luận demo/ Thư viện riêng của demo dataset_paths.py khai báo dữ liệu nằm ở đâu (không đoán mò) face_models.py dựng ConvViT cho 4 tab khuôn mặt linprobe_models.py dựng mạng cho tab ImageNet (khác một chút!) trt_backend.py nạp và chạy engine TensorRT tools/ Script chạy một lần fetch_checkpoints.py fetch_datasets.py split_celeba.py extract_casia_recordio.py build_trt.py calibrate_lfw.py selftest.py ← bộ kiểm thử duy nhất của dự án vendor/ Mã kiến trúc gốc, chép nguyên từ repo nghiên cứu assets/ 3 file dữ liệu nhỏ app đọc lúc chạy engines/ checkpoints/ datasets/ ← không đưa lên git, do script tạo ra
JOBS — một cuốn sổ điều khiển tất cả
Toàn bộ demo chạy bằng một từ điển JOBS trong app.py. Mỗi tab là một mục, khai báo loại bài, số lớp, tên file checkpoint, thư mục dữ liệu, và toàn bộ chữ hiển thị. Thêm một bài mới = thêm một mục, không phải viết lại giao diện.
Chuyện gì xảy ra khi bấm “Run both models”
- Ảnh được tiền xử lý: Resize 256 (bicubic) → CenterCrop 224 → chuẩn hoá theo ImageNet.
- Với mỗi mô hình,
infer_one()chọn backend, chạy nháp nếu cần, rồi đo đúng một lượt chạy thật. - Logits → xác suất → top-5 → tra tên lớp.
- Tab định danh còn đi tìm một ảnh huấn luyện của người được đoán để hiện lên.
- Bảng tài nguyên được dựng: tham số, trọng số, độ trễ, VRAM, cộng dòng chênh lệch Ghost − Base.
Ghi chú Tiền xử lý phải dùng bicubic. Đổi sang bilinear từng làm tụt độ chính xác thật, nặng nhất ở SCface — vì phải khớp đúng phép biến đổi đã dùng lúc huấn luyện.
Chỉ giữ một bài trên card
Card 4 GB chứa vừa cả 10 mô hình (3.447 MB) — nhưng khi đó còn trống 0 MB, nên người xem mở hết 5 tab rồi chọn TensorRT thì không còn chỗ cho engine và bị âm thầm đẩy về PyTorch. Giải pháp: cache chỉ giữ hai nhánh của tab đang mở, đổi tab là giải phóng ngay — cả mô hình lẫn engine. Đo thực tế khi đi hết 5 tab: luôn 2 mô hình + 2 engine, còn trống 1,0–2,6 GB.