Mở đầu: dạy máy hiểu rằng "hoa" gần "gió" hơn gần "toà án"
Train $1$ mạng nhỏ trên toàn bộ Truyện Kiều (không nhãn, không ai dạy nghĩa từ nào cả — chỉ cho xem từng câu thơ), rồi hỏi: "từ nào gần nghĩa hoa nhất?" Verify bằng số thật, mạng tự tìm ra gió, trăng, cành, bay, lá — đúng cụm hình ảnh thơ ca xuất hiện cạnh "hoa" hàng trăm lần trong tác phẩm. Không một dòng code nào bảo "hoa là thực vật" hay "gió liên quan tới hoa" — toàn bộ tri thức đó tự nổi lên từ việc đếm xem từ nào hay đứng CẠNH từ nào.
Đây là embedding — cách biểu diễn từ (hay bất kỳ thứ rời rạc nào) bằng $1$ vector số thực
HỌC ĐƯỢC, thay vì liệt kê tay. Bài này xây embeddingLookup() thật vào NeuroJS, train thuật
toán word2vec skip-gram + negative sampling, và verify $1$ pitfall quan trọng: embedding gần nhau không có
nghĩa là đồng nghĩa.
1. Máy không hiểu chữ: one-hot và 2 cái chết
Cách biểu diễn từ ngây thơ nhất: one-hot — vector toàn số $0$, chỉ đúng $1$ vị trí (ứng với từ đó trong từ điển) mang giá trị $1$. Với Truyện Kiều, verify bằng số thật:
$$\text{So tu duy nhat trong toan bo Truyen Kieu} = 2.382$$
Nghĩa là mỗi vector one-hot phải dài $2.382$ chiều — và con số này CHỈ tăng khi vocab tăng (tiếng Việt thật có hàng chục nghìn từ, tiếng Anh sản xuất thật dùng $100.000$–$1.000.000$+ từ). Đây là cái chết thứ nhất: chiều bùng nổ theo kích thước từ điển.
Cái chết thứ hai còn nặng hơn: lấy tích vô hướng (dot product) giữa $2$ vector one-hot BẤT KỲ của $2$ từ khác nhau — luôn luôn bằng $0$, không phụ thuộc $2$ từ đó có liên quan gì tới nhau hay không:
$$\text{one-hot}(w_1) \cdot \text{one-hot}(w_2) = 0 \quad \text{voi moi } w_1 \neq w_2$$
Ví dụ cụ thể: tích vô hướng giữa one-hot của "hoa" và "gió" cũng bằng $0$ y hệt như giữa "hoa" và "toà án" — dù cặp đầu thường đứng cạnh nhau trong thơ còn cặp sau gần như không bao giờ. One-hot coi MỌI cặp từ "cách đều nhau" như nhau — không mang theo bất kỳ tí ngữ nghĩa nào.
Embedding giải quyết cả $2$: thay vector one-hot $2.382$ chiều bằng $1$ vector DÀY đặc chỉ $16$ chiều — $2.382 \div 16 \approx 148{,}9$ lần nhỏ hơn — và quan trọng hơn, $16$ con số đó KHÔNG cố định, chúng được HỌC qua backprop sao cho từ hay xuất hiện cùng ngữ cảnh thì có vector gần nhau.
2. Giả thuyết phân bố: "từ được hiểu qua bạn bè của nó"
Ý tưởng nền tảng (Distributional Hypothesis, ngôn ngữ học từ thập niên 1950): $2$ từ xuất hiện trong ngữ cảnh giống nhau thường mang nghĩa liên quan. Skip-gram (word2vec, Mikolov 2013) biến ý tưởng đó thành bài toán học máy cụ thể: cho $1$ từ trung tâm, đoán các từ hàng xóm xung quanh nó (cửa sổ $\pm2$ từ, ví dụ). Nếu $2$ từ liên tục đoán đúng hàng xóm của nhau, vector của chúng bị kéo lại gần nhau qua gradient descent.
Cách "tự nhiên" nhất: dùng đúng softmaxCrossEntropy() đã xây ở
Bài 10 — từ trung tâm dự đoán phân bố xác suất trên TOÀN BỘ vocab, chọn
đúng từ hàng xóm. Với MNIST, vocab chỉ $10$ chữ số — rẻ. Với từ vựng thật $100.000$–$1$ triệu từ,
softmax phải tính $\exp()$ cho MỖI TỪ trong vocab, ở MỖI bước huấn luyện, hàng tỉ bước — không khả
thi.
Negative sampling lách qua bằng cách đổi bài toán: thay vì "chọn đúng $1$ trong $100.000$ lớp", biến thành nhiều bài toán NHỊ PHÂN nhỏ — "cặp (từ trung tâm, từ hàng xóm THẬT) có đi cùng nhau không?" (nhãn $1$) cộng với vài cặp "(từ trung tâm, từ NGẪU NHIÊN)" (nhãn $0$, gọi là negative). Mỗi bước chỉ cần tính $1+K$ cặp (K thường $5$–$20$) thay vì toàn bộ vocab.
Cài đặt dùng đúng $2$ op mới của NeuroJS: embeddingLookup() tra vector từ bảng $V\times d$,
rồi tích vô hướng $2$ vector qua sum(mul(...)) đã có từ Bài 5, cuối cùng
sigmoidCrossEntropy() — gộp sigmoid + binary cross-entropy giống tinh thần
softmaxCrossEntropy() Bài 10, gradient rút gọn về đúng $1$ công thức $\text{sigmoid}(z)-y$:
// 1 cap (trung tam, ngu canh THAT) + K cap negative NGAU NHIEN
const cv = embeddingLookup(centerTable, [centerIdx]);
const ov = embeddingLookup(contextTable, [contextIdx]); // THAT: nhan=1
const score = sum(mul(cv, ov)); // tich vo huong
const L = sigmoidCrossEntropy(score, oneTensor); // gop sigmoid+BCE
L.backward(); // cong don gradient (Bai 7)
// ... lap lai K lan voi negIdx ngau nhien, nhan=0 ...
3. Không gian ngữ nghĩa: cosine similarity và cạm bẫy của nó
So sánh $2$ embedding bằng cosine similarity — góc giữa $2$ vector, KHÔNG phải khoảng cách tuyệt đối (vector dài/ngắn không quan trọng, chỉ hướng mới quan trọng):
$$\cos(\theta) = \frac{u \cdot v}{\|u\|\,\|v\|}$$
Giá trị từ $-1$ (ngược hướng hoàn toàn) tới $1$ (cùng hướng). Ví dụ kinh điển nhất của word2vec (train trên corpus tiếng Anh khổng lồ, Mikolov 2013), phép toán vector $\text{king} - \text{man} + \text{woman} \approx \text{queen}$ (vua − đàn ông + đàn bà ≈ nữ hoàng) — quan hệ ngữ nghĩa "giới tính hoàng gia" trở thành $1$ phép CỘNG TRỪ TUYẾN TÍNH trong không gian embedding. Đây là phát hiện gây chấn động nhất của word2vec: embedding không chỉ nhóm từ gần nghĩa lại, nó còn mã hoá QUAN HỆ giữa các từ thành hướng vector nhất quán.
Verify bằng số thật trên embedding vừa train ở Truyện Kiều (dim $16$, vocab $300$ từ phổ biến nhất):
| Từ | 8 hàng xóm gần nhất (cosine) |
|---|---|
| hoa | gió ($0{,}854$), trăng ($0{,}820$), cành ($0{,}794$), bay ($0{,}776$), lá ($0{,}770$) |
| trăng | gió ($0{,}836$), sớm ($0{,}827$), hoa ($0{,}820$), mây ($0{,}770$), bóng ($0{,}742$) |
Không cần ai dạy — mạng tự tìm ra đúng cụm hình ảnh thơ ca cổ điển ("hoa", "gió", "trăng", "cành", "lá", "mây", "bóng" đứng cạnh nhau hàng trăm lần trong Truyện Kiều, đúng ước lệ thơ lục bát).
Verify bằng số cụ thể: hàng xóm gần nhất số $1$ của xuân (mùa xuân) là thu (mùa thu) — cosine $0{,}839$, cao hơn cả cặp "hoa"/"đào" ($0{,}764$)! Xuân và thu là $2$ MÙA KHÁC NHAU, gần như đối lập theo lịch — nhưng embedding không học "nghĩa" theo nghĩa từ điển, nó học xu hướng xuất hiện cùng ngữ cảnh. "Xuân" và "thu" đều hay xuất hiện trong các câu thơ nói về THỜI GIAN TRÔI QUA ("xuân qua thu lại", tuổi tác, mùa vụ) — nên embedding gộp chúng vào cùng $1$ vùng không gian dù nghĩa từ điển khác hẳn nhau.
Đối chiếu bằng một cặp từ có quan hệ THẬT thay vì ngẫu nhiên đồng xuất hiện: cosine của "nàng" và "chàng" (cặp đại từ đối lập giới tính, cùng xuất hiện song song cực nhiều trong cấu trúc thơ lục bát Truyện Kiều) đo được $\approx 0{,}69$ — cao, nhưng vẫn THẤP HƠN cặp "xuân"/"thu" ($0{,}839$) dù quan hệ ngữ nghĩa của "nàng"/"chàng" trực quan "chặt" hơn nhiều với người đọc. Điều này củng cố đúng bài học của cạm bẫy: độ lớn cosine phản ánh tần suất và mức độ đồng xuất hiện thống kê, không phải "độ chặt chẽ ngữ nghĩa" theo trực giác con người — hai đại lượng này tương quan nhưng không đồng nhất.
Hệ quả rộng hơn: embedding cũng "nuốt" luôn thiên kiến (bias) có sẵn trong dữ liệu huấn luyện. Một phát hiện kinh điển trên embedding tiếng Anh huấn luyện từ báo chí (Bolukbasi et al. 2016): vector "bác sĩ" (doctor) gần vector "ông ấy" (he) hơn "cô ấy" (she), phản ánh đúng thiên kiến giới trong dữ liệu báo chí gốc — KHÔNG phải embedding "cố ý phân biệt đối xử", nó chỉ trung thực phản ánh thống kê đồng xuất hiện trong dữ liệu, kể cả phần thống kê đó mang định kiến xã hội.
4. Embedding là 1 layer — không chỉ dành cho từ
Bảng embedding chỉ đơn giản là $1$ ma trận tham số $V\times d$ (V = kích thước vocab, d = số chiều
embedding) — embeddingLookup() tra đúng hàng, và hàng đó CŨNG được cập nhật qua
backward()/optimizer.step() giống hệt mọi trọng số khác trong NeuroJS từ
Bài 7. Không có phép màu nào cả — embedding chỉ là $1$ layer, và "tra
bảng" chỉ là phép chọn hàng có gradient định tuyến ngược lại đúng hàng đã chọn (giống hệt cơ chế maxPool2d
định tuyến gradient ở Bài 11, chỉ khác đây LUÔN cộng dồn thay vì chọn
$1$).
Ý tưởng này tổng quát vượt xa từ ngữ: bất kỳ thứ RỜI RẠC nào (user ID, sản phẩm ID, thể loại phim, chuyên mục tin tức) đều có thể gán $1$ hàng trong bảng embedding và HỌC được vị trí "gần ai" qua đúng cơ chế skip-gram này — đây chính là nền tảng của hệ thống gợi ý (recommendation system): "user A và user B có embedding gần nhau" ≈ "A và B có sở thích tương tự". Embedding cũng là cửa ngõ đầu tiên của mọi Transformer hiện đại (Bài 14) — token đầu vào luôn được tra bảng embedding trước khi đi vào bất kỳ lớp attention nào.
5. Thực hành: train skip-gram trên Truyện Kiều, chiếu 2D bằng PCA
Train trực tiếp trong browser trên $22.778$ token thật của Truyện Kiều (vocab $300$ từ phổ biến nhất, phủ $61{,}2\%$ tổng số token, $55.726$ cặp skip-gram cửa sổ $\pm2$), rồi gõ $1$ từ để xem hàng xóm gần nhất — không gian embedding $16$ chiều được chiếu về $2$D bằng PCA (power iteration, đúng phương pháp Bài 4, tổng quát hoá từ ma trận $2\times2$ lên $16\times16$):
Mỗi cặp (trung tâm, ngữ cảnh) forward/backward riêng qua embeddingLookup — chạy nền, không khoá
trang, có thể Dừng rồi Train lại bất kỳ lúc nào. Demo dùng cấu hình NHẸ HƠN (20.000/55.726 cặp, 2
epoch, 3 negative thay vì 5) để train xong trong vài phút — các con số verified trong bài (toàn bộ
cặp, 5 epoch, 5 negative) đến từ skipgram_demo.js chạy bằng Node, xem CTA cuối bài.
Đối chiếu công nghiệp: PyTorch nn.Embedding đúng 1-1 với Mục 2-4:
# Doi chieu 1-1 voi embeddingLookup() + skip-gram Muc 2-4
import torch.nn as nn
center_emb = nn.Embedding(vocab_size, dim) # bang tra V x d - hoc bang backprop
context_emb = nn.Embedding(vocab_size, dim)
# 1 buoc skip-gram + negative sampling
score = (center_emb(center_idx) * context_emb(context_idx)).sum(-1)
loss = nn.functional.binary_cross_entropy_with_logits(score, label) # = sigmoidCrossEntropy()
loss.backward() # gradient tu dong dinh tuyen ve DUNG hang da tra (giong Muc 4)
Tóm lược
- ✅ One-hot chết 2 lần: chiều bùng nổ theo vocab (verified: $2.382$ chiều cho Truyện Kiều) và mọi cặp từ "cách đều nhau" (tích vô hướng luôn $=0$, không mang ngữ nghĩa).
-
✅ Skip-gram + negative sampling thay softmax cả vocab (không khả thi ở quy mô thật)
bằng nhiều bài toán nhị phân nhỏ — verify bằng
embeddingLookup()+sigmoidCrossEntropy()mới cài vào NeuroJS. - ✅ Cosine similarity đo góc giữa embedding — verified: "hoa" gần "gió/trăng/cành" (cụm hình ảnh thơ mạch lạc); pitfall cosine cao ≠ đồng nghĩa (verified: "xuân" gần "thu" nhất, cosine $0{,}839$, dù là $2$ mùa khác hẳn nhau) — embedding học đồng xuất hiện, không phải định nghĩa từ điển, và có thể nuốt cả thiên kiến dữ liệu.
- ✅ Embedding là 1 layer bình thường: bảng tra $V\times d$ học bằng backprop y hệt mọi tham số khác — tổng quát cho MỌI thứ rời rạc (user, sản phẩm...), nền tảng recommendation system, và là cửa ngõ đầu vào của Transformer.
- ✅ PCA (Bài 4) tổng quát hoá từ ma trận $2\times2$/$3\times3$ lên $16\times16$ vẫn dùng đúng power iteration — chiếu không gian embedding về 2D để nhìn trực quan.
Tải file code thực hành minh họa bài học
File JavaScript train skip-gram + negative sampling thật trên Truyện Kiều, verify nearest neighbor + PCA
tổng quát hoá bằng số đo được (chạy node skipgram_demo.js, mất khoảng $8$ giây):
Bình luận