Bỏ qua để đến nội dung

Self-supervised learning với ảnh

Sau bài này, bạn sẽ:

  • Hiểu vì sao cần học tự giám sát (self-supervised learning) với ảnh, bài toán phụ (pretext task) là gì, và cách đo chất lượng biểu diễn bằng linear probe hoặc fine-tune.
  • Tự dẫn được hàm mất mát NT-Xent của SimCLR và giải thích được vai trò của cặp dương (positive pair), mẫu âm (negative example) và nhiệt độ τ\tau.
  • So sánh được ba họ phương pháp: học đối chiếu (SimCLR, MoCo), không cần mẫu âm (BYOL, DINO) và mô hình hóa ảnh bị che (masked image modeling, MAE).
  • Cài được SimCLR tí hon bằng PyTorch rồi đo nó bằng linear probe với scikit-learn.

Bạn có một triệu ảnh chụp linh kiện trong nhà máy. Chụp thì rẻ: camera chạy cả ngày. Gán nhãn thì đắt: một kỹ sư phải xem từng ảnh và ghi “lỗi hàn”, “nứt”, “bình thường”. Sau một tháng, bạn có nhãn cho 2 000 ảnh.

Học có giám sát chỉ dùng được 2 000 ảnh đó. 998 000 ảnh còn lại nằm im. Như bài ViT đã nói, mô hình lớn cần rất nhiều dữ liệu, nên 2 000 ảnh là quá ít.

Câu hỏi của bài: có cách nào để 998 000 ảnh không nhãn dạy cho mô hình biết “nhìn” không?

Ý tưởng: đặt ra một bài toán mà đáp án nằm sẵn trong ảnh. Ví dụ, xoay ảnh một góc ngẫu nhiên rồi bắt mô hình đoán góc đã xoay. Nhãn “90 độ” do chính ta tạo ra, không tốn người gán.

Muốn biết một chiếc xe đang lộn ngược, mô hình phải biết bánh xe thường ở dưới, mui xe ở trên. Tức là nó phải học được hình dạng của vật. Đó chính là thứ ta cần.

Bài toán phụ chỉ là cái cớ. Học xong, ta bỏ phần đoán góc, giữ lại bộ mã hóa (encoder) đã biết nhìn. Rồi dùng 2 000 ảnh có nhãn để dạy nó việc thật.

Cách nổi lên từ khoảng năm 2020 tinh tế hơn. Lấy một ảnh con mèo. Tạo hai bản khác nhau: bản thứ nhất cắt một góc rồi đổi màu, bản thứ hai lật ngang rồi làm mờ. Mỗi bản gọi là một góc nhìn (view).

Hai góc nhìn trông khác nhau, nhưng vẫn là một con mèo. Ta muốn bộ mã hóa cho hai vectơ gần nhau. Còn góc nhìn của các ảnh khác trong lô (con chó, chiếc xe) thì phải cho vectơ ở xa.

Để làm được, mô hình phải bỏ qua những gì augmentation làm thay đổi (màu, vị trí, độ mờ) và giữ những gì không đổi (con mèo). Đó là học đối chiếu (contrastive learning), cùng ý với CLIP ở bài Ảnh + văn bản (1), chỉ khác là cả hai phía đều là ảnh.

Bài toán của bài: làm sao học một bộ mã hóa ảnh tốt chỉ từ ảnh không nhãn, sao cho mô hình không “ăn gian” bằng một lời giải tầm thường, và đo thế nào để biết nó học được gì?

Một lô có BB ảnh (bài báo SimCLR viết NN; ở phần CV, NN đã là số patch). Mỗi ảnh x\mathbf{x} qua hai phép augmentation ngẫu nhiên thành hai góc nhìn x~i\tilde{\mathbf{x}}_i, x~j\tilde{\mathbf{x}}_j. Bộ mã hóa ff cho biểu diễn h=f(x~)\mathbf{h} = f(\tilde{\mathbf{x}}). Một MLP nhỏ gg cho z=g(h)\mathbf{z} = g(\mathbf{h}). Độ tương tự cosine:

sim(u,v)=u⊤v∥u∥ ∥v∥\mathrm{sim}(\mathbf{u}, \mathbf{v}) = \frac{\mathbf{u}^\top \mathbf{v}}{\lVert\mathbf{u}\rVert\,\lVert\mathbf{v}\rVert}

Hai bài toán phụ nổi tiếng trước năm 2020:

  • Đoán góc xoay (Gidaris và cộng sự, 2018): xoay ảnh 0, 90, 180 hoặc 270 độ, mạng phân loại 4 lớp. Đơn giản, nhưng buộc mạng biết chiều “đứng thẳng” của vật.
  • Ghép hình (jigsaw, Noroozi và Favaro, 2016): cắt ảnh thành 9 mảnh, xáo theo một trong 1 000 hoán vị chọn trước (trong 9!=362 8809! = 362\,880 hoán vị), mạng đoán hoán vị nào. Muốn ghép đúng phải biết đầu mèo nằm trên thân mèo.

Điểm yếu chung: mạng hay tìm đường tắt (shortcut). Bài báo ghép hình kể rằng mạng có thể ghép nhờ đường viền và thống kê cấp thấp khớp nhau giữa hai mảnh liền kề, không cần hiểu vật. Bài toán phụ do người thiết kế, và nó tốt tới đâu thì biểu diễn tốt tới đó.

Học xong, làm sao biết ff tốt? Có hai cách chính, đều cần một ít ảnh có nhãn:

  • Linear probe (bài báo SimCLR gọi là “linear evaluation protocol”): đóng băng ff, chỉ học một bộ phân loại tuyến tính trên h\mathbf{h}. Bộ phân loại tuyến tính rất yếu, nên nếu nó vẫn đạt độ chính xác cao thì thông tin về lớp phải nằm sẵn trong h\mathbf{h}, gần như tách được bằng một mặt phẳng.
  • Fine-tune: dùng ff làm điểm khởi đầu rồi cập nhật cả mạng trên dữ liệu có nhãn. Gần với cách dùng thật hơn, nhưng đo cả khả năng “học lại” chứ không chỉ chất lượng biểu diễn.

SimCLR (Chen và cộng sự, 2020) gồm bốn phần: augmentation, encoder ff, projection head gg (MLP một lớp ẩn, ReLU) và hàm mất mát NT-Xent.

Bước 1: tạo 2B2B góc nhìn. Mỗi ảnh trong lô cho hai góc nhìn, nên lô có 2B2B vectơ z1,…,z2B\mathbf{z}_1, \dots, \mathbf{z}_{2B}. Với một cặp dương (i,j)(i, j), bài báo không lấy mẫu âm riêng: 2(B−1)2(B-1) góc nhìn còn lại trong lô chính là mẫu âm.

Bước 2: biến bài toán thành phân loại. Từ góc nhìn ii, hãy “chọn đúng” góc nhìn jj trong số 2B−12B - 1 ứng viên (mọi góc nhìn trừ chính nó). Điểm của mỗi ứng viên kk là cosine chia nhiệt độ: sim(zi,zk)/τ\mathrm{sim}(\mathbf{z}_i, \mathbf{z}_k)/\tau. Qua softmax, xác suất chọn đúng jj là:

pi,j=exp⁡ ⁣(sim(zi,zj)/τ)∑k=12B1[k≠i]exp⁡ ⁣(sim(zi,zk)/τ)p_{i,j} = \frac{\exp\!\big(\mathrm{sim}(\mathbf{z}_i, \mathbf{z}_j)/\tau\big)}{\sum_{k=1}^{2B} \mathbb{1}_{[k \ne i]} \exp\!\big(\mathrm{sim}(\mathbf{z}_i, \mathbf{z}_k)/\tau\big)}

1[k≠i]\mathbb{1}_{[k \ne i]} bằng 1 khi k≠ik \ne i, bằng 0 khi k=ik = i: nó bỏ cặp “một vectơ với chính nó”, vì cặp này luôn có cosine bằng 1 và không dạy gì.

Bước 3: lấy cross-entropy. Đáp án đúng là jj, nên mất mát là âm log của xác suất đó:

ℓi,j=−log⁡pi,j\ell_{i,j} = -\log p_{i,j}

Muốn ℓi,j\ell_{i,j} nhỏ, tử số phải lớn (cặp dương gần nhau) và mẫu số phải nhỏ (mẫu âm xa). Loss của lô là trung bình ℓ\ell trên mọi cặp dương, tính cả (i,j)(i, j) lẫn (j,i)(j, i). Bài báo gọi nó là NT-Xent (normalized temperature-scaled cross entropy): cross-entropy trên cosine (đã chuẩn hóa) chia nhiệt độ.

Bước 4: nhiệt độ τ\tau làm gì. Chia cho τ\tau nhỏ làm các khác biệt cosine phóng to, softmax nhọn lại: mẫu âm nào gần nhất (khó nhất) bị phạt nặng nhất. Khi τ\tau rất lớn, mọi điểm sim/τ\mathrm{sim}/\tau gần bằng 0, mọi số hạng gần bằng nhau, nên pi,j→1/(2B−1)p_{i,j} \to 1/(2B-1) và

ℓi,j→log⁡(2B−1)\ell_{i,j} \to \log(2B - 1)

dù cặp dương gần hay xa: loss không còn phân biệt được gì. Ô NumPy ở dưới kiểm cả hai đầu này.

Ba phát hiện của bài báo, đều quan trọng khi bạn tự làm:

  • Augmentation quan trọng nhất. Không phép biến đổi đơn lẻ nào đủ. Ghép cắt ngẫu nhiên với đổi màu cho kết quả nổi bật. Lý do: chỉ cắt thì hai mảnh của cùng ảnh có histogram màu gần giống nhau, mạng chỉ cần so màu là ghép đúng. Đổi màu chặn đường tắt đó. Đổi màu mạnh giúp SimCLR, trong khi với học có giám sát nó không giúp, thậm chí làm hại.
  • Projection head rồi bỏ đi. Tính loss trên z\mathbf{z} nhưng dùng h\mathbf{h} cho tác vụ sau: h\mathbf{h} tốt hơn z\mathbf{z} hơn 10 điểm phần trăm ở linear probe. Giải thích của tác giả: loss ép z\mathbf{z} không đổi theo augmentation, nên z\mathbf{z} mất những thông tin như màu hay hướng, vốn có thể có ích về sau; gg gánh phần “vứt bỏ” đó thay cho h\mathbf{h}.
  • Cần lô lớn. Lô càng lớn càng nhiều mẫu âm. Bài báo thử BB từ 256 tới 8 192 (B=8 192B = 8\,192 cho 16 382 mẫu âm mỗi cặp dương, trên TPU), lợi rõ nhất khi huấn luyện ít epoch. Về τ\tau: với lô 4 096 và 100 epoch, τ=0.1\tau = 0.1 cho linear probe 64.4%, τ=0.05\tau = 0.05 còn 59.7%, τ=1\tau = 1 còn 58.0%. Khi huấn luyện trên 300 epoch, τ=0.5\tau = 0.5 tốt nhất trong ba giá trị 0.1, 0.5, 1.0.

Kết quả: linear probe 76.5% top-1 trên ImageNet với ResNet-50 rộng gấp 4, ngang ResNet-50 thường học có giám sát (ResNet-50 thường với SimCLR đạt 69.3%).

Lô 8 192 ảnh cần phần cứng mà ít ai có. MoCo (He và cộng sự, 2020) tách số mẫu âm khỏi cỡ lô.

Hàng đợi. Giữ một hàng đợi (queue) các vectơ của những lô trước, K=65 536K = 65\,536 vectơ. Mỗi bước, vectơ của lô mới vào cuối hàng, lô cũ nhất ra khỏi đầu. Mẫu âm lấy từ hàng đợi, nên lô nhỏ vẫn có hàng chục nghìn mẫu âm.

Encoder động lượng (momentum encoder). Vấn đề: vectơ trong hàng đợi được tính bởi encoder ở các bước trước. Nếu encoder đổi nhanh, vectơ cũ và mới không còn so sánh được. MoCo dùng hai encoder: encoder truy vấn θq\boldsymbol\theta_q học bằng gradient, và encoder khóa θk\boldsymbol\theta_k chỉ trôi chậm theo nó:

θk←λ θk+(1−λ) θq\boldsymbol\theta_k \leftarrow \lambda\,\boldsymbol\theta_k + (1 - \lambda)\,\boldsymbol\theta_q

Đây là trung bình động lũy thừa (EMA): mỗi bước giữ λ\lambda phần cũ, thêm 1−λ1 - \lambda phần mới. Bài báo viết hệ số này là mm; bài này dùng λ\lambda vì mm đã là số mẫu. Với λ=0.999\lambda = 0.999, encoder khóa đổi rất chậm nên các khóa trong hàng đợi nhất quán. Ablation của bài báo: λ=0.99\lambda = 0.99 tới 0.99990.9999 đều ổn, λ=0.9\lambda = 0.9 tụt rõ, λ=0\lambda = 0 (chép thẳng) thì loss dao động và không hội tụ.

Mẫu âm có một vai trò ngầm: chống sụp đổ biểu diễn (representation collapse). Nếu chỉ kéo cặp dương lại gần, lời giải dễ nhất là cho mọi ảnh cùng một vectơ: cặp dương nào cũng trùng nhau, loss bằng 0, biểu diễn vô dụng. Mẫu âm cấm điều đó. Bỏ mẫu âm thì phải chống sụp đổ bằng cách khác.

BYOL (Grill và cộng sự, 2020) xuất phát từ một quan sát lạ: lấy một mạng khởi tạo ngẫu nhiên, cố định, làm “thầy”, rồi dạy một mạng khác dự đoán vectơ của thầy. Mạng ngẫu nhiên tự nó chỉ đạt 1.4% linear probe trên ImageNet, nhưng mạng học dự đoán nó đạt 18.8%. Trò đã giỏi hơn thầy. Vậy thì lặp lại: lấy trò làm thầy mới.

BYOL làm điều đó liên tục:

  • Nhánh online gồm encoder, projector và thêm một predictor. Nhánh target có encoder và projector, trọng số là EMA của online (như encoder khóa của MoCo).
  • Online nhìn góc nhìn thứ nhất và dự đoán vectơ mà target cho với góc nhìn thứ hai. Mất mát là bình phương khoảng cách giữa hai vectơ đã chuẩn hóa, tức 2−2cos⁡2 - 2\cos.
  • Gradient chỉ đi qua online; target bị stop-gradient (coi là hằng số).

Sự bất đối xứng (predictor chỉ ở một nhánh, target trôi chậm, không nhận gradient) giúp BYOL không sụp đổ trong thực nghiệm, dù bài báo thừa nhận nó không cấm sụp đổ bằng chứng minh. Linear probe đạt 74.3% với ResNet-50 thường.

DINO (Caron và cộng sự, 2021) nhìn bài toán như tự chắt lọc (self-distillation): chắt lọc tri thức (knowledge distillation) mà không có thầy cho sẵn, thầy là bản EMA của chính trò.

  • Trò và thầy cùng kiến trúc, cùng cho ra một phân phối xác suất (softmax trên KK chiều). Trò học để phân phối của mình khớp phân phối của thầy (cross-entropy).
  • Multi-crop: mỗi ảnh cho 2 góc nhìn toàn cục 224×224224 \times 224 (phủ hơn nửa ảnh) và vài góc nhìn cục bộ 96×9696 \times 96 (phủ dưới nửa ảnh). Thầy chỉ thấy góc nhìn toàn cục, trò thấy tất cả. Trò phải đoán được “cả bức tranh” từ một mảnh nhỏ.
  • Chống sụp đổ bằng centering và sharpening ở đầu ra của thầy. Có hai kiểu sụp đổ: một chiều thống trị (mọi ảnh dồn vào một “lớp”), hoặc phân phối đều (không nói gì cả). Centering trừ đi trung bình chạy c\mathbf{c} của đầu ra thầy, nên không chiều nào thống trị mãi được, nhưng lại đẩy về phân phối đều. Sharpening dùng nhiệt độ thầy thấp (0.04 tới 0.07, so với 0.1 của trò) làm phân phối nhọn, chống phân phối đều. Hai lực ngược nhau cân bằng lẫn nhau.

Phát hiện đáng nhớ nhất của DINO: với ViT học kiểu này, attention của token [CLS] ở lớp cuối tự tách vật khỏi nền. Nhìn bản đồ attention của các đầu, bạn thấy hình con vật hay chiếc xe hiện ra rõ nét, dù mô hình chưa từng thấy một nhãn hay một mask nào. Tác giả ghi rằng tính chất này kém rõ hơn nhiều ở ViT học có giám sát và ở CNN.

Hướng thứ ba mượn ý từ BERT: che một phần đầu vào, bắt mô hình đoán phần bị che. MAE (He và cộng sự, 2022) làm với ảnh:

  1. Cắt ảnh thành patch như ViT. Che ngẫu nhiên 75% số patch.
  2. Encoder ViT chỉ nhận 25% patch còn lại, không có token nào cho chỗ bị che.
  3. Một decoder nhẹ nhận các token đã mã hóa, chèn thêm một “mask token” học được vào mỗi chỗ bị che (kèm embedding vị trí), rồi tái tạo pixel của patch.
  4. Mất mát là MSE giữa pixel tái tạo và pixel gốc, chỉ tính trên patch bị che.

Vì sao che nhiều tới vậy? Ảnh rất dư thừa: che 15% như BERT thì mô hình chỉ cần nội suy từ các điểm ảnh bên cạnh. Che 75% thì phải hiểu vật mới đoán lại được.

Vì sao rẻ? Encoder là phần nặng, và nó chỉ xử lý một phần tư số token. Self-attention tốn theo bình phương số token, nên với ảnh 224×224224 \times 224 và patch 16, encoder làm việc trên 49 token thay vì 196. Decoder chỉ tốn dưới 10% tính toán mỗi token so với encoder, và bị bỏ đi sau pre-train. Tác giả báo cáo huấn luyện nhanh hơn từ 3 lần trở lên. ViT-H học bằng MAE, fine-tune trên ImageNet-1K, đạt 87.8% top-1.

Một điểm khác với họ đối chiếu: biểu diễn MAE khó tách bằng mặt phẳng hơn. Linear probe của MAE thấp hơn MoCo v3, nhưng chỉ cần fine-tune một khối cuối của ViT-L là linear probe 73.5% lên 81.0%, và khi fine-tune vài khối thì MAE vượt MoCo v3. Tác giả nói hai cách đo “phần lớn không tương quan”. Vì vậy khi đọc một bảng so sánh, hãy xem nó đo bằng cách nào.

DINOv2 (Oquab và cộng sự, 2023) mở rộng DINO: một bộ dữ liệu 142 triệu ảnh được tuyển chọn tự động (LVD-142M), ghép loss mức ảnh của DINO với loss mức patch theo kiểu che ảnh của iBOT. Mục tiêu là đặc trưng dùng được ngay khi đóng băng, cho nhiều tác vụ (phân loại, phân đoạn, ước lượng độ sâu) mà không cần fine-tune.

CLIP là học đối chiếu giữa hai phương thức: cặp dương là (ảnh, chú thích của nó), mẫu âm là các chú thích khác trong lô. SimCLR là cùng ý trong một phương thức: cặp dương là hai góc nhìn của cùng ảnh. Công thức gần như nhau; khác biệt nằm ở chỗ “điều gì không đổi” do ai quy định: augmentation do người chọn, hay chú thích do người viết trên Internet.

  • Augmentation mã hóa giả định của bạn. Học đối chiếu học cách bỏ qua đúng những gì augmentation thay đổi. Nếu màu quan trọng cho tác vụ (phân loại quả chín hay xanh), đổi màu mạnh sẽ làm mất thông tin đó. Bài Data augmentation trong CV bàn cách chọn phép biến đổi.
  • Vẫn cần nhãn để dùng. Học tự giám sát cho bộ mã hóa, không cho bộ phân loại. Bước cuối luôn cần dữ liệu có nhãn, dù ít hơn nhiều.
  • Tốn tính toán. Hai góc nhìn mỗi ảnh, lô lớn, hàng trăm epoch: pre-train tự giám sát thường tốn hơn học có giám sát cùng dữ liệu. Lợi ích đến khi dùng lại bộ mã hóa cho nhiều tác vụ.

Hình dưới vẽ khung chung của SimCLR. Hãy nhìn vào hai nhánh của cùng một ảnh: chúng dùng cùng một bộ mã hóa, và chỉ khác nhau ở phép augmentation. Mũi tên “kéo lại gần” nối hai vectơ của cùng ảnh, mũi tên “đẩy ra xa” nối tới vectơ của ảnh khác.

Bảng dưới đặt ba họ phương pháp cạnh nhau. Các con số là của từng bài báo, đo trên ImageNet, và không so trực tiếp được với nhau vì kiến trúc và cách đo khác nhau.

Tiêu chíHọc đối chiếu (SimCLR, MoCo)Không mẫu âm (BYOL, DINO)Che ảnh (MAE)
Tín hiệu họcChọn đúng góc nhìn kia giữa nhiều mẫu âmTrò dự đoán đầu ra của thầy (EMA) trên góc nhìn khácTái tạo pixel của patch bị che
Cần augmentation mạnhCó, quan trọng nhấtCóGần như không: che ảnh là “augmentation”
Chống sụp đổ bằngMẫu âmPredictor, stop-gradient (BYOL); centering, sharpening (DINO)Không có nguy cơ: đáp án là pixel thật
Phụ thuộc cỡ lôNhiều (SimCLR); MoCo bớt nhờ hàng đợiÍt hơnÍt
Kiến trúc hay gặp trong bài báoResNetResNet (BYOL), ViT (DINO)ViT
Mạnh ởLinear probeLinear probe, k-NN; DINO cho attention tách vậtFine-tune, mô hình rất lớn
Số trong bài báoSimCLR: 76.5% linear (ResNet-50 rộng gấp 4)BYOL: 74.3% linear (ResNet-50); DINO: 80.1% linear (ViT-B)87.8% fine-tune (ViT-H)

Code trong bài chạy thử lần cuối tháng 11/2026: ô NumPy với Python 3.14 và NumPy 2.4 (bản có trong Pyodide), ô PyTorch với Python 3.14, PyTorch 2.15 và scikit-learn 1.9 trên CPU. Ô NumPy chạy ngay trên trang, ô PyTorch chạy trên máy bạn. Hai ô độc lập, không dùng biến của nhau.

Ô dưới tạo 4 “ảnh” giả, mỗi ảnh là một vectơ nội dung 8 chiều (hạt giống 0). Mỗi góc nhìn là nội dung cộng nhiễu. Nhiễu nhỏ (0.1) cho cặp dương gần nhau; nhiễu lớn (3.0) cho cặp dương gần như ngẫu nhiên. Hàm nt_xent làm đúng ba bước ở mục Lý thuyết: cosine chia τ\tau, bỏ đường chéo bằng −∞-\infty, rồi cross-entropy với đáp án là cột của cặp dương.

NT-Xent với cặp dương gần và xa

import numpy as np
rng = np.random.default_rng(0)
N, D = 4, 8 # 4 ảnh trong lô, embedding 8 chiều
img = rng.normal(size=(N, D)) # "nội dung" của từng ảnh
def views(noise): # hai góc nhìn của mỗi ảnh = nội dung + nhiễu
a = img + noise * rng.normal(size=(N, D))
b = img + noise * rng.normal(size=(N, D))
return np.vstack([a, b]) # 2N hàng: hàng i và i + N cùng ảnh
def nt_xent(z, tau):
z = z / np.linalg.norm(z, axis=1, keepdims=True)
s = z @ z.T / tau # cosine chia nhiệt độ, cỡ 2N x 2N
np.fill_diagonal(s, -np.inf) # bỏ cặp một vectơ với chính nó
m = s.max(axis=1, keepdims=True)
log_p = s - m - np.log(np.exp(s - m).sum(axis=1, keepdims=True))
pos = np.r_[np.arange(N, 2 * N), np.arange(N)] # cột của cặp dương
return -log_p[np.arange(2 * N), pos].mean()
print(f"log(2N-1) = {np.log(2 * N - 1):.3f}")
z_close, z_far = views(0.1), views(3.0)
for name, z in [("close", z_close), ("far", z_far)]:
zn = z / np.linalg.norm(z, axis=1, keepdims=True)
cos = (zn[:N] * zn[N:]).sum(axis=1).mean()
print(f"{name}: positive cos = {cos:.2f}")
print("tau close far")
for tau in [0.05, 0.1, 0.5, 1.0, 10.0]:
lc, lf = nt_xent(z_close, tau), nt_xent(z_far, tau)
print(f"{tau:<5} {lc:.3f} {lf:.3f}")

Lần chạy đầu cần tải Python, khoảng 10 MB.

Đọc kết quả:

  • log(2N-1) = 1.946: với B=4B = 4 ảnh, mỗi góc nhìn có 2B−1=72B - 1 = 7 ứng viên, và log⁡7≈1.946\log 7 \approx 1.946 là loss khi mô hình “đoán bừa”. (Trong code, N là số ảnh của lô, tức BB của bài.)
  • Cosine của cặp dương: 0.99 ở trường hợp gần, 0.22 ở trường hợp xa.
  • Cột close: cặp dương gần thì τ\tau nhỏ cho loss gần 0 (0.000 ở τ=0.05\tau = 0.05, 0.002 ở τ=0.1\tau = 0.1). Khi τ\tau tăng, loss tăng dần (0.627, 1.166) và ở τ=10\tau = 10 đạt 1.858, sát log⁡7\log 7: loss không còn “thưởng” cho việc cặp dương gần nhau.
  • Cột far: cặp dương xa thì τ\tau nhỏ phạt rất nặng: 7.087 ở τ=0.05\tau = 0.05, cao hơn hẳn log⁡7\log 7, vì có mẫu âm gần hơn cả cặp dương và τ\tau nhỏ phóng to khác biệt đó. Ở τ=10\tau = 10 hai cột gần bằng nhau (1.858 và 1.933): τ\tau quá lớn thì loss gần như không phân biệt được biểu diễn tốt và xấu.

SimCLR tí hon trên chữ số, rồi linear probe

Phần tiêu đề “SimCLR tí hon trên chữ số, rồi linear probe”

File dưới học SimCLR trên 1 797 ảnh chữ số 8×88 \times 8 của load_digits, không dùng nhãn. Augmentation viết bằng PyTorch: dịch ngẫu nhiên tối đa khoảng 1 pixel, thêm nhiễu, che một ô 3×33 \times 3 (cutout). Encoder ff là MLP 64→256→1664 \to 256 \to 16, nên biểu diễn h\mathbf{h} chỉ có 16 số; projection head gg là 16→64→3216 \to 64 \to 32. Học 40 epoch với lô 256 và τ=0.5\tau = 0.5. Phần augmentation:

theta = torch.tensor([[1.0, 0, 0], [0, 1.0, 0]]).repeat(n, 1, 1)
theta[:, :, 2] = (torch.rand(n, 2) - 0.5) * 0.5
grid = F.affine_grid(theta, list(x.shape), align_corners=False)
x = F.grid_sample(x, grid, align_corners=False)
x = x + 0.1 * torch.randn_like(x)

Sau đó là linear probe: đóng băng ff, lấy h\mathbf{h} của mọi ảnh, học hồi quy logistic của scikit-learn trên nửa dữ liệu, đo trên nửa còn lại. File so encoder đã học với một encoder cùng kiến trúc nhưng khởi tạo ngẫu nhiên, ở hai mức nhãn: chỉ 100 ảnh có nhãn, và cả 898 ảnh của nửa train. Cách cài PyTorch bản CPU có ở bài Xây dựng LLM đầu tiên.

Chạy trên máy bạn

SimCLR tí hon và linear probe

Cần cài
pip install torch scikit-learn
Phần cứng
CPU là đủ. Khoảng 20 đến 45 giây trên máy phát triển
import torch
from torch import nn
import torch.nn.functional as F
from sklearn.datasets import load_digits
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
torch.manual_seed(0)
X, y = load_digits(return_X_y=True) # nhãn y chỉ dùng ở bước linear probe
X = torch.tensor(X / 16.0, dtype=torch.float32).view(-1, 1, 8, 8)
def augment(x): # dịch tối đa 1 pixel, thêm nhiễu, che một ô 3x3
n = len(x)
theta = torch.tensor([[1.0, 0, 0], [0, 1.0, 0]]).repeat(n, 1, 1)
theta[:, :, 2] = (torch.rand(n, 2) - 0.5) * 0.5
grid = F.affine_grid(theta, list(x.shape), align_corners=False)
x = F.grid_sample(x, grid, align_corners=False)
x = x + 0.1 * torch.randn_like(x)
r, c = torch.randint(0, 6, (2, n, 1, 1, 1))
i, j = torch.arange(8).view(8, 1), torch.arange(8).view(1, 8)
hole = (i >= r) & (i < r + 3) & (j >= c) & (j < c + 3)
return x.masked_fill(hole, 0.0)
def encoder(): # f: ảnh 8x8 -> biểu diễn h có 16 chiều
return nn.Sequential(nn.Flatten(), nn.Linear(64, 256), nn.ReLU(),
nn.Linear(256, 16))
def nt_xent(z, tau=0.5): # hàng i và i + n là hai góc nhìn của một ảnh
z = F.normalize(z, dim=1)
s = (z @ z.T / tau).fill_diagonal_(float("-inf"))
n = len(z) // 2
pos = torch.cat([torch.arange(n, 2 * n), torch.arange(n)])
return F.cross_entropy(s, pos)
random_f, f = encoder(), encoder()
head = nn.Sequential(nn.Linear(16, 64), nn.ReLU(), nn.Linear(64, 32))
opt = torch.optim.Adam([*f.parameters(), *head.parameters()], lr=1e-3)
for epoch in range(40): # học không cần nhãn
for idx in torch.randperm(len(X)).split(256):
views = torch.cat([augment(X[idx]), augment(X[idx])])
loss = nt_xent(head(f(views)))
opt.zero_grad()
loss.backward()
opt.step()
print(f"last NT-Xent loss: {loss.item():.3f}")
# Linear probe: đóng băng f, chỉ học hồi quy logistic trên h
tr, te = train_test_split(range(len(y)), test_size=0.5,
random_state=0, stratify=y)
for n_labels in [100, len(tr)]:
for name, enc in [("random", random_f), ("SimCLR", f)]:
with torch.no_grad():
h = enc(X).numpy()
probe = make_pipeline(StandardScaler(),
LogisticRegression(max_iter=2000))
probe.fit(h[tr[:n_labels]], y[tr[:n_labels]])
acc = probe.score(h[te], y[te])
print(f"{n_labels} labels, {name} encoder: {acc:.3f}")

Kết quả mẫu

Lấy từ một lần chạy thật.

last NT-Xent loss: 1.428
100 labels, random encoder: 0.740
100 labels, SimCLR encoder: 0.840
898 labels, random encoder: 0.843
898 labels, SimCLR encoder: 0.918

Đọc kết quả:

  • last NT-Xent loss: 1.428: lô 256 ảnh có 511 ứng viên mỗi góc nhìn, nên đoán bừa cho log⁡511≈6.24\log 511 \approx 6.24 (tự tính). Loss 1.428 nghĩa là mô hình đã nhận ra cặp dương khá tốt.
  • Với 100 nhãn: encoder ngẫu nhiên đạt 0.740, encoder SimCLR đạt 0.840. Cùng 16 số mỗi ảnh, nhưng 16 số do SimCLR chọn mang nhiều thông tin về chữ số hơn.
  • Với 898 nhãn: 0.843 so với 0.918. Khoảng cách vẫn còn: một phép chiếu ngẫu nhiên xuống 16 chiều làm mất thông tin mà thêm nhãn cũng không lấy lại được.

Hai lần chạy trên máy phát triển cho kết quả trùng nhau. Máy khác (khác CPU hay khác bản PyTorch) có thể lệch vài phần nghìn.

Câu 1/4 Bạn huấn luyện SimCLR với lô B=512B = 512 ảnh. Với một góc nhìn ii, mẫu số của ℓi,j\ell_{i,j} có bao nhiêu số hạng, và bao nhiêu trong đó là mẫu âm?
Câu 2/4 Một bạn thử SimCLR chỉ với augmentation "cắt ngẫu nhiên", không đổi màu. Loss giảm rất nhanh, gần như đoán đúng mọi cặp dương, nhưng linear probe lại thấp. Giải thích nào hợp lý nhất?
Câu 3/4 BYOL không có mẫu âm. Một mạng "lười" có thể cho mọi ảnh cùng một vectơ, và khi đó online dự đoán target hoàn hảo. Điều gì trong BYOL giúp tránh lời giải sụp đổ này trong thực tế?
Câu 4/4 Ở MAE, ảnh 224×224224 \times 224 cắt patch 16×1616 \times 16 và che 75%. Encoder ViT xử lý bao nhiêu token patch, và vì sao điều này làm huấn luyện rẻ hơn hẳn?

Ý chính:

  • Học tự giám sát tạo nhãn từ chính ảnh (bài toán phụ) để học bộ mã hóa từ ảnh không nhãn, rồi dùng ít nhãn cho tác vụ thật. Đo bằng linear probe (đóng băng, chỉ học lớp tuyến tính) hoặc fine-tune; hai cách đo có thể xếp hạng khác nhau.
  • Học đối chiếu kéo hai góc nhìn của cùng ảnh lại gần, đẩy góc nhìn của ảnh khác ra xa. NT-Xent là cross-entropy của bài toán chọn đúng cặp dương trong 2B−12B - 1 ứng viên, trên cosine chia τ\tau. SimCLR cần augmentation mạnh (cắt + đổi màu) và lô lớn; MoCo thay lô lớn bằng hàng đợi và encoder động lượng.
  • Không có mẫu âm thì phải chống sụp đổ cách khác: BYOL dùng predictor, target EMA và stop-gradient; DINO dùng thầy EMA cùng centering và sharpening, và attention của ViT tự tách vật.
  • MAE che 75% patch, encoder chỉ xem phần còn lại, decoder nhẹ tái tạo pixel: rẻ, mở rộng tốt, mạnh khi fine-tune. CLIP là học đối chiếu giữa ảnh và chữ.

Đọc thêm:

Nội dung theo giấy phép CC BY-NC-SA 4.0. Mã nguồn theo giấy phép MIT.Giới thiệu