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

MÔN 02 / Bài 05

Ma trận không vuông và embedding

Nén và nâng chiều, bình phương tối thiểu, giả nghịch đảo, rồi embedding như một bảng tra.

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.

~2-3 giờ · cần xong bài 4 trước

Tới giờ mọi ma trận bạn gặp đều vuông: vào R² ra R², vào R³ ra R³. Bài này bỏ ràng buộc đó — và ngay lập tức bạn có embedding, nn.Embedding, Q = X·W^Q trong attention, và LoRA. Tất cả chỉ là một ma trận m × n với m ≠ n.

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

  1. Xem trước: 3Blue1Brown tập 8 Nonsquare matrices as transformations between dimensions. Xem kỹ, đừng lướt. Giữ câu hỏi: chiếu từ 100.000 chiều từ vựng xuống 768 chiều thì mất gì, giữ gì?
  2. Làm ô ⚡ bên dưới: chạy code với ma trận nén P và ma trận nâng E.
  3. Đọc §1-6. (§4b và §4c là phần mở rộng về bình phương tối thiểu, để sau.)
  4. Code §7: tự cài phép nhân ma trận m × n và hạng bằng khử Gauss. Tự viết xong mới mở code mẫu trong bài.
  5. Làm 3 bài tay ở §10 (30 phút, trên giấy).
  6. Tự kiểm §9.

Xong bài khi: nói được vì sao nén chiều thì bắt buộc mất thông tin.

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

import numpy as np
P = np.array([[1.,0.,0.],[0.,1.,0.]])      # 2×3 — NÉN R³ xuống R²
E = np.array([[1.,0.],[0.,1.],[1.,1.]])    # 3×2 — NÂNG R² lên R³

print(P @ np.array([1.,2.,5.]))            # đích: [1. 2.]
print(P @ np.array([1.,2.,99.]))           # đích: [1. 2.]  ← GIỐNG HỆT dòng trên
print(E @ np.array([3.,2.]))               # đích: [3. 2. 5.]
print(P @ E)                               # đích: ma trận đơn vị 2×2
print(E @ P)                               # đích: [[1,0,0],[0,1,0],[1,1,0]] ← KHÔNG phải I₃

Hai dòng đầu ra cùng một kết quả dù đầu vào khác nhau → bạn vừa nhìn thấy mất thông tin. Hai dòng cuối ra khác nhau → nâng-rồi-nén hồi được, nén-rồi-nâng thì không.

Đó là toàn bộ bài này. Phần còn lại giải thích vì sao, và vì sao nó chính là embedding.


1. m × n nghĩa là gì — và đọc ngược mới đúng

Một ma trận m × n có m hàng, n cột. Nhưng ý nghĩa của nó là:

Ma trận m × n là một phép biến đổi từ Rⁿ (vào) sang Rᵐ (ra). Số cột = chiều đầu vào. Số hàng = chiều đầu ra.

Đây là BẪY số 1 và gần như ai cũng vấp: m × n đọc là "m nhân n" nhưng chiều đi là n → m, ngược thứ tự chữ.

Cách nhớ không bao giờ sai — dùng lại đúng ý bài 2 (cột = nơi vector cơ sở đáp xuống):

P = [ 1  0  0 ]     3 cột  →  có 3 vector cơ sở ở đầu vào  →  vào từ R³
    [ 0  1  0 ]     2 hàng →  mỗi cột là một điểm 2 chiều  →  ra ở R²

Mỗi cột vẫn là "nơi một vector cơ sở đáp xuống", y hệt bài 2. Chỉ khác một điều: chỗ đáp xuống nằm ở không gian khác.

Ẩn dụ backend: ma trận là một hàm có chữ ký (n) => (m). Ma trận vuông là hàm (n) => (n) — cùng kiểu vào ra nên ghép chuỗi thoải mái. Ma trận không vuông đổi kiểu, nên phải khớp chữ ký khi ghép:

A (m×k) · B (k×n)   ✓ hợp lệ, ra (m×n)     — số cột của A khớp số hàng của B
A (2×3) · B (2×3)   ✗ lỗi chiều

Quy tắc "chiều trong phải khớp" không phải luật tuỳ tiện. Nó chỉ là: đầu ra của hàm này phải đúng kiểu với đầu vào của hàm kia. Y như compose hai function trong TypeScript.


2. Nén chiều (m < n) — và vì sao MẤT thông tin là điều bắt buộc

Ma trận nén đơn giản nhất, sẽ dùng suốt bài:

P = [ 1  0  0 ]      2×3   →   R³ ⟶ R²
    [ 0  1  0 ]

Đọc bằng cột (bài 2):

vector cơ sở của R³ đáp xuống = cột nào của P
î = (1,0,0) (1, 0) cột 1 ✓
ĵ = (0,1,0) (0, 1) cột 2 ✓
k̂ = (0,0,1) (0, 0) cột 3 — cột 0

Cột thứ ba bằng vector 0. Nghĩa là: trục z bị nghiền thẳng về gốc toạ độ. Nó biến mất.

Ví dụ tính tay

P · (1, 2,  5) = ( 1·1 + 0·2 + 0·5 ,  0·1 + 1·2 + 0·5 ) = (1, 2)
P · (1, 2, 99) = ( 1·1 + 0·2 + 0·99,  0·1 + 1·2 + 0·99) = (1, 2)
P · (1, 2, −7) = (1, 2)

Ba điểm khác hẳn nhau trong R³ đáp xuống đúng một điểm (1,2) trong R². Thật ra cả một đường thẳng đứng vô hạn (1, 2, t) đều dồn về đó.

Vì sao không undo được? Vì hàm không đơn ánh. Nhìn bằng phép trừ:

(1,2,99) − (1,2,5) = (0, 0, 94)          ← một vector KHÁC 0
P · (0, 0, 94)     = (0, 0)              ← nhưng ảnh của nó BẰNG 0

Tập tất cả vector bị nghiền về 0 gọi là không gian null (null space) của P — đúng khái niệm của bài 3 §6, giờ áp cho ma trận không vuông. Ở đây nó là trục z, 1 chiều.

Định lý hạng — số khuyết, cách phát biểu ngắn nhất

hạng(P) + số chiều null space = số chiều ĐẦU VÀO
     2   +          1          =        3        ✓

Nối về bài 3: hạng là số chiều mà ảnh thực sự chiếm. Với ma trận m × n:

hạng ≤ min(m, n) — luôn luôn, không có ngoại lệ.

Vì hạng bị chặn bởi cả chiều vào lẫn chiều ra. Khi m < n, hạng ≤ m < n, nên null space bắt buộc có ít nhất n − m chiều — nghĩa là bắt buộc có thông tin bị nghiền mất.

Đây là điểm tận cùng của mục này: nén chiều mất thông tin không phải vì ma trận được chọn tồi. Nó là hệ quả số học. Bạn đang cố nhét n chiều thông tin vào m ô, với m < n — pigeonhole. Không có ma trận nào thoát được.

Ẩn dụ backend: đây là hàm băm. md5 không "kém"; nó buộc phải va chạm vì đầu ra 128 bit còn đầu vào không giới hạn. Không tồn tại md5.undo().

BỎ QUA ĐƯỢC: cách tính cơ sở của null space bằng tay cho ma trận lớn. Bạn cần hiểu null space là gì; scipy.linalg.null_space sẽ tính hộ mãi mãi.


3. Nâng chiều (m > n) — không mất gì, nhưng cũng không phủ hết

Chiều ngược lại:

E = [ 1  0 ]        3×2   →   R² ⟶ R³
    [ 0  1 ]
    [ 1  1 ]

Đọc bằng cột: î = (1,0) đáp xuống (1,0,1); ĵ = (0,1) đáp xuống (0,1,1). Chỉ có 2 cột vì đầu vào chỉ có 2 vector cơ sở.

Ví dụ tính tay

E · (1, 0) = (1, 0, 1)
E · (0, 1) = (0, 1, 1)
E · (3, 2) = (3, 2, 5)          ← 3·(1,0,1) + 2·(0,1,1)
E · (1, 2) = (1, 2, 3)

Nhìn kỹ mọi kết quả: toạ độ thứ ba luôn bằng tổng hai toạ độ đầu. Tổng quát:

E · (a, b) = (a, b, a + b)      →     z = x + y     →     x + y − z = 0

Đó là phương trình một mặt phẳng đi qua gốc toạ độ trong R³ (pháp tuyến (1, 1, −1)).

Ảnh của E không phải cả R³. Nó chỉ là một mặt phẳng — một tấm giấy 2 chiều lơ lửng trong không gian 3 chiều.

Kiểm bằng số, chọn ba điểm:

điểm trong R³ x + y − z có nằm trong ảnh của E không
(1, 2, 3) 0 có — chính là E·(1,2)
(1, 2, 4) −1 không — không (a,b) nào cho ra nó
(0, 0, 1) −1 không

hạng(E) = 2 = min(3, 2). Đã kịch trần. Không ma trận 3×2 nào có hạng 3 được — vì đầu vào chỉ có 2 chiều để mà căng ra.

Nâng chiều không tạo ra thông tin. Nó chỉ đặt thông tin 2 chiều vào một không gian rộng hơn. Giống việc lưu một ảnh 2 chiều vào cột BLOB dung lượng gấp trăm lần: chỗ chứa to hơn, nội dung vẫn thế.

Đây cũng là câu trả lời cho một hiểu lầm phổ biến: "cứ tăng số chiều là mô hình mạnh hơn". Sai. Tăng chiều bằng một phép nhân ma trận không thêm được gì — vì ảnh vẫn kẹt trong một không gian con n chiều. Muốn "lấp đầy" không gian mới thì phải có phi tuyến (đúng lý do đã chứng minh ở bài 2 §4).


4. Ghép hai chiều lại: cái nào undo được?

Giờ ghép P và E. Nhớ quy ước bài 2: đọc từ phải sang trái.

P · E — nâng lên trước, rồi nén xuống (R² → R³ → R²)

P · E = [ 1  0  0 ] · [ 1  0 ]  =  [ 1  0 ]  =  I₂
        [ 0  1  0 ]   [ 0  1 ]     [ 0  1 ]
                      [ 1  1 ]

Bằng đúng ma trận đơn vị. Đưa (3,2) vào: E biến thành (3,2,5), P cắt trục z đi, còn lại (3,2). Khôi phục nguyên vẹn.

E · P — nén xuống trước, rồi nâng lên (R³ → R² → R³)

E · P = [ 1  0 ]   [ 1  0  0 ]     [ 1  0  0 ]
        [ 0  1 ] · [ 0  1  0 ]  =  [ 0  1  0 ]     ≠ I₃
        [ 1  1 ]                   [ 1  1  0 ]

Kiểm bằng số:

(E·P) · (1, 2,  5) = (1, 2, 3)
(E·P) · (1, 2, 99) = (1, 2, 3)      ← z = 5 và z = 99 đều thành 3

det(E·P) = 0, hạng(E·P) = 2. Không nghịch đảo được, vĩnh viễn.

làm gì kết quả undo được?
P·E nâng R²→R³ rồi nén về R² I₂ được — không có gì để mất
E·P nén R³→R² rồi nâng lên R³ ≠ I₃, det = 0 không — trục z đã chết ở bước 1

Kết luận đắt nhất của bài: thông tin mất ở bước nén, và không bước nào sau đó lấy lại được. Ma trận nâng chiều không phải "nghịch đảo" của ma trận nén — nó chỉ là một phép đặt-vào-không-gian-lớn-hơn.

Ẩn dụ backend: gzip → gunzip khôi phục được vì nén không mất. jpeg chất lượng 40% thì không — bạn resize ảnh lên 4K vẫn không lấy lại được chi tiết đã mất. E·P là cái thứ hai.

Đây cũng là lý do một autoencoder phải học: nó không thể khôi phục hoàn hảo, nó chỉ có thể học cách vứt đi thứ ít quan trọng nhất.


4b. ⭐ AᵀA có ĐÚNG không gian null của A — và đó là toàn bộ điều kiện của phương trình chuẩn

(bổ sung 19/8 — nguồn: Strang tr.182-187, §3.3)

E·P ≠ I₃ nghĩa là: khi A không vuông, hệ A x = b thường vô nghiệm — b đơn giản không nằm trong ảnh của A (§3). Vậy làm gì? Strang đưa ra một thao tác thô bạo đến mức dễ bỏ qua:

Cứ nhân cả hai vế của phương trình VÔ NGHIỆM A x = b với Aᵀ.

AᵀA x̂ = Aᵀb          ← phương trình CHUẨN (normal equations)

AᵀA là ma trận vuông n×n, nên nếu nó khả nghịch thì x̂ = (AᵀA)⁻¹Aᵀb. Đây chính là công thức mà môn 3 bài 5 dùng làm baseline cho hồi quy tuyến tính — giờ bạn biết nó từ đâu ra: nhân hai vế với Aᵀ, không có gì hơn.

Câu hỏi thật là: khi nào AᵀA khả nghịch? Sách hay ghi "khi các cột của A độc lập" như một giả thiết kỹ thuật tuỳ tiện. Nó không tuỳ tiện — nó suy ra từ một dòng:

xᵀAᵀAx = (Ax)ᵀ(Ax) = ‖Ax‖²

Nên AᵀAx = 0 ⟹ xᵀAᵀAx = 0 ⟹ ‖Ax‖² = 0 ⟹ Ax = 0. Chiều ngược lại hiển nhiên. Kết luận:

AᵀA có ĐÚNG cùng không gian null với A. Không "gần bằng", không "chứa" — bằng nhau. Kéo theo rank(AᵀA) = rank(A), và AᵀA khả nghịch ⟺ cột của A độc lập. Vừa cần vừa đủ.

Kiểm bằng số — A là 6×3 với cột3 = cột1 + cột2, nên x = (1, 1, −1) nằm trong null space:

import numpy as np
from scipy.linalg import null_space

rng = np.random.default_rng(0)
A = rng.normal(size=(6, 2))
A = np.column_stack([A, A[:, 0] + A[:, 1]])      # cột3 = cột1 + cột2 → cột phụ thuộc
x = np.array([1., 1., -1.])

print(f"‖Ax‖²  = {np.sum((A @ x) ** 2):.2e}")     # 0.00e+00
print(f"xᵀAᵀAx = {x @ (A.T @ A) @ x:.2e}")        # -8.88e-16   ← cùng số 0, khác đường tính

assert np.linalg.matrix_rank(A) == np.linalg.matrix_rank(A.T @ A) == 2
assert null_space(A).shape[1] == null_space(A.T @ A).shape[1] == 1

(Con số -8.88e-16 thay vì 0.0 chằn là dấu phẩy động, không phải toán sai: ‖Ax‖² cộng bình phương nên không âm được, còn xᵀAᵀAx đi qua ma trận AᵀA nên có chỗ trừ. Cùng một đại lượng, hai đường tính, hai kiểu sai số làm tròn.)

Vì sao khó tự nghĩ ra

Vì "phương trình chuẩn" được dạy như một công thức phải nhớ, và "cột độc lập" như một dòng chữ nhỏ. Không ai chỉ ra rằng hai vế đó là một sự thật. Không thấy đẳng thức null space thì bạn không biết điều kiện đó là cần, là đủ, hay chỉ là cho chắc — và khi AᵀA suy biến thật, bạn sẽ đi tìm lỗi ở chỗ khác thay vì đi tìm cột thừa trong đặc trưng của mình (đúng §2 bài 3, chỉ đổi tên).

🪤 BẪY — Strang trình bày phương trình chuẩn mà KHÔNG cảnh báo chỗ này

AᵀA bình phương số điều kiện: κ(AᵀA) = κ(A)². Nghĩa là bạn tự tay biến một bài toán hơi khó thành một bài toán khó gấp bội, chỉ bằng phép nhân trông vô hại kia.

U, _ = np.linalg.qr(rng.normal(size=(50, 3)))
V, _ = np.linalg.qr(rng.normal(size=(3, 3)))
print("  κ(A) đặt trước |   κ(A) đo được   |   κ(AᵀA) đo được  | κ(AᵀA)/κ(A)²")
for k in [1e2, 1e3, 1e6]:
    B = U @ np.diag([1.0, 1 / np.sqrt(k), 1 / k]) @ V.T     # dựng A có κ đúng bằng k
    kA, kB = np.linalg.cond(B), np.linalg.cond(B.T @ B)
    print(f"  {k:>13.0e} | {kA:>16.6e} | {kB:>17.6e} | {kB / kA**2:.6f}")
    assert abs(kB / kA ** 2 - 1.0) < 1e-3
κ(A) đặt trước κ(A) đo được κ(AᵀA) đo được tỉ số
1e+02 1.000000e+02 1.000000e+04 1.000000
1e+03 1.000000e+03 1.000000e+06 1.000000
1e+06 1.000000e+06 9.999852e+11 0.999985

Với float64 (~16 chữ số), κ(A) = 1e6 là hoàn toàn xử lý được; κ(AᵀA) = 1e12 thì ăn mất 12 trong 16 chữ số. Đó là lý do trong thực tế người ta không lập AᵀA mà giải qua QR (hoặc SVD — bài 9). np.linalg.lstsq làm đúng chuyện đó; np.linalg.solve(A.T@A, A.T@b) thì không.

Tôi đã grep toàn cuốn Strang: không có một lần nào cảnh báo về việc bình phương số điều kiện, trong khi phương trình chuẩn được trình bày như phương pháp chính. Sách không sai — nó chỉ không nói. Cơ chế số học nằm ở bài 6 môn 3, và đây đúng loại chỗ mà quy tắc 1 cảnh báo: lỗi giết bạn là chi tiết thiết lập.


4c. ⭐ Bình phương tối thiểu không "nới lỏng mô hình" — nó THAY dữ liệu

(bổ sung 19/8 — nguồn: Strang tr.171 (§3.2), tr.187 (§3.3))

§4b cho công cụ đại số. Mục này cho hình ảnh, và hình ảnh mới là thứ ở lại.

(a) Bài toán khoảng cách TỰ ĐỘNG sinh ra tính trực giao

a và b là hai vector bất kỳ, chẳng vuông góc gì với nhau. Nhưng vừa hỏi "điểm nào trên đường thẳng chứa a gần b nhất?" là trực giao xuất hiện ngay lập tức: đoạn nối b tới điểm gần nhất p bắt buộc phải vuông góc với đường thẳng đó. Nếu không vuông góc, nghiêng nó đi một chút sẽ ngắn hơn — nên nó chưa phải điểm gần nhất.

Strang: "the distance problem automatically brings in orthogonality".

Đó là lý do phép chiếu (bài 1 §2) và bình phương tối thiểu không phải hai chủ đề. Chúng là một bài toán: hỏi "gần nhất" thì được trực giao miễn phí, và trực giao chính là Aᵀe = 0 — phương trình chuẩn ở §4b, đọc ngược.

(b) p chứ không phải b mới là thứ mô hình học được

Ba điểm đo (0,1), (1,3), (2,2). Chúng không thẳng hàng, nên A x = b vô nghiệm:

A = [ 1  0 ]        b = ( 1, 3, 2 )        ← cột 1 là hằng số, cột 2 là x
    [ 1  1 ]
    [ 1  2 ]

Cách nói quen thuộc — "ta tối thiểu hoá tổng bình phương sai số" — là ngôn ngữ giải tích và nó không cho biết chuyện gì đang xảy ra. Strang nói bằng ngôn ngữ khác:

"Least squares replaces points b that are not on a line by points p that are!"

LSQ không nới lỏng gì cả. Nó thay b bằng p — bộ ba điểm thẳng hàng gần b nhất — rồi giải A x̂ = p chính xác, không sai số nào.

import numpy as np

A = np.array([[1., 0.], [1., 1.], [1., 2.]])
b = np.array([1., 3., 2.])                        # ba điểm KHÔNG thẳng hàng

xhat = np.linalg.lstsq(A, b, rcond=None)[0]       # [1.5 0.5]
p    = A @ xhat                                   # [1.5 2.  2.5]  ← THẲNG HÀNG
e    = b - p                                      # [-0.5  1.  -0.5]
print(xhat, p, e, f"‖e‖ = {np.linalg.norm(e):.6f}")   # ‖e‖ = 1.224745
print("Aᵀe =", A.T @ e)                           # [4.44e-16 2.22e-16] ≈ (0, 0)

assert np.allclose(A.T @ e, 0, atol=1e-12)        # e TRỰC GIAO với MỌI cột của A
assert np.allclose(np.diff(p, 2), 0)              # p thẳng hàng: sai phân cấp 2 = 0
x2 = np.linalg.lstsq(A, p, rcond=None)[0]
assert np.allclose(x2, xhat) and np.linalg.norm(A @ x2 - p) < 1e-12   # A x̂ = p CHÍNH XÁC

Aᵀe = 0 là toàn bộ nội dung: phần dư e trực giao với mọi cột của A, tức e nằm trong left null space — không gian null của Aᵀ. Nó là phần của b mà A không thể chạm tới, và vì thế cũng là phần mô hình không thể học.

Cách nhìn "thay dữ liệu" trả lời ngay một câu hỏi mà cách nói giải tích không trả lời được: mô hình học được cái gì? Không phải b. Là p — hình chiếu của b lên không gian cột. Mọi thứ ngoài không gian cột bị vứt trước khi việc học bắt đầu. Đó là nền để hiểu bias của mô hình ở môn 6 bài 3: bias không phải "học chưa đủ lâu", nó là khoảng cách từ b tới không gian cột, và không vòng lặp huấn luyện nào rút ngắn được nó.

Vì sao khó tự nghĩ ra: cách nói quen thuộc "tối thiểu hoá tổng bình phương sai số" là ngôn ngữ giải tích — nó mô tả một phép tối ưu và dừng ở đó. Nó không nói e sẽ nằm ở đâu, không nói vì sao Aᵀe = 0, không nói mô hình thực sự nhìn thấy dữ liệu nào. Bạn có thể dùng LSQ mười năm bằng ngôn ngữ đó mà không bao giờ có lý do nghĩ tới hình chiếu, vì công thức không bắt bạn nghĩ tới.

🪤 BẪY — sai số được bình phương là khoảng cách DỌC, không phải khoảng cách vuông góc

Đây là chỗ trực giác đánh lừa: e ⟂ cột của A không có nghĩa là ta đo khoảng cách vuông góc tới đường thẳng trên mặt phẳng dữ liệu. Trực giao xảy ra trong ℝ³ (không gian của các vector b), còn cái được bình phương là sai số dọc yᵢ − (m·xᵢ + c).

Nhầm hai cái là nhầm regression thành PCA. Cùng ba điểm, hai tiêu chí, hai đường khác nhau:

xs, ys = np.array([0., 1., 2.]), b
m_reg, c_reg = xhat[1], xhat[0]                   # tối thiểu sai số DỌC

M = np.column_stack([xs, ys]); M = M - M.mean(axis=0)
d = np.linalg.svd(M)[2][0]                        # hướng phương sai lớn nhất = PCA
m_tls = d[1] / d[0]; c_tls = ys.mean() - m_tls * xs.mean()

sse_doc   = lambda m, c: np.sum((ys - m * xs - c) ** 2)
sse_vuong = lambda m, c: np.sum((ys - m * xs - c) ** 2 / (1 + m * m))
assert abs(m_reg - 0.5) < 1e-12 and abs(m_tls - 1.0) < 1e-12
assert sse_doc(m_reg, c_reg)   < sse_doc(m_tls, c_tls)      # 1.5 < 2.0
assert sse_vuong(m_tls, c_tls) < sse_vuong(m_reg, c_reg)    # 1.0 < 1.2
đường thẳng sai số DỌC sai số VUÔNG GÓC
Regression (LSQ) y = 0.5x + 1.5 1.5000 ⭐ 1.2000
TLS / PCA y = 1.0x + 1.0 2.0000 1.0000 ⭐

Hệ số góc 0.5 so với 1.0 — gấp đôi nhau, trên đúng ba điểm đó. Mỗi đường thắng ở đúng tiêu chí của nó và thua ở tiêu chí kia. Không có đường nào "đúng hơn"; có hai câu hỏi khác nhau:

  • "biết x, đoán y" → sai số dọc → regression
  • "tìm trục chính của đám mây điểm" → sai số vuông góc → PCA (bài 9, môn 6 bài 8)

Ai vẽ "đường thẳng khớp nhất" trong đầu bằng hình ảnh khoảng cách vuông góc sẽ dùng nhầm công cụ mà không bao giờ biết — vì cả hai đều chạy, đều cho một đường, đều trông hợp lý.


4d. ⭐ Khi m < n: vô số nghiệm — chọn nghiệm NGẮN NHẤT, và đó chính là giả nghịch đảo (bổ sung từ VMLS — 2026-10)

(nguồn: VMLS tr.342 và tr.350-351, §16.1.1 và §16.3 (bài toán chuẩn nhỏ nhất); tr.332-333, §15.5.2 (đồng nhất thức "kernel trick"))

§4b-4c lo ca A cao (quá nhiều phương trình, vô nghiệm). Ca ngược lại — A thấp-béo như P ở ô ⚡ — thì Ax = b có vô số nghiệm, vì cộng thêm bất kỳ vector null (§2) vẫn là nghiệm. Chọn cái nào? Sách chọn cái ngắn nhất, và công thức rơi ra không cần giải tích:

x̂ = Aᵀ(AAᵀ)⁻¹ b = A†b          ← AAᵀ là m×m nhỏ; khả nghịch khi các HÀNG của A độc lập

x̂ = Aᵀz nằm trong không gian hàng nên vuông góc với mọi n ∈ null(A): x̂ᵀn = zᵀ(An) = 0. Mọi nghiệm khác x = x̂ + n có ‖x‖² = ‖x̂‖² + ‖n‖² — Pythagoras của bài 1, nên x̂ ngắn nhất.

import numpy as np, time
from scipy.linalg import null_space
P = np.array([[1., 0, 0], [0, 1, 0]]); Pd = P.T@np.linalg.inv(P@P.T)
print(Pd@P, Pd@[1., 2.])                       # diag(1,1,0) · (1,2,0): "giải nén" tốt nhất ném trục z, đúng §2
g = np.random.default_rng(0); A, b = g.normal(size=(3, 8)), g.normal(size=3)
xh = A.T@np.linalg.solve(A@A.T, b); assert np.allclose(xh, np.linalg.pinv(A)@b) and np.allclose(A@xh, b)
n_ = null_space(A)@g.normal(size=5)            # một vector null bất kỳ
assert np.allclose(A@(xh + n_), b) and np.isclose((xh + n_)@(xh + n_), xh@xh + n_@n_) and abs(xh@n_) < 1e-12
def gd(x, lr=.02, it=20000):
    for _ in range(it): x = x - lr*2*A.T@(A@x - b)
    return x
x0 = g.normal(size=8)                          # GD từ 0 dừng ở x̂; từ x0 mang nguyên thành phần null của x0
assert np.allclose(gd(np.zeros(8)), xh) and np.allclose(gd(x0), xh + (np.eye(8) - np.linalg.pinv(A)@A)@x0)
for lam in (1e-6, 1e-9, 1e-12):                # ridge λ→0 về A†b — hai cách tính cho HAI độ chính xác
    pri = np.linalg.solve(A.T@A + lam*np.eye(8), A.T@b); dua = A.T@np.linalg.solve(A@A.T + lam*np.eye(3), b)
    print(f"λ={lam:g}  p×p: {abs(pri - xh).max():.1e}   m×m: {abs(dua - xh).max():.1e}")
X, y = g.normal(size=(50, 2000)), g.normal(size=50)    # 50 mẫu, 2000 đặc trưng
t = time.perf_counter(); w1 = np.linalg.solve(X.T@X + np.eye(2000), X.T@y); t1 = time.perf_counter() - t
t = time.perf_counter(); w2 = X.T@np.linalg.solve(X@X.T + np.eye(50), y);   t2 = time.perf_counter() - t
assert np.allclose(w1, w2) and t1 > 10*t2; print(f"{t1*1e3:.1f} ms vs {t2*1e3:.2f} ms")   # máy này, 3 lần chạy: 39-48 ms vs 0.17-0.19 ms
λ=1e-06  p×p: 1.6e-07   m×m: 1.6e-07
λ=1e-09  p×p: 2.8e-07   m×m: 1.6e-10        ← p×p chạm sàn làm tròn rồi TỆ DẦN (λ=1e-12: 2.7e-04); m×m vẫn hội tụ (1.6e-13)

Ba hệ quả. (1) P†P = chiếu lên không gian hàng: nén-rồi-nâng tốt nhất vẫn mất trục z (§2) — E·P ≠ I ở §4 không sửa được bằng ma trận nào. (2) GD khởi tạo 0 tự chọn nghiệm này: gradient 2Aᵀ(Ax − b) luôn nằm trong không gian hàng nên bước đi không bao giờ có thành phần null — cơ chế đằng sau câu "pinv chọn nghiệm chuẩn nhỏ nhất" ở môn 6 bài 3 §7. (3) Đồng nhất thức (AᵀA + λI)⁻¹Aᵀ = Aᵀ(AAᵀ + λI)⁻¹ đổi một nghịch đảo n×n thành m×m: 50 mẫu × 2000 đặc trưng, khoảng 40-48 ms so với 0.2 ms (hơn 200 lần) — quy tắc "(lớn)×(nhỏ)²"; với embedding hàng nghìn chiều và vài trăm mẫu, đây là đường tính ridge rẻ hơn (môn 6 bài 4).

Phạm vi: "ngắn nhất" là một prior, không phải sự thật — nếu giá trị thật có thành phần trong null space (A không nhìn thấy nó), sai số ≥ chính thành phần đó, bao nhiêu dữ liệu b cũng không cứu.


5. Đây CHÍNH LÀ embedding

Giờ đổi tên các trục và không cần thêm toán nào nữa.

Một mô hình ngôn ngữ có bộ từ vựng V = 100.000 từ. Biểu diễn thô nhất của một từ là one-hot: vector 100.000 chiều, đúng một ô bằng 1, còn lại bằng 0.

"mèo"  = (0, 0, ..., 0, 1, 0, ..., 0)      ← 100.000 chiều, số 1 ở đúng vị trí của từ này

One-hot có một tính chất chết người: mọi cặp từ đều vuông góc. cos("mèo", "hổ") = 0, giống hệt cos("mèo", "cái bàn") = 0. Theo bài 1, vuông góc nghĩa là không chia sẻ thông tin gì với nhau. Vậy trong không gian one-hot, mọi từ đều xa lạ với mọi từ như nhau. Vô dụng cho ngữ nghĩa.

Embedding = một ma trận không vuông chiếu R¹⁰⁰⁰⁰⁰ → R⁷⁶⁸. Hết. Đúng thứ ở §2, chỉ khác con số.

Mô phỏng bằng số đẹp: 5 từ, nén R⁵ → R²

Bảng embedding W — hàng thứ i là embedding của từ thứ i:

        chiều 1  chiều 2
mèo  [    3   ,    4   ]
chó  [    4   ,    3   ]
hổ   [    6   ,    8   ]
xe   [   −4   ,    3   ]
bàn  [    0   ,   −5   ]

BẪY số 2 — quy ước hàng/cột. Ở §2-§4 ta viết P · v (ma trận trái, vector cột). Thư viện thật (PyTorch, numpy) viết x @ W (vector hàng trái, ma trận phải) để xử lý cả batch cùng lúc. Hai cách là chuyển vị của nhau: ma trận 2×5 theo kiểu toán = ma trận 5×2 theo kiểu thư viện. Đừng cố nhớ — cứ nhìn .shape và hỏi "chiều nào là vào, chiều nào là ra". Ở đây W là 5×2: vào 5, ra 2.

Nhân one-hot của "hổ" (vị trí 2) với W:

(0, 0, 1, 0, 0) · W  =  0·(3,4) + 0·(4,3) + 1·(6,8) + 0·(−4,3) + 0·(0,−5)  =  (6, 8)

Nối lại bài 1 — tính cosine giữa các từ sau khi nén:

cặp tích vô hướng cosine đọc là
mèo, hổ 50 1.00 trùng hướng hoàn toàn
mèo, chó 24 0.96 rất gần nghĩa
mèo, xe 0 0.00 không liên quan
mèo, bàn −20 −0.80 ngược hướng
chó, xe −7 −0.28 hơi ngược hướng

(Ba số 24 / 0 / −7 chính là ba số bạn đã tính tay ở bài 1 — cùng bộ vector, giờ có tên gọi mới.)

Đây là điều one-hot không bao giờ làm được. Trong R⁵ one-hot mọi cosine đều bằng 0. Sau khi nén xuống R², các từ có quan hệ với nhau. Đó là thứ ta được.

Còn mất gì?

hạng(W) = 2. Số chiều bị nghiền về 0 là 5 − 2 = 3. Tìm một vector trong null space bằng số đẹp:

hổ − 2·mèo   =  (6, 8) − 2·(3, 4)  =  (0, 0)          ← khác 0 ở đầu vào, bằng 0 ở đầu ra
5·chó + 5·xe + 6·bàn = 5(4,3) + 5(−4,3) + 6(0,−5) = (0, 0)

Cụ thể hơn, cos(mèo, hổ) = 1.00 chính xác bằng 1: sau khi nén, hai từ này cùng hướng tuyệt đối. Một hệ retrieval dùng cosine sẽ không phân biệt nổi chúng. Với 2 chiều thì đó là chuyện phải xảy ra — không đủ chỗ.

Mất thông tin trong embedding không phải bug. Nó là cái giá bắt buộc để có được quan hệ ngữ nghĩa. Câu hỏi kỹ thuật duy nhất là: mất cái gì — và toàn bộ việc huấn luyện embedding chính là đi tìm ma trận nén nào vứt đi thứ ít quan trọng nhất.

Chật đến mức nào? Số thật

Lấy n = 2000 vector đơn vị ngẫu nhiên, đo max |cosine| giữa mọi cặp (2000 điểm ngẫu nhiên trong d chiều):

số chiều d max │cos│ giữa 2 vector bất kỳ │cos│ trung bình
2 1.0000 0.6363
8 0.9892 0.2909
64 0.5814 0.1001
768 0.1747 0.0288

Đọc bảng này: ở 2 chiều, có hai từ trùng hướng hoàn toàn — không thể phân biệt. Ở 768 chiều, cặp giống nhau nhất cũng chỉ đạt 0.17, và trung bình 0.03 — gần như mọi cặp đều vuông góc.

Đây là câu trả lời định lượng cho "vì sao 768 chứ không phải 8". Chiều càng cao, càng nhiều hướng gần vuông góc để xếp các nghĩa khác nhau vào mà không chồng lên nhau. Cột │cos│ trung bình tiến về 0 chính là "lời nguyền số chiều" (bài 1, câu tự kiểm 4) — nhưng nhìn từ mặt tốt của nó.

Với V = 100.000 và d = 768, pigeonhole cho ngay:

số từ                                    = 100.000
số từ độc lập tuyến tính tối đa trong R⁷⁶⁸ =     768
số từ BẮT BUỘC phụ thuộc tuyến tính       =  99.232
số chiều null space của phép chiếu       ≥  99.232

99.232 chiều thông tin bị nghiền phẳng. Và mô hình vẫn hoạt động — vì thông tin bị vứt phần lớn là thứ mô hình không cần.


6. Vì sao nn.Embedding là BẢNG TRA, không phải phép nhân

Đây là hệ quả kỹ thuật, và nó rơi thẳng ra từ ví dụ trên.

Nhân một vector one-hot với ma trận W:

(0, 0, 1, 0, 0) · W  =  0·hàng₀ + 0·hàng₁ + 1·hàng₂ + 0·hàng₃ + 0·hàng₄  =  hàng₂

Nhân one-hot với ma trận = lấy ra một hàng. Toàn bộ phép nhân là V−1 lần nhân với 0 rồi cộng vào, cộng đúng một hàng thật.

Đếm chi phí thật, với V = 100.000, d = 768, batch = 32 token:

số phép nhân-cộng
nhân ma trận đầy đủ 2.457.600.000
số hạng thực sự khác 0 24.576
tỉ lệ lãng phí 100.000 lần
phần trăm phép tính vô ích 99,999 %

Đo thời gian thật trên cùng một máy (numpy, float32):

matmul one-hot :  ~6,6 ms      (lần chạy đầu 40,8 ms — cache lạnh)
lookup W[ids]  :  ~0,019 ms    (lần chạy đầu 0,037 ms — cũng cache lạnh)
→ nhanh hơn    :  ~350 lần     (6,6 / 0,019 ≈ 347 · lần đầu: 40,8 / 0,037 ≈ 1100)
kết quả giống hệt nhau

Ẩn dụ SQL cho đúng bản chất:

-- cách "nhân ma trận": quét toàn bảng rồi nhân mọi hàng với 0 hoặc 1
SELECT SUM(vec * CASE WHEN id = :word_id THEN 1 ELSE 0 END) FROM embeddings;

-- cách lookup: đi thẳng bằng khoá chính
SELECT vec FROM embeddings WHERE id = :word_id;

Cùng kết quả. Một cái O(V·d), một cái O(d). Không framework nào lại đi làm cái đầu.

Vì thế nn.Embedding không có phép nhân ma trận nào bên trong. Nó là một tensor (V, d) cộng một phép index. Nhưng về mặt toán nó vẫn đúng là một ma trận không vuông V → d, và mọi thứ ở §2 (hạng, null space, mất thông tin) vẫn áp dụng nguyên vẹn.

Hệ quả gradient, để trọn vẹn: khi backprop, chỉ những hàng được tra mới nhận gradient. Với batch = 32 token trên bảng V = 100.000 hàng, nhiều nhất 32 hàng được đụng tới — 99,968 % bảng đứng yên ở bước đó. Đó là lý do embedding thường dùng gradient thưa (sparse).

BỎ QUA ĐƯỢC: chi tiết cài đặt sparse=True và các optimizer hỗ trợ nó. Chỉ cần biết vì sao gradient thưa.


7. Code — tự cài, KHÔNG dùng hàm có sẵn

import numpy as np

# ---------- (1) nhân ma trận m×n với vector n chiều — tự cài ----------
def apply_map(M, v):
    """M: (m, n)  ·  v: (n,)  ->  ra vector (m,). Không dùng @."""
    m, n = M.shape
    assert v.shape == (n,), f"chiều vào phải là {n}, bạn đưa {v.shape[0]}"
    out = np.zeros(m)
    for i in range(m):
        for j in range(n):
            out[i] += M[i, j] * v[j]
    return out


# ---------- (2) hạng bằng khử Gauss — tự cài ----------
def rank_gauss(M, tol=1e-9):
    """Đếm số hàng khác 0 sau khi khử. Đây là ĐỊNH NGHĨA vận hành của hạng."""
    A = M.astype(float).copy()
    rows, cols = A.shape
    r = 0
    for c in range(cols):
        if r >= rows:
            break
        p = r + int(np.argmax(np.abs(A[r:, c])))     # chọn trụ lớn nhất
        if abs(A[p, c]) < tol:
            continue
        A[[r, p]] = A[[p, r]]                        # đổi chỗ hai hàng
        A[r] = A[r] / A[r, c]
        for k in range(rows):
            if k != r:
                A[k] = A[k] - A[k, c] * A[r]
        r += 1
    return r


P = np.array([[1., 0., 0.],
              [0., 1., 0.]])                        # nén: R^3 -> R^2
E = np.array([[1., 0.],
              [0., 1.],
              [1., 1.]])                            # nâng: R^2 -> R^3

# (a) nén làm MẤT thông tin: hai điểm khác nhau, một ảnh
a = apply_map(P, np.array([1., 2., 5.]))
b = apply_map(P, np.array([1., 2., 99.]))
assert np.allclose(a, [1., 2.]) and np.allclose(b, [1., 2.])
assert np.allclose(apply_map(P, np.array([0., 0., 94.])), [0., 0.])   # hiệu ↦ 0
print("nén: (1,2,5) và (1,2,99) cùng đáp xuống", a, "✓ mất thông tin")

# (b) nâng KHÔNG phủ hết R^3: ảnh nằm trọn trên mặt phẳng x + y − z = 0
for w in [np.array([1., 0.]), np.array([0., 1.]), np.array([3., 2.]), np.array([1., 2.])]:
    x, y, z = apply_map(E, w)
    assert abs(x + y - z) < 1e-12
print("nâng: mọi ảnh của E thoả x+y−z=0 ✓ chỉ là một MẶT PHẲNG, không phủ hết R^3")

# (c) hạng tối đa = min(m, n) — tự cài phải khớp numpy
for M in [P, E, np.random.default_rng(7).standard_normal((5, 2))]:
    assert rank_gauss(M) == np.linalg.matrix_rank(M) == min(M.shape)
print("rank_gauss khớp numpy, và = min(m,n) ✓")

# (d) nâng-rồi-nén khôi phục được; nén-rồi-nâng thì KHÔNG
assert np.allclose(P @ E, np.eye(2))          # R^2 -> R^3 -> R^2  : undo được
assert not np.allclose(E @ P, np.eye(3))      # R^3 -> R^2 -> R^3  : hỏng
assert abs(np.linalg.det(E @ P)) < 1e-12      # det = 0 → không nghịch đảo được
print("P@E =\n", P @ E, "\nE@P =\n", E @ P)

# ---------- (3) embedding = TRA BẢNG, không phải phép nhân ----------
words = ["mèo", "chó", "hổ", "xe", "bàn"]
W = np.array([[3., 4.], [4., 3.], [6., 8.], [-4., 3.], [0., -5.]])   # hàng i = từ i


def embed_matmul(ids, W):
    """Cách 'đúng sách': dựng one-hot rồi nhân ma trận."""
    OH = np.zeros((len(ids), W.shape[0]))
    for r, c in enumerate(ids):
        OH[r, c] = 1.0
    return OH @ W


def embed_lookup(ids, W):
    """Cách thư viện thật làm: chỉ đọc hàng."""
    return np.stack([W[i] for i in ids])


ids = [0, 3, 1]
assert np.allclose(embed_matmul(ids, W), embed_lookup(ids, W))
assert np.allclose(embed_lookup(ids, W), W[ids])          # numpy fancy-index
print("one-hot @ W  ==  W[ids]  ✓ embedding layer chỉ là bảng tra")

# (e) nén 5 chiều → 2 chiều thì một tổ hợp KHÁC 0 của các từ lại về vector 0
z = np.array([-2., 0., 1., 0., 0.])        # hổ − 2·mèo
assert np.allclose(z @ W, [0., 0.])
assert np.linalg.matrix_rank(W) == 2
print("hổ − 2·mèo ↦", z @ W, " · rank(W)=2 · nullity = 5−2 = 3 ✓")


def cosine(u, v):
    return float(np.sum(u * v) / np.sqrt(np.sum(u * u) * np.sum(v * v)))


assert abs(cosine(W[0], W[2]) - 1.0) < 1e-12       # mèo vs hổ: TRÙNG hướng
assert abs(cosine(W[0], W[1]) - 0.96) < 1e-12
assert abs(cosine(W[0], W[3]) - 0.0) < 1e-12
print("cos(mèo,hổ)=1.0  cos(mèo,chó)=0.96  cos(mèo,xe)=0.0  ✓ 2 chiều là quá chật")

Kết quả phải khớp §2-§5. Nếu rank_gauss lệch np.linalg.matrix_rank, nguyên nhân gần như luôn là ngưỡng tol hoặc quên trừ hàng trên trụ — không phải hiểu sai toán. Ghi vào nhật ký lỗi.

Bài tập thêm cho khối này: đo thời gian embed_matmul với V = 100.000, d = 768, batch = 32 rồi so với W[ids]. Trên máy tôi ra ~6,6 ms vs ~0,019 ms. Máy bạn ra số khác, tỉ lệ mới là thứ đáng nhìn.


8. Nối vào AI

Khái niệm trong bài Chỗ dùng trong AI Chiều thật
Ma trận nén m < n Embedding layer — từ vựng → không gian dày đặc 100.000 → 768
Nhân one-hot = tra hàng nn.Embedding là tensor (V, d) + index, không có matmul tiết kiệm ~350 lần
Ma trận nén Q = X·W^Q, K = X·W^K trong attention 768 → 64 mỗi head
Hạng ≤ min(m,n) hạng(W^Q) ≤ 64 → mỗi head chỉ "nhìn" được một không gian con 64 chiều nullity ≥ 704
Nén rồi nâng LoRA: ΔW = B·A, A nén d→r, B nâng r→d hạng(B·A) ≤ r
Nén rồi nâng Autoencoder / bottleneck: ép qua chỗ hẹp để buộc học cái cốt lõi tuỳ kiến trúc
Ảnh không phủ hết Vì sao xếp lớp tuyến tính vô ích nếu thiếu phi tuyến (bài 2 §4) —

Attention, số thật: d_model = 768, 12 head, mỗi head d_k = 64, và 12 × 64 = 768 — vừa khít. X (10 × 768) · W^Q (768 × 64) → (10 × 64). Mỗi head cố ý nén 12 lần: nó không được phép nhìn hết, nên buộc phải chuyên môn hoá vào một khía cạnh. Rồi QKᵀ mới đo góc (bài 1). Nén trước, đo góc sau — đúng thứ tự đó.

LoRA, số thật với d = 4096:

hạng r tham số full d×d tham số LoRA 2·d·r tỉ lệ tiết kiệm
1 16.777.216 8.192 0,049 % 2048 lần
4 16.777.216 32.768 0,195 % 512 lần
8 16.777.216 65.536 0,391 % 256 lần
16 16.777.216 131.072 0,781 % 128 lần

LoRA hoạt động được chính vì hạng của tích bị chặn: hạng(B·A) ≤ r = 8. Ta cố tình chấp nhận một cập nhật hạng thấp để đổi lấy 256 lần ít tham số. Toàn bộ ý tưởng là §2 + §3 ghép lại.


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

  1. Ma trận 768 × 100000 biến đổi từ không gian nào sang không gian nào? Còn 100000 × 768? Nói rõ cột là chiều vào hay chiều ra.
  2. Vì sao ma trận nén (m < n) bắt buộc làm mất thông tin? Trả lời bằng hạng ≤ min(m,n) và định lý hạng–số khuyết, không bằng ví dụ số.
  3. E · (a,b) = (a, b, a+b). Vì sao ảnh của E chỉ là một mặt phẳng chứ không phải cả R³? Viết phương trình mặt phẳng đó và chỉ ra một điểm không thuộc ảnh.
  4. P·E = I₂ nhưng E·P ≠ I₃. Giải thích bằng hành động, không bằng số. Điều này nói gì về việc "nghịch đảo" một ma trận không vuông?
  5. Vì sao nhân một vector one-hot với ma trận luôn cho ra đúng một hàng của ma trận đó? Chứng minh trong 2 dòng, dùng đúng ý "cột = nơi vector cơ sở đáp xuống" của bài 2.
  6. Với V = 100.000 từ và d = 768: có tối đa bao nhiêu từ độc lập tuyến tính trong không gian embedding? Bao nhiêu từ buộc phải phụ thuộc? Điều đó nghĩa là gì với hai từ có cosine = 1.0?
  7. One-hot cho cos = 0 với mọi cặp từ. Sau khi nén xuống 768 chiều thì cos khác 0. Thông tin "mèo gần chó" đó từ đâu ra, khi phép nén chỉ làm mất chứ không thêm?
  8. Bảng ở §5 cho thấy max│cos│ giảm từ 1.0000 (2 chiều) xuống 0.1747 (768 chiều). Vì sao chiều càng cao càng dễ tách các nghĩa? Và vì sao không ai dùng thẳng d = 100.000?

Câu 7 là câu khó nhất và là câu tận cùng của bài. Gợi ý: ma trận nén không tự nhiên mà có — nó được học. Thông tin ngữ nghĩa không đến từ phép chiếu, nó đến từ dữ liệu đã nhào nặn ra ma trận đó. Phép chiếu chỉ là cái khuôn; dữ liệu mới là thứ đổ vào khuôn. Trả lời được câu này là bạn đã tách được "cấu trúc đại số" khỏi "nội dung học được" — ranh giới mà rất nhiều người dùng ML cả năm vẫn chưa tách.

Câu 4 và 6 là hai câu đáng nhất còn lại. Câu 6 bạn đã có sẵn con số 99.232 ở §5.


10. Bài tập về nhà trước bài 6

3Blue1Brown — Essence of Linear Algebra:

Tập Vì sao
8 — Nonsquare matrices as transformations between dimensions ⭐ Chính là cả bài này bằng hình động: phép biến đổi giữa hai không gian khác số chiều. Xem kỹ, đừng xem lướt
7 — Inverse matrices, column space and null space Xem lại nếu §2 (null space) còn mờ. Column space chính là "ảnh" ở §3

Giữ sẵn một câu hỏi khi xem tập 8: chiếu từ 100.000 chiều từ vựng xuống 768 chiều thì mất gì, giữ gì? Bạn đã có câu trả lời ở §5 — xem để thấy nó.

Làm tay, không gõ code (30 phút, trên giấy):

  1. Cho F = [[1,0,1],[0,1,1]] (2×3). Tìm một vector khác 0 bị nghiền về (0,0). (Gợi ý: thử (1,1,−1).) Hạng của F là bao nhiêu? Số chiều null space?
  2. Cho G = [[2,0],[0,2],[2,2]] (3×2). Ảnh của G là mặt phẳng nào? So với E ở §3 — giống hay khác? Điều đó nói gì về việc nhân cả ma trận với một số?
  3. Tự nghĩ ra một ma trận 3×5 có hạng 2 (không phải 3). Giải thích tại sao nó không đạt được min(3,5) = 3.

Bài 6 sẽ là: bài này cho thấy ma trận không vuông làm mất thông tin không lấy lại được. Vậy còn ma trận vuông và khả nghịch — nó chẳng mất gì cả, chỉ viết lại cùng một vector bằng một bộ trục khác. Cùng một mũi tên, hai người mô tả bằng hai hệ toạ độ, ra hai bộ số khác nhau mà vẫn là một vector. Câu hỏi cần trả lời được: "vector (3,2)" thật ra là câu nói thiếu vế — thiếu vế nào?

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ị.