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

MÔN 02 / Bài 04

Định thức và ma trận nghịch đảo

Diện tích thay đổi ra sao, và khi nào có thể đảo ngược một phép biến đổi?

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 ↓

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

Bài 3 cho bạn hạng: bao nhiêu chiều còn sống sót sau phép biến đổi. Bài này cho bạn một con số duy nhất trả lời câu đó, và trả lời thêm câu quan trọng hơn: phép biến đổi này có UNDO được không?

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

  1. Xem trước: 3Blue1Brown tập 5 (xem nhanh), tập 6 The determinant và tập 7. Giữ câu hỏi: det = 0 nghĩa là gì về hình học?
  2. Làm ô ⚡ bên dưới: chạy code với năm ma trận.
  3. Đọc §1-6. Ở §2, tự tính tay cả năm ma trận trước khi xem bảng đối chiếu.
  4. Đọc §7 (ba bẫy), rồi code §8: tự cài định thức và nghịch đảo bằng Gauss-Jordan. Tự viết xong mới mở code mẫu trong bài.
  5. Làm 4 bài tay bắt buộc ở §11, không xem đáp án.
  6. Tự kiểm §10.
  7. Sau cùng: xem lại tập 7, rồi đọc lại §4.

Xong bài khi: nói được det = 0 nghĩa là không gian bị bóp dẹp nên không hoàn tác được.

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

Năm ma trận, ba trong số đó bạn đã gặp ở bài 2 và bài 3. Chạy ngay:

import numpy as np

R = np.array([[0., -1.], [1., 0.]])   # quay 90°    (bài 2)
S = np.array([[1.,  1.], [0., 1.]])   # trượt ngang (bài 2)
A = np.array([[1.,  2.], [2., 4.]])   # hạng 1      (bài 3)
D = np.array([[3.,  0.], [0., 2.]])   # giãn 3× ngang, 2× dọc
F = np.array([[1.,  0.], [0., -1.]])  # lật qua trục x

for name, M in [("R", R), ("S", S), ("A", A), ("D", D), ("F", F)]:
    print(name, "det =", round(float(np.linalg.det(M)), 4))

try:
    np.linalg.inv(A)
except np.linalg.LinAlgError as e:
    print("A:", e)

Đích — phải ra đúng năm số này:

R det = 1.0        S det = 1.0        A det = 0.0
D det = 6.0        F det = -1.0       A: Singular matrix

Giờ dừng lại và đoán trước khi đọc tiếp, bằng lời chứ không bằng công thức:

  • Vì sao quay và trượt lại ra cùng một số?
  • Vì sao D ra 6 — số 6 đó ở đâu ra khi ma trận chỉ có 3 và 2?
  • Vì sao F ra số âm? Âm cái gì?
  • Vì sao riêng A làm numpy nổ?

Bốn câu trên chính là §1 → §4. Nếu đoán được cả bốn thì bài này bạn đọc trong 1 giờ.


1. Định thức đo cái gì

Bài 2 nói: ma trận là một hành động lên toàn bộ không gian. Hành động đó kéo giãn chỗ này, nén chỗ kia. Câu hỏi tự nhiên: nó làm diện tích phình ra hay teo lại bao nhiêu lần?

Định thức = hệ số co giãn diện tích (2D) / thể tích (3D) của phép biến đổi.

det(M) = 2 → mọi vùng đều to gấp đôi. det(M) = 0.5 → mọi vùng teo còn một nửa.

Chữ "mọi" ở đây không phải nói cho đẹp. Nó là một hằng số duy nhất cho toàn không gian — không phụ thuộc bạn đo vùng nào, hình gì, ở đâu. Đó là điều bất ngờ và cũng là lý do định thức tồn tại như một khái niệm.

Vì sao chỉ cần một số? Vì phép biến đổi tuyến tính giữ đường thẳng vẫn thẳng và giữ song song vẫn song song (bài 2). Một lưới ô vuông đều biến thành một lưới hình bình hành đều — mọi ô phình ra đúng cùng một tỉ lệ. Không có chỗ nào phình nhiều hơn chỗ khác.

Vậy chỉ cần đo một hình là biết tỉ lệ. Hình dễ đo nhất: hình vuông đơn vị — bốn đỉnh (0,0), (1,0), (1,1), (0,1), diện tích 1.

Và bài 2 đã cho sẵn ảnh của nó: cột 1 là nơi î đáp xuống, cột 2 là nơi ĵ đáp xuống. Hai cột đó chính là hai cạnh của hình bình hành ảnh.

Công thức 2×2 — công thức duy nhất của bài này cần thuộc:

M = [ a  b ]        det(M) = a·d − b·c
    [ c  d ]

2. Năm ma trận — tính tay hết, rồi đối chiếu

Làm trên giấy. Với mỗi ma trận: biến đổi bốn đỉnh hình vuông đơn vị, vẽ ra, đo diện tích, so với a·d − b·c.

2.1 D = [[3,0],[0,2]] — trường hợp dễ nhất, làm trước

det = 3(2) − 0(0) = 6

Ảnh của hình vuông đơn vị:

(0,0) → (0,0)      (1,0) → (3,0)
(1,1) → (3,2)      (0,1) → (0,2)

Đó là hình chữ nhật 3 × 2 = 6. Diện tích đúng bằng định thức, và bạn nhìn thấy vì sao: giãn 3 lần theo chiều ngang, 2 lần theo chiều dọc, diện tích ×3×2.

2.2 R = [[0,−1],[1,0]] — quay 90°

det = 0(0) − (−1)(1) = 1

Ảnh: (0,0), (0,1), (−1,1), (−1,0) — vẫn là hình vuông cạnh 1, chỉ nằm ở góc phần tư khác. Diện tích 1.

Hợp lý đến mức nhàm: quay thì không làm gì diện tích cả. Ghi lấy — định thức của phép quay bất kỳ góc nào cũng bằng 1. Kiểm bằng máy:

Góc quay 30° 45° 90° 137°
det 1.000000000000 1.000000000000 1.000000000000 1.000000000000

2.3 S = [[1,1],[0,1]] — trượt ngang, và đây là chỗ bất ngờ

det = 1(1) − 1(0) = 1

Trượt ngang KHÔNG làm đổi diện tích. Lần đầu nghe thì thấy sai — hình rõ ràng đã méo đi cơ mà?

Ảnh của hình vuông đơn vị:

(0,0) → (0,0)      (1,0) → (1,0)
(1,1) → (2,1)      (0,1) → (1,1)

Vẽ ra: một hình bình hành với đáy nằm ngang dài 1, đỉnh trên bị đẩy sang phải nhưng vẫn ở độ cao 1. Diện tích hình bình hành = đáy × chiều cao = 1 × 1 = 1.

Bạn xô nghiêng một cỗ bài. Cỗ bài méo đi, nhưng số lá không đổi — vì mỗi lá chỉ trượt ngang, không lá nào dày lên hay mỏng đi. Đó là toàn bộ nội dung của "shear giữ nguyên diện tích".

2.4 F = [[1,0],[0,−1]] — lật, và định thức ra âm

det = 1(−1) − 0(0) = −1

Ảnh: (0,0), (1,0), (1,−1), (0,−1) — vẫn là hình vuông cạnh 1. Độ lớn diện tích là 1. Nhưng dấu âm. §3 nói dấu âm là gì.

2.5 A = [[1,2],[2,4]] — hạng 1 từ bài 3

det = 1(4) − 2(2) = 4 − 4 = 0

Ảnh: (0,0), (1,2), (3,6), (2,4). Chấm bốn điểm này lên giấy — cả bốn đều nằm trên đường thẳng y = 2x. Hình vuông không còn là hình. Nó bị bóp dẹp thành một đoạn thẳng. Diện tích = 0.

Bảng tổng — máy xác nhận cả năm

Ma trận a·d − b·c tính tay np.linalg.det Diện tích ảnh (đo bằng công thức dây giày) Hạng (bài 3)
R quay 90° 1 1.00000000 1.0 2
S trượt ngang 1 1.00000000 1.0 2
D giãn 3×2 6 6.00000000 6.0 2
F lật trục x −1 −1.00000000 −1.0 2
A hạng 1 0 0.00000000 0.0 1

Cột "diện tích ảnh" tính bằng công thức dây giày (shoelace) — một công thức đo diện tích đa giác hoàn toàn không biết gì về ma trận. Nó khớp tuyệt đối. Định thức không phải quy ước; nó là diện tích.


3. Định thức âm = không gian bị LẬT

Cột cuối bảng trên: diện tích của F ra −1, không phải 1. Đó không phải lỗi — công thức dây giày trả về diện tích CÓ DẤU, dấu phụ thuộc chiều đi vòng quanh các đỉnh.

Hình vuông gốc đi (0,0) → (1,0) → (1,1) → (0,1): ngược chiều kim đồng hồ. Sau khi qua F, ảnh đi (0,0) → (1,0) → (1,−1) → (0,−1): thuận chiều kim đồng hồ.

Chiều đã bị đảo. Không có cách nào quay hình vuông gốc để nó trùng ảnh — bạn phải lật nó qua một trục.

det < 0 = phép biến đổi lật ngược định hướng của không gian. Trong 2D: hình bị lật như soi gương. Trong 3D: bàn tay phải biến thành bàn tay trái.

|det| là hệ số co giãn. Dấu của det là một bit riêng: có lật hay không.

Cách kiểm nhanh trong 2D: nhìn î và ĵ. Trước biến đổi, đi từ î sang ĵ là quay ngược chiều kim đồng hồ. Nếu sau biến đổi vẫn vậy → det > 0. Nếu đã thành thuận chiều → det < 0.

Chỗ này BỎ QUA được: cách định nghĩa chặt chẽ "định hướng" trong n chiều (hoán vị chẵn/lẻ, ký hiệu Levi-Civita). Bạn không cần nó cho ML. Cần nhớ đúng một câu: dấu = có lật hay không, độ lớn = co giãn bao nhiêu.


4. det = 0 — ý trung tâm của cả bài

Đây là chỗ đáng bỏ 30 phút chỉ để ngồi nghĩ.

Nhìn lại A = [[1,2],[2,4]]. Cột 2 = 2 × cột 1. Bài 3 gọi đó là phụ thuộc tuyến tính, và cho biết hạng = 1.

Định thức nói cùng câu chuyện bằng một con số: det(A) = 0.

det(M) = 0 ⟺ không gian bị BÓP DẸP xuống số chiều thấp hơn ⟺ hạng không đầy đủ.

Hình vuông → đoạn thẳng. Khối lập phương → mặt phẳng (hoặc đường thẳng, hoặc điểm). Diện tích/thể tích bằng đúng 0, vì vật thể không còn đủ chiều để có diện tích.

Đây chính là câu trả lời cho câu 5 tự kiểm của bài 2 ("một ma trận bóp dẹp cả mặt phẳng thành một đường thẳng — hai cột của nó có quan hệ gì?"), giờ đã đóng gói thành một con số kiểm được trong một dòng.

Bóp dẹp = MẤT THÔNG TIN, và đây mới là điểm mấu chốt

A nhận vào mặt phẳng (vô hạn hai chiều) và trả ra một đường thẳng (vô hạn một chiều). Chuồng ít hơn bồ câu. Bắt buộc phải có va chạm. Kiểm bằng số:

A @ (1,  0) = (1, 2)
A @ (3, −1) = (1, 2)      ← HAI điểm khác nhau, CÙNG một ảnh

Cho bạn kết quả (1, 2) và bảo tìm ngược lại đầu vào — không thể. Không phải vì thuật toán yếu, mà vì thông tin đã bị xoá thật. Ít nhất hai đầu vào cho ra nó.

Và có cả một đường thẳng các vector bị nghiền thẳng về gốc toạ độ:

A @ (2, −1) = (0, 0)

Đó là null space của bài 3, và giờ nó có một tên gọi thứ hai:

det = 0 ⟺ hạng không đầy đủ ⟺ null space chứa vector khác 0 ⟺ KHÔNG có nghịch đảo.

Bốn câu này là một câu, nói bằng bốn thứ tiếng. Đọc lại tới khi thấy chúng là một.

Câu hỏi bài 3 để lại: det đánh mất thông tin gì mà hạng giữ được?

det là một con số và chỉ trả lời câu có/không: "có thiếu hạng không?" (det ≠ 0 thì đủ hạng, det = 0 thì thiếu). Nó không nói thiếu bao nhiêu chiều. Hai ma trận 3×3 dưới đây đều có det = 0:

Ma trận det hạng nullity bóp ℝ³ thành
diag(1, 1, 0) 0 2 1 một mặt phẳng
ma trận toàn số 1 0 1 2 một đường thẳng

Hai cách mất thông tin rất khác nhau (mất một chiều, và mất hai chiều), nhưng det đều là 0. Muốn biết thiếu bao nhiêu thì cần hạng (bài 3), và cách đo hạng tốt nhất là giá trị suy biến (bài 9).

Ẩn dụ: migration của database

Migration Ma trận Rollback?
ALTER TABLE RENAME COLUMN a TO b det ≠ 0 — đổi tên, không mất gì ✅ đảo tên lại là xong
UPDATE price = price * 100 det ≠ 0 — giãn, thông tin còn nguyên ✅ chia lại cho 100
ALTER TABLE DROP COLUMN email det = 0 — cột biến mất ❌ dữ liệu đã đi. Không rollback được.
UPDATE status = 'archived' (mọi hàng) det = 0 — mọi giá trị cũ bị đè ❌ giá trị cũ không tồn tại ở đâu nữa

Viết được file down.sql hay không không phụ thuộc bạn giỏi SQL đến đâu. Nó phụ thuộc migration đó có mất thông tin hay không. Định thức là con số nói cho bạn biết điều đó, trước khi bạn chạy.

Null space chính là "những gì bị DROP" — tập hợp các khác biệt ở đầu vào mà đầu ra không còn dấu vết.


5. Ma trận nghịch đảo = phép biến đổi NGƯỢC

M⁻¹ là hành động undo của M. Định nghĩa gói gọn trong một dòng:

M⁻¹ · M = I

I là ma trận đơn vị — không làm gì cả (bài 2, câu 4 tự kiểm). "Làm M, rồi làm M⁻¹" = "không làm gì".

Nghịch đảo của cả năm ma trận, đọc bằng hành động trước, bằng số sau:

M M⁻¹ là hành động gì M⁻¹ (đã kiểm bằng np.linalg.inv)
R quay +90° quay −90° [[0, 1], [−1, 0]]
S trượt phải trượt trái cùng lượng [[1, −1], [0, 1]]
D giãn ×3, ×2 co ×⅓, ×½ [[0.333333, 0], [0, 0.5]]
F lật trục x lật lại — chính nó [[1, 0], [0, −1]] = F
A bóp dẹp (không tồn tại) LinAlgError: Singular matrix

Không có gì phải học thuộc ở bảng này. Undo của quay là quay ngược. Undo của giãn là co. Bạn đọc được cả bốn dòng mà không cần công thức nào.

Công thức 2×2 — và tự chứng minh nó trong 2 dòng

M = [ a  b ]              M⁻¹ =   1    [  d  −b ]
    [ c  d ]                    ─────  [ −c   a ]
                                a·d−b·c

Đừng học thuộc — nhân thử ra:

[ a  b ] [  d  −b ]   [ ad−bc   −ab+ba ]   [ ad−bc     0    ]
[ c  d ] [ −c   a ] = [ cd−dc   −cb+da ] = [   0     ad−bc  ]

Ra đúng (ad−bc)·I. Chia cả hai vế cho ad−bc là xong. Đó là toàn bộ chứng minh, và nó cho thấy cái mẫu số kia chính là định thức — không phải trùng hợp.

Từ đó lộ ngay vì sao det = 0 là điểm chết: mẫu số bằng 0. Không phải "công thức bị hỏng", mà là vật thể đó không tồn tại.

Kiểm với D bằng tay

det(D) = 3(2) − 0(0) = 6

D⁻¹ = (1/6) [ 2   0 ] = [ 1/3   0  ]
            [ 0   3 ]   [  0   1/2 ]

Đọc: D giãn ngang 3 lần → D⁻¹ co ngang còn ⅓. Đúng như trực giác. Và diện tích: D nhân 6, D⁻¹ phải chia 6 → det(D⁻¹) = 1/6 = 0.166667. Máy xác nhận đúng số đó.

Quy tắc chung, kiểm được: det(M⁻¹) = 1 / det(M). Cùng lý do như det(AB) = det(A)·det(B): làm liên tiếp hai phép biến đổi thì hệ số co giãn nhân với nhau. Giãn 3 lần rồi giãn 2 lần = giãn 6 lần. Không cần chứng minh gì, chỉ cần đọc chữ "co giãn".

Đã kiểm bằng máy: det(D·F) = −6.000000 và det(D)·det(F) = 6 × (−1) = −6.000000 ✓.

BẪY thứ tự: (SR)⁻¹ = R⁻¹S⁻¹, KHÔNG phải S⁻¹R⁻¹

Bài 2 dạy AB = "làm B trước, rồi A". Vậy undo SR là undo S trước, rồi undo R:

(S·R)⁻¹ = R⁻¹ · S⁻¹

Kiểm bằng số:

S·R = [ 1  −1 ]        (S·R)⁻¹  = [  0   1 ]
      [ 1   0 ]        R⁻¹·S⁻¹  = [ −1   1 ]   ✓ khớp

                       S⁻¹·R⁻¹  = [  1   1 ]   ✗ KHÁC HẲN
                                  [ −1   0 ]

Bài 2: đi tất rồi đi giày. Cởi ra thì phải cởi giày trước, cởi tất sau. Thứ tự undo luôn ngược thứ tự do. Hệt như rollback một chuỗi migration: chạy down.sql từ migration mới nhất về cũ nhất.


6. Giải hệ phương trình tuyến tính

Đây là lý do lịch sử khiến hai khái niệm trên được phát minh.

2x +  y = 5
 x + 3y = 10

Viết lại thành một phương trình ma trận:

M · v = b        với   M = [ 2  1 ]    b = [  5 ]
                           [ 1  3 ]        [ 10 ]

Đọc bằng ngôn ngữ bài 2: "Tìm vector v mà sau khi bị M biến đổi thì đáp xuống đúng chỗ b."

Nếu M có nghịch đảo, câu trả lời là một dòng: cho b chạy ngược lại phép biến đổi.

v = M⁻¹ · b

Tính tay:

det(M) = 2(3) − 1(1) = 5

M⁻¹ = (1/5) [  3  −1 ] = [  0.6  −0.2 ]
            [ −1   2 ]   [ −0.2   0.4 ]

v = M⁻¹ · (5, 10) = ( 0.6(5) + (−0.2)(10),  (−0.2)(5) + 0.4(10) )
                  = ( 3 − 2,  −1 + 4 )
                  = ( 1, 3 )

Thử lại: 2(1) + 3 = 5 ✓ và 1 + 3(3) = 10 ✓. Máy cho đúng [1. 3.].

Khi det = 0 thì hệ ra sao — hai kịch bản, và cả hai đều KHÔNG có nghiệm duy nhất

A bóp cả mặt phẳng xuống đường y = 2x. Vậy A·v = b chỉ có nghiệm nếu b nằm trên đường đó.

b b có nằm trên y = 2x? Số nghiệm np.linalg.solve
(3, 6) ✅ có vô số — cả (3,0) và (1,1) đều cho A·v = (3,6) LinAlgError: Singular matrix
(3, 7) ❌ không không có nghiệm nào LinAlgError: Singular matrix

Chú ý: numpy nổ cùng một lỗi cho hai tình huống hoàn toàn trái ngược nhau — vô số nghiệm và không nghiệm nào. Nó chỉ đang nói "tôi không thể cho bạn nghiệm duy nhất". Muốn phân biệt hai ca thì cần np.linalg.lstsq (hoặc SVD ở bài 9).

Với b = (3,7), lstsq trả về (0.68, 1.36) và A @ (0.68, 1.36) = (3.4, 6.8) — không bằng (3,7). Nó không giải hệ; nó trả về điểm trên đường y=2x gần b nhất. Đó là bình phương tối tiểu, và đó là cách toàn bộ ML xử lý hệ vô nghiệm.

BẪY nghề nghiệp: đừng bao giờ tính inv để giải hệ

v = inv(M) @ b đúng về toán và sai về kỹ thuật. Dùng np.linalg.solve(M, b). Số đo thật:

Phép Thời gian (n = 800) Sai số ‖Ax − b‖
inv(A) @ b 10.46 ms 5.50e−11
solve(A, b) 4.47 ms 8.78e−11

Nhanh hơn 2.34× vì solve chỉ khử Gauss một lần, còn inv giải n hệ (mỗi cột của I) rồi mới nhân thêm một lần nữa.

Trên ma trận điều kiện xấu, khoảng cách không còn là tốc độ mà là đúng/sai. Ma trận Hilbert 12×12, nghiệm thật là (1,1,...,1):

Phép Sai số so với nghiệm thật
inv(H) @ b 30.48 ← sai bét
solve(H, b) 0.836

Cùng một bài toán, cùng một máy, sai lệch 36 lần. inv làm tròn hai lần thay vì một.

Quy tắc: inv là để hiểu. solve là để chạy. Giống hệt SELECT * rồi lọc trong Node, so với để WHERE chạy trong database.


6b. ⭐ Factor MỘT lần, solve nhiều vế phải — solve trong vòng lặp là lãng phí (bổ sung từ VMLS — 2026-10)

(nguồn: VMLS tr.208-209, §11.3 ("factor-solve", "factorization caching"))

Bảng ở trên so inv với solve cho một vế phải. Trường hợp thường gặp hơn trong thực tế là cùng A, nhiều b: cùng một ma trận hiệp phương sai với nhiều vector, cùng một hệ động lực với nhiều điều kiện đầu, cùng ma trận độ cứng với nhiều tải. Mọi bộ giải hệ vuông đều có hai bước:

factor : A = LU  (hoặc QR)        ~ (2/3)n³ flop với LU      ← đắt, chỉ phụ thuộc A
solve  : giải hai hệ tam giác      ~ 2n² flop                 ← rẻ, mỗi b một lần

Gọi solve(A, b) trong vòng lặp k lần là factor lại k lần: ~k·(2/3)n³. Factor một lần rồi dùng lại: ~(2/3)n³ + 2kn². VMLS gọi đây là factorization caching và rút ra kết luận "giải nhiều hệ cùng ma trận tốn gần bằng giải một hệ" (sách dùng QR nên đếm 2n³; ý hệt). Với n = 2000, k = 50, lý thuyết cho ~46 lần. Đo thật (mỗi lần chạy mỗi khác một chút): 50 lần np.linalg.solve mất cỡ 1.9-2.1 s; scipy.linalg.lu_factor cỡ 0.04-0.06 s cộng 50 lần lu_solve cỡ 0.06-0.07 s, nhanh khoảng 15-20 lần (thấp hơn 46 có lẽ vì mỗi lu_solve phải đọc lại cả ma trận 32 MB từ bộ nhớ). Thời gian phụ thuộc máy; điều không đổi là thứ bậc n³ so với k·n², và hai đường lệch nhau chỉ 1.1e−09.

Lối tắt: nếu đã có đủ k vế phải trong tay, np.linalg.solve(A, B) với B là ma trận n×k tự factor một lần. Dùng lu_factor khi các b đến lần lượt (mỗi vòng lặp một cái), và đừng vì thế mà quay lại inv(A) @ b (xem bảng trên: kém chính xác hơn).

import numpy as np, time
from scipy.linalg import lu_factor, lu_solve
rng = np.random.default_rng(0)
n, k = 2000, 50
A = rng.normal(size=(n, n)); B = rng.normal(size=(n, k))        # k vế phải, cùng một A

def tg(f):
    s = time.perf_counter(); r = f(); return r, time.perf_counter() - s

X1, t1 = tg(lambda: np.column_stack([np.linalg.solve(A, B[:, j]) for j in range(k)]))   # factor LẠI k lần
lu, tf = tg(lambda: lu_factor(A))                                                       # factor MỘT lần
X2, ts = tg(lambda: np.column_stack([lu_solve(lu, B[:, j]) for j in range(k)]))         # mỗi lần chỉ ~2n²
print(f"{k}×solve {t1:.3f}s · lu_factor {tf:.3f}s + {k}×lu_solve {ts:.3f}s · lệch {np.abs(X1 - X2).max():.1e}")
assert np.allclose(A @ X2, B, atol=1e-8)                        # nghiệm đúng thật
assert np.abs(X1 - X2).max() < 1e-6                             # hai đường cho cùng nghiệm
assert np.allclose(np.linalg.solve(A, B), X2, atol=1e-6)        # solve(A, B) ma trận cũng factor một lần
assert (2/3*n**3 * k) / (2/3*n**3 + 2*n*n*k) > 40               # tỉ lệ flop lý thuyết ≈ 46

7. Ba cái BẪY

Bẫy 1 — |det| nhỏ không có nghĩa là "gần suy biến"

Đây là hiểu lầm phổ biến nhất về định thức. Hai ma trận, cùng det = 1.0e−10:

Ma trận det cond (số điều kiện) Thực chất
0.1 · I₁₀ 1.000e−10 1.0 hoàn hảo. Chỉ là thu nhỏ mọi thứ 10 lần theo cả 10 trục
[[1,1],[1,1+1e−10]] 1.000e−10 4.0e+10 thảm hoạ. Hai cột gần trùng nhau

Cùng định thức, mức độ tồi lệch nhau 4 × 10¹⁰ lần.

Lý do: det(c·M) = cⁿ · det(M) — định thức nhạy khủng khiếp với thang đo. Đã kiểm: det(0.5·I₃) = 0.125 = 0.5³, det(3·D) = 54 = 3² × 6. Chỉ cần đổi đơn vị từ mét sang milimét là mọi số nhân 1000, nên định thức của ma trận 10×10 nhân với 1000¹⁰ = 10³⁰, trong khi không có gì thay đổi về bản chất.

det trả lời "có suy biến không?" — câu hỏi nhị phân. np.linalg.cond trả lời "tồi đến mức nào?" — câu hỏi định lượng. Đừng bắt det làm việc của cond. Trong code sản xuất, kiểm cond (hoặc trị kỳ dị nhỏ nhất), đừng kiểm det.

Bẫy 2 — det = 0 trên giấy nhưng 1e−17 trên máy

np.linalg.det([[1, 3], [0.1, 0.3]])   # → -2.7755575615628827e-17

Ma trận này hạng 1 rõ ràng (hàng 2 = hàng 1 ÷ 10). Định thức phải bằng 0. Máy trả về −2.78e−17 vì 3 × 0.1 = 0.30000000000000004 chứ không phải 0.3.

np.linalg.matrix_rank của nó vẫn trả về đúng 1, vì nó dùng ngưỡng dựa trên SVD chứ không so bằng 0.

Không bao giờ viết if det(M) == 0. Kể cả if abs(det(M)) < 1e-9 cũng sai (xem Bẫy 1 — 0.1·I₁₀ sẽ bị kết tội oan). Dùng np.linalg.matrix_rank hoặc np.linalg.cond.

Bẫy 3 — định thức tràn số ở nhiều chiều

Ma trận chéo 100×100 với mỗi phần tử 1e−4. Định thức thật là 1e−400 — nhỏ hơn số nhỏ nhất mà float64 biểu diễn được.

np.linalg.det(...)      → 0.0                 ← SAI, và im lặng
np.linalg.slogdet(...)  → (1.0, -921.034037)  ← đúng: log|det| = 100·ln(1e-4)

slogdet trả về (dấu, log|det|). Đây là hàm bạn thực sự cần khi làm việc với ma trận hiệp phương sai — và §9 nói vì sao.


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

8.1 Định thức, và kiểm nó đúng là diện tích

import numpy as np


def det2(M):
    """Định thức 2×2 — công thức duy nhất cần thuộc."""
    (a, b), (c, d) = M
    return a * d - b * c


def det_cofactor(M):
    """Khai triển theo hàng 1. ĐÚNG về toán, THẢM HOẠ về tốc độ: O(n!)."""
    M = np.asarray(M, dtype=float)
    n = M.shape[0]
    if n == 1:
        return M[0, 0]
    if n == 2:
        return det2(M)
    total = 0.0
    for j in range(n):
        minor = np.delete(np.delete(M, 0, axis=0), j, axis=1)
        total += ((-1) ** j) * M[0, j] * det_cofactor(minor)
    return total


def shoelace(pts):
    """Diện tích CÓ DẤU của đa giác — công thức dây giày.
    Hoàn toàn không biết gì về ma trận. Đó là điểm mấu chốt."""
    n = len(pts)
    return sum(pts[i][0] * pts[(i + 1) % n][1] - pts[(i + 1) % n][0] * pts[i][1]
               for i in range(n)) / 2


R = np.array([[0., -1.], [1., 0.]])
S = np.array([[1.,  1.], [0., 1.]])
A = np.array([[1.,  2.], [2., 4.]])
D = np.array([[3.,  0.], [0., 2.]])
F = np.array([[1.,  0.], [0., -1.]])
SQUARE = [(0., 0.), (1., 0.), (1., 1.), (0., 1.)]

for name, M in [("R", R), ("S", S), ("A", A), ("D", D), ("F", F)]:
    img = [tuple(M @ np.array(p)) for p in SQUARE]
    area = shoelace(img)
    print(f"{name}: det tay={det2(M):5.1f}  diện tích ảnh={area:5.1f}")
    assert abs(det2(M)         - np.linalg.det(M)) < 1e-9   # khớp numpy
    assert abs(det_cofactor(M) - np.linalg.det(M)) < 1e-9
    assert abs(area            - np.linalg.det(M)) < 1e-9   # det = diện tích CÓ DẤU
print("det = diện tích có dấu của ảnh hình vuông đơn vị ✓")

M3 = np.array([[1., 2., 3.], [0., 1., 4.], [5., 6., 0.]])
assert abs(det_cofactor(M3) - 1.0) < 1e-9
print("det 3×3 tự cài =", det_cofactor(M3), "✓")

Ra đúng:

R: det tay=  1.0  diện tích ảnh=  1.0
S: det tay=  1.0  diện tích ảnh=  1.0
A: det tay=  0.0  diện tích ảnh=  0.0
D: det tay=  6.0  diện tích ảnh=  6.0
F: det tay= -1.0  diện tích ảnh= -1.0

Ma trận 3×3 để tính tay — khai triển theo hàng 1, số đẹp:

M3 = [ 1  2  3 ]
     [ 0  1  4 ]
     [ 5  6  0 ]

det = 1·(1·0 − 4·6) − 2·(0·0 − 4·5) + 3·(0·6 − 1·5)
    = 1·(−24)       − 2·(−20)       + 3·(−5)
    = −24 + 40 − 15
    = 1

Chú ý dấu xen kẽ + − +. Vì det(M3) = 1, nghịch đảo của nó toàn số nguyên — hiếm, và tiện để tự kiểm trên giấy:

M3⁻¹ = [ −24   18    5 ]
       [  20  −15   −4 ]
       [  −5    4    1 ]

8.2 Nghịch đảo bằng khử Gauss-Jordan

(Viết tiếp vào cùng file với 8.1 — R, S, A, D, F đã có sẵn ở trên.)

def inv_gauss_jordan(M):
    """Ghép [M | I], biến vế trái thành I — vế phải tự trở thành M⁻¹."""
    M = np.asarray(M, dtype=float)
    n = M.shape[0]
    aug = np.hstack([M, np.eye(n)])
    for col in range(n):
        piv = max(range(col, n), key=lambda r: abs(aug[r, col]))
        if abs(aug[piv, col]) < 1e-12:
            raise ValueError("ma trận suy biến — không có nghịch đảo")
        rows = [aug[r] for r in range(n)]
        rows[col], rows[piv] = rows[piv], rows[col]       # đổi chỗ hàng
        rows[col] = rows[col] / rows[col][col]            # chuẩn hoá pivot
        rows = [r if k == col else r - r[col] * rows[col]
                for k, r in enumerate(rows)]              # khử các hàng khác
        aug = np.vstack(rows)
    return aug[:, n:]


for name, M in [("R", R), ("S", S), ("D", D), ("F", F)]:
    mine = inv_gauss_jordan(M)
    assert np.allclose(mine, np.linalg.inv(M))            # khớp numpy
    assert np.allclose(M @ mine, np.eye(2))               # undo thật sự
    print(f"{name}⁻¹ =\n{mine}")

try:
    inv_gauss_jordan(A)
    raise SystemExit("SAI: lẽ ra phải nổ")
except ValueError as e:
    print("A:", e, "✓")

assert np.allclose(np.linalg.inv(R), [[0., 1.], [-1., 0.]])   # R⁻¹ = quay −90°
assert np.allclose(np.linalg.inv(F), F)                       # lật hai lần = về chỗ cũ
assert np.allclose(np.linalg.inv(S @ R), np.linalg.inv(R) @ np.linalg.inv(S))
assert not np.allclose(np.linalg.inv(S @ R), np.linalg.inv(S) @ np.linalg.inv(R))
print("(SR)⁻¹ = R⁻¹S⁻¹  — cởi giày trước, cởi tất sau ✓")

# giải hệ:  2x + y = 5 ;  x + 3y = 10
M = np.array([[2., 1.], [1., 3.]]); b = np.array([5., 10.])
x = inv_gauss_jordan(M) @ b
assert np.allclose(x, [1., 3.]) and np.allclose(M @ x, b)
assert np.allclose(x, np.linalg.solve(M, b))
print("nghiệm hệ =", x, "✓")

Toàn bộ assert phải xanh. Cái assert not np.allclose(...) là quan trọng nhất — nó bắt bạn phải sai nếu bạn nhớ nhầm thứ tự.

8.3 Kiểm cái bẫy det vs cond

nho = 0.1 * np.eye(10)                        # det tí hon, nhưng hoàn hảo
xau = np.array([[1., 1.], [1., 1. + 1e-10]])  # det tí hon, và thật sự tồi

for name, M in [("0.1·I₁₀", nho), ("gần suy biến", xau)]:
    print(f"{name:14s} det={np.linalg.det(M):.3e}  cond={np.linalg.cond(M):.3e}")

assert np.linalg.cond(nho) < 2                # cond = 1.0  → hoàn hảo
assert np.linalg.cond(xau) > 1e9              # cond = 4e10 → tệ hại
print("→ |det| KHÔNG phải thước đo 'gần suy biến'. Dùng cond(). ✓")

Ra đúng:

0.1·I₁₀        det=1.000e-10  cond=1.000e+00
gần suy biến   det=1.000e-10  cond=4.000e+10

8.4 Vì sao không ai tính định thức bằng khai triển cofactor

import time
rng = np.random.default_rng(0)
for n in [6, 7, 8, 9]:
    Mn = rng.normal(size=(n, n))
    t0 = time.perf_counter(); det_cofactor(Mn); t1 = time.perf_counter()
    np.linalg.det(Mn);                          t2 = time.perf_counter()
    print(f"n={n}: tự cài {1000*(t1-t0):8.2f} ms   numpy {1000*(t2-t1):.4f} ms")

Số đo thật (máy khác sẽ khác, nhưng tỉ lệ tăng thì không):

n Cofactor tự cài numpy (khử LU)
6 1.72 ms 0.0124 ms
7 11.78 ms 0.0064 ms
8 94.24 ms 0.0046 ms
9 866.92 ms 0.0357 ms

Đọc kỹ cột giữa: 6→7 nhân 6.8×, 7→8 nhân 8.0×, 8→9 nhân 9.2×. Hệ số tăng dần, và nó xấp xỉ đúng bằng n+1 — vì (n+1)! = (n+1) × n!. Đó là O(n!) đang lộ mặt ngay trong đồng hồ.

n n! n³
10 3.63 × 10⁶ 1 000
20 2.43 × 10¹⁸ 8 000
50 3.04 × 10⁶⁴ 125 000

O(n!) với n = 20 là 2.4 × 10¹⁸ phép tính — nhiều hơn số giây kể từ vụ nổ Big Bang. Cofactor là định nghĩa, không phải thuật toán. Thư viện thật dùng khử LU, O(n³). Cùng một câu chuyện Big-O như N+1 query so với một JOIN: cùng kết quả, khác hạng độ phức tạp.


9. Nối vào AI

Khái niệm bài này Chỗ dùng trong AI Số đã kiểm
det = 0 ⟺ hạng không đầy đủ LoRA: cập nhật ΔW = B·A với B: d×r, A: r×d. ΔW vuông 5×5 nhưng hạng chỉ 2 → det(ΔW) ≈ 0 với default_rng(42): rank(ΔW) = 2, det(ΔW) = −3.27e−49 ≈ 0; nhưng rank(W + ΔW) = 5, det = 2.2787 — phần cập nhật suy biến, trọng số cuối vẫn khả nghịch
(I − αP)⁻¹ tồn tại hay không PageRank của Google: nghiệm dạng đóng x = (1−α)(I − αP)⁻¹e. Đồ thị 3 trang: 1→2, 2→3, 3→{1,2} α = 0.85 → det(I − αP) = 0.3316875 ✓ khả nghịch. α = 1 → det = 0, hạng tụt xuống 2 — đó là lý do toán học phải có hệ số damping
Nghịch đảo = undo Nghiệm đóng của PageRank khớp tuyệt đối với vòng lặp x = (0.214811, 0.397400, 0.387790); lặp 500 bước lệch 5.6e−17
log|det| (§7 bẫy 3) Normalizing flow và log-likelihood của Gaussian đều cần log|det Σ|; det tràn số ở nhiều chiều Σ = [[4,1],[1,1]] → det = 3, slogdet cho log|det| = 1.0986122886681096 = ln 3 ✓
Ma trận không vuông thì det không tồn tại W_Q trong attention là 768×64 — không có định thức, không có nghịch đảo, chỉ có hạng det(8×3) → LinAlgError: Last 2 dimensions of the array must be square; nhưng rank = 3 vẫn tính được

Điểm nối quan trọng nhất — vì sao AI sống được với det = 0:

Định thức đòi ma trận vuông. Nhưng gần như mọi ma trận trong deep learning đều không vuông, và những cái vuông thì thường suy biến một cách cố ý (LoRA, MLA, distillation, mọi thứ có chữ "low-rank").

Vậy vì sao vẫn học định thức? Vì nó là cách rẻ nhất và trực quan nhất để hiểu ý tưởng "bóp dẹp = mất thông tin = không undo được". Khi ý đó đã nằm trong đầu, bạn mang nó sang hạng và SVD — hai công cụ làm được đúng việc đó cho ma trận không vuông.

Định thức là bậc thang, không phải cái đích. Cái đích là SVD.

Và chú ý bốn bài đã nối thành một mạch: bài 1 (tích vô hướng đo góc) → bài 2 (ma trận biến đổi không gian) → bài 3 (hạng đếm chiều còn sống) → bài 4 (một con số nói chiều có bị mất không, và undo được không).


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

  1. Vì sao định thức chỉ cần một con số cho toàn bộ không gian, chứ không phải một con số cho mỗi vùng? Nhắc tới tính chất "đường thẳng vẫn thẳng, song song vẫn song song" của bài 2.
  2. Ma trận trượt ngang S làm hình vuông méo đi rõ ràng, nhưng det(S) = 1. Giải thích bằng hình học (đáy × chiều cao), không bằng công thức ad − bc.
  3. det < 0 nghĩa là gì? Vì sao không có phép quay nào trong 2D có định thức âm?
  4. Viết ra bốn cách phát biểu tương đương của "det(M) = 0" — bằng bốn ngôn ngữ: diện tích, hạng, null space, nghịch đảo. Rồi giải thích vì sao chúng buộc phải là một.
  5. A = [[1,2],[2,4]]. Tìm hai vector khác nhau có cùng ảnh qua A. Từ đó lập luận vì sao A⁻¹ không thể tồn tại — lập luận bằng hàm số ("hai đầu vào một đầu ra"), không bằng "mẫu số bằng 0".
  6. (SR)⁻¹ = R⁻¹S⁻¹ chứ không phải S⁻¹R⁻¹. Giải thích bằng hành động (bài 2 dùng ẩn dụ tất và giày). Rồi nói lại bằng ẩn dụ rollback chuỗi migration.
  7. Hai ma trận cùng có det = 1e−10, nhưng một cái hoàn hảo còn một cái sắp sập. Chuyện gì đang xảy ra, và đại lượng nào mới là thứ nên kiểm trong code sản xuất?
  8. W_Q trong attention có kích thước 768 × 64. Định thức của nó bằng bao nhiêu? (Câu này là một cái bẫy — đọc kỹ.)

Câu 4 là câu sâu nhất của bài. Nếu bốn phát biểu đó trong đầu bạn vẫn là bốn thứ rời rạc phải nhớ riêng, thì bài này chưa xong — đọc lại §4 và §5.

Câu 5 là câu quyết định bạn hiểu hay chỉ tính được: người tính được sẽ nói "vì mẫu số bằng 0"; người hiểu sẽ nói "vì thông tin đã bị xoá, mẫu số bằng 0 chỉ là hệ quả".

Câu 8 kiểm bạn có thật sự đọc §9 không. Câu trả lời đúng không phải một con số.


11. Bài tập về nhà trước bài 5

3Blue1Brown — Essence of Linear Algebra:

Tập Tên (để tìm trên YouTube) Vì sao
5 Three-dimensional linear transformations Không có ý gì mới — chỉ xác nhận trực giác 2D vẫn đúng khi lên 3 chiều. Xem nhanh, để §1 của bài này chuyển từ diện tích sang thể tích mà không thấy gợn
6 The determinant ⭐ Chính là §1-§4 bằng hình động. Giữ sẵn câu hỏi: det = 0 nghĩa là gì về mặt hình học?
7 Inverse matrices, column space and null space ⭐ §5-§6, cộng thêm cột sống của bài 3. Đây là tập buộc hạng, không gian cột, null space và nghịch đảo lại thành một

Xem xong tập 7, quay lại đọc §4 của bài này một lần nữa. Bốn phát biểu tương đương ở câu tự kiểm số 4 sẽ tự sập thành một.

Bài tập tay bắt buộc — không xem đáp án:

  1. Tính det và M⁻¹ của [[4, 7], [2, 6]] bằng công thức 2×2, rồi kiểm bằng np.linalg.inv.
  2. Tính det của M3 = [[1,2,3],[0,1,4],[5,6,0]] bằng khai triển theo cột 1 thay vì hàng 1. Phải vẫn ra 1 — và đó là một sự thật đáng ngạc nhiên (khai triển theo hàng hay cột nào cũng ra cùng kết quả).
  3. Tìm một ma trận 2×2 có det = −2, và mô tả bằng lời nó làm gì với không gian.
  4. Giải hệ 3x + 2y = 8 ; x + 4y = 6 bằng M⁻¹·b trên giấy, rồi kiểm bằng np.linalg.solve.

Câu teaser cho các bài sau — thử ngay 30 giây, đừng cần hiểu:

M = np.array([[2., 1.], [1., 2.]])
for lam in [0., 1., 2., 3., 4.]:
    print(lam, np.linalg.det(M - lam * np.eye(2)))

Ra:

0.0 2.9999999999999996      ← Bẫy 2 lại chào bạn: đây là số 3
1.0 0.0
2.0 -1.0
3.0 0.0
4.0 2.9999999999999996

Hai giá trị λ làm định thức bằng đúng 0 là λ = 1 và λ = 3.

Bạn vừa vô tình giải phương trình (2−λ)² − 1 = λ² − 4λ + 3 = 0. Hai nghiệm 1 và 3 chính là eigenvalue của M — np.linalg.eigvals(M) trả về đúng [3, 1]. Và tích của chúng 3 × 1 = 3 bằng đúng det(M).

Bài 5 sẽ là: ma trận không vuông — biến đổi giữa hai không gian khác số chiều, tức là embedding. Ở đó det biến mất hoàn toàn (câu tự kiểm số 8), chỉ còn hạng sống sót. Rồi bài 6 là đổi cơ sở, và bài 7 là eigenvector, nơi bạn dùng lại đúng cái det = 0 của bài này để giải det(M − λI) = 0 — cơ chế đứng sau PageRank và PCA.

Thử trước với R = [[0,−1],[1,0]]: det(R − λI) = λ² + 1, không có nghiệm thực nào. Vì sao phép quay không có eigenvector thực? Câu trả lời hiển nhiên đến bực mình khi bạn nghĩ ra — giữ nó trong đầu tới bài 7 §5, nơi nó là cái bẫy lớn nhất của bài đó. (Bài 5 và bài 6 nằm giữa: chúng dựng nốt hai thứ bài 7 cần — ma trận không vuông và đổi cơ sở.)

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