~3 giờ · cần xong bài 5 · KHÔNG có tập 3Blue1Brown nào cho bài này
Bạn đã va vào chủ đề này hai lần rồi mà chưa được gọi tên: bài 1 (
hnhỏ quá thì sai số tăng) và bài 2 (log-sum-exp— công thức đúng nhưng tràn số).Bài này gom lại thành một nguyên lý: máy tính không lưu số thực. Nó lưu xấp xỉ. Và sai số của xấp xỉ đó CÓ CẤU TRÚC — không phải nhiễu ngẫu nhiên.
⚡ 30 phút đầu
print(0.1 + 0.2 == 0.3) # False. Vì sao?
print(1e16 + 1 == 1e16) # True. Số 1 biến mất. Vì sao?
print((1e16 + 1) + 1) # 1e16
print(1e16 + (1 + 1)) # 1.0000000000000002e+16 ← KHÁC NHAU
Ba dòng đầu là chuyện ai cũng biết. Dòng thứ tư là chuyện ít người biết, và nó phá được luận văn.
1. Float không phải số thực — khoảng cách giữa chúng KHÔNG đều
float64 có 64 bit: 1 dấu, 11 mũ, 52 mantissa. Nghĩa là nó lưu ± m × 2^e — và m chỉ có 52 bit.
Hệ quả quan trọng nhất, ít ai nói: khoảng cách giữa hai float liền nhau tăng theo độ lớn.
| Ở gần giá trị | Khoảng cách tới float kế tiếp |
|---|---|
1.0 |
2.22e-16 |
1e16 |
2.0 ← lớn hơn số 1 |
Ở vùng
1e16, số 1 không tồn tại. Cộng 1 vào không đổi gì, vì không có chỗ nào để ghi. Đó là lý do1e16 + 1 == 1e16.
Ẩn dụ backend: giống INT bị tràn, nhưng âm thầm — không exception, không cảnh báo, chỉ mất dữ liệu.
2. 🪤 BẪY LỚN NHẤT — triệt tiêu tai hoạ (catastrophic cancellation)
Trừ hai số gần bằng nhau thì các chữ số đúng triệt tiêu nhau, chỉ còn lại rác làm tròn.
Ví dụ thật, và nó nằm ngay trong thống kê bạn sẽ dùng ở môn 5 — tính phương sai:
dữ liệu: 1e8+1, 1e8+2, 1e8+3, 1e8+4, 1e8+5 phương sai thật = 2.0
E[x²] − E[x]² → 4.000000 ✗ SAI GẤP ĐÔI
E[(x − x̄)²] → 2.000000 ✓ đúng
Cùng một công thức toán. Khác thứ tự phép tính. Khác kết quả.
Công thức E[x²] − E[x]² trừ hai số khổng lồ gần bằng nhau (1e16 và 1e16) để lấy ra một số bé (2).
Chữ số có nghĩa bị nuốt sạch. Với dữ liệu lệch hơn, nó còn cho phương sai ÂM — điều bất khả về mặt toán.
Đây là lý do
numpy,pandasvà mọi thư viện nghiêm túc đều dùng thuật toán hai lượt hoặc Welford để tính phương sai, chứ không dùng công thức trong sách giáo khoa.
3. Phép cộng KHÔNG có tính kết hợp
(1e16 + 1) + 1 = 1e+16
1e16 + (1 + 1) = 1.0000000000000002e+16
Ở trường bạn học (a+b)+c = a+(b+c). Trong float thì không.
Hệ quả thực tế mà rất ít người nối được:
Đổi số GPU · đổi batch size · đổi thứ tự gộp gradient → thứ tự cộng đổi → kết quả training không tái lập chính xác, dù bạn đã cố định seed.
Nên khi một paper nói "chúng tôi cố định seed nên tái lập được" — điều đó chỉ đúng trên cùng cấu hình phần cứng.
4. Hai sai số đánh nhau — giải thích bài 1 §BẪY
Đạo hàm số có hai nguồn sai số ngược chiều nhau:
| Nguồn | Đổi theo h thế nào |
|---|---|
| Sai số cắt cụt (xấp xỉ toán) | giảm khi h giảm — tỉ lệ h² |
| Sai số làm tròn (triệt tiêu tai hoạ) | tăng khi h giảm — tỉ lệ 1/h |
Với hàm đủ trơn, bảng trên mô tả sai số điển hình của sai phân trung tâm.
Riêng f(x)=x², sai số cắt cụt của trung tâm bằng 0 trong số học chính xác,
nên ví dụ dưới chỉ cho thấy làm tròn và triệt tiêu, không minh hoạ đủ cả hai nhánh.
Chạy thật tại x = 3 (đáp án đúng 6):
h |
ước lượng | sai số |
|---|---|---|
1e-1 |
6.00000000 | 5.3e-15 |
1e-4 |
6.00000000 | 1.3e-11 |
1e-8 |
5.99999996 | 3.7e-08 |
1e-12 |
6.00053340 | 5.3e-04 |
1e-15 |
5.32907052 | 6.7e-01 ← sai 11% |
hnhỏ hơn = chính xác hơn là trực giác SAI. Có một điểm tối ưu, và đi quá thì tệ đi rất nhanh. Đây là mẫu chung: trong tính toán số, "làm kỹ hơn" đôi khi làm hỏng.
5. Số điều kiện — nối thẳng về môn 2
Số điều kiện κ(A) = bài toán khuếch đại sai số đầu vào bao nhiêu lần.
ma trận [[1,1],[1,1+ε]] |
det |
κ |
|---|---|---|
| ε = 1 | 1.0 |
6.9 |
| ε = 1e−3 | 1e−3 |
4.0e3 |
| ε = 1e−8 | 1e−8 |
4.0e8 |
Với κ = 4e8: nhiễu đầu vào 1e-10 làm nghiệm đổi 1e-2 — khuếch đại 100 triệu lần.
Nối về môn 2 bài 4: xét
κ₂=σ_max/σ_min, không chỉ độ lớn của định thức. Ví dụ0.1·I₁₀códet=10⁻¹⁰nhưngκ₂=1: co đều không làm bài toán điều kiện xấu.κlớn báo khả năng khuếch đại sai số; sai số thực còn tuỳ hướng nhiễu và thuật toán. Máy vẫn có thể trả nghiệm hữu hạn khi bài toán nhạy, nên phải kiểm residual và độ nhạy.
6. bf16 vs fp16 — vì sao training chọn bf16
| mũ | mantissa | giá trị lớn nhất | |
|---|---|---|---|
| fp32 | 8 bit | 23 bit | 3.4e38 |
| fp16 | 5 bit | 10 bit | 65504 |
| bf16 | 8 bit (= fp32) | 7 bit | 3.4e38 |
bf16 hy sinh độ chính xác để giữ dải giá trị. Vì sao đó là đánh đổi đúng cho training:
Gradient có thể rất nhỏ (
1e-8) hoặc rất lớn (1e5) — bạn cần dải rộng. Gradient mini-batch vốn có nhiễu, nên nhiều phép tính chịu được độ chính xác thấp hơn. Tuy vậy 7 bit không luôn đủ cho mọi mô hình và phép tính; vẫn phải kiểm sai số và giữ các phép tính nhạy ở fp32 khi cần.
fp16với max65504thì một gradient1e5là tràn thànhinf, và mọi thứ sau đó lànan.
Mixed precision = forward/backward ở bf16 (nhanh, ít bộ nhớ), cộng dồn trọng số ở fp32
(vì cộng nhiều bước nhỏ vào số lớn — chính là §1).
Code — tự chứng minh mọi khẳng định ở trên
import numpy as np
# §1 — khoảng cách float không đều
assert 0.1 + 0.2 != 0.3
assert 1e16 + 1 == 1e16
assert np.spacing(1e16) > 1.0 # số 1 nhỏ hơn khoảng cách → biến mất
# §2 — triệt tiêu tai hoạ trong công thức phương sai
x = np.array([1e8 + i for i in range(1, 6)])
naive = np.mean(x**2) - np.mean(x)**2
good = np.mean((x - np.mean(x))**2)
assert abs(good - 2.0) < 1e-9 # công thức 2 lượt: đúng
assert abs(naive - 2.0) > 1.0 # công thức "tiện": sai gấp đôi
assert abs(np.var(x) - 2.0) < 1e-9 # numpy dùng cách đúng
# §3 — cộng không kết hợp
assert (1e16 + 1) + 1 != 1e16 + (1 + 1)
# §4 — h nhỏ quá thì TỆ ĐI
d = lambda h: ((3+h)**2 - (3-h)**2) / (2*h)
assert abs(d(1e-4) - 6) < abs(d(1e-15) - 6) # h=1e-15 sai hơn h=1e-4
# §5 — số điều kiện khuếch đại sai số
A = np.array([[1., 1.], [1., 1. + 1e-8]])
assert np.linalg.cond(A) > 1e8
s1 = np.linalg.solve(A, np.array([2., 2.]))
s2 = np.linalg.solve(A, np.array([2., 2. + 1e-10]))
assert np.abs(s1 - s2).max() > 1e-3 # nhiễu 1e-10 → nghiệm đổi 1e-2
# §6 — fp16 tràn, bf16 thì không
assert np.float16(1e5) == np.inf
assert np.finfo(np.float16).max < 1e5 < np.finfo(np.float32).max
print("mọi khẳng định trong bài đã được chứng minh ✓")
Nối vào AI
| Hiện tượng | Chỗ nó cắn |
|---|---|
| Triệt tiêu tai hoạ | Tính phương sai, hiệp phương sai — môn 5, 6 |
log-sum-exp |
softmax, cross-entropy — môn 4, 8 |
| Cộng không kết hợp | Training không tái lập khi đổi GPU/batch — quy tắc 1 |
| Số điều kiện | Ma trận gần suy biến, hồi quy không ổn định — môn 2, 6 |
bf16 / mixed precision |
Mọi lần training LLM hiện đại — môn 7, 9 |
| Đạo hàm số có điểm tối ưu | Gradient check khi tự cài backprop — bài 3, 5 |
🔗 Vì sao bài này thuộc về bốn quy tắc
Quy tắc 1 nói: lỗi giết bạn là chi tiết thiết lập, không phải toán.
Bài này là dạng thuần khiết nhất của quy tắc đó. Toàn bộ nội dung ở trên là những chỗ mà toán học hoàn toàn đúng nhưng máy tính cho ra số sai — và không có exception nào ném ra.
Nhớ lại lỗi Chinchilla ở quy tắc 1: bộ tối ưu L-BFGS-B dừng sớm do lấy trung bình Huber-loss
thay vì tổng. Đó chính là một lỗi số học — cùng họ với mọi thứ trong bài này. Một quyết định
về cách gộp số, làm sai lệch kết luận của cả một ngành trong hai năm.
Đây cũng là lý do capstone nào cũng bắt
assertđối chiếu thư viện tham chiếu. Không phải nghi ngờ khả năng toán của bạn — mà vì tầng số học bên dưới có thể phản bội cả người làm toán đúng.
Tự kiểm
- Vì sao
0.1 + 0.2 != 0.3, nhưng0.5 + 0.25 == 0.75thì đúng? (Gợi ý: cơ số 2.) - Triệt tiêu tai hoạ xảy ra khi nào? Vì sao công thức
E[x²]−E[x]²là ví dụ kinh điển? - Phép cộng float không kết hợp. Suy ra một hệ quả thực tế cho việc tái lập kết quả training.
- Đạo hàm số có
htối ưu. Hai sai số nào đánh nhau, và mỗi cái đổi theohthế nào? - Vì sao
det ≈ 0chưa đủ kết luậnκlớn? Dùng0.1·I₁₀làm phản ví dụ, rồi giải thích vai trò củaσ_min/σ_max. bf16chính xác kém hơnfp16nhưng training dùng nó. Vì sao đó là đánh đổi đúng?- Mixed precision tính ở
bf16nhưng cộng dồn trọng số ởfp32. Vì sao không cộng luôn ởbf16?
Câu 3 và 5 là hai câu đắt nhất.
κlớn cảnh báo nghiệm có thể nhạy ngay cả khi solver không báo lỗi; nó không bảo đảm mọi nghiệm tính ra đều sai lớn.
Bài tập về nhà
Ba bài, làm theo thứ tự. Mỗi bài chạy trước, giải thích sau — đó là toàn bộ tinh thần của bài này.
BT1 — cùng bốn số, ba thứ tự, ba kết quả (15 phút)
import numpy as np
x = np.array([1e8, 1.0, -1e8, 1.0], dtype=np.float32)
# cộng theo thứ tự xuôi, ngược, và sau khi sort — in cả ba
Đáp án đúng về mặt toán là 2.0. Bạn sẽ nhận được:
| Thứ tự cộng | Kết quả |
|---|---|
| Xuôi | 1.0 |
| Ngược | 0.0 |
Đã sort |
0.0 |
Không thứ tự nào cho ra 2.0. Câu hỏi phải trả lời: vì sao sort — thứ tự nghe có vẻ "an toàn nhất" — lại không cứu được? Và thứ tự nào sẽ cứu được? (Gợi ý: nhóm các số cùng độ lớn với nhau trước.)
🪤 Bẫy nằm ở chỗ này: trên GPU, thứ tự cộng phụ thuộc số luồng và cách chia batch — bạn không kiểm soát được nó. Đó là lý do §3 nói training không tái lập được khi đổi phần cứng. Bài tập này là phiên bản 4 số của đúng hiện tượng đó.
BT2 — tự cài thuật toán Kahan (30 phút)
Cộng 10⁶ lần số 0.1 ở float32. Đáp án đúng: 100000.0.
big = np.full(10**6, np.float32(0.1))
tho = np.add.reduce(big, dtype=np.float32) # → 100000.01
Sai 0.01 sau một triệu phép cộng. Giờ tự cài tổng Kahan — nó giữ một biến c để nhớ phần bị mất ở mỗi bước rồi trả lại vào bước sau:
s = 0; c = 0
với mỗi v:
y = v − c # bù phần đã mất lần trước
t = s + y # cộng, và mất một ít
c = (t − s) − y # đo ĐÚNG phần vừa mất
s = t
Chạy đúng thì ra 100000.0 chằn chặn. Câu hỏi: dòng c = (t − s) − y trông như bằng 0 về mặt đại số — vì sao trên máy nó không bằng 0, và vì sao đúng cái khác 0 đó mới là thứ cứu bạn?
BT3 — số điều kiện dự đoán được sai số (30 phút)
Ma trận Hilbert H[i][j] = 1/(i+j+1) là ví dụ kinh điển về điều kiện xấu. Dựng H, đặt b = H @ ones(n) (nên nghiệm đúng là vector toàn 1), rồi solve và đo sai số lớn nhất:
n |
κ(H) |
Sai số max |
|---|---|---|
| 4 | 1.551e+04 |
5.04e-14 |
| 8 | 1.526e+10 |
1.20e-07 |
| 12 | 1.659e+16 |
5.17e-01 |
Ở n=12, nghiệm sai 52% — trong khi numpy không hề báo lỗi. Câu hỏi chính: nhân κ với epsilon máy (2.2e-16 cho float64) rồi so với cột sai số. Bạn thấy quan hệ gì?
Đây là bài tập trả lời câu tự kiểm số 5 bằng số:
κlớn cảnh báo khả năng mất chữ số, nhưng không dự đoán chính xác sai số của từng lần giải. Solver có thể không báo lỗi.
Ghi vào nhật ký lỗi: bài nào trong ba bài trên làm bạn ngạc nhiên nhất, và bạn đã dự đoán sai điều gì trước khi chạy.