Bài 7 kết thúc với tokenizer: text thành list token IDs, ví dụ [9906, 1917, 198, ...]. Bài 1 đã nói bước tiếp theo là mỗi ID được map sang một vector — dãy số thực dài 4096 chiều (với Llama-3-8B). Nhưng vector đó từ đâu? Được sinh ra thế nào? Và tại sao nó lại “hiểu” được nghĩa của từ?
Bài này trả lời ba câu hỏi theo thứ tự:
- word2vec (2013) — origin story của embedding, nền móng mà mọi thứ sau đó đứng lên
- Contextual embedding — tại sao BERT/GPT làm tốt hơn word2vec
- Positional encoding và RoPE — tại sao cần, và cơ chế xoay vector hoạt động thế nào
Mental model tổng quát
Ba tầng embedding cần phân biệt ngay từ đầu:
Tầng 1 - Static (word2vec):
"bank" → [0.2, -0.8, 0.5, ...] <-- luôn một vector, bất kể ngữ cảnh
"bank" → [0.2, -0.8, 0.5, ...] <-- ngân hàng hay bờ sông đều vậy
Tầng 2 - Contextual (BERT/GPT):
"She went to the bank to deposit money"
"bank" → [0.1, 0.9, -0.3, ...] <-- vector ngân hàng
"The river bank was muddy"
"bank" → [-0.7, 0.2, 0.8, ...] <-- vector bờ sông
Tầng 3 - Positional:
embedding("dog", vị trí 0) ≠ embedding("dog", vị trí 5)
→ cùng token, khác vị trí, khác vector cuối
word2vec giải quyết Tầng 1. Transformer giải quyết Tầng 2 và 3.
Phần 1: word2vec — nền móng
Trước 2013, NLP dùng one-hot encoding: mỗi từ là một vector toàn số 0 trừ một vị trí bằng 1. Vocab 100,000 từ → mỗi từ là vector 100,000 chiều. Vấn đề: "cat" và "dog" có khoảng cách bằng nhau với "cat" và "airplane" — không có semantic nào được capture.
Tháng 9/2013, Tomas Mikolov và nhóm tại Google công bố word2vec. Ý tưởng cốt lõi đến từ ngôn ngữ học, cụ thể là câu của J.R. Firth (1957): “You shall know a word by the company it keeps.” Bạn biết nghĩa một từ qua những từ đi cùng với nó.
Nếu "vua" và "hoàng hậu" thường xuất hiện cạnh các từ như "cung điện", "triều đình", "thần dân" — chúng phải có vector gần nhau. word2vec biến trực giác đó thành bài toán tối ưu.
CBOW vs Skip-gram
word2vec có hai biến thể:
CBOW (Continuous Bag of Words):
context: ["The", "cat", ___, "on", "the", "mat"]
mục tiêu: dự đoán từ bị che ("sat")
→ dùng context để dự đoán target
Skip-gram:
target: "sat"
mục tiêu: dự đoán context ["The", "cat", "on", "the", "mat"]
→ dùng target để dự đoán context
Cả hai đều train một neural network 1 hidden layer. Điều thú vị: model sau khi train sẽ bị bỏ đi, chỉ giữ lại weights của hidden layer — đó chính là embedding matrix. Mỗi hàng trong matrix = vector của một từ.
King - Man + Woman ≈ Queen
Kết quả của word2vec nổi tiếng nhất: các phép tính vector phản ánh quan hệ ngữ nghĩa:
vec("king") - vec("man") + vec("woman") ≈ vec("queen")
vec("Paris") - vec("France") + vec("Italy") ≈ vec("Rome")
vec("tốt") - vec("tốt nhất") ~ vec("xấu") - vec("xấu nhất")
Không phải vì ai lập trình rule này. Mà vì sau khi nhìn hàng tỷ câu, model tự học được rằng quan hệ [quốc gia → thủ đô] tạo ra cùng một “hướng” trong không gian vector.
Giới hạn của word2vec
Một từ = một vector. Nghĩa là "bank" luôn có cùng một vector dù đứng trong câu nào. Tiếng Anh còn tạm; tiếng Việt (và nhiều ngôn ngữ khác) có rất nhiều từ đồng âm dị nghĩa. word2vec không xử lý được đa nghĩa theo ngữ cảnh.
Phần 2: Từ static sang contextual
Ba mốc quan trọng trong 2018:
- ELMo (February 2018, AllenNLP): dùng LSTM hai chiều, embedding của từ phụ thuộc vào toàn bộ câu
- GPT (June 2018, OpenAI): Transformer decoder, unsupervised pre-training
- BERT (October 2018, Google): Transformer encoder, bidirectional, masked language modeling
Khác biệt cốt lõi với word2vec: thay vì lookup table tĩnh, các model này tính embedding dynamically qua các transformer layers. Cùng token "bank", đi qua 12 hoặc 24 layer transformer với các attention khác nhau, ra vector khác nhau tùy context.
Cơ chế:
- Input: token
"bank"+ toàn bộ các token xung quanh - Mỗi attention layer “nhìn” context và điều chỉnh vector
- Sau N layers, vector của
"bank"đã được enrich bằng toàn bộ ngữ cảnh
Đây là lý do BERT làm tốt các bài toán disambiguation — phân biệt nghĩa từ — mà word2vec không thể.
Phần 3: Embedding matrix trong LLM hiện đại
Trong GPT/Llama, layer đầu tiên là một embedding lookup table đơn giản:
# PyTorch pseudo-code
embedding_matrix = nn.Embedding(vocab_size, d_model)
# Llama-3-8B: vocab_size=128256, d_model=4096
# forward pass
token_ids = [9906, 1917, 198] # token IDs từ tokenizer
x = embedding_matrix(token_ids) # shape: [3, 4096]
Với Llama-3-8B: 128,256 tokens × 4,096 chiều = 525 triệu parameters chỉ riêng embedding matrix — khoảng 6.5% toàn bộ model.
Một detail quan trọng thường bị bỏ qua: nhiều model dùng weight tying — embedding matrix đầu vào và output projection matrix cuối cùng (dùng để tính logits) dùng chung weights (transpose của nhau). Lý do: token nào “gần nhau” trong không gian input embedding cũng nên “gần nhau” khi model quyết định output. Đồng thời giảm đáng kể số parameters.
Embedding matrix chỉ là điểm khởi đầu. Công việc thực sự xảy ra ở các attention layers tiếp theo — đó là nơi vector được “enrich” bằng context.
Phần 4: Positional encoding — tại sao cần
Transformer self-attention có một đặc điểm: nó không có thứ tự built-in.
Khi tính attention score giữa hai token, ta chỉ dùng dot product của query và key vector. Không có gì trong phép tính đó cho biết token A đứng trước hay sau token B. Kết quả là:
"Chó cắn người"
"Người cắn chó"
Nếu không thêm positional info, attention layer xử lý hai câu này giống hệt nhau — vì tập token giống nhau, chỉ khác thứ tự. Điều này rõ ràng sai.
Cần inject thông tin về vị trí vào embedding. Có nhiều cách:
Absolute positional encoding (Transformer 2017)
Paper gốc “Attention is All You Need” dùng sinusoidal functions:
PE(pos, 2i) = sin(pos / 10000^(2i/d_model))
PE(pos, 2i+1) = cos(pos / 10000^(2i/d_model))
Vector vị trí được cộng vào embedding vector:
final_embedding = token_embedding + position_embedding
Lý do dùng sinusoidal: các tần số khác nhau theo dimension cho phép model học được khoảng cách tương đối giữa các vị trí (vì sin(a) - sin(b) có quan hệ với a - b). Nhưng trong thực tế, mối quan hệ này không đủ mạnh.
Learned positional encoding (BERT, GPT-2)
Thay sinusoidal bằng một matrix trainable: mỗi vị trí 0, 1, 2, … có một vector riêng được học trong quá trình training. Đơn giản hơn, nhưng không extrapolate tốt ra ngoài độ dài đã thấy khi train.
Relative position encoding (T5, Transformer-XL)
Thay vì encode vị trí tuyệt đối, encode khoảng cách tương đối giữa hai token. Token ở vị trí 5 và token ở vị trí 8 có khoảng cách 3 — đó là thông tin quan trọng hơn vị trí tuyệt đối. Cách tiếp cận này tốt hơn cho long context nhưng phức tạp khi implement.
RoPE — standard hiện đại
Kể từ GPT-J (2021), Llama, Mistral, và phần lớn các LLM mạnh hiện nay đều dùng Rotary Position Embedding (RoPE).
Phần 5: RoPE — sao lại “xoay”?
Paper gốc: “RoFormer: Enhanced Transformer with Rotary Position Embedding” (Su et al., 2021).
Intuition: Thay vì cộng position vector vào embedding, RoPE xoay query và key vector theo một góc phụ thuộc vào vị trí của token.
Trong 2D, xoay một vector (x, y) theo góc θ là:
[x'] = [cos θ -sin θ] [x]
[y'] [sin θ cos θ] [y]
RoPE mở rộng ý tưởng này sang vector d-chiều bằng cách chia vector thành các cặp và xoay từng cặp:
vector 4D: [x1, x2, x3, x4]
→ xoay cặp (x1, x2) theo góc θ1 * position
→ xoay cặp (x3, x4) theo góc θ2 * position
→ mỗi cặp dùng tần số khác nhau (θ1 ≠ θ2)
import numpy as np
def rotate_2d(x, y, theta):
"""Xoay vector 2D (x, y) theo góc theta"""
cos_t, sin_t = np.cos(theta), np.sin(theta)
return (cos_t * x - sin_t * y,
sin_t * x + cos_t * y)
def apply_rope(vector, position, base=10000):
"""
Áp dụng RoPE cho vector d-chiều tại vị trí position.
vector: np.array shape [d], d phải chẵn
"""
d = len(vector)
result = vector.copy().astype(float)
for i in range(d // 2):
theta = position / (base ** (2 * i / d))
x, y = vector[2 * i], vector[2 * i + 1]
result[2 * i], result[2 * i + 1] = rotate_2d(x, y, theta)
return result
# Demo: cùng vector, vị trí khác nhau
v = np.array([1.0, 0.0, 1.0, 0.0])
v_pos0 = apply_rope(v, position=0)
v_pos5 = apply_rope(v, position=5)
print("position 0:", v_pos0.round(3))
print("position 5:", v_pos5.round(3))
Tại sao xoay lại tốt hơn cộng?
Tính chất quan trọng nhất của RoPE: khi tính dot product giữa query tại vị trí m và key tại vị trí n, kết quả chỉ phụ thuộc vào (m - n) — khoảng cách tương đối, không phải vị trí tuyệt đối.
Chứng minh tóm tắt: nếu q_m = R(m) · q và k_n = R(n) · k (R là ma trận xoay), thì:
dot(q_m, k_n) = q^T · R(m)^T · R(n) · k
= q^T · R(n - m) · k
R(m)^T · R(n) = R(n - m) vì rotation matrices có tính chất này. Kết quả là attention score chỉ phụ thuộc vào khoảng cách (n - m), không phụ thuộc vào vị trí tuyệt đối m hay n.
Điều này quan trọng vì nó giúp RoPE extrapolate tốt hơn sang context dài hơn lúc training. Một model train với context 4096 tokens vẫn có thể xử lý tạm được câu dài hơn — vì nó đã học được quan hệ tương đối, không phải tuyệt đối. (Kỹ thuật mở rộng context xa hơn — YaRN, LongRoPE — sẽ gặp ở bài sau trong series.)
Phần 6: Hands-on — visualize embedding similarity
Demo sau dùng embeddings giả (4 chiều) để minh họa phép tính analogy của word2vec mà không cần load model lớn:
import numpy as np
# Fake GloVe-style embeddings, 4 chiều, để demo
emb = {
"king": np.array([ 0.80, 0.10, -0.20, 0.50]),
"queen": np.array([ 0.75, 0.15, 0.90, 0.40]),
"man": np.array([ 0.90, 0.05, -0.30, 0.10]),
"woman": np.array([ 0.85, 0.10, 0.80, 0.05]),
"apple": np.array([-0.20, 0.90, 0.10, 0.70]),
}
def cos_sim(a, b):
return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
def nearest(query_vec, embeddings, exclude=()):
scores = {
word: cos_sim(query_vec, vec)
for word, vec in embeddings.items()
if word not in exclude
}
return sorted(scores.items(), key=lambda x: x[1], reverse=True)
# Analogy: king - man + woman = ?
query = emb["king"] - emb["man"] + emb["woman"]
print("king - man + woman:")
for word, score in nearest(query, emb, exclude=("king", "man", "woman")):
print(f" {word:6}: {score:.3f}")
# Similarity matrix
print("\nCosine similarity matrix:")
words = list(emb.keys())
header = f"{'':8}" + "".join(f"{w:8}" for w in words)
print(header)
for w1 in words:
row = f"{w1:8}" + "".join(f"{cos_sim(emb[w1], emb[w2]):.3f} " for w2 in words)
print(row)
Chạy đoạn này sẽ thấy queen có similarity cao nhất với king - man + woman, và apple cô lập với phần còn lại — đúng kỳ vọng.
Thực tế với OpenAI embeddings API
Để làm thực sự với embeddings chất lượng production:
from openai import OpenAI
import numpy as np
client = OpenAI() # cần OPENAI_API_KEY trong env
words = ["king", "queen", "man", "woman", "apple", "banana",
"Paris", "France", "Tokyo", "Japan"]
response = client.embeddings.create(
model="text-embedding-3-small",
input=words,
)
# text-embedding-3-small: 1536 chiều
embeddings = {
words[i]: np.array(response.data[i].embedding)
for i in range(len(words))
}
# Thử analogy
query = embeddings["Paris"] - embeddings["France"] + embeddings["Japan"]
for word, score in nearest(query, embeddings, exclude=("Paris", "France", "Japan")):
print(f" {word:8}: {score:.3f}")
# Expect: Tokyo xuất hiện gần top
Đây cũng là nền tảng của RAG — thay vì so sánh từng từ, so sánh embedding của toàn bộ đoạn văn để tìm đoạn liên quan nhất. Sẽ gặp lại ở bài 25.
Cheatsheet
| Loại | Đặc điểm | Ví dụ |
|---|---|---|
| One-hot | Sparse, không semantic | NLP trước 2013 |
| word2vec (static) | Dense, có semantic, 1 từ = 1 vector | Word2Vec, GloVe, FastText |
| Contextual | Dense, vector phụ thuộc ngữ cảnh | ELMo, BERT, GPT |
| Positional (sinusoidal) | Cộng vào embedding, không learn | Transformer 2017 gốc |
| Positional (learned) | Trainable vector mỗi vị trí | BERT, GPT-2 |
| RoPE | Xoay Q/K theo vị trí, relative-aware | Llama, Mistral, GPT-J |
Năm điều phải nhớ:
- Embedding = bảng tra cứu. Token ID → hàng tương ứng trong matrix. Không có công thức, chỉ là lookup.
- word2vec học embedding bằng cách dự đoán context. Weights học được là embedding, không phải model chính.
- Contextual embedding (BERT/GPT) tính vector dynamically qua attention — cùng token, khác ngữ cảnh, khác vector.
- Transformer không có thứ tự built-in. Positional encoding inject thứ tự vào vector trước khi vào attention.
- RoPE xoay Q và K theo góc phụ thuộc vị trí. Dot product kết quả chỉ phụ thuộc khoảng cách tương đối (m - n), không phải vị trí tuyệt đối.
Lời kết
Part 2 của series (Math Foundations) đã xong với bài này. Từ bài 2 đến bài 8: linear algebra, calculus, probability, neural network từ zero, training loop, tokenizer, và giờ là embeddings. Tất cả cộng lại đủ để bạn đọc architecture paper mà không bị mắc ở phần notation toán.
Part 3 bắt đầu từ bài 9: Attention mechanism — cơ chế cốt lõi làm cho Transformer vượt trội mọi thứ trước đó. Sẽ code scaled dot-product attention từ zero bằng NumPy, sau đó multi-head attention. Đây là phần mà nhiều dev biết tên nhưng chưa thực sự implement bao giờ.
Trước khi sang bài 9, bài tập nhỏ: lấy OpenAI text-embedding-3-small API, tính embedding cho 10 từ bất kỳ, vẽ cosine similarity matrix, quan sát nhóm nào cluster với nhau. Bạn sẽ thấy pattern rõ ràng hơn nhiều so với demo 4D ở trên — và intuition đó sẽ giúp bài 25 (RAG) dễ hơn đáng kể.