~3-4 giờ · cần xong bài 2 trước
Bài 2 dạy bạn: ma trận = một hành động lên không gian. Bài này trả lời câu hỏi tiếp theo, và là câu hỏi đắt nhất: hành động đó giữ lại được bao nhiêu chiều, và phần bị mất đi đâu? Con số đó tên là hạng. Nó là nền của LoRA, PCA, SVD và mọi thứ "nén được".
🧭 Làm theo thứ tự này
- Xem trước: 3Blue1Brown tập 2 (xem lại) và tập 7 Inverse matrices, column space and null space. Giữ câu hỏi: 3 vector trong không gian 3 chiều mà span chỉ là một mặt phẳng, chuyện gì đã xảy ra?
- Làm ô ⚡ bên dưới: chạy code với hai ma trận
A,B, nhìn kết quả và hạng. - Đọc §1-9, tự tính tay các ví dụ trước khi xem đáp án. (§4b và §8b là phần mở rộng, để sau.) Cuối §9 có bài tập "ma trận là bộ nhớ" (khoảng 15 dòng code): tự viết rồi mới đối chiếu bảng.
- Làm 3 bài giấy ở mục "Bài tập về nhà" cuối bài, trước khi gõ code.
- Code: tự cài hạng, không dùng
np.linalg.matrix_rank. Tự viết xong mới mở code mẫu trong bài. - Tự kiểm.
- Thưởng: xem tập 6 và tập 8.
Xong bài khi: hạng tự cài khớp numpy, và bạn nói được hạng là "số chiều còn sống sót sau phép biến đổi".
⚡ 30 phút đầu — làm trước, đọc sau
import numpy as np
A = np.array([[1., 2.], [2., 4.]])
B = np.array([[1., 2.], [3., 4.]])
for v in [(1., 1.), (3., 0.), (2., -1.)]:
v = np.array(v)
print(v, " -> A@v =", A @ v, " | B@v =", B @ v)
print("rank:", np.linalg.matrix_rank(A), np.linalg.matrix_rank(B))
Đích phải ra đúng:
| đầu vào | A @ v |
B @ v |
|---|---|---|
(1, 1) |
(3, 6) |
(3, 7) |
(3, 0) |
(3, 6) |
(3, 9) |
(2, −1) |
(0, 0) |
(0, 2) |
rank(A) = 1, rank(B) = 2.
Dừng lại ở hai chỗ trước khi đọc tiếp:
Abiến hai đầu vào khác nhau(1,1)và(3,0)thành cùng một đầu ra(3,6). Đây là đụng độ — như hash collision. Đã đụng độ thì không có hàm nghịch đảo.Abiến(2,−1)— một vector khác 0 — thành đúng gốc toạ độ. Cả một hướng bị xoá sạch.
B không làm cả hai chuyện đó. Khác biệt giữa chúng gói trong một số: hạng.
1. Tổ hợp tuyến tính và SPAN
Trong đại số tuyến tính bạn chỉ được dùng đúng hai phép: cộng vector, và nhân vector với một số. Ghép chúng lại thì được tổ hợp tuyến tính:
a·v₁ + b·v₂ + ... + k·vₙ (a, b, ..., k là số thực bất kỳ)
SPAN của một bộ vector = tập hợp TẤT CẢ tổ hợp tuyến tính của chúng. Nói bằng ngôn ngữ backend: span = tập mọi kết quả bạn có thể tạo ra từ bộ nguyên liệu đó. Nó là reachable set, không phải là bộ nguyên liệu.
Các trường hợp trong mặt phẳng, cần nhớ như bảng tra:
| Bộ vector | Span là gì | Số chiều |
|---|---|---|
chỉ (0,0) |
một điểm — gốc toạ độ | 0 |
một vector khác 0, ví dụ (1,2) |
một đường thẳng qua gốc | 1 |
hai vector không cùng phương, ví dụ (1,3) và (2,4) |
cả mặt phẳng | 2 |
hai vector cùng phương, ví dụ (1,2) và (2,4) |
vẫn chỉ một đường thẳng | 1 |
Dòng cuối là bẫy, và là toàn bộ bài này. Hai vector không đảm bảo hai chiều.
Ví dụ tính tay — span của (1,2) và (2,4)
Thử tạo ra (5, 6):
a·(1,2) + b·(2,4) = (a + 2b, 2a + 4b)
Để ý: thành phần thứ hai luôn gấp đôi thành phần thứ nhất, vì 2a + 4b = 2(a + 2b).
Nên mọi kết quả đều có dạng (t, 2t) — tức là nằm trên đường y = 2x.
(5,6) cần 6 = 2×5 = 10. Sai. → (5,6) nằm ngoài span. Không có a, b nào tạo ra được nó.
Còn (6,12) thì 12 = 2×6 ✓ → tạo ra được, và tạo được bằng rất nhiều cách:
a=6, b=0 → (6, 12)
a=0, b=3 → (6, 12)
a=2, b=2 → (6, 12)
a=4, b=1 → (6, 12)
a=-2, b=4 → (6, 12)
Giữ danh sách này lại. §6 sẽ cho thấy năm dòng đó không ngẫu nhiên — chúng lệch nhau đúng một quy luật.
2. Phụ thuộc tuyến tính — cột nào là cột thừa
Đây là chỗ trả lời dứt điểm câu 5 của bài 2.
Một bộ vector PHỤ THUỘC tuyến tính nếu có một tổ hợp tuyến tính của chúng bằng 0 mà không phải mọi hệ số đều bằng 0. Ngược lại thì ĐỘC LẬP tuyến tính.
Câu đó nghe khô. Nói lại bằng tiếng người:
Phụ thuộc = có ít nhất một vector là thừa — nó đã nằm sẵn trong span của những vector kia, nên thêm nó vào không mở rộng thêm chiều nào.
Ẩn dụ SQL: một bảng có cột gia_ban và cột gia_ban_x2. Cột thứ hai là cột dẫn xuất — nó chiếm chỗ,
làm bảng rộng ra, nhưng không mang một bit thông tin mới nào. Chuẩn hoá là bỏ nó đi. Phụ thuộc tuyến
tính chính là chuyện đó, viết bằng ký hiệu toán.
Kiểm bằng tay — hai cột của A
A = [[1,2],[2,4]]. Nhắc lại bài 2: cột của ma trận là nơi vector cơ sở đáp xuống.
Cột của A là c₁ = (1,2) và c₂ = (2,4).
Giải a·c₁ + b·c₂ = 0:
a·(1,2) + b·(2,4) = (a + 2b, 2a + 4b) = (0, 0)
phương trình 1: a + 2b = 0
phương trình 2: 2a + 4b = 0 ← chính là phương trình 1 nhân 2, KHÔNG cho tin gì mới
Còn đúng một ràng buộc: a = −2b. Chọn b = −1 → a = 2:
2·(1,2) − 1·(2,4) = (2−2, 4−4) = (0,0) ✓ hệ số (2, −1) khác 0
→ A có hai cột PHỤ THUỘC tuyến tính. Cụ thể c₂ = 2·c₁.
Kiểm bằng tay — hai cột của B
B = [[1,2],[3,4]], cột là c₁ = (1,3) và c₂ = (2,4). (Chú ý: cột, không phải hàng. Đây là lỗi
sai phổ biến nhất khi đọc ma trận viết trên giấy.)
a·(1,3) + b·(2,4) = 0
a + 2b = 0 → a = −2b
3a + 4b = 0 → −6b + 4b = 0 → −2b = 0 → b = 0 → a = 0
Chỉ có nghiệm tầm thường a = b = 0 → ĐỘC LẬP tuyến tính.
Đây là câu trả lời cho câu 5 của bài 2. Một ma trận bóp dẹp mặt phẳng thành đường thẳng ⟺ hai cột của nó phụ thuộc tuyến tính ⟺ hai cột cùng phương ⟺ nó chỉ có một hướng thật sự. Bốn cách nói, một sự thật.
(Nếu bạn đã nghe tới định thức: det(A) = 1·4 − 2·2 = 0, det(B) = 1·4 − 2·3 = −2. Định thức = 0
là cùng một sự thật nhìn từ góc khác — nó là hệ số co giãn diện tích, mà bóp thành đường thì diện tích = 0.
Chưa cần hiểu kỹ ở bài này, BỎ QUA được, tập 6 của 3B1B lo phần đó.)
3. Cơ sở — bộ toạ độ tối thiểu
Cơ sở của một không gian = một bộ vector vừa ĐỘC LẬP tuyến tính, vừa SPAN được cả không gian đó.
Độc lập = không dư. Span = không thiếu. Cơ sở là điểm cân bằng đúng giữa hai yêu cầu.
Trong ℝ², {(1,0), (0,1)} là cơ sở quen thuộc. Nhưng không phải cơ sở duy nhất — hai cột của B
cũng là một cơ sở, vì chúng độc lập (§2) và span cả mặt phẳng.
Ví dụ tính tay: viết (3,5) theo cơ sở {(1,3), (2,4)}.
a·(1,3) + b·(2,4) = (3,5)
a + 2b = 3
3a + 4b = 5
từ pt1: a = 3 − 2b
thay: 3(3 − 2b) + 4b = 5 → 9 − 6b + 4b = 5 → −2b = −4 → b = 2, a = −1
kiểm: −1·(1,3) + 2·(2,4) = (−1+4, −3+8) = (3, 5) ✓
Vậy (3,5) trong hệ toạ độ chuẩn = (−1, 2) trong cơ sở mới. Cùng một mũi tên, hai bộ số.
Số chiều của một không gian = số vector trong một cơ sở của nó. Con số này không phụ thuộc vào việc bạn chọn cơ sở nào — đó là một định lý, và nó là lý do "chiều" là khái niệm có nghĩa.
(Chuyện "cùng một vector, nhiều bộ toạ độ" chính là bài 6 — đổi cơ sở. Ở đây chỉ cần thấy nó tồn tại.)
4. HẠNG — số chiều mà phép biến đổi GIỮ LẠI được
Đây là ý trung tâm của cả bài. Đọc chậm.
HẠNG (rank) của ma trận = số chiều của không gian mà nó ĐẨY MỌI THỨ VÀO. Tương đương: số cột độc lập tuyến tính. Tương đương: số chiều mà phép biến đổi giữ lại được.
Ẩn dụ DevOps: hạng là băng thông thật của đường ống. Bạn đổ vào 2 chiều dữ liệu; nếu ống chỉ có 1 làn thì đầu kia ra 1 chiều, bất kể bạn đổ vào bao nhiêu. Số làn = hạng.
Đối chiếu A và B
A = [[1,2],[2,4]] |
B = [[1,2],[3,4]] |
|
|---|---|---|
| Hai cột | (1,2), (2,4) — cùng phương |
(1,3), (2,4) — khác phương |
| Quan hệ | c₂ = 2·c₁ |
không có tỉ lệ nào |
| Span của các cột | đường thẳng y = 2x |
cả mặt phẳng ℝ² |
| Hạng | 1 | 2 |
| Đầu vào 2 chiều → đầu ra | 1 chiều — mất 1 chiều | 2 chiều — giữ nguyên |
| Có nghịch đảo? | KHÔNG | CÓ |
det |
0 | −2 |
Kiểm bằng số — A bóp cả mặt phẳng xuống đúng đường y = 2x:
A·(v₁, v₂) = (1·v₁ + 2·v₂, 2·v₁ + 4·v₂)
= (t, 2t) với t = v₁ + 2v₂
Thành phần thứ hai luôn gấp đôi thành phần thứ nhất. Không có ngoại lệ, không phụ thuộc v.
Chạy 100.000 vector ngẫu nhiên qua A: sai lệch lớn nhất của y − 2x là đúng 0.0 (code §Code kiểm điều này).
Ma trận
Acó 4 số, mô tả một hành động lên vô hạn điểm — và toàn bộ vô hạn điểm đó đáp xuống một đường thẳng. Cả mặt phẳng bị ép vào một chiều. Đó là hạng 1, nhìn tận mắt.
Các trường hợp cực đoan — để chốt khái niệm
| Ma trận | Hạng | Làm gì với mặt phẳng |
|---|---|---|
[[0,0],[0,0]] |
0 | bóp mọi thứ về đúng gốc toạ độ |
[[1,0],[0,0]] |
1 | chiếu vuông góc lên trục x: (3,5) → (3,0), (3,−2) → (3,0) |
[[1,2],[2,4]] |
1 | bóp lên đường y = 2x |
[[1,2],[3,4]] |
2 | méo mó nhưng không mất chiều nào |
[[1,0],[0,1]] |
2 | không làm gì cả |
BẪY 1: "hạng 1" không có nghĩa là ma trận chỉ có một hàng hoặc một cột khác 0.
[[1,2],[2,4]]có đủ 4 số khác 0 mà hạng vẫn là 1. Hạng đếm hướng độc lập, không đếm ô khác 0.
BẪY 2: hạng luôn
≤ min(số hàng, số cột). Ma trận768×4có hạng tối đa 4, dù nó có 768 hàng. Kiểm bằng số: ma trận ngẫu nhiên(3,2),(2,3),(5,2),(768,4)cho hạng lần lượt2, 2, 2, 4— luôn bằngmincủa hai chiều, vì ngẫu nhiên thì gần như chắc chắn không có phụ thuộc nào ngoài phần kích thước ép buộc (ma trận2×3có 3 cột trongℝ²nên chắc chắn có một cột thừa; hạng bị chặn bởi 2 hàng).
BẪY 3 (đắt nhất, và bạn sẽ gặp thật): hạng theo cột luôn bằng hạng theo hàng. Nghe vô lý, nhưng đúng, và là một trong những định lý đẹp nhất của môn này. Ở đây chấp nhận và dùng, đừng dừng lại chứng minh — bạn sẽ thấy nó hiển nhiên sau khi học SVD.
4b. ⭐ Ba mặt phẳng không cắt nhau ⟺ ba vector nằm cùng mặt phẳng
(bổ sung 19/8 — nguồn: Strang tr.10 (§1.2), tr.vii (Preface), tr.119 (§2.4))
BẪY 3 vừa bảo bạn "chấp nhận và dùng". Trước khi làm vậy, hãy nhìn nó một lần ở dạng hình học — vì ở dạng đó nó không còn là một định lý phải nhớ, nó là một chuyện đáng ngạc nhiên.
Một hệ 3 phương trình 3 ẩn có hai cách đọc, đã gặp ở bài 2:
| Đọc theo HÀNG | Đọc theo CỘT |
|---|---|
mỗi hàng = một mặt phẳng trong ℝ³ |
mỗi cột = một vector trong ℝ³ |
| giải hệ = tìm giao điểm của 3 mặt phẳng | giải hệ = tìm tổ hợp của 3 vector ra được b |
| hỏng khi 3 mặt phẳng không cắt nhau tại một điểm | hỏng khi 3 vector nằm cùng một mặt phẳng |
Hai kiểu hỏng đó LUÔN xảy ra cùng lúc. Ba mặt phẳng trượt vào nhau thì ba vector cũng dẹp xuống cùng một mặt phẳng, và ngược lại. Không có ma trận nào hỏng kiểu này mà không hỏng kiểu kia.
Đó chính là "hạng hàng = hạng cột" đội lốt hình học. Và đây là chỗ đáng dừng lại: hàng là một phương trình, cột là một vector — hai loại vật hoàn toàn khác nhau. Không có lý do trực giác nào để chúng phải hỏng cùng nhau. Ai đọc xong thấy "hiển nhiên mà" thì thường là chưa hiểu câu hỏi.
Bằng chứng rằng nó không hiển nhiên: chính tác giả cũng không nhìn ra
Trong Preface (tr.vii), Strang đưa ma trận:
A = [ 1 1 2 ]
[ 1 2 3 ]
[ 1 3 4 ]
Cột thì nhìn ra trong 5 giây: cột1 + cột2 = cột3. Ba vector cột phụ thuộc.
Theo định lý, ba hàng cũng phải phụ thuộc. Nhưng tổ hợp nào? Strang viết nguyên văn: "You might find that combination quickly (I didn't)." — tác giả cuốn sách phải dùng khử Gauss mới tìm ra. Trước khi chạy code dưới đây, tự thử 2 phút.
import numpy as np
from scipy.linalg import null_space
A = np.array([[1., 1., 2.],
[1., 2., 3.],
[1., 3., 4.]])
print(A[:, 0] + A[:, 1] - A[:, 2]) # [0. 0. 0.] ← CỘT: dễ thấy
# HÀNG: tổ hợp hàng cho vector 0 chính là nullspace của Aᵀ
n = null_space(A.T)[:, 0]
print(np.round(n / n[0], 6)) # [ 1. -2. 1.]
c = np.array([1., -2., 1.])
print(c @ A) # [0. 0. 0.] ← hàng1 − 2·hàng2 + hàng3 = 0
print(np.linalg.matrix_rank(A)) # 2
assert np.allclose(A[:, 0] + A[:, 1] - A[:, 2], 0) # cột phụ thuộc
assert np.allclose(c @ A, 0) # hàng cũng phụ thuộc
assert np.linalg.matrix_rank(A) == 2
assert null_space(A).shape[1] == null_space(A.T).shape[1] == 1 # CÙNG số chiều
Ma trận 3×3 nhưng hạng 2. Hai dòng null_space cuối là phần đáng nhìn nhất: không gian null
của A và của Aᵀ sống ở hai chỗ khác nhau, nhưng cùng số chiều — đó là hạng hàng = hạng cột,
viết bằng ngôn ngữ bài này.
Vì sao khó tự nghĩ ra
Vì bạn học "hàng" trong ngữ cảnh giải hệ phương trình và "cột" trong ngữ cảnh tổ hợp vector —
hai chương khác nhau, hai hình ảnh khác nhau, không ai bảo chúng liên quan. Cột cột1+cột2=cột3
đập vào mắt; hàng hàng1−2·hàng2+hàng3=0 thì không có hệ số nào gợi ý. Cùng một ma trận, cùng một
sự thật, nhưng một mặt lộ ra còn mặt kia phải đi tìm. Đó là lý do định lý này đáng ngạc nhiên
chứ không đáng bỏ qua.
Bài tập tại chỗ: lấy chính
Aᵀ = [[1,1,1],[1,2,3],[2,3,4]]. Bây giờ HÀNG mới là mặt lộ ra (hàng1 + hàng2 = hàng3), còn tổ hợp CỘT thì phải đi tìm. Tìm nó trên giấy trước khi chạynull_space. Đây đúng là bài tập Strang đã đặt cho chính mình.
🪤 BẪY: "hạng hàng = hạng cột" không có nghĩa là không gian null của A và của Aᵀ là một.
Chúng chỉ cùng số chiều. Ở ví dụ trên: null(A) chứa (1,1,−1) và sống trong ℝ³ đầu vào;
null(Aᵀ) chứa (1,−2,1) và sống trong ℝ³ đầu ra — đúng BẪY 4 ở §7. Với ma trận 768×4 thì
hai chỗ đó còn khác cả số chiều của vũ trụ chứa chúng (ℝ⁴ và ℝ⁷⁶⁸), chỉ có hạng là chung.
5. Không gian cột — tập mọi đầu ra khả dĩ
KHÔNG GIAN CỘT (column space) của
M= span của các cột củaM= tập tất cả những vector màM·xcó thể tạo ra.
Backend: đây là tập giá trị trả về khả dĩ của hàm x ↦ M·x. Không phải kiểu dữ liệu trả về —
mà tập giá trị thật sự chạm tới được.
không gian cột của A = span{(1,2), (2,4)} = span{(1,2)} = đường y = 2x → 1 chiều
không gian cột của B = span{(1,3), (2,4)} = cả ℝ² → 2 chiều
Hạng = số chiều của không gian cột. Đó là định nghĩa gọn nhất của hạng, và nó giải thích luôn vì sao "số cột độc lập" và "số chiều giữ lại được" là cùng một số.
Hệ quả trực tiếp: khi nào M·x = t giải được?
Hệ
M·x = tcó nghiệm ⟺tnằm trong không gian cột củaM. Hết. Không có điều kiện nào khác.
Kiểm bằng số với A (không gian cột = đường y = 2x):
t |
y − 2x |
Kết luận |
|---|---|---|
(3, 6) |
0 | trong span → có nghiệm (ví dụ x = (1,1) hoặc (3,0)) |
(6, 12) |
0 | trong span → có nghiệm, và có vô số |
(−1, −2) |
0 | trong span → có nghiệm |
(5, 6) |
−4 | ngoài span → VÔ NGHIỆM |
(1, 1) |
−1 | ngoài span → VÔ NGHIỆM |
Cẩn thận khi kiểm bằng numpy: np.linalg.solve(A, t) ném lỗi Singular matrix với mọi t — kể cả
t = (3,6) vốn có nghiệm — vì solve chỉ chạy được trên ma trận khả nghịch, mà A thì không. Lỗi đó nói
"A suy biến", không nói "t ngoài không gian cột". Muốn biết (5,6) vô nghiệm thì kiểm đúng điều kiện
của bảng trên: y − 2x = −4 ≠ 0, (5,6) không nằm trên đường y = 2x, nên không có x nào bay tới đó được.
Với B thì không gian cột là cả ℝ², nên B·x = t luôn có nghiệm với mọi t.
Ví dụ t = (3,5) → x = (−1, 2) (đúng phép tính tay ở §3).
6. Không gian null — cái bị bóp về gốc
Không gian cột nói về đầu ra. Không gian null nói về đầu vào, và nó là chỗ chiều bị mất đi.
KHÔNG GIAN NULL (null space, hay kernel) của
M= tập tất cảxsao choM·x = 0.
Backend: đây là tập input bị nghiền về cùng một giá trị. Nếu nó chứa nhiều hơn mỗi vector 0, hàm của bạn không đơn ánh — có đụng độ, và không invert được.
Tìm không gian null của A bằng tay
A·(x, y) = (x + 2y, 2x + 4y) = (0, 0)
pt1: x + 2y = 0
pt2: 2x + 4y = 0 ← lại là pt1 nhân 2, không cho ràng buộc mới
Còn đúng một ràng buộc x = −2y. Đặt y = −1 → x = 2:
A·(2, −1) = (1·2 + 2·(−1), 2·2 + 4·(−1)) = (2−2, 4−4) = (0, 0) ✓
Và mọi bội số của nó cũng vậy: A·(4,−2) = (0,0), A·(−2,1) = (0,0).
Không gian null của
A= span{(2, −1)} = một ĐƯỜNG THẲNG, 1 chiều.
Đối chiếu scipy.linalg.null_space(A) trả về (−0.8944, 0.4472). Trông khác? Không hề:
(2,−1)/√5 = (0.8944, −0.4472). Cùng một đường thẳng, chỉ khác dấu và đã chuẩn hoá về độ dài 1.
Cơ sở của một không gian không duy nhất (§3) — thư viện chọn cơ sở trực chuẩn, bạn chọn số nguyên.
Còn B?
B·(2, −1) = (1·2 + 2·(−1), 3·2 + 4·(−1)) = (0, 2) ≠ (0,0)
scipy.linalg.null_space(B) trả về ma trận shape (2, 0) — rỗng, không có cột nào.
Không gian null của B chỉ chứa đúng vector 0. Không có gì bị bóp mất → B invert được.
Quay lại danh sách năm dòng ở §1
A·x = (6,12) có các nghiệm (6,0), (0,3), (2,2), (4,1), (−2,4). Trừ từng cặp:
(6,0) − (0,3) = ( 6, −3) = 3 · (2, −1) ✓
(6,0) − (2,2) = ( 4, −2) = 2 · (2, −1) ✓
(2,2) − (−2,4) = ( 4, −2) = 2 · (2, −1) ✓
Mọi nghiệm lệch nhau đúng một bội số của vector null. Không ngẫu nhiên chút nào:
Tập nghiệm của
M·x = t= một nghiệm bất kỳ + toàn bộ không gian null. Vì nếuM·x₀ = tvàM·n = 0thìM·(x₀ + n) = t + 0 = t.Không gian null chính là số nghiệm dư. Nó rỗng → nghiệm duy nhất. Nó 1 chiều → vô số nghiệm nằm trên một đường thẳng.
7. Định lý hạng–nullity: chiều không tự mất, nó chuyển chỗ
Ghép §5 và §6 lại:
hạng + số chiều không gian null = SỐ CỘT của ma trận
dim(không gian cột) + dim(không gian null) = n
Đọc bằng tiếng người: mỗi chiều đầu vào hoặc là được giữ lại, hoặc là bị bóp về 0. Không có lựa chọn thứ ba, và không chiều nào biến mất khỏi sổ sách. Đây là một luật bảo toàn.
Kiểm bằng số trên các ví dụ đã tính:
| Ma trận | số cột | hạng | nullity | tổng |
|---|---|---|---|---|
A = [[1,2],[2,4]] |
2 | 1 | 1 | 2 ✓ |
B = [[1,2],[3,4]] |
2 | 2 | 0 | 2 ✓ |
C (3×3, §8) |
3 | 2 | 1 | 3 ✓ |
[[0,0],[0,0]] |
2 | 0 | 2 | 2 ✓ |
I₂ |
2 | 2 | 0 | 2 ✓ |
BẪY 4: không gian cột sống trong không gian ĐẦU RA (
ℝᵐ), không gian null sống trong không gian ĐẦU VÀO (ℝⁿ). Với ma trận vuông thì hai chỗ trùng nhau nên dễ lẫn. Với ma trận3×2thì không gian null ⊂ℝ²còn không gian cột ⊂ℝ³— hai vũ trụ khác nhau. Cộng chúng lại là vô nghĩa; chỉ có số chiều của chúng mới cộng được, và tổng làn = 2(số cột), không phải 3.
8. Tính hạng bằng tay — khử Gauss trên ma trận 3×3
Với 2×2 bạn nhìn ra ngay. Với ma trận thật thì cần một quy trình. Quy trình đó là khử Gauss:
biến đổi hàng cho tới khi ra dạng bậc thang, rồi đếm số hàng khác 0.
Ba phép biến đổi hàng được dùng, và điểm mấu chốt: cả ba đều KHÔNG làm đổi hạng — chúng chỉ đổi cách mô tả, không đổi không gian. (Đổi chỗ hai hàng · nhân một hàng với số khác 0 · cộng bội của hàng này vào hàng kia.)
C = [ 1 2 3 ]
[ 4 5 6 ]
[ 7 8 9 ]
Bước 1 — dùng hàng 1 để dọn cột 1:
R2 ← R2 − 4·R1 : (4,5,6) − 4(1,2,3) = ( 0, −3, −6)
R3 ← R3 − 7·R1 : (7,8,9) − 7(1,2,3) = ( 0, −6, −12)
[ 1 2 3 ]
[ 0 −3 −6 ]
[ 0 −6 −12 ]
Bước 2 — dùng hàng 2 để dọn cột 2:
R3 ← R3 − 2·R2 : (0,−6,−12) − 2(0,−3,−6) = (0, 0, 0)
[ 1 2 3 ]
[ 0 −3 −6 ]
[ 0 0 0 ] ← MỘT HÀNG BIẾN MẤT
Hai hàng khác 0 → rank(C) = 2. np.linalg.matrix_rank(C) cũng trả 2 ✓.
Ma trận 3×3 mà hạng chỉ 2 → nó bóp không gian 3 chiều xuống một mặt phẳng. Theo §7,
nullity = 3 − 2 = 1, nên phải có một đường thẳng bị bóp về gốc. Tìm nó bằng cách rút gọn tiếp:
rref(C) = [ 1 0 −1 ] x₁ − x₃ = 0 → x₁ = x₃
[ 0 1 2 ] → x₂ + 2x₃ = 0 → x₂ = −2x₃
[ 0 0 0 ] x₃ tự do
chọn x₃ = 1 → vector null = (1, −2, 1)
kiểm: C·(1,−2,1) = (1−4+3, 4−10+6, 7−16+9) = (0, 0, 0) ✓
scipy trả (−0.4082, 0.8165, −0.4082), đúng bằng −(1,−2,1)/√6. Cùng một đường thẳng ✓.
Cột có trụ (pivot) = cột độc lập. Cột không có trụ = cột thừa, và mỗi cột thừa sinh ra đúng một chiều của không gian null. Đó là toàn bộ định lý hạng–nullity, nhìn qua quy trình tính.
(Ví dụ nữa, gọn hơn: ba vector (1,0,0), (0,1,0), (1,1,0) trong ℝ³. Cột 3 = cột 1 + cột 2 → thừa.
Hạng 2, span chỉ là mặt phẳng z = 0, nullity 1 với vector null (1,1,−1). Đây đúng là câu hỏi
3B1B đặt ra ở tập 2: "3 vector trong không gian 3 chiều mà span chỉ là một mặt phẳng — chuyện gì đã xảy ra?"
Câu trả lời: một trong ba là tổ hợp của hai cái kia.)
8b. ⭐ Trụ bé: vì sao lập luận "det bảo toàn nên đổi hàng vô nghĩa" là SAI
(bổ sung 19/8 — nguồn: Strang tr.226, §4.1)
Ở §8 bạn vừa viết p = r + argmax(|M[r:, c]|) — chọn trụ lớn nhất rồi đổi hàng. Nếu chưa hỏi
"vì sao phải làm thế" thì hỏi bây giờ, vì có một lập luận nghe rất thuyết phục rằng làm thế
là vô nghĩa:
det A = ±(tích các trụ). Định thức là bất biến — nó không đổi khi bạn đổi hàng (trừ dấu). Vậy tích các trụ luôn bằng chừng ấy. Tránh một trụ bé thì cái bé đó phải quay lại ở chỗ khác. Đổi hàng chỉ đẩy vấn đề đi lòng vòng.
Lập luận này đúng về đại số và sai về hậu quả. Strang trả lời: đúng là tích không đổi, nhưng trong thực tế một trụ bé bất thường thường bị theo sau ngay bởi một trụ lớn bất thường. Tích về lại bình thường — "but it leaves the numerical solution in ruins."
Chạy thật. Hệ [[ε, 1], [1, 1]]·x = [1+ε, 2], nghiệm đúng là (1, 1) với mọi ε.
Khử tay, không đổi hàng, tức bắt buộc lấy ε làm trụ:
import numpy as np
def khu_khong_doi_hang(eps):
"""Khử Gauss 2×2, KHÔNG đổi hàng — buộc phải lấy ε bé làm trụ."""
a11, a12, b1 = eps, 1.0, 1.0 + eps
a21, a22, b2 = 1.0, 1.0, 2.0
m = a21 / a11
y = (b2 - m * b1) / (a22 - m * a12)
x = (b1 - a12 * y) / a11
return x, y
print(" ε | det | nghiệm tay | sai số")
for eps in [1e-1, 1e-4, 1e-8, 1e-12, 1e-15]:
M = np.array([[eps, 1.], [1., 1.]])
x, y = khu_khong_doi_hang(eps)
print(f" {eps:.0e} | {np.linalg.det(M):9.6f} | ({x:.6f}, {y:.6f}) | {max(abs(x-1), abs(y-1)):.2e}")
assert abs(np.linalg.det(np.array([[1e-15, 1.], [1., 1.]])) + 1.0) < 1e-9 # det vẫn "khoẻ"
assert max(abs(np.array(khu_khong_doi_hang(1e-15)) - 1.0)) > 0.1 # nghiệm thì nát
# numpy CÓ đổi hàng (partial pivoting) nên không sao
assert np.allclose(np.linalg.solve([[1e-15, 1.], [1., 1.]], [1 + 1e-15, 2.]), [1., 1.])
| ε | det |
nghiệm tay | sai số |
|---|---|---|---|
| 1e-1 | −0.900000 | (1.000000, 1.000000) | 8.88e-16 |
| 1e-4 | −0.999900 | (1.000000, 1.000000) | 1.10e-13 |
| 1e-8 | −1.000000 | (1.000000, 1.000000) | 6.08e-09 |
| 1e-12 | −1.000000 | (0.999867, 1.000000) | 1.33e-04 |
| 1e-15 | −1.000000 | (0.888178, 1.000000) | 1.12e-01 |
Đọc hai cột cạnh nhau. det đi từ −0.9 về −1.0 rồi đứng yên — trông ngày càng lành mạnh.
Trong khi đó nghiệm rời khỏi (1,1) và ở dòng cuối sai 11%. Không có exception, không có
cảnh báo, ma trận không suy biến (det = −1, cách xa 0), matrix_rank trả về 2.
Vì sao khó tự nghĩ ra: lập luận "det bảo toàn nên đổi hàng vô nghĩa" không có lỗi logic nào — nó đúng ở mọi bước, và nó dùng đúng một sự thật bạn vừa học ở bài 4. Cái sai nằm ở chỗ kết luận được rút ra về một đại lượng khác với đại lượng được lập luận. Đó là loại sai lầm không bao giờ tự lộ ra khi ngồi nghĩ; nó chỉ lộ khi có ai đó chạy số và cho bạn xem hai cột cạnh nhau.
Vì sao đây là bài học rộng, không phải mẹo về khử Gauss
Một chỉ số tổng hợp giữ nguyên KHÔNG có nghĩa là quá trình bên trong lành mạnh.
det là một đại lượng bảo toàn — nó không quan tâm thứ tự hàng, không quan tâm bạn khử thế nào.
Chính vì thế nó không thể nói gì về chuyện xảy ra trong lúc khử. Suy từ "đại lượng bảo toàn
không đổi" sang "mọi thứ ổn" là một bước nhảy sai, và nó sai ở khắp nơi:
| Đại lượng bảo toàn / tổng hợp | Thứ nó KHÔNG nói được |
|---|---|
det = tích các trụ |
từng trụ có bé đến mức mất chữ số có nghĩa không |
| loss trung bình của một epoch | vài batch có phát nổ rồi tự lành hay không |
‖∇L‖ tổng |
lớp nào đang nhận gradient bằng 0 (môn 3 bài 3) |
| accuracy tổng | mô hình có chết hẳn trên một lớp nhãn hiếm không |
Và nó là hình dạng chính xác của sai lầm ở quy tắc 1: Kaplan không sai toán, ông sai vì thước đo tổng hợp (đường cong loss) chưa chạm đáy mà vẫn được đọc như đã ổn định. Cùng một lỗi suy luận, chỉ đổi đối tượng.
🪤 BẪY: đừng rút ra "vậy dùng np.linalg.solve là an toàn". solve có đổi trụ nên qua được
ví dụ trên, nhưng nó không cứu được ma trận có số điều kiện thật sự xấu — đổi trụ chỉ chữa
trụ bé do sắp xếp, không chữa bài toán bản thân nó nhạy. Phân biệt hai thứ đó là nội dung
của bài 6 môn 3; nhớ trước một câu:
trụ bé là lỗi của thuật toán, số điều kiện xấu là tính chất của ma trận.
8c. ⭐ Gram–Schmidt — kiểm độc lập bằng cách chiếu, và phân rã A = QR (bổ sung từ VMLS — 2026-10)
(nguồn: VMLS tr.97-102, §5.4; QR: tr.189-190, §10.4)
§8 kiểm độc lập bằng khử Gauss. Có cách thứ hai, dùng đúng thứ bài 1 dạy (phép chiếu), và bài 1 §8 câu 7 đã hứa nó. Duyệt các vector theo thứ tự; với mỗi vector, trừ đi cái bóng của nó lên mọi vector đã xử lý, phần dư vuông góc với tất cả, rồi chuẩn hoá thành vector mới. Phần dư bằng 0 nghĩa là vector đó nằm trong span của các vector trước.
với i = 1..k:
q̃ᵢ = aᵢ − (q₁ᵀaᵢ)q₁ − … − (qᵢ₋₁ᵀaᵢ)qᵢ₋₁ ← trừ các cái bóng (bài 1 §2)
nếu q̃ᵢ = 0: DỪNG, aᵢ là tổ hợp của a₁…aᵢ₋₁ (phụ thuộc)
qᵢ = q̃ᵢ / ‖q̃ᵢ‖
Tính tay (VMLS tr.100) với a₁ = (−1,1,−1,1), a₂ = (−1,3,−1,3), a₃ = (1,3,5,7):
i=1: ‖a₁‖ = 2 q₁ = (−½, ½, −½, ½)
i=2: q₁ᵀa₂ = 4 q̃₂ = a₂ − 4q₁ = (1,1,1,1) ‖q̃₂‖ = 2 q₂ = (½, ½, ½, ½)
i=3: q₁ᵀa₃ = 2, q₂ᵀa₃ = 8 q̃₃ = a₃ − 2q₁ − 8q₂ = (−2,−2,2,2) ‖q̃₃‖ = 4 q₃ = (−½,−½,½,½)
Không dừng ở đâu, nên ba vector độc lập. Các số vừa tính (2, 4, 2 / 2, 8 / 4) xếp thành ma trận tam giác trên
R = [[2,4,2],[0,2,8],[0,0,4]], với Rᵢⱼ = qᵢᵀaⱼ (i < j) và Rᵢᵢ = ‖q̃ᵢ‖. Gom lại: A = QR, Q có cột trực
chuẩn, R tam giác trên. Cột j của R là toạ độ của aⱼ trong cơ sở trực chuẩn Q (bài 6 §9b: toạ độ trong cơ sở
trực chuẩn chỉ là Qᵀaⱼ). Dừng sớm = phụ thuộc: thay a₃ bằng a₁ + 2a₂ thì q̃₃ = 0. Để kiểm b có thuộc
span(a₁…aₖ) không (§5), chạy Gram–Schmidt trên (a₁,…,aₖ,b): dừng ở bước k+1 khi và chỉ khi có.
import numpy as np
def gram_schmidt(V, tol=1e-10):
"""Trả (Q, R, k): k = chỉ số cột gây phụ thuộc (None nếu độc lập hết)."""
n, m = V.shape
Q, R = np.zeros((n, m)), np.zeros((m, m))
for i in range(m):
qt = V[:, i].copy()
for j in range(i):
R[j, i] = Q[:, j] @ V[:, i]
qt -= R[j, i] * Q[:, j]
if np.linalg.norm(qt) < tol * max(1.0, np.linalg.norm(V[:, i])):
return Q[:, :i], R[:i, :i], i
R[i, i] = np.linalg.norm(qt); Q[:, i] = qt / R[i, i]
return Q, R, None
a1, a2, a3 = np.array([-1., 1, -1, 1]), np.array([-1., 3, -1, 3]), np.array([1., 3, 5, 7])
V = np.column_stack([a1, a2, a3])
Q, R, k = gram_schmidt(V)
assert k is None and np.allclose(R, [[2, 4, 2], [0, 2, 8], [0, 0, 4]])
assert np.allclose(Q.T @ Q, np.eye(3)) and np.allclose(Q @ R, V) # A = QR
assert gram_schmidt(np.column_stack([a1, a2, a1 + 2 * a2]))[2] == 2 # a₃ thừa: dừng ở chỉ số 2
assert gram_schmidt(np.column_stack([a1, a2, [1., 0, 0, 0]]))[2] is None # b ∉ span
Qn, Rn = np.linalg.qr(V)
print(np.diag(Rn)) # [ 2. -2. -4.] numpy đổi dấu
s = np.sign(np.diag(Rn))
assert np.allclose(Qn * s, Q) and np.allclose(Rn * s[:, None], R)
U = np.random.default_rng(0).normal(size=(50, 3)) # cột 3 = 0.3·cột 1 − 1.7·cột 2
W = np.column_stack([U[:, 0], U[:, 1], 0.3 * U[:, 0] - 1.7 * U[:, 1]])
Q2, _, k2 = gram_schmidt(W)
print(k2, np.linalg.matrix_rank(W)) # 2 2
print(np.linalg.norm(W[:, 2] - Q2 @ (Q2.T @ W[:, 2]))) # 1.3e-15, KHÔNG phải 0
🪤 Hai bẫy thiết lập (quy tắc 1). (1) Trên máy, q̃ thường không đúng bằng 0: cột phụ thuộc
hay để lại phần dư cỡ 1e-15 (ví dụ ngẫu nhiên ở trên ra cỡ 1e-15; riêng ví dụ VMLS số đẹp thì ra đúng 0), nên phải có ngưỡng tol. Đây là cùng bệnh "hạng số học vs hạng thật" ở cuối §9.
(2) np.linalg.qr trả R có đường chéo (2, −2, −4): LAPACK dùng phép phản xạ Householder (ổn định hơn
Gram–Schmidt), không bảo đảm đường chéo dương, nên đổi dấu từng cột của Q và hàng của R. So thẳng với bản tự cài sẽ
tưởng mình cài sai. Muốn khớp, nhân với dấu của diag(R) như dòng assert ở trên.
9. Hạng là cổ chai — mất rồi không lấy lại được
Sự thật quan trọng nhất về hạng cho phần AI:
rank(A·B) ≤ min(rank A, rank B)Ghép hai phép biến đổi thì hạng không bao giờ tăng. Chiều đã mất, mất vĩnh viễn.
Kiểm bằng số. rank(A) = 1, rank(B) = 2:
A·B = [ 7 10 ] rank = 1 (hàng 2 = 2 × hàng 1)
[ 14 20 ]
B·A = [ 5 10 ] rank = 1
[ 11 22 ]
Nhân thêm bao nhiêu lần B (hạng 2) cũng vô ích:
rank(B¹·A) = 1 rank(B²·A) = 1 rank(B³·A) = 1 rank(B⁴·A) = 1 rank(B⁵·A) = 1
Ẩn dụ pipeline: bạn có 8 stage xử lý, stage thứ 3 nén dữ liệu xuống 1 byte. Năm stage sau đó dù mạnh đến mấy cũng không phục hồi được thứ stage 3 đã vứt. Hạng của cả pipeline bị chặn bởi stage hẹp nhất.
Đây chính là cơ chế của bottleneck layer trong autoencoder — và cũng là cơ chế bị lợi dụng có chủ đích trong LoRA (§Nối vào AI).
Mặt kia của đồng xu: hạng thấp thì NÉN được
Ma trận hạng 1 luôn viết được thành tích ngoài của hai vector:
A = [ 1 2 ] = (1,2) ⊗ (1,2) tức là A[i][j] = u[i] · w[j]
[ 2 4 ]
Kiểm: np.outer([1,2],[1,2]) ra đúng A ✓.
Tổng quát: ma trận m×n hạng r cần lưu r·(m+n) số thay vì m·n số. Với m=1000, n=768, r=50:
lưu đầy đủ : 1000 × 768 = 768.000 số
lưu hạng 50: 50 × (1000 + 768) = 88.400 số → 11,5%
Đó là toàn bộ ý tưởng của LoRA, PCA, SVD, và nén mô hình nói chung — sẽ học kỹ ở bài 9.
Ma trận là bộ nhớ — bài tập nhỏ nối sang các paper về LLM memory
Tích ngoài (ở trên) và nhân ma trận (bài 2) ghép lại thành một cái nhìn rất hay dùng trong các paper về trí nhớ của mô hình:
Ghi một cặp (key
k, valuev) vào ma trậnW: cộng thêm tích ngoàiv ⊗ k(hạng 1). Đọc bằng khoáq: nhânW·q.
Ghi n cặp: W = Σᵢ vᵢ ⊗ kᵢ. Đọc bằng khoá kⱼ: W·kⱼ = Σᵢ vᵢ (kᵢ·kⱼ). Dùng bài 1: nếu các khoá trực chuẩn thì kᵢ·kⱼ = 0 khi i ≠ j, nên chỉ còn vⱼ — đọc đúng. Nếu không trực giao, các vᵢ khác lẫn vào: nhiễu. Mà trong d chiều chỉ có tối đa d vector độc lập, nên tối đa d khoá trực giao. Lưu quá d cặp thì không thể đọc đúng hết — đây là lập luận trực giác ở §4.1 của paper Linear Transformers Are Secretly Fast Weight Programmers (Schlag và cộng sự, 2021).
Tự làm (khoảng 15 dòng, đừng chép): d = 16, n cặp với khoá chuẩn hoá về độ dài 1, value ngẫu nhiên. Ghi bằng np.outer, đọc lại bằng nhân ma trận, đo lỗi tương đối ‖giá trị đọc − giá trị thật‖ / ‖giá trị thật‖. Thử ba cách:
- khoá ngẫu nhiên, ghi cộng dồn;
- khoá trực chuẩn (lấy từ
np.linalg.qr), chỉ được khin ≤ d; - delta rule: trước khi ghi cặp mới, đọc giá trị cũ
v̄ = W·k, rồi ghi(v − v̄) ⊗ k(xoá cũ, ghi mới).
Đáp án mẫu, rng = np.random.default_rng(0), một lần chạy, một seed, đừng coi là số cố định:
n |
khoá ngẫu nhiên | khoá trực chuẩn | delta rule |
|---|---|---|---|
| 4 | 0.373 | 0.000 | 0.254 |
| 8 | 0.662 | 0.000 | 0.539 |
| 16 | 0.907 | 0.000 | 0.729 |
| 32 | 1.406 | không tồn tại (quá d) |
1.029 |
| 64 | 2.076 | không tồn tại (quá d) |
1.186 |
Đọc bảng: khoá trực chuẩn đọc chính xác đến n = d (đúng bài 1 và bài 3). Khoá ngẫu nhiên đã nhiễu ngay từ n = 4, vì ở 16 chiều chúng chưa đủ gần vuông góc. Delta rule giảm lỗi nhưng không đưa về 0, và khi n > d lỗi vẫn vượt 1. Câu hỏi mở cho bạn: vì sao delta rule một lượt không về 0? Đoán, rồi thiết kế một thí nghiệm để kiểm đoán đó (quy tắc 3 trong nguyen-tac.md).
Giới hạn: đây là bản đơn giản (một lượt, β = 1, không có hàm đặc trưng φ như paper). Nó minh hoạ ý, không tái hiện kết quả của paper. Chữ số cụ thể phụ thuộc thứ tự bạn sinh số ngẫu nhiên, nên hãy so xu hướng (trực chuẩn về 0, ngẫu nhiên tăng theo n, delta rule thấp hơn nhưng không về 0), không so từng chữ số.
Bẫy thực tế: hạng số học vs hạng thật
Chạy mô phỏng: tạo X = U·V với U là 1000×50 và V là 50×768. Ma trận X có 768 cột
nhưng chỉ có 50 chiều thật.
rank(X) = 50 ← đúng như thiết kế
rank(X + nhiễu 1e-3) = 768 ← "hạng đầy" ✗ vô nghĩa
Chỉ cần thêm nhiễu cực nhỏ là matrix_rank nhảy lên 768. Nhưng nhìn giá trị kỳ dị (singular values):
s[50] / s[0] = 4.96e-05 ← chiều thứ 51 trở đi nhỏ hơn chiều đầu 20.000 lần
số chiều cần để giữ 99% năng lượng = 49
BẪY 5: với dữ liệu thực (luôn có nhiễu),
matrix_rankgần như luôn trả về hạng đầy và gần như luôn vô dụng. Cái bạn cần là hạng hiệu dụng: nhìn dãy giá trị kỳ dị tụt xuống ở đâu. "Hạng" trên lý thuyết là số nguyên; trên dữ liệu thật nó là một đường cong tụt dần.
Code — tự cài, KHÔNG dùng np.linalg.matrix_rank
Cài khử Gauss từ đầu. Mỗi assert dưới đây tương ứng một khẳng định trong bài.
import numpy as np
from scipy.linalg import null_space
TOL = 1e-10
def rref(M):
"""Đưa về dạng bậc thang rút gọn. Trả về (ma trận, danh sách cột trụ)."""
M = np.array(M, dtype=float).copy() # KHÔNG sửa đầu vào
rows, cols = M.shape
pivots, r = [], 0
for c in range(cols):
if r >= rows:
break
p = r + int(np.argmax(np.abs(M[r:, c]))) # chọn trụ lớn nhất cho ổn định số
if abs(M[p, c]) < TOL:
continue # cột này không có trụ -> cột thừa
M[[r, p]] = M[[p, r]] # đổi chỗ hai hàng
M[r] = M[r] / M[r, c] # chuẩn hoá trụ về 1
for k in range(rows): # khử cả trên lẫn dưới
if k != r and abs(M[k, c]) > TOL:
M[k] = M[k] - M[k, c] * M[r]
pivots.append(c)
r += 1
return M, pivots
def rank(M):
"""HẠNG = số cột trụ = số chiều còn sống sót."""
return len(rref(M)[1])
def null_basis(M):
"""Cơ sở không gian null: mỗi cột KHÔNG có trụ sinh đúng một vector."""
R, pivots = rref(M)
cols = M.shape[1]
free = [c for c in range(cols) if c not in pivots]
basis = []
for f in free:
v = np.zeros(cols)
v[f] = 1.0
for i, p in enumerate(pivots):
v[p] = -R[i, f]
basis.append(v)
return np.array(basis).T if basis else np.zeros((cols, 0))
A = np.array([[1., 2.], [2., 4.]])
B = np.array([[1., 2.], [3., 4.]])
C = np.array([[1., 2., 3.], [4., 5., 6.], [7., 8., 9.]])
# (a) hạng tự cài phải khớp numpy, và hạng + nullity = số cột
for name, M in [("A", A), ("B", B), ("C", C)]:
r, k = rank(M), null_basis(M).shape[1]
assert r == np.linalg.matrix_rank(M), name
assert r + k == M.shape[1], name
print(f"rank({name}) = {r} nullity = {k} tổng = {r + k} (số cột = {M.shape[1]})")
# (b) mọi vector null tự tìm phải thật sự bị bóp về gốc
for name, M in [("A", A), ("C", C)]:
assert np.allclose(M @ null_basis(M), 0), name
print("null basis tự cài: M @ v = 0 ✓")
# (c) số chiều null space khớp scipy
for name, M in [("A", A), ("B", B), ("C", C)]:
assert null_basis(M).shape[1] == null_space(M).shape[1], name
print("số chiều null space khớp scipy ✓")
# (d) hướng khác nhau nhưng CÙNG PHƯƠNG — cơ sở không duy nhất
v_mine = null_basis(A)[:, 0] # (-2, 1)
v_scipy = null_space(A)[:, 0] # (-0.8944, 0.4472)
assert abs(v_mine[0] * v_scipy[1] - v_mine[1] * v_scipy[0]) < 1e-12
print(f"null(A): tự cài {v_mine} ~ scipy {np.round(v_scipy, 4)} → cùng phương ✓")
# (e) A bóp CẢ mặt phẳng xuống đúng đường y = 2x
rng = np.random.default_rng(0)
out = rng.normal(size=(100_000, 2)) @ A.T
assert np.abs(out[:, 1] - 2 * out[:, 0]).max() < 1e-9
print("100.000 vector ngẫu nhiên qua A → ĐỀU rơi lên y = 2x ✓")
# (f) hai đầu vào khác nhau, một đầu ra — mất thông tin không hồi phục
assert np.allclose(A @ np.array([1., 1.]), A @ np.array([3., 0.]))
print("A@(1,1) = A@(3,0) =", A @ np.array([1., 1.]), " ← đụng độ, như hash collision")
# (g) rank(AB) ≤ min(rank A, rank B): cổ chai không mở lại được
assert np.linalg.matrix_rank(A @ B) == 1 and np.linalg.matrix_rank(B @ A) == 1
for k in range(1, 6):
assert np.linalg.matrix_rank(np.linalg.matrix_power(B, k) @ A) == 1
print("nhân thêm bao nhiêu ma trận hạng 2 vào A, hạng vẫn = 1 ✓")
# (h) hạng thực tế: ma trận 1000×768 nhưng chỉ có 50 chiều thật
r_true = 50
X = rng.normal(size=(1000, r_true)) @ rng.normal(size=(r_true, 768))
assert np.linalg.matrix_rank(X) == r_true
Xn = X + 1e-3 * rng.normal(size=X.shape) # thêm nhiễu tí xíu
assert np.linalg.matrix_rank(Xn) == 768 # hạng "đầy" — nhưng vô nghĩa
s = np.linalg.svd(Xn, compute_uv=False)
print(f"rank(X)={np.linalg.matrix_rank(X)} rank(X+nhiễu)={np.linalg.matrix_rank(Xn)}"
f" nhưng s[50]/s[0] = {s[50]/s[0]:.2e}")
energy = np.cumsum(s**2) / np.sum(s**2)
print("số chiều cần để giữ 99% năng lượng:", int(np.searchsorted(energy, 0.99)) + 1)
print("\nTẤT CẢ ASSERT ĐỀU QUA ✓")
Output phải ra:
rank(A) = 1 nullity = 1 tổng = 2 (số cột = 2)
rank(B) = 2 nullity = 0 tổng = 2 (số cột = 2)
rank(C) = 2 nullity = 1 tổng = 3 (số cột = 3)
null basis tự cài: M @ v = 0 ✓
số chiều null space khớp scipy ✓
null(A): tự cài [-2. 1.] ~ scipy [-0.8944 0.4472] → cùng phương ✓
100.000 vector ngẫu nhiên qua A → ĐỀU rơi lên y = 2x ✓
A@(1,1) = A@(3,0) = [3. 6.] ← đụng độ, như hash collision
nhân thêm bao nhiêu ma trận hạng 2 vào A, hạng vẫn = 1 ✓
rank(X)=50 rank(X+nhiễu)=768 nhưng s[50]/s[0] = 4.96e-05
số chiều cần để giữ 99% năng lượng: 49
TẤT CẢ ASSERT ĐỀU QUA ✓
Quy tắc đang được luyện ở đây: khi rank() tự cài lệch numpy, nguyên nhân gần như luôn là ngưỡng
TOL — không phải hiểu sai toán. Số thực dấu phẩy động không bao giờ bằng 0 tuyệt đối. Ghi vào nhật ký lỗi.
Nối vào AI
| Khái niệm bài này | Chỗ dùng trong AI |
|---|---|
| Hạng thấp = nén được | LoRA: thay vì cập nhật cả W (4096×4096 = 16.777.216 tham số), học W + B·A với B: 4096×r, A: r×4096. Với r=8 chỉ còn 65.536 tham số = 0,39%, giảm 256 lần. Toàn bộ đặt cược: thay đổi cần thiết cho một tác vụ có hạng thấp |
| Hạng chặn trên bởi chiều nhỏ nhất | Attention: W^Q là 512×64 → Q = X·W^Q chỉ có d_k = 64 chiều. Ma trận điểm số S = Q·Kᵀ là 100×100 nhưng hạng chỉ 64. Bảng attention nhìn như 10.000 số độc lập, thật ra chỉ có 64 chiều thông tin |
Nút cổ chai rank(AB) ≤ min |
MLA (Multi-head Latent Attention) trong DeepSeek: cố ý ép KV cache qua một không gian tiềm ẩn hạng thấp để giảm bộ nhớ. Nút cổ chai không còn là lỗi — nó là thiết kế |
| Hạng hiệu dụng < số chiều danh nghĩa | Embedding: vector 768 chiều, nhưng ma trận embedding của cả tập dữ liệu thường tụt năng lượng rất nhanh. Mô phỏng ở §9: 1000×768, hạng danh nghĩa 768 sau khi thêm nhiễu, nhưng 49 chiều đã giữ 99% năng lượng. Đó là lý do nén được mà gần như không mất gì |
| Không gian cột = tập đầu ra khả dĩ | PCA (bài 9): tìm không gian cột k chiều tốt nhất để chiếu dữ liệu vào. Đúng bài toán "chọn cơ sở" của §3, thêm tiêu chí tối ưu |
| Không gian null ≠ {0} ⟹ không invert được | Vì sao lớp nén trong autoencoder không khôi phục nguyên bản: nó có không gian null khác rỗng, thông tin trong đó bị xoá thật |
det = 0 ⟺ hạng chưa đầy ⟺ hệ vô nghiệm/vô số nghiệm |
Vì sao ma trận hiệp phương sai bị suy biến làm hỏng thuật toán: đặc trưng phụ thuộc tuyến tính (một cột là tổ hợp của các cột khác) — đúng §2, chỉ đổi tên |
Điểm nối ba bài: bài 1 cho tích vô hướng (đo góc). Bài 2 cho ma trận (biến đổi không gian).
Bài 3 cho hạng (biến đổi giữ lại được bao nhiêu). Trong attention cả ba xuất hiện cùng lúc:
W^Q, W^K biến đổi (bài 2), hạ hạng xuống d_k (bài 3), rồi QKᵀ đo góc (bài 1).
Tự kiểm — trả lời được mới sang bài 4
- Span của hai vector trong
ℝ²có thể là những gì? Kể đủ cả ba khả năng, và nói rõ điều kiện của mỗi khả năng. A = [[1,2],[2,4]]bóp mặt phẳng thành đườngy = 2x. Chứng minh điều đó bằng đại số (không dùng code): tínhA·(v₁,v₂)tổng quát rồi chỉ ray = 2xluôn đúng.A·(1,1) = A·(3,0) = (3,6). Hai sự thật nào suy ra từ đó? (Gợi ý: một về khả nghịch, một về không gian null.) Và(1,1) − (3,0)bằng bao nhiêu, nó nằm ở đâu?- Hạng + nullity = số cột. Giải thích vì sao đó là một luật bảo toàn, không phải một công thức phải học thuộc.
- Ma trận
768×4— hạng tối đa là bao nhiêu và vì sao? Ma trận4×768thì sao? Không gian null của mỗi cái sống trongℝbao nhiêu chiều? - Vì sao
rank(A·B) ≤ min(rank A, rank B)? Trả lời bằng hành động (như bài 2 trả lờiAB ≠ BA), không bằng ví dụ số. (Gợi ý:AB·x = A·(Bx). Mọi đầu ra củaABcó phải là đầu ra củaAkhông? VàBlàm gì vớixtrước khiAnhìn thấy nó?) - Ma trận
1000×768cómatrix_ranktrả về 768, nhưng giá trị kỳ dị thứ 51 nhỏ hơn giá trị đầu 20.000 lần. Hạng "thật" của nó là bao nhiêu, và câu hỏi đó có câu trả lời duy nhất không? - LoRA thay
W(4096×4096) bằngW + B·Avớir = 8.B·Acó hạng tối đa bao nhiêu? Giả định nào về thế giới thật phải đúng thì LoRA mới hoạt động — và khi nào giả định đó sai?
Câu 6 và câu 7 là hai câu đắt nhất. Câu 6 là chỗ bạn chuyển từ tính được hạng sang hiểu hạng là gì. Câu 7 là chỗ toán sách vở va vào dữ liệu thật — và là cửa vào SVD ở bài 9. Nếu câu 8 làm bạn lúng túng, đó là dấu hiệu tốt: nó là một câu hỏi nghiên cứu chưa đóng, không phải bài tập.
Bài tập về nhà trước bài 4
3Blue1Brown — Essence of Linear Algebra:
| Tập | Ưu tiên | Vì sao |
|---|---|---|
| 2 — Linear combinations, span, and basis vectors | 🔴 | XEM LẠI. Bạn đã xem trước bài 2 khi chưa có ngữ cảnh. Lần này giữ trong đầu đúng câu hỏi Grant đặt ra: "3 vector trong ℝ³ mà span chỉ là một mặt phẳng — chuyện gì đã xảy ra?" Bạn đã có câu trả lời ở §8 |
| 7 — Inverse matrices, column space and null space | 🔴🔴 | ⭐ Tập của bài này. Rank · column space · null space bằng hình động. Xem xong quay lại đọc §4-§7 một lần nữa |
| 6 — The determinant | 🟠 | Thưởng. det = 0 và "hạng chưa đầy" là cùng một sự thật; tập này cho bạn hình ảnh diện tích bị bóp về 0 |
| 8 — Nonsquare matrices as transformations between dimensions | 🟠 | Thưởng. Trả lời trực tiếp câu 5 phần tự kiểm, và là hình ảnh đúng của embedding |
Bài tập giấy (làm trước khi gõ code):
- Tính hạng của
[[2,4,6],[1,2,3],[3,6,9]]bằng khử Gauss. Tìm cơ sở không gian null. (Nhìn kỹ ba hàng trước khi tính — có thể bạn không cần khử gì cả.) - Ma trận nào biến
(1,0)thành(2,4)và(0,1)thành(3,6)? Hạng của nó là bao nhiêu? Nó bóp mặt phẳng thành đường nào? (Dùng đúng quy tắc "cột = nơi vector cơ sở đáp xuống" của bài 2.) - Tìm một ma trận
3×3có hạng đúng bằng 1. Viết nó thành tích ngoàiu ⊗ w. Không gian null của nó có mấy chiều?
Bài 4 sẽ là: định thức và ma trận nghịch đảo — một con số duy nhất nói cho bạn biết không gian có bị bóp dẹp không, và phép biến đổi có undo được không.
Câu hỏi mang theo sang bài 4: ở §2 bạn thấy
det(A) = 0đi cùng hạng 1, còndet(B) = −2 ≠ 0đi cùng hạng 2. Vậy một con số có đủ sức nói thay cho cả khái niệm hạng không? Thử nghĩ trước: một ma trận3×3hạng 1 và một ma trận3×3hạng 2 đều códet = 0, nhưng chúng bóp không gian khác hẳn nhau.detđánh mất thông tin gì mà hạng vẫn giữ được?Còn một câu để dành tới bài 6 (đổi cơ sở): ở §3 bạn thấy
(3,5)trong hệ chuẩn =(−1,2)trong cơ sở{(1,3),(2,4)}— cùng một mũi tên, hai bộ số hoàn toàn khác nhau. Vậy HẠNG có đổi không khi đổi cơ sở? Nếu có thì "số chiều giữ lại được" là một đại lượng vô nghĩa. Nếu không thì cái gì làm nó bất biến?