QuangCler / ghostconvmae-face-demo

GhostConvMAE — demo đối chứng hai backbone

Cùng một ảnh, cùng một máy, cùng một cách đo: ConvMAE-BaseGhost+ConvMAE chạy cạnh nhau trên 5 bài kiểm tra, hiện ra dự đoán, độ trễ và bộ nhớ GPU. Trang này giải thích sản phẩm từ số 0 — không giả định người đọc biết gì về học sâu.

ConvMAE-Base · bản gốc · 84,6 M Ghost+ConvMAE · bản cải tiến · 81,2 M
Mục lục · 9 phần
00

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ạybộ nhớ GPU. Có 5 bài kiểm tra (5 tab) và 3 chế độ chạy.

Câu mở bài
“Đâ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.”
Tham số tiết kiệm−4,0 %84,6 M → 81,2 M
VRAM tiết kiệm−21 %771 → 609 MB, A5000 FP16 batch 32
SCface Top-145,1 → 31,4chỗ trả giá đắt nhất
Linear probe Top-164,1 → 58,6phép so công tâm nhất
01

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ầnTên gọiViệc của nóVí dụ
Phần thânbackbonebiến 150.528 số thành 768 số đặc trưng“cao, tóc xoăn, mắt to…”
Phần đầuheadtừ 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.

  1. 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).
  2. 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).
  3. 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).

02

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:

Cấu hình: img_size [224,56,28] · patch_size [4,2,2] · embed_dim [256,384,768] · depth [2,2,11]
TầngBản đồSố vị tríSố chiềuSố khốiLoại khối
Stage 156 × 563.1362562convolution
Stage 228 × 287843842convolution
Stage 314 × 1419676811Transformer

Ý 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ầnCBlock (baseline)GhostV2Block (ghost)
Nhánh chínhconv 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 gianconv 5×5 depthwiseDFC: conv 1×9 + 9×1 depthwise
Cổng chú ýsigmoid(DFC) nhân vào
Chấm điểm kênhSE (giảm 4 lần rồi phục hồi)
MLP (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

Đo trực tiếp trên checkpoint
TabBaselineGhostChênh
CelebA84,6 M / 339 MB81,2 M / 325 MB−3,4 M (−4,0 %)
SCface84,7 M81,3 M−3,4 M
LFW85,9 M82,5 M−3,4 M
ImageNet89,7 M86,3 M−3,4 M
CASIA92,7 M89,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.

Câu hỏi nguy hiểm 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:

Câu chốt
“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.”
Báo cáo · A5000 · FP16 · batch 32
Chỉ sốBaselineGhostChênh
VRAM (FP16)771 MB609 MB−21 %
VRAM (FP32)1.518 MB1.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ồnChế độBaselineGhostKết luận
Báo cáo · A5000 · FP16 · batch 32thông lượng738 ảnh/s612 ảnh/sGhost chậm hơn 17 %
Demo · GTX 1650 · FP16 · batch 1độ trễ23,0 ms20,9 msGhost 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ẹ.
Câu trả lời an toàn
“Đó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ế độ.”
03

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.

Báo cáo · bảng VI-5a · trung bình 3 seed · identity = Top-1, CelebA = mAP, LFW = AUC
DatasetEpoch cấu hìnhBestĐạt bestChững lại (≥99 % best)% lịch
CASIA · Top-14091,32ep ~38ep ~2973 %
CelebA · mAP300,79ep ~23ep ~20~67 %
LFW · AUC500,99ep ~45ep ~1734 %
SCface · Top-1 (base)10045,13ep ~69ep ~69~70 %
SCface · Top-1 (ghost)100~31,7ep ~54ep ~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.

Vì sao quan trọng

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 %.

04

Năm tab, mỗi tab kiểm tra điều gì

TabKiểu bàiDữ liệuThước đoBaseGhost
CelebAđa nhãn thuộc tính39.829 ảnh testmAP0,7890,778
CASIAđịnh danh tập đóng10.571 ngườiTop-191,49 %91,32 %
SCfacexuyên độ phân giải130 ngườiTop-145,13 %31,36 %
LFWxác minh cặp5.749 ngườiROC-AUC0,99210,9833
ImageNetlinear probe50.000 ảnh valTop-164,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.

Cạm bẫy lớn nhất của cả dự án

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.idx501.196 bản ghi còn train.lst494.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 < 9282i+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ómLà gìSố ảnh
cam1cam5 × 3 khoảng cáchcamera an ninh ánh sáng thường — đúng thiết lập của báo cáo1.950
cam6, cam7camera 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ý.

Chuyện về ngưỡng 0,235

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.
05

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.

PyTorch≈ 39 msGTX 1650, batch 1
TensorRT FP32≈ 27 msgộp kernel + chọn thuật toán
TensorRT FP16≈ 23 msnhanh hơn ~1,7 lầ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.

Câu chắc chắn bị hỏi

“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 ở FP160,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.

06

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.

Chạy nguội251,3 mslần đầu, chưa autotune
Chạy đã ấm41,3 mstrung vị 20 lượt
Chênh lệch6,1 ×đây là lý do phải warm-up

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.

07

Toàn bộ số liệu

Độ chính xác

BàiThước đoBáo cáo BaseBáo cáo GhostĐo lại trên laptop (mẫu 400)
CelebAmAP0,7890,7780,783 / 0,780
CASIATop-191,49 %91,32 %91,3 % / 88,8 %
SCface (camera thường)Top-145,13 %31,36 %45,9 % / 31,5 %
LFWROC-AUC0,99210,98330,995 / 0,986
ImageNet linear probeTop-164,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àiPyTorch (B / G)TensorRT FP16TensorRT FP32
CelebA38,8 / 35,9 ms22,9 / 21,3 ms26,7 / 21,7 ms
CASIA40,8 / 38,6 ms23,0 / 20,8 ms27,4 / 22,2 ms
SCface41,2 / 37,7 ms23,0 / 20,9 ms27,2 / 22,2 ms
ImageNet40,3 / 37,9 ms24,0 / 21,3 ms27,6 / 22,5 ms
LFW (cả cặp)66,8 / 59,8 ms47,4 / 42,5 ms53,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 đoBaselineGhost
Trọng số (tab CelebA)339 MB325 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 32771 MB609 MB
08

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”

  1. Ảnh được tiền xử lý: Resize 256 (bicubic) → CenterCrop 224 → chuẩn hoá theo ImageNet.
  2. 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.
  3. Logits → xác suất → top-5 → tra tên lớp.
  4. 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.
  5. 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.