Skip to content
Phạm Khánh Minh Mẫn / syllabus

MÔN 02 / Bài 01

Vector, tích vô hướng và phép chiếu

Từ mũi tên, độ dài và bóng chiếu đến dot product và cosine similarity.

PDF ghi tay

Bản cô đọng: hình trước, công thức sau. Nền trắng, chữ đen.

Trong bài này ↓

Chữ gạch chấm là tham chiếu tới môn hoặc tài liệu chưa được đưa lên website.

~3 giờ · không cần điều kiện gì trước

Đích của bài này: hiểu vì sao toàn bộ RAG/retrieval hoạt động được. Không phải "biết công thức cosine" — mà biết vì sao nó là công thức đúng.

🧭 Làm theo thứ tự này

  1. Xem trước: 3Blue1Brown tập 1 Vectors, what even are they? (~10 phút). Không ghi chép.
  2. Làm ô ⚡ bên dưới: tính tay a·c và b·c, rồi tự viết dot, norm, cosine (không dùng np.dot).
  3. Đọc §1-5 để hiểu vì sao. (§4b là phần bổ sung, để sau cũng được.)
  4. Code §6-7: mở code mẫu để đối chiếu với code bạn đã tự viết ở bước 2, rồi so với numpy và sklearn.
  5. Tự kiểm §8: trả lời 7 câu bằng lời.
  6. Sau cùng: xem tập 9 Dot products and duality (phần "duality" khó thì xem lướt).

Xong bài khi: ra đúng 24 / 0 / -7 và 0.96 / 0.00 / -0.28, và trả lời được 7 câu tự kiểm.

⚡ 30 phút đầu — làm trước, đọc sau

Đừng đọc hết file này rồi mới bắt tay. Bài tập dưới đây cần ba thuật ngữ — đây là định nghĩa ngắn nhất (§1-2 giải thích vì sao chúng đúng, đọc sau). Ví dụ dùng a = (3, 4) và b = (4, 3):

Thuật ngữ Nghĩa Ví dụ
dot(u, v) — tích vô hướng nhân từng cặp số cùng vị trí rồi cộng lại a·b = 3×4 + 4×3 = 24
norm(u) — độ dài căn bậc hai của dot(u, u) (Pythagoras) norm(a) = √(3×3 + 4×4) = √25 = 5
cosine(u, v) dot(u, v) chia cho tích hai độ dài 24 / (5 × 5) = 0.96

Đọc cosine như sau: gần 1 = gần cùng hướng · 0 = vuông góc · âm = ngược hướng.

Giờ mở terminal:

import numpy as np
a = np.array([3.0, 4.0]); b = np.array([4.0, 3.0]); c = np.array([-4.0, 3.0])
# tự viết dot(u,v), norm(u), cosine(u,v) — KHÔNG dùng np.dot
# đích: ra đúng   dot = 24 / 0 / -7    và   cos = 0.96 / 0.00 / -0.28

Ra đúng ba số → đọc tiếp §1-5 để hiểu vì sao. Ra sai → đó là dòng đầu tiên của nhật ký lỗi, và §3 có bảng đối chiếu.


1. Vector — hai cách nhìn, và cách thứ hai mới quan trọng

Bạn đã quen cách nhìn thứ nhất: vector = danh sách số.

a = [3, 4]

Với backend dev thì đây là một mảng, hết. Nhưng cách nhìn thứ hai mới là chỗ toán bắt đầu:

Vector = một mũi tên trong không gian, xuất phát từ gốc toạ độ.

[3, 4] = đi sang phải 3, đi lên 4. Nó có độ dài và có hướng.

Vì sao phân biệt này quan trọng: khi bạn embed một câu thành [0.21, -0.03, ..., 0.88] (768 số), bạn không tạo ra một danh sách — bạn đặt câu đó vào một điểm trong không gian 768 chiều. Hai câu gần nghĩa nằm gần nhau. Toàn bộ retrieval đứng trên ý đó.

Độ dài (chuẩn, norm):

|a| = √(3² + 4²) = √25 = 5

Đây chỉ là Pythagoras. Trong d chiều: |a| = √(a₁² + a₂² + ... + a_d²).


2. Tích vô hướng — định nghĩa, rồi ý nghĩa

Định nghĩa (nhân từng cặp rồi cộng lại):

a · b = a₁b₁ + a₂b₂ + ... + a_d b_d

Đây là phép tính bạn cài được trong 1 dòng. Nhưng nó đo cái gì?

Tích vô hướng đo mức độ hai vector cùng hướng.

  • Cùng hướng → dương, càng lớn càng cùng hướng
  • Vuông góc → bằng đúng 0
  • Ngược hướng → âm

Công thức nối hai cách nhìn:

a · b = |a| × |b| × cos(θ)

θ là góc giữa hai mũi tên. Đây là cây cầu giữa "danh sách số" và "hình học" — và là toàn bộ lý do bài này tồn tại.

Phép chiếu — hình ảnh đứng sau công thức đó

Công thức a · b = |a||b|cos θ vẫn còn là hai ký hiệu dán vào nhau cho tới khi bạn thấy cái bóng.

Chiếu a lên b = rọi đèn vuông góc xuống đường thẳng chứa b, rồi hỏi cái bóng của a dài bao nhiêu.

Chia a · b cho |b| để bỏ ảnh hưởng độ dài của b, còn lại đúng độ dài cái bóng:

độ dài bóng của a trên b  =  (a · b) / |b|  =  |a| · cos(θ)

Muốn lấy bóng dưới dạng vector (đúng hướng b), nhân độ dài đó với vector đơn vị b/|b|:

proj_b(a)  =  ( (a · b) / (b · b) ) · b

Tính tay với a = (3,4), b = (4,3) — chính hai vector của §3:

a · b = 24        |b| = 5
độ dài bóng = 24 / 5 = 4.8
kiểm chéo   = |a|·cos θ = 5 × 0.96 = 4.8      ✓ hai đường tính, một kết quả

proj_b(a) = (24/25)·(4,3) = (3.84, 2.88)
|proj_b(a)| = √(3.84² + 2.88²) = √23.04 = 4.8  ✓

Và với c = (−4, 3), nơi a · c = 0:

proj_c(a) = (0/25)·(−4,3) = (0, 0)

Vuông góc = KHÔNG có bóng. a không đóng góp một chút nào theo hướng c. Đó là nghĩa hình học của số 0 ở §3, và là lý do "trực giao" đồng nghĩa với "không chia sẻ thông tin".

Nối thẳng sang §4: nếu chia độ dài bóng cho |a| nữa, bạn được cos θ — tức cosine similarity chính là "bao nhiêu phần trăm của a sống sót sau khi chiếu lên b". Không phải một độ đo được phát minh ra cho retrieval; nó là tỉ lệ bóng, có sẵn từ hình học phổ thông.

(Phép chiếu sẽ quay lại hai lần nữa: bài 5 — embedding chính là chiếu từ 100k chiều xuống 768 chiều; và bài 9 — xấp xỉ hạng thấp chính là chiếu dữ liệu lên không gian con k chiều tốt nhất.)


3. Ví dụ tính tay — làm trên giấy trước khi gõ code

Ba vector, cố ý cho cùng độ dài = 5 để độ dài không gây nhiễu, chỉ còn hướng khác nhau:

a = ( 3, 4)      |a| = √(9+16)  = 5
b = ( 4, 3)      |b| = √(16+9)  = 5
c = (-4, 3)      |c| = √(16+9)  = 5

Tính tích vô hướng:

a · b = 3(4)  + 4(3) =  12 + 12 =  24
a · c = 3(-4) + 4(3) = -12 + 12 =   0
b · c = 4(-4) + 3(3) = -16 +  9 =  -7

Chia cho |a||b| = 25 để ra cos(θ):

Cặp tích vô hướng cos(θ) θ Đọc là
a, b 24 0.96 16.3° rất giống hướng
a, c 0 0.00 90° vuông góc —không liên quan gì nhau
b, c −7 −0.28 106.3° hơi ngược hướng

Tự kiểm bằng góc thật: a nghiêng atan2(4,3) = 53.13°, b nghiêng 36.87°, c nghiêng 143.13°. → góc(a,b) = 53.13 − 36.87 = 16.26° ✓ → góc(a,c) = 143.13 − 53.13 = 90° ✓ → góc(b,c) = 143.13 − 36.87 = 106.26° ✓

Ba con số khớp. Không phải phép màu — chỉ là hai cách viết cùng một đại lượng.

Điểm đáng dừng lại: a · c = 0 chính xác bằng 0. Trong không gian nhiều chiều, "vuông góc" nghĩa là không chia sẻ thông tin gì với nhau. Đó là lý do bạn sẽ gặp lại chữ "trực giao" (orthogonal) ở khắp nơi — cơ sở trực giao, SVD, PCA.


4. Từ tích vô hướng → cosine similarity

Chia cho tích hai độ dài để loại bỏ ảnh hưởng của độ dài, chỉ giữ lại hướng:

cosine(a, b) = (a · b) / (|a| × |b|)     ∈ [−1, 1]

Đây chính là hàm mà mọi hệ thống retrieval/RAG đang gọi. Không có gì hơn.


4a. ⭐ Vì sao cosine luôn nằm trong [−1, 1] — Cauchy–Schwarz, và "góc" trong 768 chiều là một ĐỊNH NGHĨA (bổ sung từ VMLS — 2026-10)

(nguồn: VMLS tr.56-58, §3.4)

§2 giải thích a·b = |a||b|cos θ bằng hình (tam giác vuông, cái bóng). Hình đó vẽ được ở 2-3 chiều. Ở 768 chiều không có hình nào để vẽ, và "góc giữa hai embedding" nghe như ẩn dụ. Nó không phải ẩn dụ, nhưng lý do không nằm ở hình học. Nó nằm ở một định nghĩa cộng với một bất đẳng thức:

Định nghĩa: θ := arccos( aᵀb / (‖a‖‖b‖) ), cho mọi số chiều.

Định nghĩa này chỉ có nghĩa nếu biểu thức trong arccos nằm trong [−1, 1]. Thứ bảo đảm điều đó là bất đẳng thức Cauchy–Schwarz: |aᵀb| ≤ ‖a‖‖b‖. (Đây là đáp án thật cho câu 2 của §8; lập luận "kề ≤ huyền" ở §10 chỉ đúng khi vẽ được tam giác vuông.)

Chứng minh (VMLS tr.57; a, b ≠ 0, đặt α = ‖a‖, β = ‖b‖). Bình phương một độ dài thì không âm:

0 ≤ ‖βa − αb‖² = β²‖a‖² − 2αβ·aᵀb + α²‖b‖² = 2‖a‖²‖b‖² − 2‖a‖‖b‖·aᵀb

Chia cho 2‖a‖‖b‖ được aᵀb ≤ ‖a‖‖b‖. Áp dụng cho −a được −aᵀb ≤ ‖a‖‖b‖. Gộp lại là xong. Dấu bằng khi và chỉ khi βa = αb: hai vector là bội dương của nhau (cos = 1; với −a thì cos = −1). Tính tay với a = (3,4), b = (4,3) (α = β = 5):

βa − αb = 5(3,4) − 5(4,3) = (−5, 5)            ‖·‖² = 50
vế phải : 2·25·25 − 2·5·5·24 = 1250 − 1200 = 50          ✓
khoảng hở ‖a‖‖b‖ − aᵀb = 25 − 24 = 1 = 50 / (2·5·5)     ✓

Hệ quả: (1) Bất đẳng thức tam giác ‖a+b‖ ≤ ‖a‖+‖b‖: khai triển ‖a+b‖² = ‖a‖² + 2aᵀb + ‖b‖², thế aᵀb ≤ ‖a‖‖b‖ ra (‖a‖+‖b‖)². Số: ‖(7,7)‖ = 9.8995 ≤ 10. Nhờ vậy khoảng cách Euclid là khoảng cách đúng nghĩa, nền của mọi lập luận "láng giềng gần nhất". (2) Độ nhạy: nếu ‖d‖ = 1 thì |q·d − q′·d| ≤ ‖q − q′‖ (VMLS tr.65): truy vấn nhích ε thì điểm của mọi tài liệu chuẩn hoá đổi không quá ε. (3) Với vector đơn vị, L2 và cosine cùng xếp hạng: môn 10 bài 5.

import numpy as np
a, b = np.array([3., 4.]), np.array([4., 3.])
al, be = np.linalg.norm(a), np.linalg.norm(b)
w = be * a - al * b                                    # (-5, 5)
assert np.isclose(w @ w, 50.0)
assert np.isclose(w @ w, 2 * (a @ a) * (b @ b) - 2 * al * be * (a @ b))
assert np.isclose(al * be - a @ b, (w @ w) / (2 * al * be))    # khoảng hở = 1
assert np.linalg.norm(a + b) <= al + be                        # 9.8995 <= 10

rng = np.random.default_rng(0)                         # 10.000 cặp ngẫu nhiên, 768 chiều
A, B = rng.normal(size=(10_000, 768)), rng.normal(size=(10_000, 768))
dots = np.einsum('ij,ij->i', A, B)
nrm = np.linalg.norm(A, axis=1) * np.linalg.norm(B, axis=1)
assert (np.abs(dots) <= nrm).all()
print(round(float(np.abs(dots / nrm).max()), 4))       # 0.1382  (gần vuông góc: câu 4 của §8)

🪤 BẪY: bất đẳng thức đúng với số thực, máy tính thì làm tròn. Hai vector song song tuyệt đối phải cho θ = 0. Trên máy:

a = np.random.default_rng(1).normal(size=768)
b = 3.0 * a                                            # song song tuyệt đối
c = (a @ b) / (np.linalg.norm(a) * np.linalg.norm(b))
print(repr(float(c)), c > 1.0)                         # 1.0000000000000004 True
print(np.arccos(c))                                    # nan   (kèm RuntimeWarning)
print(np.arccos(np.clip(c, -1.0, 1.0)))                # 0.0

cos = lambda u, v: (u @ v) / (np.linalg.norm(u) * np.linalg.norm(v))
X = np.random.default_rng(1).normal(size=(2000, 768))
print(sum(cos(x, k * x) > 1.0 for x in X for k in (3.0, 3.7, 10.0, 0.37)))   # 2338 trên 8000 cặp song song
Y = np.random.default_rng(5).normal(size=(2000, 768))
print(sum(cos(y, y) > 1.0 for y in Y))                                       # 390 trên 2000: cả cos(y, y)

c vượt 1 đúng một đơn vị làm tròn và arccos trả nan, kể cả với hai bản sao y hệt (hai dòng đo cuối). Tình huống thật: corpus có tài liệu trùng lặp. Chữa bằng np.clip(c, -1, 1) trước arccos. Đây là chi tiết thiết lập chứ không phải lỗi toán, đúng loại quy tắc 1.


4b. ⭐ Cosine bạn vừa học CHÍNH LÀ hệ số tương quan của thống kê

(bổ sung 19/8 — nguồn: MML tr.195-196, §6.4.6)

Bạn sẽ gặp lại "tương quan" ở môn 4 và môn 5, và gần như chắc chắn sẽ học nó như một công thức thống kê riêng biệt — không liên quan gì tới bài này.

Nó không riêng biệt. Nó là đúng công thức bạn vừa gõ.

Định nghĩa ⟨X, Y⟩ := Cov[x, y] cho biến ngẫu nhiên kỳ vọng 0. MML chứng minh đây là một tích trong đủ tiêu chuẩn — thoả mọi tiên đề. Khi đó ba thứ rơi ra:

Ngôn ngữ hình học (bài này) Ngôn ngữ thống kê (môn 4-5)
Độ dài ‖a‖ Độ lệch chuẩn σ
cos(a, b) Hệ số tương quan corr(X, Y)
Trực giao a ⟂ b Không tương quan
Định lý Pythagoras ‖a+b‖² = ‖a‖² + ‖b‖² V[x+y] = V[x] + V[y]

Chạy thật — trừ trung bình rồi tính cosine đúng như §4, so với np.corrcoef:

import numpy as np
rng = np.random.default_rng(0)
x = rng.normal(size=200); y = 0.7*x + 0.7*rng.normal(size=200)

a, b = x - x.mean(), y - y.mean()          # trừ trung bình = đưa về "kỳ vọng 0"
cos  = (a @ b) / (np.linalg.norm(a) * np.linalg.norm(b))     # đúng công thức §4
corr = np.corrcoef(x, y)[0, 1]

print(f"{corr:.12f}")                       # 0.658253509574
print(f"{cos:.12f}")                        # 0.658253509574
assert abs(corr - cos) < 1e-12              # lệch 1.11e-16

# "độ dài" của biến ngẫu nhiên chính là độ lệch chuẩn
assert np.isclose(np.linalg.norm(a)/np.sqrt(len(x)), x.std())   # 0.961179 cả hai

Và vế Pythagoras — dựng z trực giao với a, rồi kiểm rằng phương sai cộng được:

z = rng.normal(size=200); z = z - z.mean()
z = z - (z @ a)/(a @ a) * a                 # ép z ⟂ a
print(f"{np.corrcoef(x, z)[0,1]:.2e}")      # -1.85e-17  ← không tương quan
print(f"{a.var() + z.var() - (a+z).var():.2e}")   # 2.22e-16  ← V[a]+V[z] = V[a+z]

V[x+y] = V[x] + V[y] không phải một quy tắc phải nhớ. Nó là định lý Pythagoras, viết bằng từ vựng khác. Điều kiện "hai biến không tương quan" (Cov[x,y] = 0) chính là điều kiện "hai vector vuông góc" trong bài này (MML tr.196). Lưu ý: độc lập là điều kiện mạnh hơn — độc lập thì suy ra không tương quan, nhưng không tương quan thì chưa chắc độc lập. Pythagoras chỉ cần mức yếu hơn.

Vì sao đáng dừng lại ở đây

Ba khoản trả về sau, mỗi khoản ở một môn khác:

  1. Môn 6 — PCA. Sẽ có lúc bạn hỏi "vì sao đi tìm hướng phương sai lớn nhất lại là phép chiếu trực giao?". Câu trả lời nằm ở bảng trên: phương sai là độ dài bình phương, nên "hướng phương sai lớn nhất" là "hướng vector dài nhất". Cùng một bài toán.
  2. Vẽ biến độc lập bằng trục vuông góc — thói quen ai cũng thấy trong sách thống kê, hầu như không ai giải thích. Giờ bạn biết nó không phải quy ước vẽ cho đẹp.
  3. Biết cả chỗ nó GÃY. MML tr.197 cảnh báo ngược: đừng dùng khoảng cách Euclid để so hai phân phối. Phân phối không sống trong không gian phẳng như vector — chúng sống trên một mặt cong, và ở đó phải dùng KL divergence (môn 4 bài 6). §5 ngay dưới đây nói vì sao Euclid sai cho embedding; đây là một lý do khác, cho một loại đối tượng khác.

🪤 BẪY — và nó dẫn thẳng tới một lỗi bạn sẽ gặp thật ở môn 10

Phải trừ trung bình trước. Bỏ hai dòng - x.mean() đi thì cosine ≠ corr. Với dữ liệu chuẩn ở trên chênh lệch chỉ 0.0019 — nhỏ đến mức dễ tưởng là làm tròn. Nhưng thử dịch tâm dữ liệu, đúng tình huống của embedding thật:

for s in [0, 1, 3, 10]:
    xs, ys = x + s, y + s                    # dịch cả hai đi s
    raw  = (xs @ ys) / (np.linalg.norm(xs) * np.linalg.norm(ys))
    corr = np.corrcoef(xs, ys)[0, 1]
    print(f"{s:>3} | {raw:.6f} | {corr:.6f} | {abs(raw-corr):.4f}")
Dịch tâm cosinethô corr Chênh
0 0.656363 0.658254 0.0019
1 0.833221 0.658254 0.1750
3 0.968124 0.658254 0.3099
10 0.996892 0.658254 0.3386

corr không đổi một chữ số nào — nó tự trừ trung bình. Còn cosine thô tiến về 1.0 và nuốt sạch thông tin: ở cột cuối, hai đại lượng tương quan 0.66 trông như trùng khít hoàn toàn.

Đây chính xác là bệnh anisotropy mà môn 10 bài 3 đo được trên embedding thật: mọi cặp cho cosine 0.87–0.93, ngưỡng nào cũng vô dụng. Nguyên nhân giống hệt bảng trên — không gian bị lệch tâm. Cách chữa cũng giống hệt: trừ trung bình toàn corpus.

Bạn vừa gặp nó ở bài đầu tiên của môn đầu tiên, trên 6 dòng numpy — trước khi chạm vào một mô hình ngôn ngữ nào. Đó là ý nghĩa của việc học nền tảng trước: cùng một lỗi, nhìn từ chỗ nó còn nhỏ và đơn giản.

Trong retrieval người ta thường không trừ trung bình. Nên cosine của embedding không phải hệ số tương quan — cùng công thức, khác tiền xử lý, khác ý nghĩa. Đúng loại chi tiết mà quy tắc 1 cảnh báo: lỗi giết bạn là chi tiết thiết lập, không phải toán.


5. Vì sao retrieval dùng cosine chứ không dùng khoảng cách Euclid

Đây là câu hỏi "tận cùng" số 3 của môn 2. Trả lời bằng một ví dụ:

truy vấn   q    = (1, 0)     ← "mèo"
tài liệu 1 d₁   = (1, 0)     ← bài NGẮN về mèo
tài liệu 2 d₂   = (10, 0)    ← bài DÀI về mèo (cùng hướng, dài gấp 10)

Khoảng cách Euclid:

|q − d₁| = 0        → rất liên quan
|q − d₂| = 9        → xa lắc  ✗ SAI

Cosine:

cosine(q, d₁) = 1 / (1 × 1)  = 1.0    → liên quan hoàn toàn
cosine(q, d₂) = 10 / (1 × 10) = 1.0   → liên quan hoàn toàn  ✓ ĐÚNG

Độ dài vector embedding phản ánh độ dài văn bản / tần suất từ, không phản ánh chủ đề. Một bài dài về mèo vẫn là bài về mèo. Cosine bỏ qua độ dài, chỉ giữ chủ đề — nên nó là thước đo đúng. Euclid thì phạt tài liệu dài, và đó là phạt nhầm thứ.


6. Code — tự cài, KHÔNG dùng np.dot

Mục đích là hiểu, không phải chạy nhanh.

import numpy as np

def dot(u, v):
    """Tích vô hướng — tự cài, không gọi np.dot."""
    return float(np.sum(u * v))

def norm(u):
    """Độ dài — chính là Pythagoras."""
    return float(np.sqrt(dot(u, u)))

def cosine(u, v):
    return dot(u, v) / (norm(u) * norm(v))


def proj(u, v):
    """Bóng của u trên v — vector, cùng hướng v."""
    return (dot(u, v) / dot(v, v)) * v


a = np.array([3.0,  4.0])
b = np.array([4.0,  3.0])
c = np.array([-4.0, 3.0])

for name, (u, v) in {"a·b": (a, b), "a·c": (a, c), "b·c": (b, c)}.items():
    print(f"{name}: dot={dot(u,v):6.2f}  cos={cosine(u,v):6.3f}")

# BẮT BUỘC: đối chiếu với numpy, phải khớp
assert abs(dot(a, b)      - np.dot(a, b))          < 1e-9
assert abs(norm(a)        - np.linalg.norm(a))     < 1e-9
print("khớp numpy ✓")

# phép chiếu: ba khẳng định của §2 phải đúng
assert np.allclose(proj(a, b), [3.84, 2.88])            # bóng của a trên b
assert abs(norm(proj(a, b)) - 24 / norm(b))   < 1e-9    # độ dài bóng = (a·b)/|b|
assert abs(norm(proj(a, b)) - norm(a) * cosine(a, b)) < 1e-9   # = |a|·cos θ
assert np.allclose(proj(a, c), [0.0, 0.0])              # vuông góc → KHÔNG có bóng
assert abs(dot(a - proj(a, b), b)) < 1e-9               # phần dư vuông góc với b
print("phép chiếu ✓  |bóng của a trên b| =", round(float(norm(proj(a, b))), 4))

Kết quả phải ra đúng bảng ở §3. Nếu lệch, bạn có bug — tìm ra bug đó chính là bài học.


7. Đối chiếu với thư viện thật

from sklearn.metrics.pairwise import cosine_similarity
print(cosine_similarity([a], [b]))   # phải khớp cosine() bạn vừa viết

Rồi tự trả lời: nếu chuẩn hoá sẵn mọi vector về độ dài 1 ngay khi lưu, thì mẫu số |u| × |v| thành bao nhiêu? Và vì sao làm vậy lại nhanh hơn nhiều khi phải so 1 truy vấn với 10.000 tài liệu?

Đây chính là thủ thuật mà mọi vector database đều dùng — và bạn vừa tự suy ra được nó.


8. Tự kiểm — trả lời được mới sang bài 2

  1. a · b = 0 nghĩa là gì về mặt hình học? Và về mặt thông tin?
  2. Vì sao cosine luôn nằm trong [−1, 1], không bao giờ ra ngoài?
  3. Nếu nhân cả hai vector với 100, tích vô hướng đổi thế nào? Cosine đổi thế nào? Vì sao khác nhau?
  4. Trong không gian 768 chiều, hai vector ngẫu nhiên có xu hướng gần vuông góc. Điều đó nói gì về việc "gần nghĩa" là chuyện hiếm hay thường?
  5. Một hệ retrieval chuẩn hoá tất cả embedding về độ dài 1 ngay khi lưu. Khi đó cosine rút gọn thành phép gì? Tiết kiệm được gì?
  6. Viết công thức chiếu a lên b mà không nhìn lại §2 — rồi kiểm bằng a = (3,4), b = (4,3), phải ra (3.84, 2.88). Vì sao mẫu số là b · b chứ không phải |b|?
  7. Chiếu a lên b rồi lấy a − proj_b(a). Vector còn lại đó có quan hệ gì với b, và vì sao bắt buộc phải vậy? (Đây là hạt giống của cơ sở trực giao, Gram–Schmidt và SVD ở bài 9.)

Câu 4 là câu sâu nhất. Nó là cửa vào khái niệm "lời nguyền số chiều" mà bạn sẽ gặp lại ở môn 6. Câu 7 là câu trả cổ tức xa nhất: "phần dư vuông góc với phần đã chiếu" là một ý tưởng, và nó chạy suốt từ đây tới phép nén hạng thấp ở bài 9.


9. Bài tập về nhà trước bài 2

Xem 3Blue1Brown — Essence of Linear Algebra:

  • Chương 1 — Vectors, what even are they?
  • Chương 9 — Dot products and duality

Chương 9 nằm ở giữa series, không phải đầu — nhưng nó thuộc về bài này. Xem không ghi chép, không dừng lại giải bài. Mục tiêu duy nhất: có hình ảnh trong đầu. Giữ sẵn một câu hỏi khi xem chương 9: vì sao "chiếu một vector lên vector khác" lại bằng đúng phép nhân-rồi-cộng? Bạn đã có nửa câu trả lời ở §2 — xem để thấy nốt nửa kia. Phần "duality" ở chương 9 khó, xem lướt cũng được — sẽ hiểu sau khi học phép biến đổi tuyến tính ở bài 2.

Bài 2 sẽ là: ma trận = phép biến đổi không gian — chỗ đa số người học sai và không bao giờ hồi phục.


10. Tổng kết cô đọng — phải nhớ gì, và tự suy lại được gì

(ghi 12/10 sau khi học xong bài; dùng để ôn trước khi sang bài 2)

Nhớ đúng một hình

a·b = (cái bóng của a trên b) × |b|, và bóng dài |a| cos θ. Bóng là một số (âm khi θ > 90°). Muốn bóng dạng vector thì nhân thêm b/|b|.

Bốn định nghĩa

Khái niệm Công thức Ví dụ a=(3,4), b=(4,3)
Vector mũi tên = dịch chuyển có hướng = mảng số (768 chiều = mảng 768 số) (3,4): sang phải 3, lên 4
Độ dài ‖u‖ = √(u·u) (Pythagoras). Với vector, ‖·‖ (hay |·|) là độ dài, không phải trị tuyệt đối ‖a‖ = 5
Tích vô hướng Σ aᵢbᵢ (khi có toạ độ) = ‖a‖‖b‖ cos θ (khi muốn hiểu) 3×4 + 4×3 = 24
Cosine a·b / (‖a‖‖b‖), luôn trong [−1, 1] 24/25 = 0.96

Chuỗi tự suy được (quên công thức thì dựng lại từ đây)

  1. Tam giác vuông: cos θ = kề / huyền, mà a là huyền, kề là bóng. Vậy bóng = |a| cos θ.
  2. Nhân thêm |b|: a·b = |a||b| cos θ.
  3. Hai cách viết của dot bằng nhau: chứng minh bằng định lý cosin, hoặc xoay hình cho b nằm ngang (xoay không đổi Σ aᵢbᵢ). Góc cần xoay c (của b) khác θ, và nó biến mất khỏi kết quả.
  4. Chia hai vế cho |a||b| ra cosine. Nó nằm trong [−1, 1] vì |kề| ≤ huyền.
  5. Hình chiếu dạng vector: proj_b(a) = (a·b / b·b) · b. Mẫu số là b·b vì phép chia cho |b| xuất hiện hai lần.
  6. Phần còn lại a − proj_b(a) luôn vuông góc với b. Kiểm bằng số: (3,4) − (3.84, 2.88) = (−0.84, 1.12), và (−0.84, 1.12)·(4,3) = 0. (Câu 7 của §8; bài 9 sẽ dùng lại ý này.)

Dấu và độ lớn của dot

  • > 0: góc nhỏ hơn 90° (cùng phía). = 0: vuông góc (không đóng góp theo hướng nhau). < 0: ngược phía.
  • Độ lớn trộn độ dài với cos θ. Nhân cả hai vector với k thì dot nhân k², cosine không đổi.
  • Muốn so hướng thì dùng cosine. Nếu vector đã chuẩn hoá về độ dài 1 thì cosine bằng dot.

Nối vào AI

  • Mô hình embedding đổi văn bản thành vector; retrieval so bằng cosine, vì độ dài không mang chủ đề (§5).
  • Lưu embedding đã chuẩn hoá một lần: mỗi lần tìm kiếm chỉ còn dot.
  • Trong 768 chiều, hai vector ngẫu nhiên gần vuông góc, nên cosine cao là chuyện hiếm, có ý nghĩa. Embedding thật thì lệch tâm, cần đo nền riêng (§4b).

Chỗ dễ nhầm (đã gặp khi học bài này)

  • |a| cos θ là số, không phải vector. Vế trái của một đẳng thức cho nó không thể là a.
  • Mẫu số của phép chiếu là b·b, không phải |b|.
  • Bóng của a không luôn bằng |b|: chỉ bằng khi chân đường vuông góc rơi đúng đầu mũi tên b.
  • Nhân cả hai vector với 100: dot tăng 100² = 10.000 lần, không phải 100⁴.
  • Cùng hướng (θ = 0, b = k·a với k > 0) khác cùng phương (cả ngược hướng cũng tính). Hai vector bất kỳ luôn nằm trên một mặt phẳng, nên đó không phải điều kiện của cùng hướng.
  • Góc giữa hai vector nằm trong [0°, 180°]. Cos bị chặn vì kề ≤ huyền, không phải vì khoảng góc.
  • Xoay hình chỉ là mẹo để chứng minh; khi tính thì dùng thẳng Σ aᵢbᵢ.

Đáp số để tự kiểm trong 30 giây

a·b = 24    a·c = 0      b·c = −7
cos = 0.96  cos = 0.00   cos = −0.28
proj_b(a) = (3.84, 2.88),  độ dài bóng = 4.8

Vị trí đọc được lưu trên trình duyệt này khi cho phép lưu trữ; không đồng bộ giữa các thiết bị.