Giải tích Số (Numerical Methods).
Updated Mar 12, 2026

Tags: machine learning data science math

Bài 8: Giải tích Số (Numerical Methods) — Khi Toán học Gặp Máy tính

Series: Toán học trong AI/ML & Deep Learning

Topics: numerical-stability floating-point mixed-precision FP16 BF16


🎯 Tại sao Giải tích Số quan trọng?

Máy tính không thể lưu số thực chính xác. $\pi$, $\sqrt{2}$, hay ngay cả $0.1$ đều bị làm tròn.

Trong Deep Learning, các phép tính lặp hàng tỷ lần — sai số nhỏ cộng dồn thành thảm họa:


1. Floating Point — Máy tính lưu số thế nào?

IEEE 754 Float32 (FP32):
┌──┬─────────┬──────────────────────┐
│S │ Exponent│      Mantissa        │
│1 │   8 bit │       23 bit         │
└──┴─────────┴──────────────────────┘
   Sign    Range        Precision

Float16 (FP16): 1 + 5 + 10 bits  ← nhỏ hơn, nhanh hơn, nhưng dễ overflow
BFloat16 (BF16): 1 + 8 + 7 bits  ← range như FP32, precision thấp hơn
import numpy as np
import torch

# Minh họa sai số floating point
print(0.1 + 0.2)           # 0.30000000000000004 — KHÔNG phải 0.3!
print(0.1 + 0.2 == 0.3)    # False ← đây là lý do không dùng == với float

# FP32 vs FP16 range
fp32_max = torch.finfo(torch.float32).max
fp16_max = torch.finfo(torch.float16).max
bf16_max = torch.finfo(torch.bfloat16).max

print(f"FP32 max: {fp32_max:.2e}")   # 3.40e+38
print(f"FP16 max: {fp16_max:.2e}")   # 6.55e+04  ← rất nhỏ!
print(f"BF16 max: {bf16_max:.2e}")   # 3.39e+38

# FP16 overflow demonstration
x = torch.tensor(65000.0, dtype=torch.float16)
print(f"65000 in FP16: {x}")          # 65000.0 ✓
y = torch.tensor(65600.0, dtype=torch.float16)
print(f"65600 in FP16: {y}")          # inf ← OVERFLOW!

2. Mixed Precision Training — FP16/BF16 + FP32

Tại sao Mixed Precision?

Giải pháp: Loss Scaling + Gradient Scaler

Forward:  FP16/BF16  ← tính nhanh
Backward: FP16/BF16  ← gradient
Scale up: × 2^k trước khi backward  ← tránh underflow
Update:   FP32       ← độ chính xác cao
Scale down: ÷ 2^k khi update FP32 weights
import torch
from torch.cuda.amp import autocast, GradScaler

model = MyModel().cuda()
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4)
scaler = GradScaler()   # tự động quản lý loss scaling

for batch in dataloader:
    x, y = batch
    optimizer.zero_grad()
    
    # autocast: tự động cast sang FP16 cho các ops phù hợp
    with autocast(dtype=torch.bfloat16):   # BF16 ổn định hơn FP16
        output = model(x)
        loss = criterion(output, y)
    
    # Scale loss lên trước backward (tránh gradient underflow)
    scaler.scale(loss).backward()
    
    # Unscale gradient, kiểm tra overflow/NaN
    scaler.unscale_(optimizer)
    torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
    
    # Update (skip nếu gradient có NaN/inf)
    scaler.step(optimizer)
    scaler.update()   # tự điều chỉnh scale factor

# FP16 vs BF16 — khi nào dùng gì?
# FP16: GPU cũ (V100), precision cao hơn trong range nhỏ
# BF16: GPU mới (A100, H100), stable hơn, range như FP32 → DEFAULT

3. Numerical Stability — Các Trick Quan trọng

3.1 Numerically Stable Softmax

import numpy as np
import torch

# ❌ Naive softmax — overflow với large logits
def softmax_naive(x):
    return np.exp(x) / np.exp(x).sum()

# ✅ Stable softmax — trừ max trước
def softmax_stable(x):
    x = x - x.max()               # shift → không thay đổi output
    exp_x = np.exp(x)
    return exp_x / exp_x.sum()

logits = np.array([1000., 1001., 1002.])
print("Naive :", softmax_naive(logits))   # [nan, nan, nan]
print("Stable:", softmax_stable(logits))  # [0.090, 0.245, 0.665] ✓

# PyTorch tự xử lý:
x = torch.tensor([1000., 1001., 1002.])
print(torch.softmax(x, dim=0))   # [0.090, 0.245, 0.665] ✓

3.2 Numerically Stable Cross-Entropy

# ❌ Naive: log(softmax(x)) — mất precision với giá trị lớn
def cross_entropy_naive(logits, label):
    probs = np.exp(logits) / np.sum(np.exp(logits))
    return -np.log(probs[label])

# ✅ Log-softmax trick: log(softmax(x)) = x - log(Σexp(x))
def cross_entropy_stable(logits, label):
    # log-sum-exp: tránh tính exp của giá trị lớn
    log_sum = logits.max() + np.log(np.sum(np.exp(logits - logits.max())))
    return -(logits[label] - log_sum)

# PyTorch: F.cross_entropy tự dùng log-softmax bên trong
import torch.nn.functional as F
logits = torch.tensor([[2.0, 1.0, 0.1]])
label  = torch.tensor([0])
loss = F.cross_entropy(logits, label)   # stable by default ✓

3.3 Epsilon — Tránh Division by Zero

# Batch Normalization ổn định
def batch_norm(x, gamma, beta, eps=1e-5):
    mean = x.mean(dim=0)
    var  = x.var(dim=0)
    x_norm = (x - mean) / torch.sqrt(var + eps)   # ← eps quan trọng!
    return gamma * x_norm + beta

# Attention scaling — tránh gradient vanishing trong softmax
def attention(Q, K, V):
    d_k = Q.shape[-1]
    scores = Q @ K.T / (d_k ** 0.5)   # ← chia √d_k để tránh dot product quá lớn
    weights = F.softmax(scores, dim=-1)
    return weights @ V

4. Condition Number — Khi Ma trận “Nhạy cảm”

Condition number $\kappa(A) = \frac{\sigma_{max}}{\sigma_{min}}$ đo mức độ nhạy cảm của nghiệm với nhiễu nhỏ trong input.

import numpy as np

# Ma trận ill-conditioned — condition number cao
A_bad = np.array([[1., 1.],
                   [1., 1.0001]])   # gần singular!

kappa = np.linalg.cond(A_bad)
print(f"Condition number (bad): {kappa:.2e}")   # ~4e4 — rất cao!

# Ma trận well-conditioned
A_good = np.eye(2)
print(f"Condition number (good): {np.linalg.cond(A_good):.2f}")   # 1.0

# Giải hệ phương trình với ma trận ill-conditioned → kết quả không tin được
b = np.array([2., 2.0001])
try:
    x = np.linalg.solve(A_bad, b)
    print(f"Solution: {x}")   # có thể rất sai so với nghiệm đúng
except np.linalg.LinAlgError:
    print("Singular matrix!")

5. Cholesky Decomposition — Gaussian Process

Gaussian Process cần nghịch đảo covariance matrix — dùng Cholesky thay vì full inverse:

\(K = LL^T \quad \text{(Cholesky)}\) \(K^{-1}b = L^{-T}(L^{-1}b) \quad \text{(giải 2 triangular systems)}\)

import torch

def gp_predict(X_train, y_train, X_test, kernel_fn, noise=1e-4):
    """
    Gaussian Process prediction với Cholesky decomposition
    """
    n = X_train.shape[0]
    
    # Covariance matrix
    K = kernel_fn(X_train, X_train)
    K += noise * torch.eye(n)   # jitter để đảm bảo positive definite
    
    # Cholesky: K = L @ L.T
    L = torch.linalg.cholesky(K)
    
    # Solve K⁻¹y dùng Cholesky (ổn định hơn trực tiếp nghịch đảo)
    alpha = torch.cholesky_solve(y_train.unsqueeze(-1), L)
    
    # Predict
    K_star = kernel_fn(X_test, X_train)
    mu = K_star @ alpha
    
    return mu.squeeze()

6. Debugging Numerical Issues

import torch

def check_numerics(tensor, name="tensor"):
    """Utility: kiểm tra NaN/Inf trong training"""
    has_nan = torch.isnan(tensor).any()
    has_inf = torch.isinf(tensor).any()
    
    if has_nan or has_inf:
        print(f"⚠️  {name}: NaN={has_nan}, Inf={has_inf}")
        print(f"   min={tensor.min():.4f}, max={tensor.max():.4f}")
        return False
    return True

# Sử dụng trong training loop:
def training_step(model, batch):
    x, y = batch
    logits = model(x)
    
    check_numerics(logits, "logits")
    
    loss = F.cross_entropy(logits, y)
    
    if not check_numerics(loss, "loss"):
        return None   # skip batch này
    
    loss.backward()
    
    # Kiểm tra gradient
    for name, param in model.named_parameters():
        if param.grad is not None:
            check_numerics(param.grad, f"grad/{name}")
    
    return loss

# PyTorch anomaly detection (debug mode, chậm hơn):
with torch.autograd.detect_anomaly():
    loss = model(x)
    loss.backward()   # sẽ throw exception với stack trace tại NaN đầu tiên

7. Lỗi Phổ biến

Lỗi Nguyên nhân Giải pháp
Loss → NaN FP16 overflow, lr quá lớn GradScaler, giảm lr, dùng BF16
Softmax → all zeros Logits quá âm Stable softmax, check normalization
Cholesky failed Matrix không positive definite Thêm jitter + eps * I
Kết quả khác nhau giữa CPU/GPU Floating point không deterministic torch.use_deterministic_algorithms(True)
Catastrophic cancellation Trừ 2 số gần nhau Reformulate bằng log-space

8. Checklist


🎓 Kết thúc Series

Bạn đã hoàn thành 8 bài về Toán học trong AI/ML:

# Lĩnh vực Ứng dụng cốt lõi
1 Giải tích Backpropagation, Gradient Descent
2 Đại số Tuyến tính Attention, Embedding, LoRA
3 Xác suất MLE, VAE, Diffusion
4 Thống kê A/B testing, Batch Norm
5 Tối ưu hóa Adam, PPO, LR scheduling
6 Lý thuyết Thông tin Cross-entropy, KL, Perplexity
7 Toán học Rời rạc GNN, BPE, Tree-of-Thought
8 Giải tích Số Mixed Precision, Stable Numerics

Bước tiếp theo: Implement một mini Transformer từ đầu bằng NumPy — đó là bài kiểm tra tốt nhất để biết bạn đã thực sự hiểu cả 8 lĩnh vực này.


🔗 Series

← Bài 7: Toán học Rời rạc
→ Bài 1: Giải tích — Quay lại đầu series