Đại số tuyến tính · bài 1

Tích vô hướng và phép chiếu

Tích vô hướng đo hai vector cùng hướng tới đâu. Kéo hai mũi tên để thấy cái bóng, cosine, và vì sao tìm kiếm ngữ nghĩa so bằng cosine.

Bài toán

Một hệ thống RAG nhận câu hỏi “mèo ăn gì?” và phải tìm, trong hàng triệu đoạn văn, những đoạn gần nghĩa nhất. Trước đó, mô hình embedding đã đổi mỗi đoạn văn thành một vector, thường từ vài trăm tới vài nghìn số. “Gần nghĩa” phải trở thành một phép tính trên hai dãy số. Phép tính đó là tích vô hướng, hoặc bản chuẩn hoá của nó là cosine. Trang này trả lời hai câu: con số đó đo cái gì, và vì sao nó đo đúng thứ ta cần.

Kéo thử trước

Kéo đầu mũi tên a (xanh dương) và b (cam). Đường đứt nét là đường chứa b, đoạn xanh lá là bóng của a trên đường đó. Thử đặt hai mũi tên vuông góc, rồi bấm “Nhân đôi b” và xem số nào đổi, số nào giữ nguyên.

Bật JavaScript để kéo hai mũi tên. Bảng số đi kèm ứng với a = (3, 4), b = (4, 3).

a
(3, 4)
b
(4, 3)
a·b
3×4 + 4×3 = 24
|a|
√25 = 5
|b|
√25 = 5
cos θ
24 / (5 × 5) = 0.96
θ
≈ 16.26°
bóng của a trên b
a·b / |b| = 4.8
proj_b(a)
(3.84, 2.88)

a·b > 0: góc nhỏ hơn 90°, bóng nằm cùng phía với b.

Ba điều nên tự thấy trước khi đọc tiếp:

  1. Góc nhọn thì a·b dương, góc vuông thì a·b bằng 0, góc tù thì a·b âm.
  2. a·b bằng 0 đúng lúc bóng của a co lại thành một điểm.
  3. Nhân đôi b thì a·b nhân đôi, còn cos θ không đổi.

Vector: một dãy số, và một mũi tên

Với lập trình viên, [3, 4] là một mảng hai phần tử. Toán nhìn nó thêm một cách: mũi tên đi từ gốc toạ độ, sang phải 3, lên 4. Mũi tên có hướng và có độ dài. Độ dài tính bằng Pythagoras:

∣a∣=32+42=25=5|a| = \sqrt{3^2 + 4^2} = \sqrt{25} = 5

Ở dd chiều công thức giữ nguyên dạng: ∣a∣=a12+a22+⋯+ad2|a| = \sqrt{a_1^2 + a_2^2 + \dots + a_d^2}.

Khi mô hình embedding đổi một câu thành 768 số, nó đặt câu đó vào một điểm trong không gian 768 chiều. Hai câu gần nghĩa thì chỉ về gần cùng một hướng. Phần còn lại của trang là cách đo “gần cùng hướng”.

Tích vô hướng

Định nghĩa: nhân từng cặp số cùng vị trí rồi cộng lại.

a⋅b=a1b1+a2b2+⋯+adbda \cdot b = a_1 b_1 + a_2 b_2 + \dots + a_d b_d

Viết bằng code chỉ một dòng. Ý nghĩa nằm ở đẳng thức nối nó với hình học:

a⋅b=∣a∣ ∣b∣cos⁡θa \cdot b = |a|\,|b| \cos\theta

với θ\theta là góc giữa hai mũi tên. Vì ∣a∣|a| và ∣b∣|b| luôn dương, dấu của a⋅ba \cdot b là dấu của cos⁡θ\cos\theta:

a·bgóc θđọc là
dươngnhỏ hơn 90°cùng phía
bằng 0đúng 90°vuông góc
âmlớn hơn 90°ngược phía

Phép chiếu: cái bóng của a trên b

Rọi đèn vuông góc xuống đường thẳng chứa bb: cái bóng của aa dài bao nhiêu? Chia a⋅ba \cdot b cho ∣b∣|b| để bỏ phần độ dài của bb, ta được đúng độ dài cái bóng, gọi là ss:

s=a⋅b∣b∣=∣a∣cos⁡θs = \frac{a \cdot b}{|b|} = |a|\cos\theta

Bóng có dấu: góc tù thì bóng nằm ngược phía với bb và mang dấu âm. Muốn bóng ở dạng vector nằm trên đường chứa bb, nhân độ dài đó với vector đơn vị b/∣b∣b/|b|:

proj⁡b(a)=a⋅b∣b∣⋅b∣b∣=a⋅bb⋅b b\operatorname{proj}_b(a) = \frac{a \cdot b}{|b|} \cdot \frac{b}{|b|} = \frac{a \cdot b}{b \cdot b}\, b

Từ đây đọc lại được nghĩa của tích vô hướng: độ dài bóng của a trên b, nhân với độ dài của b.

Tính tay

Ba vector, cố ý cho cùng độ dài 5 để chỉ còn hướng là khác nhau:

a = ( 3, 4)    b = ( 4, 3)    c = (−4, 3)      |a| = |b| = |c| = 5

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 tích hai độ dài, 25, được cosine, rồi ra góc:

cặptích vô hướngcos θθ
a, b240.9616.26°
a, c00.0090°
b, c−7−0.28106.26°

Kiểm bằng góc thật: a nghiêng arctan⁡(4/3)≈53.13°\arctan(4/3) \approx 53.13° so với trục ngang, b nghiêng 36.87°36.87°, c nghiêng 143.13°143.13°. Hiệu các góc là 16.26°, 90° và 106.26°, khớp cả ba.

Bóng của a trên b dài 24/5=4.824 / 5 = 4.8, kiểm chéo bằng ∣a∣cos⁡θ=5×0.96=4.8|a|\cos\theta = 5 \times 0.96 = 4.8. Dạng vector:

proj⁡b(a)=2425 (4,3)=(3.84, 2.88)\operatorname{proj}_b(a) = \tfrac{24}{25}\,(4, 3) = (3.84,\ 2.88)

Còn bóng của a trên c là 025 (−4,3)=(0,0)\tfrac{0}{25}\,(-4, 3) = (0, 0). Vuông góc nghĩa là không có bóng: a không có thành phần nào theo hướng c.

Cosine, và vì sao tìm kiếm dùng nó

Chia tích vô hướng cho tích hai độ dài để bỏ hẳn ảnh hưởng của độ dài:

cos⁡(a,b)=a⋅b∣a∣ ∣b∣\cos(a, b) = \frac{a \cdot b}{|a|\,|b|}

Đây là hàm cosine similarity mà các hệ thống tìm kiếm vector gọi. Nó cũng là tỉ lệ bóng: độ dài bóng của a trên b chia cho chính độ dài của a.

Nhân b với một số dương k thì a⋅ba \cdot b nhân k, nhưng ∣b∣|b| cũng nhân k, nên cosine giữ nguyên. Nhân cả hai vector với 100 thì tích vô hướng tăng 1002=10 000100^2 = 10\,000 lần, cosine vẫn không đổi.

Tính chất đó quan trọng ở chỗ nào? Lấy vector đếm từ: truy vấn q=(1,0)q = (1, 0) hỏi về “mèo”, tài liệu d1=(1,0)d_1 = (1, 0) là một bài ngắn về mèo, d2=(10,0)d_2 = (10, 0) là bài dài gấp 10, cùng chủ đề.

thước đoq với d₁q với d₂kết quả
khoảng cách Euclid09phạt bài dài
tích vô hướng110thưởng bài dài
cosine11chấm như nhau

Ở vector đếm từ, độ dài phản ánh độ dài văn bản chứ không phản ánh chủ đề, nên cosine là thước đo đúng. Với embedding của một mô hình ngôn ngữ, độ dài mang nghĩa gì là tuỳ mô hình; nên đo trên mô hình mình dùng trước khi dựa vào.

Nhiều hệ thống chuẩn hoá mọi embedding về độ dài 1 ngay khi lưu. Khi đó ∣a∣=∣b∣=1|a| = |b| = 1, cosine bằng đúng tích vô hướng, và mỗi lần tìm chỉ còn phép nhân rồi cộng.

Ở 768 chiều thì góc ở đâu ra?

Không vẽ được 768 chiều, nhưng hai vector không cùng phương luôn nằm trên một mặt phẳng đi qua gốc, và góc giữa chúng là góc trên mặt phẳng đó. Ở mọi số chiều, bất đẳng thức Cauchy–Schwarz ∣a⋅b∣≤∣a∣ ∣b∣|a \cdot b| \le |a|\,|b| bảo đảm cosine nằm trong đoạn [−1,1][-1, 1], nên góc luôn định nghĩa được.

Một hệ quả đáng nhớ: hai vector ngẫu nhiên càng nhiều chiều càng gần vuông góc. Khi các toạ độ lấy ngẫu nhiên theo phân phối chuẩn, trung bình của ∣cos⁡∣|\cos| là 2/π≈0.6372/\pi \approx 0.637 ở 2 chiều, nhưng chỉ khoảng 0.029 ở 768 chiều. Embedding thật thì không ngẫu nhiên như vậy: chúng thường lệch chung về một phía, nên hai câu chẳng liên quan cũng có thể có cosine cao. Muốn biết cosine bao nhiêu là “gần”, phải đo mức nền trên chính mô hình mình dùng.

Hiểu nhầm hay gặp

Các ý dưới đây là lỗi thật đã gặp khi học bài này.

  • “Vector trong toạ độ là mũi tên, còn trong không gian là một điểm.” Cả hai cách nhìn dùng được ở mọi số chiều. Cần hướng và độ dài thì nhìn là mũi tên; cần vị trí và khoảng cách thì nhìn là điểm.
  • “∣a∣cos⁡θ|a|\cos\theta là một vector.” Nó là một số: độ dài có dấu của bóng. Bóng dạng vector là proj⁡b(a)\operatorname{proj}_b(a).
  • “Mẫu số của phép chiếu dạng vector là ∣b∣|b|.” Mẫu số là b⋅b=∣b∣2b \cdot b = |b|^2: chia một lần ∣b∣|b| để ra độ dài bóng, thêm một lần nữa để biến bb thành vector đơn vị.
  • “Bóng của a dài bằng ∣b∣|b|.” Chỉ đúng khi chân đường vuông góc hạ từ đầu a rơi đúng vào đầu mũi tên b. Nói chung độ dài bóng là ∣a∣cos⁡θ|a|\cos\theta.
  • “Nhân cả hai vector với 100 thì tích vô hướng tăng 1004100^4 lần.” Mỗi số hạng aibia_i b_i nhận một thừa số 100 từ mỗi vector, nên tích vô hướng tăng 1002=10 000100^2 = 10\,000 lần.
  • “Cosine nằm trong [−1,1][-1, 1] vì góc nằm giữa 0° và 180°.” Khoảng của góc không chặn được một tỉ số. Ở nhiều chiều, cái chặn là bất đẳng thức Cauchy–Schwarz.
  • “Vuông góc nghĩa là hai thứ không chia sẻ thông tin gì.” Vuông góc chỉ nói a không có thành phần theo hướng b. Với dữ liệu đã trừ trung bình, vuông góc nghĩa là không tương quan, và không tương quan còn yếu hơn độc lập.

Code: tự viết rồi đối chiếu

Viết dot, norm, cosine, proj bằng Python thuần, không dùng np.dot, rồi so với numpy. Mỗi assert là một con số trong trang này; chạy hết không báo lỗi tức là các số ở trên đúng.

# Tích vô hướng, độ dài, cosine, phép chiếu: tự viết bằng Python thuần rồi đối chiếu với numpy.
# Mọi assert dưới đây là số được nhắc trong bài; chạy không báo lỗi tức là số trong bài đúng.
import math

import numpy as np


def dot(u, v):
    return sum(x * y for x, y in zip(u, v))


def norm(u):
    return math.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 đường chứa v, dạng vector: (u·v / v·v) v."""
    k = dot(u, v) / dot(v, v)
    return [k * x for x in v]


a, b, c = [3, 4], [4, 3], [-4, 3]

# Ba cặp tính tay
for ten, u, v, d, cs, goc in [
    ("a·b", a, b, 24, 0.96, 16.26),
    ("a·c", a, c, 0, 0.00, 90.00),
    ("b·c", b, c, -7, -0.28, 106.26),
]:
    g = math.degrees(math.acos(cosine(u, v)))
    print(f"{ten} = {dot(u, v):>3}   cos = {cosine(u, v):+.2f}   góc = {g:.2f}°")
    assert dot(u, v) == d == np.dot(u, v)
    assert round(cosine(u, v), 2) == cs and round(g, 2) == goc

# Phép chiếu a lên b, và a lên c (vuông góc nên không có bóng)
p = proj(a, b)
print("proj_b(a) =", [round(x, 2) for x in p], "  độ dài bóng =", dot(a, b) / norm(b))
assert np.allclose(p, [3.84, 2.88])
assert math.isclose(dot(a, b) / norm(b), 4.8) and math.isclose(norm(p), 4.8)
assert proj(a, c) == [0, 0]

# Nhân b với k thì dot nhân k, cosine giữ nguyên; nhân cả hai với 100 thì dot nhân 100² = 10 000
for k in [2, 10, 0.5]:
    bk = [k * x for x in b]
    assert math.isclose(dot(a, bk), k * dot(a, b)) and math.isclose(cosine(a, bk), cosine(a, b))
a100, b100 = [100 * x for x in a], [100 * x for x in b]
assert dot(a100, b100) == 10_000 * dot(a, b)

# Truy vấn "mèo": Euclid phạt bài dài, cosine thì không
q, d1, d2 = [1, 0], [1, 0], [10, 0]


def euclid(u, v):
    return norm([x - y for x, y in zip(u, v)])


print("Euclid:", euclid(q, d1), euclid(q, d2), "  cosine:", cosine(q, d1), cosine(q, d2))
assert (euclid(q, d1), euclid(q, d2)) == (0, 9) and cosine(q, d1) == cosine(q, d2) == 1

# Hai vector ngẫu nhiên: càng nhiều chiều càng gần vuông góc.
# Lý thuyết: trung bình |cos| = Γ(n/2) / (√π · Γ((n+1)/2)); n = 2 cho 2/π ≈ 0.637.
rng = np.random.default_rng(0)
for n, so_mau in [(2, 20_000), (768, 2_000)]:
    ly_thuyet = math.exp(math.lgamma(n / 2) - math.lgamma((n + 1) / 2)) / math.sqrt(math.pi)
    x, y = rng.standard_normal((2, so_mau, n))
    cos = np.abs((x * y).sum(1)) / (np.linalg.norm(x, axis=1) * np.linalg.norm(y, axis=1))
    print(f"n = {n:>3}: |cos| trung bình = {cos.mean():.3f}   lý thuyết = {ly_thuyet:.3f}")
    assert abs(cos.mean() - ly_thuyet) < 0.01
assert round(2 / math.pi, 3) == 0.637
assert round(math.exp(math.lgamma(384) - math.lgamma(384.5)) / math.sqrt(math.pi), 3) == 0.029
print("đúng hết")

Nằm ở đâu trong AI

Khái niệmChỗ dùng
Tích vô hướngĐiểm attention: mỗi ô của QK⊤QK^\top là tích vô hướng giữa một truy vấn và một khoá (bài 2)
CosineTìm kiếm vector trong RAG; embedding đã chuẩn hoá về độ dài 1 thì cosine bằng tích vô hướng
Phép chiếuHồi quy tuyến tính chiếu dữ liệu lên không gian cột (bài 3); PCA chiếu lên vài hướng giữ nhiều thông tin nhất
Vuông gócCơ sở trực giao tách một vector thành các phần không lẫn vào nhau, nền của SVD và PCA

Tự kiểm

Câu 1

Cho a = (3, 4) và c = (−4, 3). Tích vô hướng a·c bằng bao nhiêu, và nó nói gì về hai mũi tên?

  • A. 24, hai mũi tên gần cùng hướng
  • B. −7, hai mũi tên hơi ngược hướng
  • C. 0, hai mũi tên vuông góc
  • D. 25, hai mũi tên dài bằng nhau
Xem đáp án

C. 3×(−4) + 4×3 = −12 + 12 = 0. Tích vô hướng bằng 0 nghĩa là vuông góc: a không có bóng trên c. Số 24 là a·b, −7 là b·c, còn 25 là tích hai độ dài.

Câu 2

Truy vấn q = (1, 0) hỏi về “mèo”. Tài liệu d₁ = (1, 0) là bài ngắn về mèo, d₂ = (10, 0) là bài dài gấp 10 lần, cùng chủ đề. Thước đo nào chấm hai tài liệu liên quan như nhau?

  • A. cosine của góc giữa q và tài liệu
  • B. khoảng cách Euclid từ q tới tài liệu
  • C. tích vô hướng của q với tài liệu
  • D. tổng các toạ độ của tài liệu
Xem đáp án

A. cos(q, d₁) = 1 và cos(q, d₂) = 10 / (1 × 10) = 1. Euclid cho 0 và 9, tích vô hướng cho 1 và 10, tổng toạ độ cũng 1 và 10: cả ba để độ dài văn bản lẫn vào điểm.

Câu 3

Nhân cả hai vector a và b với 100. Tích vô hướng và cosine đổi thế nào?

  • A. tích vô hướng nhân 100, cosine nhân 100
  • B. tích vô hướng nhân 10 000, cosine giữ nguyên
  • C. tích vô hướng nhân 100⁴, cosine giữ nguyên
  • D. tích vô hướng giữ nguyên, cosine nhân 10 000
Xem đáp án

B. (100a)·(100b) = 100²·(a·b). Mẫu số của cosine là |100a|·|100b|, cũng gấp 10 000 lần, nên tỉ số không đổi.

Nguồn của trang này