Cosine similarity — đo gần nghĩa bằng góc
Phép so khớp mặc định của tìm kiếm ngữ nghĩa: đo góc giữa hai vector, bỏ qua độ dài. Và vì sao điểm similarity cao chưa chắc là thứ người dùng cần.
TL;DR: Cosine similarity đo góc giữa hai vector, bỏ qua độ dài của chúng (magnitude): hai vector cùng hướng cho điểm gần 1 dù một vector "dài" gấp mấy lần vector kia. Đây là phép so khớp mặc định của tìm kiếm ngữ nghĩa. Nhưng điểm cao chỉ nói một điều: hai vector gần nhau theo hướng mà model đã học ở bài trước — nó không nói "đoạn này trả lời đúng câu hỏi của bạn". Hai việc đó khác nhau, và nhầm lẫn giữa chúng là nguồn gốc của rất nhiều ca RAG trả lời sai mà không ai hiểu tại sao.
Nhóm ở bài trước đã thấy embedding tìm ra đúng đoạn dù câu hỏi và tài liệu không chung từ khoá nào. Vector store trả kết quả kèm một con số cho mỗi đoạn — 0.91, 0.84, 0.79... Ai đó chốt luôn: "cứ lấy đoạn điểm cao nhất, chắc chắn đúng nhất." Hai tuần sau, đoạn điểm 0.91 đứng đầu danh sách nhưng không trả lời được câu hỏi nhân viên hỏi; đoạn đứng thứ ba, điểm 0.79, lại đúng thứ cần. Con số đó đo cái gì mà đứng đầu bảng vẫn có thể sai?
1. Analogy — kim đồng hồ dài ngắn
Một chiếc đồng hồ có kim ngắn ba đốt tay, một chiếc khác kim dài gấp đôi — cả hai cùng chỉ đúng ba giờ. Hỏi "hai kim này đang trỏ về hướng nào", câu trả lời giống hệt nhau dù độ dài kim khác hẳn nhau. Không ai đo "kim nào dài hơn" để trả lời câu đó — chỉ đo hướng trỏ.
Cosine similarity hỏi đúng câu đó về vector: bỏ qua vector nào "dài" hơn, chỉ so hướng hai vector đang trỏ về đâu trong không gian nghĩa.
| Kim đồng hồ | Cosine similarity |
|---|---|
| Kim ngắn và kim dài cùng chỉ 3 giờ | Hai vector độ lớn khác nhau nhưng cùng hướng |
| Câu hỏi là "trỏ về hướng nào", không phải "kim dài bao nhiêu" | Câu hỏi là "góc giữa hai vector", không phải "vector dài bao nhiêu" |
| Kim dài gấp đôi không đổi hướng đang trỏ | Vector nhân đôi mỗi toạ độ không đổi hướng — cosine vẫn bằng 1 |
Analogy hỏng ở một chỗ: kim đồng hồ chỉ quay trên một mặt phẳng 2 chiều, còn vector nghĩa có hàng trăm tới hàng nghìn chiều — "góc" ở đây là một phép tính, không phải thứ mắt nhìn ra được.
Cosine similarity là "đo hướng, kệ độ dài": kim ngắn hay kim dài không quan trọng, quan trọng là đang chỉ về đâu.
2. Cosine similarity đo cái gì?
Với hai vector A và B cùng số chiều, cosine similarity được định nghĩa:
cosine(A, B) = (A · B) / (|A| × |B|)
A · B -- dot product: nhân từng cặp toạ độ rồi cộng lại
(a1×b1) + (a2×b2) + ... + (an×bn)
|A| -- độ lớn (norm) của vector A: căn bậc hai tổng bình phương từng toạ độ
căn( a1² + a2² + ... + an² )
Bài trước dùng "độ dài cố định" để nói số chiều — một câu hay cả trang đều ra ngần ấy con số, không đổi. Ở đây "độ dài" (|A|) là độ lớn hình học của vector đó, tính từ chính các con số bên trong — hai vector cùng 1536 chiều vẫn có thể có |A| khác |B| hoàn toàn. Cosine similarity chia cho đúng đại lượng này để loại nó ra khỏi kết quả.
Chia cho |A| × |B| là bước then chốt: nó chuẩn hoá độ lớn hai vector về 1, chỉ còn lại phần đo hướng. Kết quả nằm trong khoảng từ -1 đến 1 — càng gần 1 càng cùng hướng, càng gần 0 càng vuông góc (không liên quan theo cách model học), càng gần -1 càng ngược hướng.

3. Tính tay: cùng hướng khác độ dài, cùng độ dài khác hướng
Ba vector đồ chơi, 2 chiều để tính tay được (thực tế một vector nghĩa có hàng trăm tới hàng nghìn chiều, không ai tính tay): A = [4, 1], B = [8, 2] — mỗi toạ độ của B đúng bằng gấp đôi A — và C = [1, 4]. Đoán trước hai điều: cosine(A, B) sẽ gần 0, gần 0.5 hay gần 1? Và giữa hai cặp (A, B) với (A, C), cặp nào cho điểm cao hơn? Viết ra trước khi đọc tiếp.
Coi hai chiều này là hai trục ý nghĩa đồ chơi, nối lại ví dụ ở bài trước: trục thứ nhất là "độ liên quan tới nghỉ phép", trục thứ hai là "độ liên quan tới kỹ thuật". A đại diện "cách xin nghỉ phép năm", B đại diện một câu cùng chủ đề nhưng viết dài hơn — "thủ tục đăng ký ngày nghỉ hằng năm, giải thích chi tiết từng bước" — nên mang nhiều tín hiệu hơn ở cả hai trục. C đại diện "cách cài Docker trên máy Mac", nghiêng hẳn sang trục kỹ thuật.
A = [4, 1]
B = [8, 2] -- mỗi toạ độ gấp đôi A
C = [1, 4] -- nghiêng hẳn sang trục còn lại
cosine(A, B):
A · B = (4 × 8) + (1 × 2) = 32 + 2 = 34
|A| = căn(4² + 1²) = căn(17) ≈ 4.123
|B| = căn(8² + 2²) = căn(68) ≈ 8.246
cosine(A, B) = 34 / (4.123 × 8.246) = 34 / 34.00 ≈ 1.00
cosine(A, C):
A · C = (4 × 1) + (1 × 4) = 4 + 4 = 8
|C| = căn(1² + 4²) = căn(17) ≈ 4.123 -- CÙNG độ lớn với A
cosine(A, C) = 8 / (4.123 × 4.123) = 8 / 17 ≈ 0.47
Kết quả xác nhận đúng trực giác ở mục 1. A và B nằm trên cùng một đường thẳng qua gốc toạ độ — B chỉ là A phóng to gấp đôi — nên góc giữa chúng bằng 0 và cosine bằng đúng 1, bất kể B "dài" gấp đôi A. Còn A và C thì ngược lại: hai vector có cùng độ lớn hệt nhau (cùng bằng căn 17) nhưng lệch hướng, nên cosine tụt xuống 0.47. Cùng độ lớn không cứu được điểm số nếu hướng lệch nhau — điều duy nhất cosine quan tâm là góc.
4. Vì sao bỏ qua độ dài?
Độ lớn của một vector nghĩa thường phản ánh những thứ không liên quan tới nghĩa: đoạn văn dài hơn, lặp từ nhiều hơn, hay đơn giản là cách một mô hình cụ thể biểu diễn cường độ tín hiệu — không phải "nghĩa đúng hơn" hay "quan trọng hơn". Ở ví dụ trên, B dài gấp đôi A không phải vì nó đúng chủ đề hơn, mà vì câu viết dài hơn mang nhiều tín hiệu hơn trên cùng hai trục.
Nếu dùng một phép đo nhạy với độ lớn — ví dụ chỉ lấy dot product thô, không chia cho |A| × |B| — thì một đoạn tài liệu dài, lặp ý nhiều lần thường thắng một đoạn ngắn, súc tích, dù đoạn ngắn mới là câu trả lời đúng. Cosine chủ động triệt tiêu lệch lạc đó bằng phép chia ở mục 2: nó ép mọi vector về cùng một độ lớn 1 trước khi so sánh, nên kết quả chỉ còn phản ánh hướng — đúng thứ nối lại được với "không gian nghĩa" và "gần nghĩa" ở bài trước, chứ không phải phản ánh đoạn văn dài hay ngắn.
5. Vì sao điểm cao không đồng nghĩa đúng ý người dùng?
Quay lại câu hỏi mở đầu: vì sao đoạn điểm 0.91 đứng đầu mà không trả lời được, còn đoạn điểm 0.79 đứng thứ ba lại đúng? Cosine đo hướng của một vector đại diện cho cả đoạn văn — thứ hướng đó phản ánh chủ đề và ngữ cảnh tổng thể, chứ không phản ánh "đoạn này có chứa câu trả lời cụ thể hay không". Một đoạn giới thiệu chung — "Chính sách nghỉ phép của công ty áp dụng cho mọi loại hình nhân viên" — rất gần chủ đề nên cosine cao, nhưng không có bước nào để làm theo. Một đoạn liệt kê thao tác — "Bước 1: đăng nhập hệ thống nhân sự, bước 2: chọn mục nghỉ phép năm..." — dùng từ ngữ ít giống câu hỏi hơn nên cosine thấp hơn, nhưng lại chứa đúng thứ người hỏi cần.
Đây chính là hệ quả trực tiếp của mục 3 ở bài trước: không gian nghĩa được học theo mục tiêu "cặp nào được coi là liên quan" trong dữ liệu huấn luyện, không theo định nghĩa "trả lời được câu hỏi" của bạn. Cosine similarity là phép đo chính xác cho không gian đó — nó làm đúng việc được thiết kế để làm, và việc đó không phải là "chấm điểm câu trả lời đúng hay sai". Điểm số càng gần 1 chỉ nên đọc là "gần theo hướng model đã học", không phải "độ tin cậy của câu trả lời".
6. Pitfall tổng hợp
❌ Nhầm 1 — đọc điểm cosine như một tỉ lệ phần trăm đúng:
✅ 0.91 không phải "91% đúng". Cosine là cos của một góc, không phải xác suất hay tỉ lệ khớp. Và không có hằng số chung "trên 0.8 là đủ gần" áp dụng cho mọi model — mỗi embedding model dựng một không gian riêng (bài trước), nên ngưỡng "đủ gần" phải đo trên câu hỏi thật của hệ thống bạn, không suy ra từ một con số đọc được ở đâu đó.
❌ Nhầm 2 — coi điểm càng cao càng tốt trong mọi hệ thống:
✅ Một số vector store trả về khoảng cách (thường tính bằng 1 trừ cosine similarity) thay vì độ giống nhau. Với khoảng cách, điểm thấp mới là gần nhau — đọc nhầm chiều thì lấy ngược danh sách kết quả mà không có lỗi nào hiện ra để báo. Luôn kiểm tra tài liệu của vector store đang dùng trả về loại điểm nào trước khi diễn giải.
7. Liên hệ các bài khác
- Bài 01 — Embedding, không gian của nghĩa — bài đó nói vector gần nhau; bài này là phép đo cụ thể cho chữ "gần" đó.
- Bài 03 — Semantic vs keyword search — điểm cosine cao mà vẫn sai câu trả lời là một trong những lý do keyword search đôi khi thắng semantic search.
- Bài 06 — RAG failure modes — "điểm cao nhất nhưng không trả lời được" là mầm của một failure mode có tên riêng ở bài đó.
8. Tóm tắt
- Cosine similarity đo góc giữa hai vector:
cosine(A, B) = (A · B) / (|A| × |B|), kết quả nằm trong khoảng -1 đến 1. - Chia cho độ lớn hai vector (
|A| × |B|) là bước triệt tiêu ảnh hưởng của độ dài — hai vector cùng hướng cho điểm bằng 1 dù độ lớn khác nhau bao nhiêu. - Cùng độ lớn không đồng nghĩa cùng hướng: hai vector độ lớn bằng hệt nhau vẫn có thể cho cosine thấp nếu hướng lệch nhau.
- Điểm cosine cao chỉ nói "gần theo hướng model đã học" — không nói "trả lời đúng câu hỏi", vì không gian nghĩa được học theo mục tiêu huấn luyện, không theo định nghĩa đúng/sai của bạn.
- Không có ngưỡng chung cho "đủ gần"; và một số hệ thống trả khoảng cách thay vì độ giống nhau — điểm thấp mới là tốt trong trường hợp đó.
9. Tự kiểm tra
Q1Vector A = [4, 1] và vector B = [8, 2]. Không tính số, dự đoán cosine(A, B) rồi giải thích bằng một câu vì sao.▸
Cosine(A, B) bằng đúng 1, vì B chỉ là A phóng to gấp đôi từng toạ độ — hai vector nằm trên cùng một đường thẳng qua gốc toạ độ nên góc giữa chúng bằng 0. Cosine chỉ đo hướng, không đo độ lớn, nên việc B "dài" gấp đôi A không làm giảm điểm.
Q2Hai vector có độ lớn (norm) bằng hệt nhau. Điều đó có đảm bảo cosine similarity giữa chúng cao không? Vì sao?▸
Không. Độ lớn bằng nhau chỉ nói hai vector "dài" như nhau, không nói gì về hướng chúng trỏ về đâu. Ví dụ A = [4, 1] và C = [1, 4] có cùng độ lớn (căn 17) nhưng lệch hướng, nên cosine giữa chúng chỉ khoảng 0.47 — thấp hơn hẳn so với cặp cùng hướng. Cosine chỉ quan tâm góc, không quan tâm độ lớn có bằng nhau hay không.
Q3Một đồng nghiệp thấy đoạn tài liệu điểm cosine 0.91 đứng đầu kết quả tìm kiếm nhưng không trả lời được câu hỏi, trong khi đoạn điểm 0.79 đứng thứ ba lại đúng. Đây có phải lỗi của thuật toán cosine không? Giải thích.▸
Không phải lỗi — cosine đang làm đúng việc nó được thiết kế để làm: đo hướng vector của cả đoạn văn theo không gian nghĩa mà model đã học, phản ánh chủ đề và ngữ cảnh tổng thể chứ không phải "có chứa câu trả lời cụ thể hay không". Đoạn 0.91 có thể chỉ gần về chủ đề chung, còn đoạn 0.79 tuy dùng từ ngữ khác câu hỏi hơn nhưng lại chứa đúng bước cần làm. Điểm cosine cao không phải lời hứa về tính đúng của câu trả lời.
Q4Một vector store trong hệ thống của bạn trả về cột 'distance' thay vì 'similarity' cho mỗi kết quả. Bạn nên sắp xếp kết quả theo chiều nào, và vì sao đọc nhầm chiều lại nguy hiểm?▸
Với khoảng cách (distance), điểm thấp nhất mới là gần nhau nhất — ngược với similarity, nơi điểm cao nhất là gần nhau nhất. Nếu lấy nhầm theo chiều "cao nhất" cho một cột distance, hệ thống sẽ trả về đúng những đoạn xa nhất, mà không có lỗi runtime nào báo hiệu — kết quả vẫn chạy, vẫn trả về danh sách, chỉ là toàn bộ thứ tự bị đảo ngược một cách âm thầm.
Q5Vì sao cosine similarity chia thêm cho |A| × |B| thay vì chỉ dùng dot product (A · B) để so sánh hai đoạn văn?▸
Dot product thô nhạy với độ lớn của cả hai vector: một đoạn văn dài hơn hoặc lặp ý nhiều lần thường cho vector có độ lớn cao hơn, nên dot product của nó với một câu hỏi bất kỳ cũng dễ cao hơn — dù nội dung không liên quan hơn. Chia cho |A| × |B| chuẩn hoá cả hai vector về cùng một độ lớn trước khi so sánh, nên kết quả chỉ còn phản ánh hướng (nghĩa), không bị độ dài văn bản làm lệch.
Bài tiếp theo: Semantic vs keyword search
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
Bài này có giúp bạn hiểu bản chất không?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên