Word2Vec (CBOW, Skip-gram), GloVe, FastText
Mục tiêu
Phần tiêu đề “Mục tiêu”Sau bài này, bạn sẽ:
- Hiểu ý tưởng của Word2Vec: học embedding dày bằng một bài toán dự đoán giả, với hai kiến trúc skip-gram và CBOW (continuous bag-of-words).
- Tự dẫn được hàm mất mát và gradient của negative sampling (skip-gram with negative sampling, SGNS), và giải thích vì sao nó rẻ hơn softmax trên cả từ vựng.
- Đọc được hàm mất mát của GloVe và cách FastText ghép vectơ từ n-gram ký tự (character n-gram), kể cả cho từ chưa gặp (out-of-vocabulary).
- Nhận ra giới hạn của các embedding này: phép tương tự (analogy) vectơ không thần kỳ như quảng cáo, embedding mang theo thiên lệch (bias) của dữ liệu, và mỗi từ chỉ có một vectơ dù có nhiều nghĩa.
Cần biết trước
Phần tiêu đề “Cần biết trước”- Vector Semantics và Embeddings, thuộc phần Xử lý ngôn ngữ tự nhiên (Đã học)
- Mạng nơ-ron nhân tạo: perceptron, MLP, lan truyền xuôi, hàm kích hoạt, thuộc phần Học sâu (Đã học)
Trực giác
Phần tiêu đề “Trực giác”Từ đếm sang đoán
Phần tiêu đề “Từ đếm sang đoán”Ở bài Vector Semantics và Embeddings, bạn đã gặp giả thuyết phân bố (distributional hypothesis): từ có nghĩa gần nhau thì xuất hiện trong ngữ cảnh giống nhau. Cách làm ở đó là đếm: lập ma trận từ–ngữ cảnh, đổi sang PPMI, rồi nén bằng SVD.
Word2Vec (Mikolov và cộng sự, 2013) đi đường khác: đoán. Hãy đọc câu sau, có một chỗ trống:
Sáng nay tôi ăn một bát ____ nóng hổi.
Bạn sẽ đoán “phở”, “bún”, “cháo”, có lẽ “cơm”. Bạn sẽ không đoán “mèo” hay “trà”. Muốn đoán đúng, bạn phải biết phở, bún, cháo là cùng một loại thứ: món ăn đựng trong bát, ăn nóng. Một mô hình tập đoán như vậy trên hàng tỉ câu sẽ buộc phải xếp phở, bún, cháo vào cùng một vùng của không gian vectơ, dù không ai dạy nó “phở là món ăn”.
Bài toán giả, sản phẩm thật
Phần tiêu đề “Bài toán giả, sản phẩm thật”Điểm lạ của Word2Vec: ta không cần mô hình đoán từ giỏi. Việc đoán chỉ là cái cớ. Thứ ta giữ lại sau huấn luyện là bảng vectơ mà mô hình dùng để đoán. Hai cách đặt câu hỏi:
- CBOW: biết các từ xung quanh (“một”, “bát”, “nóng”, “hổi”), đoán từ ở giữa (“phở”).
- Skip-gram: biết từ ở giữa (“phở”), đoán từng từ xung quanh.
Dữ liệu huấn luyện là văn bản thô, không cần ai gán nhãn: mỗi câu tự tạo ra các cặp (từ, ngữ cảnh) để đoán. Người ta gọi kiểu học này là tự giám sát (self-supervised).
Bài toán của bài này là: từ văn bản thô, học cho mỗi từ một vectơ chiều sao cho các từ hay đi cùng ngữ cảnh giống nhau có vectơ gần nhau, và làm việc đó đủ rẻ để chạy trên hàng tỉ từ.
Lý thuyết
Phần tiêu đề “Lý thuyết”Skip-gram: phát biểu bài toán
Phần tiêu đề “Skip-gram: phát biểu bài toán”Đi dọc văn bản. Ở mỗi vị trí, gọi từ ở đó là từ đích . Các từ cách nó không quá vị trí là từ ngữ cảnh (cửa sổ , ví dụ ). Với câu “tôi ăn một bát phở nóng hổi”, từ đích “phở” cho bốn cặp: (phở, một), (phở, bát), (phở, nóng), (phở, hổi).
Gọi là từ vựng, có từ. Mỗi từ có hai vectơ chiều:
- khi nó đóng vai từ đích, xếp thành hàng của ma trận ;
- khi nó đóng vai từ ngữ cảnh, xếp thành hàng của .
Lưu ý ký hiệu: trong bài này là vectơ của từ đích, không phải vectơ trọng số chung như ở các bài ML. Vì sao lại cần hai vectơ? Một từ hiếm khi đứng ngay cạnh chính nó. Nếu chỉ có một vectơ, điểm của cặp (từ, chính nó) là , luôn dương và càng lớn khi vectơ càng dài, nên mô hình có xu hướng tin mỗi từ hay đứng cạnh chính nó. Hai bảng riêng gỡ được ràng buộc đó.
Skip-gram gốc cho xác suất “thấy quanh ” bằng softmax trên toàn bộ từ vựng:
Tử số lớn khi và cùng hướng. Mẫu số cộng trên mọi từ để các xác suất cộng lại bằng 1. Hàm mất mát là cross-entropy: tổng của trên mọi cặp lấy từ văn bản.
Vì sao softmax trên cả từ vựng đắt
Phần tiêu đề “Vì sao softmax trên cả từ vựng đắt”Mẫu số cần tích vô hướng, và gradient của nó chạm tới mọi hàng của , cho từng cặp. Mikolov và cộng sự (2013b) huấn luyện trên một tỉ từ tin tức, từ vựng 692 nghìn từ, và nói thẳng rằng chi phí tỉ lệ với , thường từ tới , nên công thức trên “không thực tế”. Một cách giảm chi phí là softmax phân cấp (hierarchical softmax), dùng cây nhị phân để chỉ tính khoảng nút. Cách được dùng nhiều hơn, và là trọng tâm của bài, là negative sampling.
Negative sampling: đổi sang câu hỏi có/không
Phần tiêu đề “Negative sampling: đổi sang câu hỏi có/không”Thay câu hỏi “trong 692 nghìn từ, từ nào đứng cạnh phở?” bằng câu hỏi dễ hơn: “cặp (phở, bát) là cặp thật lấy từ văn bản, hay cặp ghép bừa?” Đó là phân loại nhị phân, nên dùng sigmoid:
Dấu nghĩa là “cặp thật”. Xác suất cặp là giả bằng .
Với mỗi cặp thật , ta bốc ngẫu nhiên từ làm mẫu âm (negative sample), coi các cặp là giả. Hàm mất mát cho một cặp thật và cặp giả của nó:
- Số hạng đầu nhỏ khi gần 1: mô hình tin cặp thật là thật.
- Mỗi số hạng trong tổng dùng , tức xác suất “là giả”. Nó nhỏ khi mô hình tin cặp ghép bừa là giả.
- Mỗi cặp giờ chỉ cần tích vô hướng thay cho . Với và : lần ít hơn (tự tính).
Bốc mẫu âm từ đâu? Mikolov và cộng sự thử nhiều phân phối và thấy tốt nhất là tần suất unigram mũ 3/4:
Mũ 3/4 kéo từ phổ biến xuống, đẩy từ hiếm lên. Ví dụ, “của” gặp nhiều gấp 500 lần “phở” thì chỉ được bốc nhiều gấp lần (tự tính). Về , bài báo gợi ý 5 tới 20 với dữ liệu nhỏ, và chỉ 2 tới 5 với dữ liệu lớn.
Dẫn gradient từng bước
Phần tiêu đề “Dẫn gradient từng bước”Bước 1: đạo hàm của . Sigmoid có đạo hàm . Theo quy tắc dây chuyền cho :
Với , đạo hàm của phần trong là , nên:
Dấu bằng cuối dùng lại .
Bước 2: tích vô hướng theo từng vectơ. Với , đạo hàm theo là , và theo là .
Bước 3: ghép lại. Mỗi số hạng của có dấu trừ ở trước, nên đổi dấu kết quả Bước 1 rồi nhân với Bước 2:
Dòng thứ ba cộng hai đóng góp, vì có mặt trong mọi số hạng.
Bước 4: đọc ý nghĩa. Gradient descent cập nhật . Hệ số luôn âm, nên bước này cộng thêm một phần vào : kéo ngữ cảnh thật lại gần từ đích. Ngược lại, mỗi bị trừ đi một phần : đẩy ra xa. Lực kéo hay đẩy tỉ lệ với mức mô hình đang sai: cặp thật mà đã gần 1 thì gần như không đổi.
CBOW: đoán từ giữa từ trung bình ngữ cảnh
Phần tiêu đề “CBOW: đoán từ giữa từ trung bình ngữ cảnh”CBOW đảo vai. Ở vị trí , lấy trung bình vectơ của các từ ngữ cảnh, gọi là tập các vị trí đó:
Ở đây là hàng của ứng với từ tại vị trí : các từ xung quanh giờ là đầu vào. Rồi mô hình đoán từ ở giữa bằng , với là hàng của ứng với từ ở giữa, cũng kèm mẫu âm. Gradient giống hệt phần trên, chỉ thay bằng , rồi chia đều cho các từ ngữ cảnh.
Lấy trung bình làm mất trật tự từ, nên mới có tên “bag-of-words”. Mỗi vị trí chỉ cho một lần đoán thay vì lần, nên CBOW nhanh hơn. Trong bảng so sánh của Mikolov và cộng sự (2013a) với vectơ 1000 chiều trên 6 tỉ từ, skip-gram đúng 66.1% câu hỏi tương tự về nghĩa, so với 57.3% của CBOW; CBOW nhỉnh hơn ở câu hỏi ngữ pháp (68.9% so với 65.1%).
Hình ở mục Minh họa đặt hai kiến trúc cạnh nhau trên cùng một câu.
Vài chi tiết làm Word2Vec chạy tốt
Phần tiêu đề “Vài chi tiết làm Word2Vec chạy tốt”- Subsampling từ phổ biến. Cặp (phở, của) ít thông tin vì “của” đứng cạnh gần như mọi từ. Mikolov và cộng sự bỏ mỗi lần xuất hiện của từ với xác suất , với là tần suất tương đối của từ và ngưỡng . Từ chiếm 1% văn bản bị bỏ khoảng 96.8% số lần (tự tính). Huấn luyện nhanh hơn nhiều lần, và vectơ của từ hiếm tốt hơn.
- Cửa sổ co giãn và bảng giữ lại. Ở mỗi từ, kích thước cửa sổ được chọn ngẫu nhiên từ 1 tới (Mikolov và cộng sự, 2013a), nên từ gần được dùng thường hơn từ xa. Sau huấn luyện thường giữ , bỏ .
SGNS thật ra đang phân tích ma trận PMI
Phần tiêu đề “SGNS thật ra đang phân tích ma trận PMI”Word2Vec trông khác hẳn PPMI cộng SVD ở bài trước. Levy và Goldberg (2014) chỉ ra hai cách gần nhau hơn ta tưởng. Với mẫu âm bốc theo tần suất unigram thường (không mũ 3/4) và số chiều đủ lớn, SGNS đạt tối ưu khi:
PMI ở đây tính bằng logarit tự nhiên , vì sigmoid dùng ; bài trước dùng , hai cách chỉ khác một hằng số nhân. Khi mẫu âm bốc theo như Word2Vec thật, trong PMI được thay bằng phân phối ngữ cảnh làm trơn mũ 0.75, đúng mục làm trơn ở bài Vector Semantics và Embeddings (Levy, Goldberg và Dagan, 2015).
Tức là cố gắng tái tạo ma trận PMI dịch xuống một hằng số . Word2Vec ngầm phân tích một ma trận đồng xuất hiện, như SVD làm tường minh. Khác biệt nằm ở trọng số: SGNS chú ý cặp gặp nhiều hơn cặp hiếm, và không bao giờ phải lập ra ma trận . Từ kết quả này, họ đề xuất ma trận Shifted PPMI, , rồi SVD trên đó.
GloVe: hồi quy trên log số lần đồng xuất hiện
Phần tiêu đề “GloVe: hồi quy trên log số lần đồng xuất hiện”GloVe (Pennington, Socher, Manning, 2014) đi đường ngược lại: đếm trước, học sau. Gọi là số lần từ nằm trong cửa sổ quanh từ , đếm trên cả kho văn bản. GloVe tìm vectơ sao cho tích vô hướng xấp xỉ , bằng bình phương tối thiểu có trọng số (weighted least squares). Bài báo gọi hàm mất mát là :
- , là hai hệ số tự do riêng cho mỗi từ, hấp thụ chuyện từ hay từ vốn phổ biến.
- Chỉ cộng trên các ô , vì không xác định. Bài báo viết tổng trên mọi ô nhưng đặt , nên hai cách như nhau. Theo bài báo, 75–95% số ô bằng 0 tùy từ vựng và kho văn bản, nên tổng này thưa.
- Hàm trọng số khi , và bằng 1 khi ; bài báo dùng . Cặp gặp 1 lần có trọng số , gặp 10 lần , từ 100 lần trở lên là 1 (tự tính). Cặp hiếm, nhiều nhiễu, ít ảnh hưởng; cặp quá phổ biến không lấn át phần còn lại.
Vectơ cuối của GloVe là tổng hai bảng, . Ý tưởng dẫn tới GloVe là tỉ số xác suất: trong ví dụ của bài báo, “solid” gặp quanh “ice” nhiều hơn quanh “steam” khoảng 8.9 lần, còn “water” (liên quan cả hai) có tỉ số gần 1. Hiệu được thiết kế để mang thông tin tỉ số đó.
FastText: từ là tổng các mảnh ký tự
Phần tiêu đề “FastText: từ là tổng các mảnh ký tự”Word2Vec và GloVe coi mỗi từ là một ký hiệu nguyên khối. “nhà_thơ” và “nhà_báo” chung tiền tố nhưng vectơ của chúng không chia sẻ gì, và một từ chưa từng gặp thì không có vectơ. FastText (Bojanowski và cộng sự, 2017) sửa chỗ này:
- Thêm
<ở đầu và>ở cuối từ, để phân biệt tiền tố, hậu tố với phần giữa. - Lấy mọi n-gram ký tự dài 3 tới 6. Từ “nhà” thành
<nh,nhà,hà>,<nhà,nhà>,<nhà>. - Thêm chính từ đó như một chuỗi đặc biệt (nếu nó có trong từ vựng).
- Mỗi n-gram có một vectơ . Vectơ của từ là tổng các vectơ đó, rồi huấn luyện như SGNS.
Để không phải lưu một bảng cho hàng chục triệu n-gram, FastText băm (hash) mỗi n-gram vào một trong ô bằng hàm FNV-1a, với . Hai n-gram khác nhau có thể rơi chung một ô; mô hình chấp nhận chút va chạm đó.
Lợi ích: từ hiếm mượn được thông tin từ các mảnh chung với từ phổ biến, và từ chưa gặp vẫn có vectơ bằng cách cộng các n-gram của nó. Bài báo lấy ví dụ tiếng Phần Lan, nơi danh từ có 15 cách (case), và đo trên các ngôn ngữ như tiếng Đức, tiếng Séc, tiếng Nga: lợi ích rõ nhất ở những ngôn ngữ biến đổi hình thái nhiều.
Còn tiếng Việt thì sao? Thành thật mà nói, lợi ích nhỏ hơn. Tiếng Việt viết theo âm tiết cách nhau bằng dấu cách, không chia đuôi theo thì hay số. Nếu tách token theo dấu cách, mỗi token là một âm tiết ngắn (thường 1 tới 7 chữ cái), nên n-gram 3–6 ký tự gần như chỉ lặp lại âm tiết đó. Lợi ích lớn hơn khi văn bản đã tách từ, nối âm tiết bằng _ (bài Tách từ): nhà_văn, nhà_thơ, nhà_báo chung các mảnh <nhà_, đúng với cách tiếng Việt tạo từ ghép. FastText cũng giúp với lỗi gõ, tên riêng và từ mượn. Hai bẫy riêng của tiếng Việt:
- Văn bản không dấu là một chuỗi khác hẳn. “người” và “nguoi” chỉ chung đúng một n-gram,
<ng(tự đếm), nên FastText không tự nối được hai cách viết. Muốn dùng văn bản không dấu, hãy khôi phục dấu trước, hoặc học vectơ trên dữ liệu cùng kiểu. - Cùng một chữ có dấu có thể là nhiều chuỗi Unicode. Dạng dựng sẵn NFC và dạng tổ hợp NFD của “người” khác nhau từng ký tự, nên n-gram khác nhau. Chuẩn hóa Unicode (bài Chuẩn hóa văn bản) để cùng một chữ luôn ra cùng một chuỗi.
Bạn không phải tự huấn luyện để dùng thử: Grave và cộng sự (2018) công bố vectơ FastText cho 157 ngôn ngữ, có tiếng Việt (cc.vi.300.bin, trang tải, giấy phép CC BY-SA 3.0). Bộ vectơ học trên Common Crawl và Wikipedia, 300 chiều, kiến trúc CBOW, n-gram ký tự dài 5. Văn bản tiếng Việt được tách từ bằng UETsegmenter trước khi học, nên bộ vectơ có token kiểu nhà_văn.
Phép tương tự vectơ và giới hạn của nó
Phần tiêu đề “Phép tương tự vectơ và giới hạn của nó”Ví dụ nổi tiếng nhất của Word2Vec: vectơ gần nhất với là . Nói bằng tiếng Việt: “vua” trừ “đàn ông” cộng “phụ nữ” ra “nữ hoàng”. Cách giải một câu ” với như với ?”:
Ý trực quan: hiệu là một “hướng giới tính”, cộng hướng đó vào “king” thì tới “queen”. Nhưng có hai điều cần biết (và một hộp ở mục Lỗi hay gặp):
- Phải loại các từ đầu vào. Điều kiện không phải chi tiết nhỏ. Mikolov và cộng sự (2013b) ghi rõ họ loại các từ đầu vào khỏi phép tìm. Không loại thì từ gần nhất thường chính là một từ đầu vào hoặc biến thể của nó (Jurafsky và Martin, SLP3).
- Chỉ chạy tốt với vài loại quan hệ. Thủ đô–quốc gia, dạng số nhiều, so sánh hơn thì tốt; nhiều quan hệ khác thì kém hẳn.
Thiên lệch trong embedding
Phần tiêu đề “Thiên lệch trong embedding”Embedding học từ văn bản người viết, nên học luôn định kiến trong đó. Bolukbasi và cộng sự (2016) dùng vectơ Word2Vec học trên Google News và thấy phép tương tự cho ra cặp như “man : computer programmer :: woman : homemaker” (đàn ông với lập trình viên như phụ nữ với nội trợ). Họ cho thấy thiên lệch giới tính nằm gần một hướng trong không gian, và đề xuất cách chiếu bớt hướng đó khỏi các từ trung tính về giới. Gonen và Goldberg (2019) cho thấy cách làm này chỉ che bớt: thiên lệch vẫn còn trong cách các từ xếp gần nhau. Đây vẫn là vấn đề mở (SLP3 mục 5.8).
Nếu bạn dùng embedding trong hệ thống lọc hồ sơ hay chấm điểm, thiên lệch này đi thẳng vào quyết định. Hãy đo nó trên dữ liệu của bạn trước khi tin.
Tĩnh và theo ngữ cảnh
Phần tiêu đề “Tĩnh và theo ngữ cảnh”Cả ba phương pháp trong bài cho embedding tĩnh (static embedding): mỗi từ đúng một vectơ, dùng cho mọi câu. “Đường” trong “một cân đường” và trong “con đường” có cùng một vectơ, một kiểu trung bình của hai nghĩa. Embedding theo ngữ cảnh (contextual embedding) cho mỗi lần xuất hiện một vectơ riêng, tính từ cả câu; bài BERT dạy cách làm đó.
Giới hạn
Phần tiêu đề “Giới hạn”- Một vectơ cho mọi nghĩa (mục trên). Mô hình theo ngữ cảnh ở bài BERT xử lý chuyện này.
- Từ chưa gặp: Word2Vec và GloVe không có vectơ; FastText ghép được (ô code thứ hai).
- Thiên lệch của dữ liệu đi vào vectơ, và phép tương tự có thể khuếch đại nó.
- Không có trật tự từ. Trong cửa sổ, từ đứng trước hay sau từ đích đều được tính như nhau. Và nếu lấy trung bình vectơ các từ làm vectơ câu, “chó cắn mèo” và “mèo cắn chó” cho đúng cùng một vectơ.
Minh họa
Phần tiêu đề “Minh họa”Hình dưới đặt hai kiến trúc của Word2Vec cạnh nhau trên cùng một câu. Hãy nhìn chiều của mũi tên: CBOW gom bốn từ xung quanh thành một vectơ rồi đoán từ giữa, còn skip-gram đi từ một từ giữa ra bốn lần đoán riêng.
Bảng dưới so ba phương pháp theo cùng các tiêu chí.
| Tiêu chí | Word2Vec (SGNS) | GloVe | FastText |
|---|---|---|---|
| Học từ | Từng cặp (từ, ngữ cảnh) khi quét văn bản | Bảng đếm đồng xuất hiện của cả kho văn bản | Từng cặp, như SGNS |
| Hàm mất mát | Phân loại cặp thật / giả bằng sigmoid | Bình phương tối thiểu có trọng số trên | Như SGNS, nhưng vectơ từ là tổng n-gram |
| Đơn vị có vectơ | Từ | Từ | N-gram ký tự 3–6 (và từ) |
| Từ chưa gặp | Không có vectơ | Không có vectơ | Cộng vectơ các n-gram |
| Nối với PMI | Ngầm phân tích PMI dịch xuống | Tường minh khớp | Như SGNS |
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, với Python 3.14 và NumPy 2.4 (bản có trong Pyodide). Cả hai ô chạy ngay trên trang và chạy độc lập: ô nào cũng tự import và tự tạo dữ liệu.
Skip-gram với negative sampling bằng NumPy
Phần tiêu đề “Skip-gram với negative sampling bằng NumPy”Ô dưới dựng một kho văn bản (corpus) nhỏ tự viết: 180 câu từ ba khuôn câu, mỗi khuôn cho một nhóm từ. Con vật (mèo, chó, gà, vịt) đi với loại từ “con”; món ăn (phở, bún, cháo, cơm) đi với “bát”; đồ uống (trà, sữa, bia, nước) đi với “ly”. Từ vựng có 40 từ, cửa sổ cho 2604 cặp (từ, ngữ cảnh). Mô hình có chiều, mẫu âm bốc theo , học bằng gradient descent theo lô 64 cặp, , 13 lượt (epoch). Dòng gw tính . Ba dòng np.add.at trừ lần ba gradient vào (ngữ cảnh thật), (mẫu âm) và . Gradient của các cặp trong lô được cộng lại, không lấy trung bình; np.add.at cộng dồn đúng khi một từ xuất hiện nhiều lần trong cùng một lô.
Skip-gram với negative sampling trên kho văn bản tiếng Việt nhỏ
# Skip-gram với negative sampling (SGNS) bằng NumPy trên một kho văn bản nhỏimport numpy as np
rng = np.random.default_rng(0)# Ba nhóm từ, mỗi nhóm đi với ngữ cảnh riêng (loại từ "con", "bát", "ly")groups = { "con {} chạy ngoài sân|nhà tôi nuôi một con {}|con {} đang ngủ": ["mèo", "chó", "gà", "vịt"], "tôi ăn một bát {} nóng|bát {} này rất ngon|mẹ nấu {} buổi sáng": ["phở", "bún", "cháo", "cơm"], "anh uống một ly {} lạnh|ly {} này hơi ngọt|rót {} ra ly": ["trà", "sữa", "bia", "nước"],}corpus = []for tpl, words in groups.items(): for _ in range(60): s = rng.choice(tpl.split("|")).format(rng.choice(words)) corpus.append(s.split())vocab = sorted({w for s in corpus for w in s})idx = {w: i for i, w in enumerate(vocab)}V, d, k, win = len(vocab), 10, 3, 2
# Cặp (từ đích, từ ngữ cảnh) trong cửa sổ ±winpairs = np.array([(idx[s[i]], idx[s[j]]) for s in corpus for i in range(len(s)) for j in range(max(0, i - win), min(len(s), i + win + 1)) if j != i])count = np.bincount(pairs[:, 1], minlength=V)p_neg = count ** 0.75 / (count ** 0.75).sum() # unigram mũ 3/4
W = rng.normal(0, 0.1, (V, d)) # vectơ từ đíchC = rng.normal(0, 0.1, (V, d)) # vectơ ngữ cảnhsig = lambda z: 1 / (1 + np.exp(-z))eta, B = 0.03, 64 # tốc độ học, cỡ lôfor epoch in range(13): order, total = rng.permutation(len(pairs)), 0.0 for b0 in range(0, len(order), B): w, c = pairs[order[b0:b0 + B]].T # chỉ số w, c_pos n = rng.choice(V, size=(len(w), k), p=p_neg) # k mẫu âm mỗi cặp sp = sig((W[w] * C[c]).sum(1)) # σ(c_pos·w) sn = sig((W[w][:, None] * C[n]).sum(2)) # σ(c_neg·w) total -= np.log(sp).sum() + np.log(1 - sn).sum() # Ba gradient dẫn trong bài, rồi trừ eta lần gradient gw = (sp - 1)[:, None] * C[c] + (sn[..., None] * C[n]).sum(1) np.add.at(C, c, -eta * (sp - 1)[:, None] * W[w]) np.add.at(C, n, -eta * sn[..., None] * W[w][:, None]) np.add.at(W, w, -eta * gw) if epoch % 3 == 0: print(f"epoch {epoch:2d} loss {total / len(pairs):.3f}")
E = W / np.linalg.norm(W, axis=1, keepdims=True) # chuẩn hóa để lấy cosineprint(f"cos(mèo, phở) = {E[idx['mèo']] @ E[idx['phở']]:.2f}")for q in ["mèo", "phở", "trà"]: sims = E @ E[idx[q]] top = [i for i in np.argsort(-sims) if i != idx[q]][:3] print(q, "->", ", ".join(f"{vocab[i]} {sims[i]:.2f}" for i in top))Lần chạy đầu cần tải Python, khoảng 10 MB.
Đọc kết quả:
- Loss đi từ 2.751 xuống 1.132. Ở đầu, mọi vectơ gần 0 nên mọi tích vô hướng gần 0 và . Loss của một cặp thật cùng 3 mẫu âm khi đó là (tự tính), khớp số đầu tiên. Số in ra là trung bình trong lúc học ở mỗi lượt, nên lượt 0 đã thấp hơn 2.77 một chút.
- Loss không về 0, và không nên về 0. Mẫu âm được bốc ngẫu nhiên, nên đôi khi chính là một ngữ cảnh thật. “một” đứng cạnh mọi nhóm, nên mô hình không thể chắc chắn mọi cặp chứa “một” là giả.
- Hàng xóm gần nhất đúng nhóm: “mèo” gần vịt 0.99, gà 0.98, chó 0.97; “phở” gần cơm 0.99, cháo 0.99, bún 0.96; “trà” gần bia 0.96, nước 0.96, sữa 0.95. Không ai dạy mô hình đâu là con vật hay món ăn; nó tự suy ra từ ngữ cảnh.
- cos(mèo, phở) = 0.28, thấp hơn hẳn trong nhóm nhưng không âm. Lý do: cả hai có “một” trong ngữ cảnh (“nuôi một con mèo”, “ăn một bát phở”).
Thử đổi seed ở dòng default_rng(0). Số cụ thể đổi, nhưng ba nhóm vẫn tách ra: ở các lần chạy thử với seed 1 và 2, mọi hàng xóm gần nhất vẫn cùng nhóm. Kho này dễ một cách cố ý; văn bản thật cần hàng triệu câu mới có kết quả tương tự. Ô cũng không làm subsampling vì kho quá nhỏ.
N-gram ký tự kiểu FastText và từ chưa gặp
Phần tiêu đề “N-gram ký tự kiểu FastText và từ chưa gặp”Ô thứ hai làm hai việc. Thứ nhất, liệt kê n-gram 3–6 ký tự của một từ, có < và >. Thứ hai, cho thấy cơ chế ghép vectơ cho từ chưa gặp: băm mỗi n-gram bằng FNV-1a vào 10 000 ô, mỗi ô một vectơ 300 chiều, rồi cộng lại. Lưu ý quan trọng: các vectơ n-gram ở đây là số ngẫu nhiên (có seed), chưa huấn luyện gì. Ô này chỉ minh họa cách ghép, không phải embedding thật.
N-gram ký tự kiểu FastText và vectơ cho từ chưa gặp
# N-gram ký tự kiểu FastText và vectơ cho một từ chưa gặpimport numpy as np
def ngrams(word, nmin=3, nmax=6): w = "<" + word + ">" # đánh dấu đầu và cuối từ return [w[i:i + n] for n in range(nmin, nmax + 1) for i in range(len(w) - n + 1)]
def fnv1a(s): # băm FNV-1a 32 bit (FastText dùng một biến thể) h = 2166136261 for b in s.encode("utf-8"): h = ((h ^ b) * 16777619) % 2**32 return h
print(ngrams("nhà"))print(len(ngrams("nhà_văn")), "n-grams for nhà_văn")
# Bảng vectơ n-gram: số NGẪU NHIÊN, chỉ để xem cơ chế ghépBUCKET, d = 10_000, 300Z = np.random.default_rng(0).normal(size=(BUCKET, d))
def vec(word): # vectơ từ = tổng vectơ các n-gram của nó return sum(Z[fnv1a(g) % BUCKET] for g in ngrams(word))
def cos(u, v): return u @ v / (np.linalg.norm(u) * np.linalg.norm(v))
new = "nhà_văn" # giả sử từ này không có trong dữ liệu huấn luyệnfor w in ["nhà_thơ", "nhà_báo", "văn_học", "con_mèo"]: shared = len(set(ngrams(new)) & set(ngrams(w))) print(f"{w:8s} shared {shared:2d} cos {cos(vec(new), vec(w)):.2f}")Lần chạy đầu cần tải Python, khoảng 10 MB.
Đọc kết quả:
- “nhà” có 6 n-gram:
<nh,nhà,hà>,<nhà,nhà>,<nhà>. Từ ngắn đến mức n-gram 5 ký tự đã trùng với cả từ có dấu biên. Đây là chuyện thường gặp với âm tiết tiếng Việt. nhà_văncó 22 n-gram. Nó chung 6 n-gram vớinhà_thơvà vớinhà_báo(<nh,<nhà,<nhà_,hà_,nhà,nhà_), chung 1 vớivăn_học(văn), không chung gì vớicon_mèo.- Cosine đi theo số mảnh chung, dù vectơ là ngẫu nhiên: 0.35 với
nhà_thơ, 0.27 vớinhà_báo, 0.05 vớivăn_học, -0.00 (tức là 0) vớicon_mèo. Với vectơ ngẫu nhiên 300 chiều, các mảnh khác nhau gần như vuông góc, nên cosine xấp xỉ tỉ lệ mảnh chung, khoảng , cộng nhiễu.
Trong FastText thật, vectơ n-gram được huấn luyện. Mảnh <nhà_ gặp trong nhà_thơ, nhà_báo, nhưng cũng trong nhà_cửa, nhà_máy, nên nó học một thứ trộn giữa các nghĩa đó. Phần còn lại của từ (_văn>, văn) kéo vectơ về đúng hướng hơn. Nhờ vậy một từ chưa gặp vẫn có một vectơ hợp lý, tốt hơn hẳn việc không có gì, dù không bằng vectơ học từ nhiều lần xuất hiện thật. Thư viện fasttext lấy trung bình thay cho tổng; cosine không đổi vì hai cách chỉ khác nhau một hệ số.
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:
- Word2Vec học embedding bằng một bài toán dự đoán giả trên văn bản thô. Skip-gram đoán ngữ cảnh từ từ giữa; CBOW đoán từ giữa từ trung bình ngữ cảnh. Mỗi từ có hai vectơ: (từ đích) và (ngữ cảnh).
- Softmax trên cả từ vựng tốn phép tính cho mỗi cặp. Negative sampling thay bằng phân loại cặp thật với cặp ghép bừa: , mẫu âm bốc theo . Gradient kéo ngữ cảnh thật lại gần, đẩy mẫu âm ra xa.
- SGNS ngầm phân tích ma trận ; GloVe khớp tường minh bằng bình phương tối thiểu có trọng số . FastText biểu diễn từ bằng tổng vectơ n-gram ký tự 3–6, nên ghép được vectơ cho từ chưa gặp; với tiếng Việt, lợi ích rõ nhất khi văn bản đã tách từ.
- Các embedding này là tĩnh: một vectơ mỗi từ, mang theo thiên lệch của dữ liệu, và phép tương tự vectơ chỉ đúng với vài loại quan hệ. Embedding theo ngữ cảnh ở bài BERT giải quyết chuyện nhiều nghĩa.
Đọc thêm:
- Jurafsky, D., Martin, J. H. (bản nháp 8/2026). Speech and Language Processing, chương 5 “Embeddings” (PDF), mục 5.5 tới 5.8. Giải thích word2vec rất kỹ, có các lưu ý về phép tương tự và thiên lệch. Bản nháp cũ đánh số chương này là 6.
- Mikolov, T., Sutskever, I., Chen, K., Corrado, G., Dean, J. (2013b). Distributed Representations of Words and Phrases and their Compositionality, NeurIPS 2013, mục 2. Negative sampling, phân phối mũ 3/4, subsampling. Bài báo gọi vectơ từ đích là và vectơ ngữ cảnh là , chỗ bài này viết và .
- Pennington, J., Socher, R., Manning, C. D. (2014). GloVe: Global Vectors for Word Representation, EMNLP 2014, mục 3. Hàm mất mát có trọng số và ví dụ ice/steam. Bài báo viết cho vectơ ngữ cảnh.
- Bojanowski, P., Grave, E., Joulin, A., Mikolov, T. (2017). Enriching Word Vectors with Subword Information, TACL 5, mục 3.2. Mô hình n-gram ký tự của FastText.
- Levy, O., Goldberg, Y. (2014). Neural Word Embedding as Implicit Matrix Factorization, NeurIPS 2014, mục 3. Chứng minh SGNS phân tích ma trận PMI dịch chuyển, nối bài này với bài trước. Bài báo viết cho logarit tự nhiên.
Trình duyệt đang chặn lưu dữ liệu. Tiến độ sẽ mất khi bạn tải lại trang.
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