Đại số tuyến tính · bài 3
Hạng và không gian null
Hạng đếm số chiều một ma trận giữ lại được, không gian null là phần bị bóp về 0. Kéo hai cột về cùng một đường để thấy cả mặt phẳng dồn xuống một đường, và vì sao LoRA chỉ cần chưa tới 1% tham số.
Bài toán
Tinh chỉnh một ma trận trọng số 4096×4096 nghĩa là cập nhật 16 777 216 tham số. LoRA chỉ học hai ma trận nhỏ, cỡ 4096×16 và cỡ 16×4096, tổng cộng 131 072 tham số (khoảng 0.78%), rồi cộng tích vào ma trận gốc. Cách làm này đặt cược vào một giả định: thay đổi cần cho một tác vụ chỉ sống trong một không gian ít chiều. Muốn hiểu giả định đó nói gì, cần ba khái niệm: span, hạng và không gian null.
Kéo thử trước
Ở hình Đầu ra M·x, kéo hai cột của M (xanh lá và đỏ); ở hình Đầu vào x, kéo x. Khi hai cột cùng phương, mặt phẳng đầu ra chỉ còn một đường (xanh dương): đó là không gian cột. Đường đỏ ở hình đầu vào là không gian null, gồm mọi đầu vào bị bóp về gốc. Các chấm tím ở hình đầu vào nằm trên đường song song với nó đi qua x, và tất cả cùng đáp xuống một điểm M·x.
Nên thử: bấm B để thấy cả mặt phẳng đầu ra được phủ kín; bấm P và tự đoán không gian null trước khi nhìn.
Thử bằng số trước
Hai ma trận trông giống nhau, và :
| đầ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) |
làm hai chuyện mà không làm. Nó biến hai đầu vào khác nhau, và , thành cùng một đầu ra , giống một hàm băm bị đụng độ; đã đụng độ thì không đi ngược lại được. Và nó biến một vector khác 0, , thành đúng gốc toạ độ: cả một hướng bị xoá. Khác biệt đó gói trong một con số, gọi là hạng.
Span: tập mọi thứ tạo được
Trong đại số tuyến tính chỉ có hai phép: cộng vector và nhân vector với một số. Ghép chúng lại được tổ hợp tuyến tính với các hệ số tuỳ ý. Span của một bộ vector là tập tất cả các tổ hợp tuyến tính của chúng, tức mọi điểm tạo ra được từ bộ đó.
| bộ vector | span | 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 |
| (1, 3) và (2, 4), không cùng phương | cả mặt phẳng | 2 |
| (1, 2) và (2, 4), cùng phương | vẫn chỉ một đường thẳng | 1 |
Dòng cuối là chỗ cần nhớ: hai vector không bảo đảm hai chiều. Với và :
Mọi kết quả nằm trên đường . Điểm nằm ngoài đường đó nên không tạo ra được. Điểm thì tạo ra được, và bằng nhiều cách: .
Độc lập và phụ thuộc tuyến tính
Một bộ vector phụ thuộc tuyến tính khi có ít nhất một vector biểu diễn được từ các vector còn lại; nói cách khác, có một tổ hợp bằng 0 mà các hệ số không đều bằng 0. Không có vector nào như vậy thì bộ đó độc lập tuyến tính. Vector phụ thuộc là vector thừa: thêm nó vào không mở thêm chiều nào, giống một bảng có cột gia_ban và cột gia_ban_x2.
Hai cột của là và : với hệ số khác 0, nên phụ thuộc. Hai cột của là và : giải , chỉ ra , nên độc lập.
Một bộ vector vừa độc lập vừa span được cả không gian gọi là cơ sở: không dư, không thiếu. Hai cột của là một cơ sở của mặt phẳng; viết theo cơ sở đó được hệ số , vì . Số vector trong một cơ sở là số chiều của không gian.
Hạng: số chiều giữ lại được
Hạng của ma trận là số chiều của tập mọi đầu ra, cũng bằng số cột độc lập tuyến tính.
| xét | ||
|---|---|---|
| hai cột | (1, 2), (2, 4): cùng phương | (1, 3), (2, 4): khác phương |
| mọi đầu ra | đường | cả mặt phẳng |
| hạng | 1 | 2 |
| đi ngược lại được không | không | có |
Vài trường hợp để 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ề gốc |
| [[1, 0], [0, 0]] | 1 | chiếu vuông góc xuống trục ngang: (3, 5) thành (3, 0) |
| [[1, 2], [2, 4]] | 1 | bóp xuống đường |
| [[1, 2], [3, 4]] | 2 | méo đi nhưng không mất chiều nào |
| [[1, 0], [0, 1]] | 2 | không làm gì |
Hai bẫy: hạng 1 không có nghĩa chỉ có một hàng khác 0, vì có đủ bốn số khác 0 mà hạng vẫn là 1. Và hạng không vượt quá số hàng lẫn số cột: ma trận 768×4 có hạng tối đa là 4.
Khi nào M·x = t có nghiệm
Tập mọi đầu ra của gọi là không gian cột: nó là span của các cột, vì là tổ hợp các cột với hệ số là toạ độ của (bài 2). Từ đó:
Hệ có nghiệm khi và chỉ khi nằm trong không gian cột của .
Không gian cột của là đường . Với thì : điểm nằm ngoài đường, hệ vô nghiệm. Với thì : có nghiệm, ví dụ hoặc .
Cẩn thận khi kiểm bằng numpy: np.linalg.solve(A, t) báo lỗi Singular matrix với mọi , kể cả vốn có nghiệm. Lỗi đó nói ” không khả nghịch”, không nói ” nằm ngoài không gian cột”.
Không gian null: phần bị bóp về 0
Không gian null của là tập mọi mà . Với :
Phương trình thứ hai chỉ là phương trình thứ nhất nhân 2, nên còn đúng một ràng buộc . Chọn được : không gian null của là span của , một đường thẳng. Hàm scipy.linalg.null_space(A) trả về : cùng đường đó, chỉ đổi dấu và chuẩn hoá về độ dài 1. Với , hàm này trả về ma trận không có cột nào: chỉ vector 0 bị bóp về 0, nên đi ngược lại được.
Không gian null giải thích vì sao có nhiều nghiệm. Các nghiệm lệch nhau đúng một bội của , vì nếu và thì :
Tập nghiệm của là một nghiệm bất kỳ cộng với cả không gian null.
Hạng cộng số chiều null bằng số cột
Mỗi chiều của đầu vào hoặc được giữ lại, hoặc bị bóp về 0; không chiều nào biến mất khỏi sổ sách. Với là số cột của :
| ma trận | số cột | hạng | số chiều null |
|---|---|---|---|
| A = [[1, 2], [2, 4]] | 2 | 1 | 1 |
| B = [[1, 2], [3, 4]] | 2 | 2 | 0 |
| [[0, 0], [0, 0]] | 2 | 0 | 2 |
| [[1, 0], [0, 1]] | 2 | 2 | 0 |
Không gian cột nằm ở phía đầu ra, không gian null nằm ở phía đầu vào. Với ma trận vuông hai phía trùng nhau nên dễ lẫn; với ma trận 3×2 thì không gian null ở trong mặt phẳng còn không gian cột ở trong không gian 3 chiều.
Hạng là cổ chai, và hạng thấp thì nén được
Ghép hai phép biến đổi thì hạng không bao giờ tăng:
Với hạng 1 và hạng 2: và đều hạng 1, và nhân thêm bao nhiêu lần cũng vẫn hạng 1. Chiều đã mất thì mất hẳn.
Mặt kia: ma trận hạng thấp lưu được gọn. Ma trận hạng 1 là tích của một cột với một hàng, ví dụ . Ma trận hạng viết được thành tích hai ma trận và , tức số thay vì số. Với , , : 88 400 số thay vì 768 000, bằng 11.5%.
Quay lại LoRA: phần cập nhật có hạng không quá . Với ma trận 4096×4096, cần 131 072 tham số (0.78%), cần 65 536 (0.39%, ít hơn 256 lần so với cập nhật cả ma trận).
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.
- “Phụ thuộc tuyến tính là các vector nằm trên cùng một đường thẳng.” Chỉ đúng khi có hai vector. Ba vector , , phụ thuộc, vì , dù không nằm trên một đường.
- “Độc lập là không có ít nhất một vector biểu diễn được từ các vector khác.” Phủ định đúng của “có ít nhất một” là “không có vector nào”: độc lập nghĩa là không vector nào biểu diễn được từ các vector còn lại.
- “Hạng là số hàng” hoặc “số cột”. có 2 hàng mà hạng 1; có 3 cột mà hạng 2, vì cột bằng tổng hai cột đầu. Số hàng và số cột chỉ là giới hạn trên.
- “Không gian null của là gốc toạ độ.” bằng 0 chỉ cần , còn tuỳ ý: không gian null là cả trục dọc. Một ẩn tự do cho một đường thẳng, không phải một điểm.
- Lẫn không gian null với không gian cột. Với , không gian null là trục dọc (các đầu vào bị xoá), không gian cột là trục ngang (mọi đầu ra có thể có).
- Viết hệ theo cột. Với , hệ đúng là và : phương trình thứ lấy hàng thứ . Kiểm nhanh: phải ra cột 1.
- Lẫn đầu ra với nghiệm. Với và , các nghiệm là , lệch nhau đúng . Điểm nằm trong không gian cột nhưng không phải nghiệm: .
Code: tự viết rồi đối chiếu
Tự viết hàm tính hạng bằng khử Gauss rồi so với numpy và scipy. Mỗi assert là một con số trong trang này, kể cả hạng 64 của bảng điểm attention ở mục dưới.
# Span, hạng, không gian cột, không gian null: tự tính bằng Python thuần rồi đối chiếu numpy và scipy.
# Mọi assert dưới đây là số được nhắc trong bài.
import numpy as np
from scipy.linalg import null_space
def mat_vec(M, v):
return [sum(M[i][t] * v[t] for t in range(len(v))) for i in range(len(M))]
def rank(M, eps=1e-9):
"""Hạng = số hàng khác 0 còn lại sau khử Gauss (có đổi hàng để lấy trụ lớn nhất)."""
M = [[float(x) for x in hang] for hang in M]
so_hang, so_cot, r = len(M), len(M[0]), 0
for c in range(so_cot):
if r == so_hang:
break
tru = max(range(r, so_hang), key=lambda i: abs(M[i][c]))
if abs(M[tru][c]) < eps:
continue
M[r], M[tru] = M[tru], M[r]
for i in range(r + 1, so_hang):
k = M[i][c] / M[r][c]
M[i] = [x - k * y for x, y in zip(M[i], M[r])]
r += 1
return r
A = [[1, 2], [2, 4]]
B = [[1, 2], [3, 4]]
# Thử trước: A làm đụng độ và xoá một hướng, B thì không
for v, ra_A, ra_B in [((1, 1), [3, 6], [3, 7]), ((3, 0), [3, 6], [3, 9]), ((2, -1), [0, 0], [0, 2])]:
assert mat_vec(A, v) == ra_A and mat_vec(B, v) == ra_B
assert rank(A) == 1 == np.linalg.matrix_rank(A) and rank(B) == 2 == np.linalg.matrix_rank(B)
# Span của (1, 2) và (2, 4) là đường y = 2x: (6, 12) tạo được bằng nhiều cách, (5, 6) thì không
nghiem = [(6, 0), (0, 3), (2, 2), (4, 1), (-2, 4)]
assert all(mat_vec(A, x) == [6, 12] for x in nghiem)
# các nghiệm lệch nhau đúng một bội của (2, -1)
for x in nghiem:
dx, dy = x[0] - 6, x[1] - 0
assert dx * (-1) - dy * 2 == 0 # (dx, dy) cùng phương với (2, -1)
for t in [(5, 6), (1, 1)]:
assert t[1] - 2 * t[0] != 0 # nằm ngoài đường y = 2x nên A·x = t vô nghiệm
assert mat_vec(A, (1, 1)) == mat_vec(A, (3, 0)) == [3, 6] # t = (3, 6) có nghiệm
print("y - 2x của (5, 6) và (1, 1):", 6 - 2 * 5, 1 - 2 * 1)
# np.linalg.solve báo lỗi với MỌI t, kể cả t có nghiệm, vì A suy biến
for t in [(3, 6), (5, 6)]:
try:
np.linalg.solve(np.array(A, float), t)
raise AssertionError("solve lẽ ra phải báo lỗi")
except np.linalg.LinAlgError:
pass
# Không gian null
nA = null_space(np.array(A, float))
print("null_space(A) =", nA.ravel().round(4), " null_space(B) có", null_space(np.array(B, float)).shape[1], "cột")
assert nA.shape == (2, 1) and np.allclose(np.abs(nA.ravel()), np.array([2, 1]) / np.sqrt(5))
assert np.allclose(np.abs(nA.ravel()), [0.8944, 0.4472], atol=1e-4)
assert null_space(np.array(B, float)).shape == (2, 0)
# Cột của B là một cơ sở: (3, 5) = -1·(1, 3) + 2·(2, 4)
assert mat_vec(B, (-1, 2)) == [3, 5]
# Hai cột của A phụ thuộc: tổ hợp với hệ số (2, -1) ra vector 0
assert (2 * np.array([1, 2]) - 1 * np.array([2, 4])).tolist() == [0, 0]
# Hạng + số chiều không gian null = số cột
for M, hang, nullity in [(A, 1, 1), (B, 2, 0), ([[0, 0], [0, 0]], 0, 2), ([[1, 0], [0, 1]], 2, 0), ([[1, 0], [0, 0]], 1, 1)]:
assert rank(M) == hang == np.linalg.matrix_rank(np.array(M))
assert null_space(np.array(M, float)).shape[1] == nullity and hang + nullity == 2
# Các chỗ hay nhầm
assert rank([[1, 3], [2, 6]]) == 1 and rank([[1, 0, 1], [0, 1, 1]]) == 2
assert mat_vec([[1, 3], [2, 6]], (4, 8)) == [28, 56] # (4, 8) nằm trên đường nhưng không phải nghiệm
M = [[1, 3], [2, 6]]
assert all(mat_vec(M, x) == [2, 4] for x in [(2, 0), (-1, 1), (-4, 2)]) and mat_vec(M, (3, -1)) == [0, 0]
assert np.allclose(np.abs(null_space(np.array([[1.0, 0], [0, 0]])).ravel()), [0, 1]) # null của P là cả trục y
assert rank([[1, 0], [0, 1], [1, 1]]) == 2 # ba vector (1,0), (0,1), (1,1) phụ thuộc
# Hạng là cổ chai: ghép thêm bao nhiêu phép biến đổi cũng không tăng
AB, BA = np.array(A) @ np.array(B), np.array(B) @ np.array(A)
assert AB.tolist() == [[7, 10], [14, 20]] and BA.tolist() == [[5, 10], [11, 22]]
assert np.linalg.matrix_rank(AB) == np.linalg.matrix_rank(BA) == 1
assert all(np.linalg.matrix_rank(np.linalg.matrix_power(np.array(B), k) @ np.array(A)) == 1 for k in range(1, 6))
# Hạng thấp thì nén được
assert np.array_equal(np.outer([1, 2], [1, 2]), A)
m, n, r = 1000, 768, 50
print(f"lưu đầy đủ {m * n:,} số, lưu dạng hạng {r}: {r * (m + n):,} số = {r * (m + n) / (m * n):.1%}")
assert (m * n, r * (m + n), round(100 * r * (m + n) / (m * n), 1)) == (768_000, 88_400, 11.5)
# LoRA trên ma trận 4096×4096
d = 4096
assert d * d == 16_777_216
for r, so, phan_tram, giam in [(16, 131_072, 0.78, 128), (8, 65_536, 0.39, 256)]:
assert 2 * d * r == so and round(100 * so / d**2, 2) == phan_tram and d * d // so == giam
# Attention: bảng điểm Q·Kᵀ cỡ 100×100 nhưng hạng chỉ 64 khi mỗi đầu có 64 chiều
rng = np.random.default_rng(0)
X = rng.standard_normal((100, 512))
WQ, WK = rng.standard_normal((2, 512, 64))
diem = (X @ WQ) @ (X @ WK).T
assert diem.shape == (100, 100) and np.linalg.matrix_rank(diem) == 64
print("đúng hết")
Nằm ở đâu trong AI
| Khái niệm | Chỗ dùng |
|---|---|
| Hạng thấp thì nén được | LoRA: học phần cập nhật hạng thay cho cả ma trận trọng số |
| Hạng không vượt chiều nhỏ nhất | Attention: với 100 token, mỗi token 512 chiều và , cỡ 512×64, bảng điểm cỡ 100×100 nhưng hạng chỉ 64 |
| Lớp cổ chai trong autoencoder; Multi-head Latent Attention của DeepSeek nén KV cache qua một không gian ít chiều để giảm bộ nhớ | |
| Không gian null khác {0} | Lớp nén không khôi phục được nguyên bản: phần rơi vào không gian null bị xoá thật |
| Cột phụ thuộc | Đặc trưng trùng lặp (đa cộng tuyến) làm ma trận hiệp phương sai suy biến, mô hình hồi quy mất ổn định |
| Không gian cột | Hồi quy tuyến tính: khi vô nghiệm, bình phương tối thiểu tìm điểm gần nhất trong không gian cột |
Tự kiểm
Câu 1
Với A = [[1, 2], [2, 4]], hệ A·x = (5, 6) có bao nhiêu nghiệm?
- A. đúng một nghiệm
- B. vô số nghiệm, vì A có không gian null
- C. vô nghiệm
- D. không biết được nếu không chạy np.linalg.solve
Xem đáp án
C. Mọi đầu ra của A nằm trên đường y = 2x. Với (5, 6), y − 2x = −4 ≠ 0, nên không có x nào đáp tới đó. Không gian null chỉ cho vô số nghiệm khi đã có ít nhất một nghiệm.
Câu 2
Hạng của [[1, 0, 1], [0, 1, 1]] là bao nhiêu?
- A. 1
- B. 2
- C. 3, vì có 3 cột
- D. 6, vì có 6 số
Xem đáp án
B. Cột thứ ba (1, 1) bằng tổng hai cột đầu. Mỗi cột chỉ có 2 số, nên hạng không vượt quá 2.
Câu 3
LoRA với r = 8 trên một ma trận 4096×4096 học bao nhiêu tham số?
- A. 4 096
- B. 32 768
- C. 65 536, khoảng 0.39% so với cả ma trận
- D. 16 777 216
Xem đáp án
C. Hai ma trận 4096×8 và 8×4096: 2 × 4096 × 8 = 65 536. Cả ma trận có 4096² = 16 777 216 tham số, gấp 256 lần.
Nguồn của trang này
- Khung bài và bảng “nằm ở đâu trong AI”: AI Engineering from Scratch, Linear Algebra Intuition (mục Linear Independence, Basis and Rank, Connections), giấy phép MIT, © Rohit Ghumare.
- Khung bài và bảng “nằm ở đâu trong AI”: AI Engineering from Scratch, Linear Systems (mục What Ax = b means geometrically, Column picture vs row picture), giấy phép MIT, © Rohit Ghumare.
- Phần giảng, tính tay và chỗ hay nhầm: syllabus AI foundations, môn 2 bài 3.
- Mọi con số trong bài đã chạy lại bằng numpy. Cập nhật 2026-11-05.