Vector Semantics và Embeddings
This content is not available in your language yet.
Mục tiêu
Phần tiêu đề “Mục tiêu”Sau bài này, bạn sẽ:
- Hiểu giả thuyết phân bố (distributional hypothesis): nghĩa của một từ được phản ánh qua các từ hay đứng cạnh nó, và vì sao one-hot không đo được hai từ giống nhau tới đâu.
- Lập được ma trận từ–văn bản và ma trận từ–ngữ cảnh, rồi đổi số đếm thành trọng số bằng TF-IDF hay PPMI, và giải thích vì sao cần đổi.
- Đo độ tương tự bằng cosine, và nén vectơ thưa (sparse) thành vectơ dày (dense) bằng SVD rút gọn, tức cách làm của LSA.
- Cài đặt bằng NumPy và scikit-learn, và nhận ra các bẫy: mẫu tách token mặc định với tiếng Việt, thiên lệch của PMI với từ hiếm, chọn số chiều quá nhỏ.
Cần biết trước
Phần tiêu đề “Cần biết trước”- Text Normalization, in Natural Language Processing(Not translated yet) (Learned)
- Linear Algebra, in Math for AI(Not translated yet) (Learned)
Trực giác
Phần tiêu đề “Trực giác”Một từ bạn chưa từng gặp
Phần tiêu đề “Một từ bạn chưa từng gặp”Đọc ba câu sau. Từ “thồn” là từ bịa ra, bạn chưa gặp bao giờ:
Mẹ nấu một nồi thồn rất thơm. Thồn ăn nóng với rau sống thì ngon nhất. Tôi ăn hai bát thồn mà vẫn còn thèm.
Bạn đã đoán được khá nhiều: thồn là một món ăn, nấu trong nồi, múc ra bát, ăn nóng. Chắc nó gần với phở, bún, canh hơn là với xe máy hay con mèo. Không ai định nghĩa cho bạn. Bạn đoán nhờ các từ đứng cạnh: “nấu”, “nồi”, “ăn”, “bát”, “ngon” là những từ bạn từng thấy quanh phở và bún.
Các nhà ngôn ngữ học thập niên 1950 đã nói đúng điều này. Harris (1954) cho rằng hai từ khác nghĩa bao nhiêu thì môi trường xuất hiện của chúng cũng khác nhau đại khái (roughly) chừng ấy. Firth (1957, tr. 11) viết gọn hơn: “You shall know a word by the company it keeps”, tức là xem một từ hay đi với những từ nào thì biết nó là từ gì. Ý tưởng này gọi là giả thuyết phân bố: các từ xuất hiện trong ngữ cảnh giống nhau thì có nghĩa gần nhau.
Biến “hay đi với ai” thành con số
Phần tiêu đề “Biến “hay đi với ai” thành con số”Máy tính không đoán như bạn được, nhưng nó đếm rất giỏi. Lấy một kho văn bản (corpus), với mỗi từ, đếm xem những từ nào hay đứng cạnh nó:
| Từ | ăn | bát | nóng | nuôi | con | đường |
|---|---|---|---|---|---|---|
| phở | 5 | 4 | 3 | 0 | 0 | 0 |
| bún | 4 | 3 | 2 | 0 | 0 | 0 |
| mèo | 0 | 0 | 0 | 3 | 6 | 0 |
(Số tự đặt để minh họa.) Mỗi hàng giờ là một dãy số, tức một vectơ. Phở và bún có hàng gần giống nhau, mèo thì khác hẳn. Nghĩa của từ đã thành một điểm trong không gian, và “hai từ giống nhau” thành “hai điểm gần nhau”. Cách nhìn này gọi là ngữ nghĩa vectơ (vector semantics). Vectơ biểu diễn nghĩa của từ gọi là embedding.
Nhưng còn nhiều chuyện phải quyết. Đếm trong phạm vi nào? Các từ như “một”, “tôi”, “rất” đứng cạnh mọi thứ, có nên tính ngang với “bát”? So hai vectơ bằng cách nào khi một từ phổ biến có số đếm lớn gấp trăm lần? Và vectơ dài bằng cả từ vựng, toàn số 0, có dùng được không?
Bài toán của bài này là: từ một kho văn bản thô, gán cho mỗi từ (hay mỗi văn bản) một vectơ, sao cho hai từ dùng trong ngữ cảnh giống nhau có vectơ gần nhau, và đo được “gần” bằng một con số.
Lý thuyết
Phần tiêu đề “Lý thuyết”One-hot: mọi từ cách đều nhau
Phần tiêu đề “One-hot: mọi từ cách đều nhau”Cách đơn giản nhất để biến từ thành vectơ: đánh số các từ trong từ vựng từ 1 tới . Từ thứ là vectơ dài , toàn số 0, chỉ có vị trí bằng 1. Đây là vectơ one-hot (one-hot vector).
Với gồm (bún, chó, mèo, phở): , . Tích vô hướng của hai vectơ one-hot khác nhau luôn bằng 0, vì không có vị trí nào cả hai cùng khác 0. Vậy mèo “cách” chó đúng bằng mèo cách phở. One-hot chỉ nói hai từ có phải là một hay không, không nói chúng giống nhau tới đâu.
Giả thuyết phân bố gợi ý cách sửa: thay vì một số 1 ở vị trí của chính từ, hãy mô tả từ bằng các ngữ cảnh của nó.
Ma trận từ–văn bản và ma trận từ–ngữ cảnh
Phần tiêu đề “Ma trận từ–văn bản và ma trận từ–ngữ cảnh”Có hai cách chọn “ngữ cảnh”, cho hai loại ma trận (Turney và Pantel, 2010):
- Ma trận từ–văn bản (term-document matrix): mỗi hàng là một từ, mỗi cột là một văn bản, ô là số lần từ xuất hiện trong văn bản . Đọc theo cột, ta có vectơ của một văn bản; hai văn bản dùng nhiều từ chung thì có cột giống nhau. Đây là nền của tìm kiếm cổ điển, sẽ gặp lại ở bài Truy xuất thông tin. Đọc theo hàng, ta có vectơ của một từ: hai từ hay xuất hiện trong cùng văn bản thì có hàng giống nhau.
- Ma trận từ–ngữ cảnh (word-context matrix), còn gọi là ma trận đồng xuất hiện (co-occurrence matrix): cả hàng lẫn cột là từ. Gọi từ ở hàng là từ đích (target word) , từ ở cột là từ ngữ cảnh (context word) . Ô đếm số lần nằm trong cửa sổ (window) quanh , tức cách không quá vị trí. Cửa sổ này khác “cửa sổ ngữ cảnh” (context window) của LLM, vốn là số token tối đa mô hình đọc được trong một lần gọi. Ma trận có kích thước .
Ví dụ với và câu “tôi ăn một bát phở nóng”: từ đích “phở” ở vị trí 5, nên các từ ngữ cảnh là “một”, “bát” (bên trái) và “nóng” (bên phải, câu hết ở đó). Ta cộng 1 vào ba ô , , , rồi làm vậy với mọi từ của mọi câu.
Cửa sổ hẹp ( từ 1 tới 3) thường gom các từ cùng loại, thay được cho nhau trong câu, như phở và bún. Cửa sổ rộng hay cả văn bản thì gom các từ cùng chủ đề, như phở với bát, đũa, quán.
Với từ vựng thật vài chục nghìn từ, mỗi hàng dài vài chục nghìn số, mà phần lớn là 0, vì mỗi từ chỉ đứng cạnh một phần rất nhỏ của từ vựng. Ta gọi đó là vectơ thưa (sparse vector).
Cosine: so hướng, không so độ dài
Phần tiêu đề “Cosine: so hướng, không so độ dài”Muốn biết hai hàng , giống nhau tới đâu, thử dùng tích vô hướng : nó lớn khi hai vectơ có số lớn ở cùng chỗ. Nhưng nó có một lỗi. Từ “phở” gặp 1000 lần trong kho có số đếm lớn gấp 100 lần từ “bún” gặp 10 lần, nên tích vô hướng của “phở” với mọi từ đều lớn. Từ phổ biến sẽ trông “giống” mọi thứ.
Cách sửa là chia cho độ dài của hai vectơ. Ta được độ tương tự cosine (cosine similarity):
Nhân với một số thì tử số nhân , mẫu số cũng nhân , nên cosine không đổi: nó chỉ nhìn hướng của vectơ, tức tỉ lệ giữa các ngữ cảnh, không nhìn tổng số lần từ xuất hiện. Đúng như tên gọi, đây là cosin của góc giữa hai vectơ: bằng 1 khi cùng hướng, 0 khi vuông góc (không có ngữ cảnh chung nào). Số đếm không bao giờ âm, nên với ma trận đếm, cosine nằm trong . Bài RAG và chatbot hiện đại có thêm một ví dụ tính tay.
TF-IDF: hạ trọng số của từ có mặt khắp nơi
Phần tiêu đề “TF-IDF: hạ trọng số của từ có mặt khắp nơi”Số đếm thô còn một vấn đề nữa. Trong ma trận từ–văn bản, “và”, “của”, “tôi” có mặt ở gần như mọi văn bản, nên chúng chiếm phần lớn độ dài vectơ mà chẳng nói gì về nội dung. Ta muốn một trọng số lớn khi từ xuất hiện nhiều trong văn bản này, và nhỏ khi từ có mặt ở mọi văn bản. Đó là TF-IDF (term frequency – inverse document frequency), tích của hai phần.
Tần suất từ (term frequency) : số lần từ xuất hiện trong văn bản . Một biến thể hay gặp là (khi ), vì một từ xuất hiện 100 lần không quan trọng gấp 100 lần từ xuất hiện 1 lần.
Tần suất văn bản ngược (inverse document frequency). Gọi là số văn bản trong kho, là số văn bản có chứa . Dạng sách giáo khoa, theo ý tưởng của Spärck Jones (1972):
Từ chỉ có trong 1 văn bản có idf lớn nhất, . Từ có trong mọi văn bản có idf bằng , tức bị xóa hẳn. Trọng số cuối cùng là .
Phiên bản của scikit-learn. TfidfVectorizer mặc định dùng tf là số đếm thô và một idf làm trơn (smooth), với logarit tự nhiên:
- Cộng 1 ở tử và mẫu giống như có thêm một văn bản chứa mọi từ đúng một lần, nên không bao giờ chia cho 0.
- Cộng 1 ở cuối để từ có mặt khắp nơi vẫn còn trọng số 1, thay vì bằng 0.
- Sau khi nhân tf với idf, mỗi hàng (một văn bản) được chia cho chuẩn L2 của nó, nên mọi vectơ văn bản có độ dài 1. Khi đó cosine chính là tích vô hướng.
Với : từ có trong 3 văn bản có ; trong 2 văn bản, ; trong 1 văn bản, . Ô code thứ hai in đúng ba số này. Muốn dùng tf dạng log thì đặt sublinear_tf=True (scikit-learn dùng ).
PMI và PPMI: cặp từ gặp nhau nhiều hơn mức ngẫu nhiên bao nhiêu
Phần tiêu đề “PMI và PPMI: cặp từ gặp nhau nhiều hơn mức ngẫu nhiên bao nhiêu”Ma trận từ–ngữ cảnh cũng có vấn đề của từ phổ biến: “một” đứng cạnh phở, bún, mèo, chó. Ta không cần biết bao nhiêu lần gặp , mà cần biết chúng gặp nhau nhiều hơn mức ngẫu nhiên bao nhiêu.
Bước 1: đổi số đếm thành xác suất. Gọi là tổng mọi ô, tức tổng số cặp (từ, ngữ cảnh) đã đếm, khác với (số văn bản) ở mục TF-IDF. Ta ước lượng:
là xác suất bốc ngẫu nhiên một cặp trong kho và được đúng . là tổng của hàng chia , là tổng của cột chia .
Bước 2: so với mức độc lập. Nếu và không liên quan gì, xác suất gặp cả hai là . PMI (pointwise mutual information, thông tin tương hỗ điểm), mà Church và Hanks (1990) đưa vào ngôn ngữ học máy tính, là log của tỉ số giữa thực tế và mức đó:
Dạng thứ hai có được khi thay ba xác suất ở bước 1 vào: ở mẫu của và hai ở mẫu của để lại đúng một thừa số trên tử. PMI bằng 0 khi cặp gặp nhau đúng như mức ngẫu nhiên, dương khi gặp nhiều hơn, âm khi gặp ít hơn. Với , PMI bằng 1 nghĩa là gặp nhau gấp đôi mức ngẫu nhiên. Cơ số log chỉ nhân mọi PMI với cùng một hằng số; bài này dùng như Church và Hanks, bài Word2Vec, GloVe, FastText dùng .
Áp vào kho nhỏ ở ô code đầu: (biến N trong code), “phở” có tổng hàng 6, “bát” có tổng cột 10, và cặp (phở, bát) gặp 2 lần. Vậy : phở đứng cạnh bát gấp 6 lần mức ngẫu nhiên. Còn (phở, một) chỉ được , vì “một” đứng cạnh đủ thứ.
Bước 3: bỏ phần âm. Có hai lý do. Cặp chưa gặp lần nào có và , không dùng được. Còn PMI âm vừa phải (“gặp nhau ít hơn mức ngẫu nhiên”) cần kho văn bản cực lớn mới ước lượng tin được: với hai từ mỗi từ có xác suất cỡ , muốn khẳng định chúng gặp nhau ít hơn mức thì cần quan sát rất nhiều. Vì vậy người ta dùng PPMI (positive PMI), thay mọi giá trị âm bằng 0:
PMI thiên vị ngữ cảnh hiếm. Mẫu số có . Ngữ cảnh càng hiếm, càng nhỏ, nên chỉ cần gặp một lần là PMI đã cao. Trong ô code đầu, “sủa” chỉ xuất hiện trong một câu, mà , cao hơn , dù “con” đứng cạnh “chó” hai lần. Các cách giảm thiên lệch:
- Bỏ các cặp quá ít. Church và Hanks bỏ qua mọi cặp gặp nhau từ 5 lần trở xuống, vì với số đếm nhỏ, tỉ số của họ không ổn định.
- Làm trơn phân phối ngữ cảnh (context distribution smoothing). Làm trơn (smoothing) là sửa nhẹ các xác suất ước lượng để vài số đếm nhỏ không quyết định kết quả. Levy, Goldberg và Dagan (2015) thay bằng , với là tổng cột và . Lũy thừa nhỏ hơn 1 làm ngữ cảnh hiếm có xác suất lớn hơn một chút, nên PMI của chúng giảm. Trong thí nghiệm của họ, cách này cải thiện kết quả đều đặn qua các tác vụ và cấu hình, nhất là với PPMI.
Từ vectơ thưa sang vectơ dày: SVD rút gọn
Phần tiêu đề “Từ vectơ thưa sang vectơ dày: SVD rút gọn”Sau PPMI, mỗi từ vẫn là một hàng dài số, phần lớn bằng 0. Hai bất tiện: tốn bộ nhớ, và quan trọng hơn, hai từ cùng nhóm mà không có ngữ cảnh chung nào thì cosine bằng 0. Nếu kho chỉ có “phở … ngon” và “bún … thơm”, vectơ thưa không nối được phở với bún, dù “ngon” và “thơm” lại hay đi cùng nhau ở chỗ khác.
SVD rút gọn (truncated SVD) giải quyết cả hai. Gọi ma trận PPMI là . SVD viết nó thành:
Ở đây in đậm là một ma trận của SVD, khác với từ vựng . Các cột của là những hướng trong không gian từ, cho biết mỗi hướng mạnh bao nhiêu (giá trị suy biến ). Giữ hướng mạnh nhất:
với là cột đầu của , là góc trên trái của . Theo định lý Eckart–Young, đây là bản xấp xỉ hạng tốt nhất của , theo nghĩa tổng bình phương sai số nhỏ nhất. Hàng của ma trận là embedding chiều của từ : chỉ còn số, thường vài trăm, thay cho số, và hầu như không số nào bằng 0. Đó là vectơ dày.
Vì sao nén lại giúp? Để tái tạo tốt với ít hướng, SVD phải dùng một hướng chung cho các ngữ cảnh hay đi cùng nhau (ăn, bát, nóng, ngon). Phở và bún cùng dựa vào hướng đó, nên dù không có ngữ cảnh chung trực tiếp, chúng vẫn gần nhau. Turney và Pantel gọi đây là bắt được đồng xuất hiện bậc cao (high-order co-occurrence): hai từ gần nhau vì ngữ cảnh của chúng giống nhau, không cần ngữ cảnh trùng khớp. Những chi tiết lẻ tẻ, như “mèo nằm ngủ” và “chó sủa”, bị coi là nhiễu và bỏ đi.
Deerwester và cộng sự (1990) áp SVD rút gọn lên ma trận từ–văn bản và gọi cách làm này là LSA (latent semantic analysis, phân tích ngữ nghĩa tiềm ẩn). Các hướng giữ lại được coi là những “chủ đề” ẩn. Cũng có người dùng không nhân , hoặc nhân với ; Levy và cộng sự (2015) thấy bỏ thường cho kết quả tốt hơn trên các bộ đo tương tự từ. Ô code đầu dùng cho khớp định nghĩa ở trên; bạn có thể thử bỏ * S[:d] để so. Ở kho này, cosine (phở, xe_máy) giảm từ 0.16 xuống 0.05, còn các cặp cùng nhóm gần như không đổi.
Chọn thế nào? Không có số đúng chung. Nhìn các giá trị suy biến: nếu vài giá trị đầu lớn rồi tụt hẳn, số giá trị lớn đó là một gợi ý. Thường thì điểm tụt không rõ, nên hiểu biết về dữ liệu và kết quả trên tác vụ quan trọng hơn. Chọn quá nhỏ thì các nhóm nghĩa khác nhau bị ép vào chung một hướng; quá lớn thì giữ lại cả nhiễu. Với kho thật, người ta thường chọn vài trăm và chỉnh theo kết quả trên tác vụ.
Embedding là gì, và đánh giá thế nào
Phần tiêu đề “Embedding là gì, và đánh giá thế nào”Embedding là một vectơ số thực có số chiều cố định , đại diện cho một từ (hay một câu, một văn bản), sao cho quan hệ hình học giữa các vectơ (góc, khoảng cách) phản ánh quan hệ nghĩa. Từ embedding này được giữ nguyên trong cả lĩnh vực: hàng của là embedding, vectơ học bằng Word2Vec cũng là embedding, và lớp đầu tiên của mọi LLM là một bảng embedding.
Làm sao biết embedding tốt? Có hai cách:
- Đánh giá nội tại (intrinsic evaluation): so với phán đoán của người. Bộ tương tự từ (word similarity) gồm các cặp từ được người chấm điểm giống nhau, ví dụ WordSim-353 hay SimLex-999 cho tiếng Anh, ViSim-400 cho tiếng Việt (Nguyen và cộng sự, 2018). Ta tính cosine cho từng cặp, rồi đo xem thứ hạng cosine khớp thứ hạng của người tới đâu (tương quan Spearman). Một kiểu khác là bài tương tự quan hệ (analogy) “Hà Nội với Việt Nam như Paris với ?”, sẽ gặp ở bài Word2Vec, GloVe, FastText.
- Đánh giá ngoại tại (extrinsic evaluation): đưa embedding vào một tác vụ thật (phân loại, tìm kiếm) và xem kết quả có tốt hơn không. Đây mới là thước đo quan trọng nhất, vì điểm nội tại cao chưa chắc giúp tác vụ của bạn.
Giới hạn
Phần tiêu đề “Giới hạn”- Một từ, một vectơ. “Đường” trong “đi trên đường” và “cho thêm đường” có chung một hàng, nên vectơ của nó là hỗn hợp của hai nghĩa. Embedding theo ngữ cảnh, như của BERT, cho mỗi lần xuất hiện một vectơ riêng.
- Gần nhau chưa chắc là đồng nghĩa. “Nóng” và “lạnh” đứng trong ngữ cảnh rất giống nhau, nên vectơ của chúng gần nhau. Hộp thứ ba ở mục Lỗi hay gặp nói thêm.
- Ma trận rất lớn và SVD trên nó tốn kém; thêm văn bản mới thì phải đếm lại và tính lại. Word2Vec học thẳng vectơ dày bằng cách đoán từ, từng câu một, không cần lập ma trận.
- Từ chưa gặp không có vectơ, và từ hiếm có vectơ kém tin cậy. FastText ở bài sau ghép vectơ từ các mảnh ký tự để xử lý chuyện này.
Minh họa
Phần tiêu đề “Minh họa”Các bảng dưới lấy từ ô code đầu ở mục sau: 12 câu ngắn về món ăn, con vật và phương tiện, cửa sổ . Bảng đầu là 8 trong 33 cột của ma trận đồng xuất hiện . Hãy nhìn các khối số khác 0: mỗi nhóm từ có một vùng cột riêng.
| Từ đích | một | bát | nóng | rất | con | cả | đường | chạy | Tổng hàng |
|---|---|---|---|---|---|---|---|---|---|
| phở | 1 | 2 | 1 | 1 | 0 | 0 | 0 | 0 | 6 |
| bún | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 7 |
| mèo | 1 | 0 | 0 | 0 | 2 | 0 | 0 | 0 | 5 |
| chó | 1 | 0 | 0 | 0 | 2 | 1 | 0 | 0 | 5 |
| xe_máy | 0 | 0 | 0 | 1 | 0 | 0 | 1 | 1 | 6 |
| ô_tô | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 6 |
Cột “một” và “rất” là ngữ cảnh chung của nhiều nhóm. Chúng làm cosine giữa các nhóm lớn hơn 0, và đó là lý do cần PPMI để hạ trọng số của chúng. Tổng hàng tính trên cả 33 cột, không chỉ 8 cột in ra.
Bảng thứ hai là cosine của sáu cặp, tính trên hai loại vectơ: hàng PPMI (33 số, thưa) và embedding sau SVD rút gọn (, dày). Hãy so cột “Cùng nhóm” với hai cột số.
| Cặp | Cùng nhóm | Cosine PPMI | Cosine SVD, |
|---|---|---|---|
| phở, bún | có | 0.43 | 1.00 |
| mèo, chó | có | 0.34 | 1.00 |
| xe_máy, ô_tô | có | 0.53 | 0.98 |
| phở, mèo | không | 0.04 | 0.07 |
| phở, xe_máy | không | 0.10 | 0.16 |
| chó, ô_tô | không | 0.00 | 0.01 |
Trên PPMI, ba cặp cùng nhóm chỉ đạt 0.34 tới 0.53, vì mỗi từ còn những ngữ cảnh riêng lẻ (“nằm”, “ngủ”, “sủa”) làm các vectơ lệch nhau. Sau SVD, các cặp cùng nhóm lên gần 1, còn các cặp khác nhóm vẫn gần 0.
Ví dụ code
Phần tiêu đề “Ví dụ code”Code trong bài chạy thử lần cuối tháng 11/2026 bằng Pyodide 314.0.7 (Python 3.14, NumPy 2.4, scikit-learn 1.8), đúng bản chạy trên trang. Hai ô chạy độc lập, ngay trên trang. Ô thứ hai lần đầu chạy lâu hơn, vì trình duyệt phải tải thêm scikit-learn.
PPMI, cosine và SVD bằng NumPy
Phần tiêu đề “PPMI, cosine và SVD bằng NumPy”Ô này làm lại toàn bộ mục Lý thuyết trên 12 câu ngắn. Từ ghép như “xe máy” đã được nối bằng dấu gạch dưới, để mỗi từ là một token khi tách theo khoảng trắng (cách tách từ tiếng Việt thật có ở bài Tách từ). Code đếm với cửa sổ , tính PPMI, chạy SVD và giữ chiều, rồi in cosine của sáu cặp trên cả hai loại vectơ.
Đồng xuất hiện, PPMI, cosine và SVD rút gọn
# Đồng xuất hiện cửa sổ ±2, PPMI, cosine và SVD rút gọn trên corpus nhỏimport numpy as np
np.set_printoptions(precision=2, suppress=True)corpus = [ "tôi ăn một bát phở nóng", "bát phở này rất ngon", "tôi ăn một bát bún nóng", "mẹ nấu bún rất ngon", "nhà tôi nuôi một con mèo", "con mèo nằm ngủ cả ngày", "nhà tôi nuôi một con chó", "con chó sủa cả đêm", "tôi đi xe_máy trên đường", "xe_máy chạy rất nhanh", "tôi lái ô_tô trên đường", "ô_tô chạy về quê",]sents = [line.split() for line in corpus] # từ ghép đã nối bằng "_"vocab = sorted({w for sent in sents for w in sent})idx = {w: i for i, w in enumerate(vocab)}s, d = 2, 3 # cửa sổ ±s, giữ d chiều sau SVDF = np.zeros((len(vocab), len(vocab)))for sent in sents: for i, w in enumerate(sent): for j in range(max(0, i - s), min(len(sent), i + s + 1)): if j != i: F[idx[w], idx[sent[j]]] += 1# PPMI = max(0, log2 P(w,c) / (P(w) P(c)))N = F.sum()pw = F.sum(axis=1, keepdims=True) / Npc = F.sum(axis=0, keepdims=True) / Nwith np.errstate(divide="ignore"): # log2(0) = -inf, bị max cắt về 0 ppmi = np.maximum(np.log2(F / N / (pw * pc)), 0)# SVD rút gọn: mỗi từ còn d số thay cho |V| sốU, S, Vt = np.linalg.svd(ppmi)emb = U[:, :d] * S[:d]
def cos(M, a, b): u, v = M[idx[a]], M[idx[b]] return u @ v / (np.linalg.norm(u) * np.linalg.norm(v))
print(f"|V| = {len(vocab)}, nonzero = {(F > 0).mean():.0%}")print("singular values:", S[:5])for a, b in [("phở", "bún"), ("mèo", "chó"), ("xe_máy", "ô_tô"), ("phở", "mèo"), ("phở", "xe_máy"), ("chó", "ô_tô")]: print(f"{a:>6} {b:<6} ppmi {cos(ppmi, a, b):.2f}" f" svd {cos(emb, a, b):.2f}")for w in ["phở", "bún", "mèo", "chó", "xe_máy", "ô_tô"]: print(f"{w:>6}", emb[idx[w]])The first run downloads Python, about 10 MB.
Đọc kết quả:
|V| = 33, nonzero = 13%. Chỉ 13% số ô của khác 0, dù kho chỉ có 12 câu. Với kho thật, tỉ lệ này còn nhỏ hơn nhiều.- Giá trị suy biến
12.36 12.18 10.75 8.58 7.76. Không có điểm gãy rõ. Khoảng cách lớn nhất trong năm giá trị đầu nằm giữa giá trị thứ 3 và thứ 4, và ta biết trước kho có ba nhóm, nên chọn . Đặt hay cũng cho kết quả gần như vậy: cặp cùng nhóm từ 0.93 trở lên, cặp khác nhóm từ 0.26 trở xuống. - Cosine. Đúng như bảng ở mục Minh họa. Cặp (phở, xe_máy) có PPMI cosine 0.10 chứ không phải 0, vì cả hai cùng đứng cạnh “rất” (“phở này rất ngon”, “xe_máy chạy rất nhanh”).
- Tọa độ. Tọa độ thứ nhất dương ở mọi từ: đó là một hướng chung, không phân biệt nhóm. Ba nhóm tách nhau ở hai tọa độ còn lại: món ăn có tọa độ thứ ba dương lớn (phở 2.61, bún 4.04), con vật có tọa độ thứ hai dương (2.43), phương tiện có cả tọa độ thứ hai và thứ ba âm. Mèo và chó trùng tọa độ: các ngữ cảnh làm chúng khác nhau (“nằm”, “ngủ” với mèo; “sủa”, “cả” với chó) quá ít, nên SVD bỏ đi như nhiễu. Dấu của từng cột trong có thể đảo giữa các bản thư viện, nhưng cosine thì không đổi.
Hãy thử hai thay đổi. Đổi cos(ppmi, a, b) thành cos(F, a, b) để so trên số đếm thô: cosine (phở, bún) thành 0.67, cao hơn PPMI. Trên kho tí hon này, PPMI đẩy các ngữ cảnh hiếm lên quá mức, đúng thiên lệch đã nói ở mục Lý thuyết; với kho thật, PPMI thường tốt hơn số đếm thô (Turney và Pantel, 2010, dẫn Bullinaria và Levy, 2007). Rồi đổi d = 3 thành d = 2: câu hỏi cuối ở mục Kiểm tra nhanh nói chuyện gì xảy ra. Muốn thử làm trơn , hãy nâng tổng mỗi cột lên lũy thừa 0.75 trước khi chia để tính : thứ tự đảo lại, thấp hơn .
TF-IDF và cosine giữa văn bản bằng scikit-learn
Phần tiêu đề “TF-IDF và cosine giữa văn bản bằng scikit-learn”Ô này chuyển sang ma trận từ–văn bản. Bốn văn bản ngắn được đưa thẳng vào TfidfVectorizer, không tách từ trước, nên mỗi âm tiết là một token (“Hà Nội” thành “hà” và “nội”). Ô làm ba việc: cho thấy mẫu tách token mặc định bỏ mất chữ gì, in idf của ba từ, và tính cosine giữa mọi cặp văn bản.
TF-IDF và cosine giữa văn bản
# TF-IDF và cosine giữa các văn bản bằng scikit-learnimport numpy as npfrom sklearn.feature_extraction.text import TfidfVectorizerfrom sklearn.metrics.pairwise import cosine_similarity
np.set_printoptions(precision=2, suppress=True)docs = [ "tôi ăn phở bò ở Hà Nội", "phở bò và bún chả Hà Nội", "con mèo nhà tôi rất lười", "đi ô tô từ Hà Nội về quê",]# Mẫu mặc định (?u)\b\w\w+\b bỏ mọi token một ký tựdefault = TfidfVectorizer().fit(docs)print("dropped:", sorted(set(" ".join(docs).lower().split()) - set(default.vocabulary_)))# Giữ cả âm tiết một ký tự (\w cần văn bản ở dạng NFC)vec = TfidfVectorizer(token_pattern=r"(?u)\b\w+\b")X = vec.fit_transform(docs) # ma trận thưa, mỗi hàng một văn bảnterms = vec.get_feature_names_out()print("shape:", X.shape)for t in ["hà", "phở", "mèo"]: print(f"idf({t}) = {vec.idf_[vec.vocabulary_[t]]:.3f}")row = X[0].toarray().ravel() # văn bản 0, đã chuẩn hóa L2print("doc 0:", ", ".join(f"{terms[j]} {row[j]:.2f}" for j in np.flatnonzero(row)))print(f"norm doc 0 = {np.linalg.norm(row):.2f}")print(cosine_similarity(X))The first run downloads Python, about 10 MB.
Đọc kết quả:
dropped: ['ô', 'ở']. Mẫu mặc định(?u)\b\w\w+\bchỉ giữ token có từ hai ký tự trở lên. Với tiếng Anh, nó chỉ bỏ “a” hay “I”; với tiếng Việt, nó bỏ cả những âm tiết có nghĩa như “ở”, “ô”, “ý”, “ủ”. Ô code dùngtoken_pattern=r"(?u)\b\w+\b": mọi dãy chữ và số, kể cả dài một ký tự, là một token, còn dấu câu bị bỏ. Bộ tách mặc định cũng chuyển hết về chữ thường (lowercase=True), nên “Hà” thành “hà”.shape: (4, 21). scikit-learn xếp mỗi văn bản thành một hàng, tức chuyển vị của ma trận từ–văn bản ở mục Lý thuyết.- idf: 1.223, 1.511, 1.916. “hà” có trong 3 văn bản, “phở” trong 2, “mèo” trong 1, khớp với phép tính tay ở mục Lý thuyết.
- Văn bản 0. Mọi từ trong “tôi ăn phở bò ở Hà Nội” xuất hiện một lần, nên trọng số chỉ tỉ lệ với idf: “ăn”, “ở” (chỉ có ở văn bản này) được 0.46, “hà”, “nội” (có ở 3 văn bản) chỉ được 0.30. Chuẩn của hàng bằng 1.00 vì đã chuẩn hóa L2.
- Ma trận cosine. Văn bản 0 và 1 (cùng nói về phở bò Hà Nội) được 0.42, cao nhất. Văn bản 0 và 3 chỉ chung “Hà Nội” nên được 0.14. Văn bản 1 và 2 không có từ chung nào nên bằng 0. Đây chính là giới hạn của vectơ thưa: “con mèo nhà tôi rất lười” và một câu về “chú chó” sẽ có cosine 0 dù cùng nói về thú nuôi.
Lỗi hay gặp
Phần tiêu đề “Lỗi hay gặp”Kiểm tra nhanh
Phần tiêu đề “Kiểm tra nhanh”Tóm tắt và đọc thêm
Phần tiêu đề “Tóm tắt và đọc thêm”Ý chính:
- Giả thuyết phân bố: từ dùng trong ngữ cảnh giống nhau thì có nghĩa gần nhau. One-hot không đo được điều này, vì mọi cặp từ khác nhau đều có tích vô hướng bằng 0.
- Ma trận từ–văn bản đếm từ trong văn bản; ma trận từ–ngữ cảnh đếm từ trong cửa sổ . Số đếm thô bị từ phổ biến chi phối, nên đổi thành TF-IDF (; scikit-learn dùng với là số văn bản, rồi chuẩn hóa L2) hoặc PPMI (, thiên vị ngữ cảnh hiếm).
- Cosine so hướng, không so độ dài, nên từ hay văn bản dài không tự động “giống” mọi thứ.
- SVD rút gọn (LSA) nén vectơ thưa chiều thành embedding dày chiều, nối được các từ không có ngữ cảnh chung trực tiếp. Mỗi từ vẫn chỉ có một vectơ; Word2Vec ở bài sau học vectơ dày trực tiếp bằng cách đoán từ.
Đọc thêm:
- Jurafsky, D., Martin, J. H. (2026). Speech and Language Processing, bản nháp 3rd ed. ngày 19/8/2026, chương 5 “Embeddings” (mục 5.1–5.4 và 5.9) và chương 11, mục 11.1.2 (tf-idf). Giáo trình mở, có ví dụ “ongchoi” cùng tinh thần với từ “thồn” ở đầu bài. Bản nháp này không còn mục PPMI; phần đó xem ở nguồn thứ hai.
- Turney, P. D., Pantel, P. (2010). From Frequency to Meaning: Vector Space Models of Semantics, JAIR 37, mục 2, 4.2 và 4.3. Tổng quan ba loại ma trận, các cách đặt trọng số (TF-IDF, PMI, PPMI) và bốn cách hiểu SVD rút gọn.
- Church, K. W., Hanks, P. (1990). Word Association Norms, Mutual Information, and Lexicography, Computational Linguistics 16(1). Bài đưa PMI vào ngôn ngữ học máy tính, kèm nhận xét về số đếm nhỏ.
- Deerwester, S., Dumais, S. T., Furnas, G. W., Landauer, T. K., Harshman, R. (1990). Indexing by Latent Semantic Analysis, Journal of the American Society for Information Science 41(6). Bài gốc của LSA: SVD rút gọn trên ma trận từ–văn bản.
- Spärck Jones, K. (1972). A Statistical Interpretation of Term Specificity and Its Application in Retrieval, Journal of Documentation 28(1): 11–21. Nguồn của ý tưởng idf: từ có mặt trong ít văn bản thì có ích hơn khi tìm kiếm. Harris (1954) và Firth (1957) ở mục Trực giác được dẫn qua Turney và Pantel (2010), mục 2.
- scikit-learn (2025). Tf–idf term weighting, tài liệu bản 1.8, cùng trang API
TfidfVectorizer. Công thức idf làm trơn, chuẩn hóa L2 và tham sốtoken_pattern, đúng bản chạy trên trang.
Your browser is blocking storage. Your progress will be lost when you reload the page.
Content licensed under CC BY-NC-SA 4.0. Code under the MIT license.About