Bài 1 kết thúc với một câu: “mọi token thành một vector, và mọi thứ sau đó đều là toán ma trận”. Rồi tiếp tục bỏ lửng. Bài này trả nợ câu đó.

Vector là gì, ngoài định nghĩa “list số”? Matrix là gì ngoài “bảng số”? Và tại sao phép tính gọi là dot product lại xuất hiện ở khắp mọi nơi trong LLM — từ attention, RAG retrieval, cho đến projection cuối cùng trước khi chọn token?

Dev thường né linear algebra vì hình dung đến mấy trang công thức ký hiệu Hy Lạp. Bài này không làm vậy. Bốn khái niệm, intuition trước công thức, code NumPy copy-paste được ngay. Đủ để bạn đọc paper mà không bị khớp khi thấy ký hiệu Q @ K.T.

Dành cho: ai đã đọc bài 1, viết code hàng ngày, nhưng cuối cùng vẫn skip phần math mỗi lần thấy nó trong tutorial.

Mental model tổng quát

Trước hết, xác định quan hệ giữa các khái niệm sẽ gặp:

scalar   <   vector   <   matrix   <   tensor
  1 số      list số      2D grid     nD grid
  3.14      [0.2, -1.8]  [[...],[...]]  [[[...]]]

Mỗi cấp là cấp dưới xếp thành bảng. Và LLM — về mặt cơ học — là một chuỗi dài các phép tính trên matrix và tensor, nối tiếp nhau từ đầu đến cuối pipeline:

text input
    |
    v
[ tokenize ]   -> list integer (scalar)
    |
    v
[ embed ]      -> matrix [N_tokens x D_dim]
    |
    v
[ attention ]  -> matmul chain bên trong
    |
    v
[ project ]    -> matmul cuối ra logits
    |
    v
text output

Bốn khái niệm cần ngấm để hiểu sơ đồ trên: vector, dot product, matrix, matrix multiplication. Đủ, không cần thêm.

Phần 1: Vector — list số có siêu năng lực

Định nghĩa kỹ thuật: vector là một mảng số có thứ tự, kích thước cố định.

import numpy as np

v = np.array([0.2, -1.8, 0.6])
# shape: (3,)   <- 3 chiều, 1 chiều

Nhìn như một Python list bình thường. Điểm khác biệt nằm ở hai “siêu năng lực”:

Siêu năng lực 1: Hướng (direction). Vector trong không gian 2D có thể hình dung là mũi tên từ gốc toạ độ đến điểm (x, y). Vector [1, 0] chỉ thẳng sang phải. Vector [0, 1] chỉ thẳng lên trên. Vector [0.7, 0.7] chỉ chéo 45 độ.

   y
   |
 1 +        . [0.7, 0.7]
   |      /
   |    /
   |  /
   |/
---+-----------> x
   0         1

Siêu năng lực 2: Độ lớn (magnitude). Độ dài của mũi tên — tính theo Pythagoras: sqrt(x^2 + y^2). Vector [3, 4] có độ lớn sqrt(9+16) = 5.

Hai vector “tương tự nhau” khi chúng chỉ về cùng hướng, bất kể độ lớn. Vector [1, 0][100, 0] khác độ lớn nhưng cùng hướng — “ý nghĩa” giống nhau, chỉ khác “cường độ”.

Tại sao LLM cần vector 4096 chiều? Vì ý nghĩa của một từ không thể nắm bắt bằng 1 số duy nhất. “Bank” trong “river bank” và “Bank of America” khác nhau. Bằng cách dùng vector nhiều chiều, mỗi chiều capture một khía cạnh khác nhau của nghĩa — ngữ pháp, ngữ nghĩa, register, domain, quan hệ với các từ khác. 4096 chiều là đủ phức tạp để biểu diễn sự tinh tế đó.

Không thể vẽ vector 4096 chiều, nhưng toán học hoạt động giống hệt như 2D. Đó là điều đẹp của linear algebra — nguyên tắc 2D mở rộng lên N chiều không thay đổi.

# Embedding vector thực trong LLM
token_embedding = np.array([0.234, -1.827, 0.041, 0.682, ...])
# shape: (4096,)  <- 4096 chiều

Nhớ: embedding = vector. Mỗi token ID trong bài 1 được map sang một vector như thế này. Đó là “từ điển số” của model.

Phần 2: Dot product — đo độ tương đồng

Dot product (tích vô hướng) là phép tính giữa hai vector cùng kích thước. Cách tính: nhân từng cặp phần tử tương ứng, rồi cộng tất cả lại.

a = [1, 2, 3]
b = [4, 5, 6]

a · b = (1×4) + (2×5) + (3×6)
      = 4 + 10 + 18
      = 32

Kết quả là một số duy nhất (scalar) — không phải vector. Đây là lý do tên tiếng Việt là “tích vô hướng”.

import numpy as np

a = np.array([1, 2, 3])
b = np.array([4, 5, 6])

result = np.dot(a, b)   # 32
# hoặc dùng @ operator (đọc được hơn trong code LLM):
result = a @ b           # 32

Intuition quan trọng: dot product lớn khi hai vector “cùng hướng”. Dot product nhỏ (hoặc âm) khi hai vector “ngược hướng”. Dot product gần 0 khi hai vector “vuông góc nhau” (không liên quan).

Ví dụ cụ thể: giả sử vector biểu diễn “vua” và “nữ hoàng” có nhiều chiều giống nhau (cả hai là royalty, cả hai là người lớn, cả hai có quyền lực). Dot product của chúng sẽ cao. Dot product giữa “vua” và “táo” sẽ thấp — hai khái niệm không liên quan.

# Giả lập: vector nhỏ hơn thực tế để minh hoạ
king   = np.array([0.9, 0.7, 0.1, 0.8])  # royalty, adult, power, person
queen  = np.array([0.8, 0.6, 0.9, 0.8])  # royalty, adult, female, person
apple  = np.array([0.1, 0.0, 0.0, 0.0])  # food, không liên quan

print(king @ queen)  # ~ 1.5  (cao - cùng domain)
print(king @ apple)  # ~ 0.09 (thấp - khác domain)

Normalize để so sánh công bằng. Nếu một vector có độ lớn rất lớn, dot product của nó với mọi thứ sẽ lớn, bất kể hướng có giống hay không. Giải pháp: chia mỗi vector cho độ lớn của nó trước khi tính — gọi là cosine similarity, kết quả nằm trong khoảng -1 đến 1.

def cosine_similarity(a, b):
    return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))

# 1.0  = giống hệt nhau (cùng hướng)
# 0.0  = không liên quan (vuông góc)
# -1.0 = đối nghịch nhau

LLM dùng dot product ở đâu?

  • Attention score: khi tính token này “liên quan” đến token kia bao nhiêu, model tính dot product giữa vector Q (query) và vector K (key). Score cao = liên quan nhiều = pha trộn nhiều thông tin từ token đó. Đây là core của cơ chế attention bài 1 mô tả.
  • RAG retrieval: khi search tài liệu liên quan đến câu hỏi, hệ thống tính cosine similarity giữa embedding câu hỏi và embedding từng đoạn văn. Top-k đoạn cao nhất được đưa vào context.
  • Embedding similarity: so sánh hai câu, hai từ, hai khái niệm — đều quy về dot product giữa các embedding vector của chúng.

Một phép tính, xuất hiện ở khắp mọi nơi. Đó là lý do bài 1 gọi dot product là backbone.

Phần 3: Matrix — biểu diễn tập hợp vector

Matrix là bảng số 2 chiều — N hàng, D cột. Cũng hiểu được như một list các vector.

Matrix 4x3 (4 hàng, 3 cột):

[ 0.2  -1.8   0.6 ]   <- vector của token 1
[ 0.5   0.3  -0.9 ]   <- vector của token 2
[-1.1   2.1   0.0 ]   <- vector của token 3
[ 0.8  -0.4   1.3 ]   <- vector của token 4

Shape: (4, 3)  ->  4 tokens, mỗi token 3 chiều
import numpy as np

M = np.array([
    [ 0.2, -1.8,  0.6],
    [ 0.5,  0.3, -0.9],
    [-1.1,  2.1,  0.0],
    [ 0.8, -0.4,  1.3],
])
print(M.shape)   # (4, 3)

# Lấy vector của token thứ 2 (index 1):
print(M[1])      # [0.5, 0.3, -0.9]

Embedding matrix của LLM là trường hợp lớn nhất của khái niệm này. Nếu vocab có 100,000 tokens và mỗi token được biểu diễn bằng vector 4096 chiều:

Embedding matrix shape: (100_000, 4096)
Số phần tử:             100,000 × 4096 = 409,600,000 (410 triệu số)

Mỗi hàng là một token. Để lấy embedding của token ID 26683, chỉ cần lấy hàng thứ 26683 — đây là “embedding lookup” bài 1 mô tả. Không có phép tính nào — chỉ là indexing vào matrix.

# Embedding lookup (giả lập)
embedding_matrix = np.random.randn(100_000, 4096)
token_id = 26683
token_embedding = embedding_matrix[token_id]   # shape: (4096,)

Sau bước embed trong pipeline LLM, cả câu input (ví dụ 16 tokens) thành một matrix (16, 4096). Đây là input cho tất cả các Transformer layers phía sau.

Phần 4: Matrix multiplication — ánh xạ giữa không gian

Đây là phép tính nặng nhất và quan trọng nhất trong LLM. Ký hiệu: A @ B (trong NumPy và PyTorch).

Cách tính: phần tử [i, j] của kết quả = dot product của hàng i trong A với cột j trong B.

A (2x3)          B (3x2)          A @ B (2x2)

[1  2  3]   @   [7  8]       =   [(1×7+2×9+3×11)  (1×8+2×10+3×12)]
[4  5  6]       [9  10]          [(4×7+5×9+6×11)  (4×8+5×10+6×12)]
                [11 12]

                             =   [58   64]
                                 [139  154]

Shape rule quan trọng nhất cần nhớ:

(M x K) @ (K x N) = (M x N)

Chiều trong phải khớp nhau (cả hai đều là K). Kết quả có chiều ngoài của cả hai (M từ A, N từ B).

import numpy as np

A = np.array([[1, 2, 3],
              [4, 5, 6]])          # shape (2, 3)

B = np.array([[7,  8],
              [9,  10],
              [11, 12]])           # shape (3, 2)

C = A @ B                          # shape (2, 2)
print(C)
# [[ 58  64]
#  [139 154]]

Intuition 1: transformation. Matrix multiplication ánh xạ vector từ không gian này sang không gian khác. Nhân vector (D,) với matrix (D, H) cho ra vector mới (H,) — “chuyển” nó từ không gian D chiều sang H chiều. Mỗi Transformer layer làm điều này nhiều lần: biến đổi đặc trưng, chiếu sang chiều mới, rồi chiếu lại.

Intuition 2: batch dot product. Nhìn matrix A như một tập hợp nhiều vector xếp thành hàng, matmul tính dot product của từng hàng trong A với từng cột trong B cùng lúc. Khi model tính attention score cho cả câu 16 tokens, nó không tính từng token một — nó làm một phép Q @ K.T duy nhất, trong đó Q và K là matrix, kết quả là toàn bộ bảng score (16, 16) cùng một lúc.

# Batch attention score (giả lập, không cần hiểu chi tiết)
seq_len = 16
d_model = 64

Q = np.random.randn(seq_len, d_model)    # queries, shape (16, 64)
K = np.random.randn(seq_len, d_model)    # keys,    shape (16, 64)

scores = Q @ K.T                          # shape (16, 16)
# scores[i, j] = dot product giữa query token i và key token j
# = "token i chú ý bao nhiêu đến token j"

Một phép matmul thay vì 256 phép dot product rời rạc. GPU cực kỳ giỏi loại tính toán này.

Phần 5: Tất cả gặp nhau trong LLM

Giờ có đủ công cụ để đọc lại pipeline bài 1 và hiểu rõ hơn từng bước:

Tokenize + Embedding lookup: Token IDs là list số nguyên. Embedding lookup là chọn hàng từ embedding matrix — không có phép nhân nào, chỉ là indexing. Kết quả: matrix (N_tokens, D_model).

Attention: Với mỗi layer, matrix input X được nhân với ba matrix trọng số để ra Q, K, V:

Q = X @ W_Q    # (N, D) @ (D, D_k) = (N, D_k)
K = X @ W_K
V = X @ W_V

scores = Q @ K.T / sqrt(D_k)   # (N, N) — ma trận "ai chú ý ai"
weights = softmax(scores)        # normalize thành xác suất
output = weights @ V             # (N, N) @ (N, D_v) = (N, D_v)

Toàn bộ “attention là cơ chế pha trộn context” từ bài 1 gói gọn trong bốn dòng matmul.

Transformer layer: Attention output đi qua một MLP (thêm hai matmul nữa), cộng vào input gốc (residual connection), normalize. Lặp lại 32-96 lần tuỳ model.

Output projection: Vector cuối cùng (D_model,) nhân với unembedding matrix (D_model, V_vocab) ra logits (V_vocab,). Đây là phép matmul cuối trước khi softmax và sampling.

Con số để hình dung quy mô: GPT-3 có 175 tỷ parameters. Đại đa số là entries trong các matrix trọng số: embedding matrix, W_Q, W_K, W_V, W_O cho mỗi head, matrix MLP — tất cả đều là matrix. Training GPT-3 nghĩa là tìm giá trị tối ưu cho 175 tỷ con số đó sao cho model dự đoán token tiếp theo chính xác nhất trên 300 tỷ token training data.

Phần 6: Hands-on — tính word similarity bằng NumPy

Đây là bài tập kinh điển của NLP: word analogy. king - man + woman ≈ queen. Ý nghĩa: nếu bỏ đặc trưng “nam giới” khỏi vector “vua” và thêm đặc trưng “nữ giới” vào, kết quả gần với vector “nữ hoàng” nhất.

Code dưới đây dùng fake embeddings — số ngẫu nhiên có kiểm soát để analogy vẫn đúng — để bạn chạy được ngay mà không cần tải model.

import numpy as np

# Fake embeddings: mỗi chiều represent một đặc trưng ngầm định
# [royalty, adult, male, female, power]
#  dim 0    dim1   dim2  dim3    dim4

embeddings = {
    "king":   np.array([0.99, 0.95, 0.98, 0.05, 0.90]),
    "queen":  np.array([0.99, 0.95, 0.05, 0.97, 0.88]),
    "man":    np.array([0.05, 0.90, 0.97, 0.04, 0.30]),
    "woman":  np.array([0.05, 0.92, 0.04, 0.96, 0.28]),
}

def cosine_similarity(a, b):
    """Dot product sau khi normalize -- ket qua tu -1 den 1."""
    norm_a = np.linalg.norm(a)
    norm_b = np.linalg.norm(b)
    if norm_a == 0 or norm_b == 0:
        return 0.0
    return np.dot(a, b) / (norm_a * norm_b)

# Word analogy: king - man + woman
# Bỏ đặc trưng "male" của "king", thêm đặc trưng "female"
analogy_vector = embeddings["king"] - embeddings["man"] + embeddings["woman"]

print("=== Cosine similarity giữa analogy vector và các từ ===")
for word, vec in embeddings.items():
    sim = cosine_similarity(analogy_vector, vec)
    print(f"  {word:<8}  {sim:.4f}")

# Tìm từ gần nhất (loại trừ king, man, woman)
candidates = {w: cosine_similarity(analogy_vector, v)
              for w, v in embeddings.items()
              if w not in ("king", "man", "woman")}

best_word = max(candidates, key=candidates.get)
print(f"\nking - man + woman = '{best_word}'")  # queen


print("\n=== Similarity matrix toàn bộ ===")
words = list(embeddings.keys())
matrix = np.array([embeddings[w] for w in words])  # shape (4, 5)

# Tính toàn bộ cosine similarity một lúc (batch style)
norms = np.linalg.norm(matrix, axis=1, keepdims=True)  # (4, 1)
normalized = matrix / norms                             # (4, 5)
sim_matrix = normalized @ normalized.T                  # (4, 4)

print(f"{'':8}", end="")
for w in words:
    print(f"{w:>8}", end="")
print()
for i, w in enumerate(words):
    print(f"{w:<8}", end="")
    for j in range(len(words)):
        print(f"{sim_matrix[i, j]:>8.3f}", end="")
    print()

Output mong đợi:

=== Cosine similarity giữa analogy vector và các từ ===
  king      0.9742
  queen     0.9988   <-- gần nhất với analogy vector
  man       0.9198
  woman     0.9301

king - man + woman = 'queen'

=== Similarity matrix toàn bộ ===
          king   queen     man   woman
king     1.000   0.960   0.886   0.872
queen    0.960   1.000   0.820   0.905
man      0.886   0.820   1.000   0.978
woman    0.872   0.905   0.978   1.000

Chú ý dòng cuối: similarity matrix được tính bằng một phép normalized @ normalized.T — đúng pattern batch matmul bên trong attention. RAG retrieval cũng làm y hệt: normalize tất cả document embeddings, normalize query embedding, rồi một phép matmul duy nhất cho ra điểm tương đồng với tất cả documents cùng lúc.

Cheatsheet

Khái niệmBản chấtKý hiệuShape ví dụ
ScalarMột số duy nhấtx()
VectorList số có thứ tựv(D,)
MatrixBảng số 2 chiềuM(N, D)
Dot productScalar từ hai vectora · b hoặc a @ b(D,) · (D,) = ()
MatmulMatrix từ hai matrixA @ B(M, K) @ (K, N) = (M, N)
Cosine similarityDot product sau normalizecos(a, b)(D,) -> () trong [-1, 1]

Bốn điểm phải nhớ:

  • Vector = list số; trong LLM mỗi token là một vector; các vector “gần nhau” có nghĩa tương đồng
  • Dot product = nhân từng phần tử rồi cộng; kết quả lớn = hai vector cùng hướng = tương đồng cao; đây là công thức tính attention score và RAG similarity
  • Matrix = list các vector xếp thành hàng; embedding matrix là bảng tra cứu toàn bộ vocabulary
  • Matmul shape rule: (M, K) @ (K, N) = (M, N) — chiều trong phải khớp, kết quả có chiều ngoài; mỗi Transformer layer là một chuỗi matmul như vậy

Lời kết

Vậy là xong foundation. Bốn khái niệm — vector, dot product, matrix, matmul — là tất cả những gì bạn cần để đọc code PyTorch của một Transformer mà không bị lạc ở bước nào. Khi thấy Q @ K.T trong paper từ giờ, bạn biết đó là “tính attention score giữa tất cả cặp token cùng một lúc”.

Math này đủ cho đến khi bạn đụng phần training. Lúc đó cần thêm một thứ nữa: Calculus — cụ thể là gradient, chain rule, và backpropagation. Đó là bài 3. Intuition của backprop không đáng sợ như tên gọi — về cơ bản là “lỗi sai được tính ngược từ output về input, mỗi matrix trọng số được điều chỉnh theo mức độ đóng góp vào lỗi đó”. Sẽ có code minh hoạ.

Nếu muốn xem trực quan hoá về vector và matrix trước khi đọc tiếp, 3Blue1Brown có series “Essence of Linear Algebra” — xem 4 tập đầu là đủ, mỗi tập khoảng 10 phút. Không cần toàn bộ series, chỉ cần đủ để cái intuition “vector là mũi tên trong không gian” ăn sâu vào đầu.