Học sâugiáo trình tự học
Tra cứu3 script

Mã nguồn thí nghiệm

Mọi hình và mọi con số thực nghiệm trong giáo trình đều do các script dưới đây sinh ra. Chúng được chép nguyên vẹn vào site nên tải về là chạy được ngay.

Cách chạy lại toàn bộ#

Ba script dưới đây sinh ra toàn bộ 15 hình và mọi con số đo được của giáo trình. Chúng đặt hạt giống cố định nên chạy lại cho kết quả y hệt.

Bash
pip install numpy scipy matplotlib scikit-learn
python code/models/experiments.py     # Hình 2, 4, 5, 7, 10, 11, 14, 15, vài phút
python code/models/thi_giac.py        # Hình 9 và số liệu về IoU, NMS, AP, vài giây
python code/models/fig_diagrams.py    # Hình 1, 3, 6, 8, 12, 13

Môi trường đã dùng để sinh số liệu: Python 3.13, NumPy 2.3, SciPy 1.16, scikit-learn 1.7, matplotlib 3.10.

Về phạm vi: phần lớn số liệu đến từ dữ liệu mô phỏng, mỗi thí nghiệm được thiết kế để cô lập một cơ chế, ví dụ thí nghiệm gradient qua 40 lớp chỉ đổi cách dựng lớp, và thí nghiệm phát hiện đối tượng chỉ làm nhiễu hộp thật thay vì huấn luyện một mô hình. Chúng cho thấy cơ chế tồn tại và có độ lớn đáng kể, không dùng để suy ra con số cho một mô hình cụ thể.

Một số kết quả là đối chiếu với con số đã công bố, không phải mô phỏng: số tham số của GPT-2 small, medium, large; số tham số của Llama 2 7B và 13B; ngân sách giờ-GPU của Llama 2 7B; và phương sai của tích vô hướng trong chú thích 4 của bài báo Transformer. Các phép đối chiếu này kiểm tra rằng công thức ở Chương 12 đúng, không chỉ hợp lý.

Từng script#

code/models/experiments.py

viết thêm cho repo này531 dòngTải về

Sinh Hình 2, 4, 5, 7, 10, 11, 14, 15 và mọi số liệu đo được của giáo trình: phân rã độ chệch – phương sai trên 250 tập huấn luyện, bagging so với gradient boosting, phép dựng hàm răng cưa cho thấy lợi ích của độ sâu, gradient qua 40 lớp với sáu cách dựng lớp, gradient theo thời gian của RNN và đường ô nhớ của LSTM, phương sai của tích vô hướng và entropy của attention, tính chất tương đối của RoPE, các chiến lược giải mã, và phép đếm tham số khớp chính xác với GPT-2 và Llama 2.

Xem mã nguồn
Python
"""Thí nghiệm sinh số liệu và hình cho giáo trình "Học sâu".

Mọi con số thực nghiệm trong tài liệu đều in ra từ script này. Hạt giống cố định
nên chạy lại cho kết quả y hệt. Chỉ cần NumPy, SciPy, scikit-learn, matplotlib.

    python code/models/experiments.py
"""

import numpy as np
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from sklearn.tree import DecisionTreeRegressor
from sklearn.ensemble import BaggingRegressor, GradientBoostingRegressor
from sklearn.neural_network import MLPRegressor

OUT = "figs/"
rng = np.random.default_rng(7)
plt.rcParams.update({
    "figure.dpi": 150, "font.size": 9, "axes.grid": True,
    "grid.alpha": .25, "axes.spines.top": False, "axes.spines.right": False,
})
C_MAIN, C_WARN, C_BAD, C_DIM = "#1f6f68", "#b8860b", "#b0413e", "#8a857c"


def head(t):
    print("\n" + "=" * 74 + "\n" + t + "\n" + "=" * 74)


# =====================================================================
# (A) Hình 2 — Phân rã độ chệch / phương sai, đo bằng mô phỏng
# =====================================================================
head("(A) Phan ra do chech - phuong sai cua da thuc")

NOISE, N_TRAIN, N_SETS = 0.35, 40, 250
f_true = lambda x: np.sin(2.2 * x) + 0.35 * x
x_test = np.linspace(-3, 3, 200)
y_true = f_true(x_test)

degrees = list(range(1, 15))
bias2, var, total = [], [], []
for d in degrees:
    preds = np.empty((N_SETS, len(x_test)))
    for s in range(N_SETS):
        r = np.random.default_rng(1000 + s)
        xt = r.uniform(-3, 3, N_TRAIN)
        yt = f_true(xt) + r.normal(0, NOISE, N_TRAIN)
        # Polynomial.fit chuan hoa mien truoc khi khop nen on dinh so o bac cao
        pf = np.polynomial.Polynomial.fit(xt, yt, d)
        preds[s] = pf(x_test)
    mean_pred = preds.mean(axis=0)
    b2 = np.mean((mean_pred - y_true) ** 2)
    v = np.mean(preds.var(axis=0))
    bias2.append(b2); var.append(v); total.append(b2 + v + NOISE ** 2)

best = int(np.argmin(total))
print(f"{'bậc':>4}{'độ chệch²':>14}{'phương sai':>13}{'nhiễu²':>10}{'tổng MSE':>12}")
for i, d in enumerate(degrees):
    star = "  <- nhỏ nhất" if i == best else ""
    print(f"{d:4d}{bias2[i]:14.4f}{var[i]:13.4f}{NOISE**2:10.4f}{total[i]:12.4f}{star}")
print(f"\nBac toi uu = {degrees[best]}; tai do do chech^2={bias2[best]:.4f}, phuong sai={var[best]:.4f}")
print(f"Bac 1  : do chech^2 = {bias2[0]:.3f} (chiem {bias2[0]/total[0]*100:.0f}% MSE) -> UNDERFITTING")
print(f"Bac {degrees[-1]}: phuong sai  = {var[-1]:.3f} (chiem {var[-1]/total[-1]*100:.0f}% MSE) -> OVERFITTING")
# Ước lượng độ chệch² từ N_SETS lần khớp bị cộng thêm sai số Monte Carlo cỡ phương sai / N_SETS
print(f"Sai so Monte Carlo cua uoc luong do chech^2 (~ phuong sai / {N_SETS}): "
      f"bac 12 ~ {var[11]/N_SETS:.2f}, bac 13 ~ {var[12]/N_SETS:.2f}, bac 14 ~ {var[13]/N_SETS:.2f}")

fig, ax = plt.subplots(figsize=(6.0, 3.2))
ax.plot(degrees, bias2, "-o", ms=4, color=C_MAIN, label="độ chệch²")
ax.plot(degrees, var, "-o", ms=4, color=C_BAD, label="phương sai")
ax.plot(degrees, total, "-o", ms=4, color=C_WARN, label="tổng MSE")
ax.axhline(NOISE ** 2, ls=":", color=C_DIM, lw=1.2)
ax.annotate("nhiễu không thể giảm", (1.2, NOISE ** 2 * 1.15), fontsize=8, color=C_DIM)
ax.axvline(degrees[best], ls="--", lw=1.1, color=C_DIM)
ax.set_yscale("log"); ax.set_xlabel("bậc đa thức"); ax.set_ylabel("sai số bình phương (log)")
ax.set_title("Phân rã đo được: MSE = độ chệch² + phương sai + nhiễu²")
ax.legend(frameon=False, fontsize=8)
fig.tight_layout(); fig.savefig(OUT + "models02_biasvar.png"); plt.close(fig)


# =====================================================================
# (B) Hình 4 — Bagging giảm phương sai, boosting giảm độ chệch
# =====================================================================
head("(B) Bagging va boosting cat vao thanh phan nao")

def make(seed, n=120):
    r = np.random.default_rng(seed)
    x = r.uniform(-3, 3, n)
    return x.reshape(-1, 1), f_true(x) + r.normal(0, NOISE, n)

models = {
    "Một cây (sâu 8)": lambda: DecisionTreeRegressor(max_depth=8, random_state=0),
    "Bagging 100 cây": lambda: BaggingRegressor(
        DecisionTreeRegressor(max_depth=8), n_estimators=100, random_state=0),
    "Boosting 100 gốc": lambda: GradientBoostingRegressor(
        n_estimators=100, max_depth=2, learning_rate=0.1, random_state=0),
}
Xte = x_test.reshape(-1, 1)
rows = []
for name, mk in models.items():
    preds = np.empty((40, len(x_test)))
    for s in range(40):
        X, y = make(2000 + s)
        preds[s] = mk().fit(X, y).predict(Xte)
    mp = preds.mean(axis=0)
    b2 = np.mean((mp - y_true) ** 2); v = np.mean(preds.var(axis=0))
    rows.append((name, b2, v, b2 + v))
    print(f"{name:<20} độ chệch²={b2:.4f}  phương sai={v:.4f}  tổng={b2+v:.4f}")
base = rows[0]
print(f"\nBagging giam phuong sai {base[2]/rows[1][2]:.1f} lan, do chech doi {rows[1][1]/base[1]:.2f} lan")
print(f"Boosting giam do chech {base[1]/rows[2][1]:.1f} lan, phuong sai doi {rows[2][2]/base[2]:.2f} lan")

fig, ax = plt.subplots(figsize=(5.6, 3.1))
xs = np.arange(len(rows)); w = .38
ax.bar(xs - w/2, [r[1] for r in rows], w, label="độ chệch²", color=C_MAIN)
ax.bar(xs + w/2, [r[2] for r in rows], w, label="phương sai", color=C_BAD)
ax.set_xticks(xs); ax.set_xticklabels([r[0] for r in rows], fontsize=8)
ax.set_ylabel("đóng góp vào MSE"); ax.legend(frameon=False, fontsize=8)
ax.set_title("Độ chệch² và phương sai của ba mô hình")
fig.tight_layout(); fig.savefig(OUT + "models04_ensemble.png"); plt.close(fig)


# =====================================================================
# (C) Hình 5 — Độ sâu mua được sự gọn gàng: chứng minh bằng kiến tạo
# =====================================================================
head("(C) Sau mua duoc su gon gang (chung minh kien tao, khong phu thuoc toi uu)")

# Ham leu: g(x) = 2*relu(x) - 4*relu(x - 1/2). Dung DUNG 2 don vi ReLU.
def tent(x):
    return 2 * np.maximum(x, 0.0) - 4 * np.maximum(x - 0.5, 0.0)

def deep_sawtooth(x, k):
    """Hop k lan ham leu -> song rang cua co 2^k doan tuyen tinh, dung 2k don vi."""
    h = x
    for _ in range(k):
        h = tent(h)
    return h

xg = np.linspace(0, 1, 20001)

def one_layer_best_mse(y, width):
    """Mang MOT lop an tot nhat co the voi `width` don vi ReLU.

    Dat diem gay deu tren (0,1) roi giai binh phuong toi thieu cho lop ra.
    Day la CAN TREN cua sai so ma mang mot lop dat duoc, va vi diem gay dat deu
    la vi tri toi uu cho ham tuan hoan nay nen no rat sat can duoi.
    """
    b = np.linspace(0, 1, width + 2)[1:-1]
    Phi = np.concatenate([np.maximum(xg[:, None] - b[None, :], 0.0),
                          xg[:, None], np.ones((len(xg), 1))], axis=1)
    coef, *_ = np.linalg.lstsq(Phi, y, rcond=None)
    return float(np.mean((Phi @ coef - y) ** 2))

widths = [2, 4, 8, 16, 32, 64, 128, 256]
print(f"{'k lớp sâu':>10}{'đoạn 2^k':>10}{'tham số sâu':>13}   sai số của mạng MỘT lớp theo bề rộng")
print(f"{'':>33}   " + "".join(f"{w:>9}" for w in widths))
Crows = []
for k in [2, 3, 4, 5, 6, 7]:
    y = deep_sawtooth(xg, k)
    deep_params = 6 * k          # moi lop: 2 trong so vao + 2 do lech + 2 trong so ra
    errs = [one_layer_best_mse(y, w) for w in widths]
    # be rong nho nhat de tot hon DANG KE so voi du doan hang so
    baseline = float(np.var(y))
    need = next((w for w, e in zip(widths, errs) if e < 0.5 * baseline), None)
    Crows.append((k, 2 ** k, deep_params, need, errs))
    print(f"{k:>10}{2**k:>10}{deep_params:>13}   " + "".join(f"{e:>9.1e}" for e in errs))
print()
print(f"{'k':>3}{'đoạn 2^k':>10}{'tham số mạng sâu':>19}{'bề rộng 1 lớp cần':>20}{'tham số 1 lớp':>16}{'gấp':>7}")
for k, npieces, dp, need, _ in Crows:
    sp = need * 3 + 2 if need else float("nan")
    print(f"{k:>3}{npieces:>10}{dp:>19}{need if need else 0:>20}{sp:>16.0f}{sp/dp:>7.1f}x")
print("\nBe rong can thiet dung bang so DOAN TUYEN TINH = 2^k, tuc TANG THEO HAM MU cua do sau,")
print("trong khi mang sau chi can TUYEN TINH theo k. Day la 'do sau mua duoc su gon gang'.")

fig, ax = plt.subplots(1, 2, figsize=(8.6, 3.1))
for k, npieces, _, _, errs in Crows:
    ax[0].plot(widths, np.maximum(errs, 1e-12), "-o", ms=4, label=f"$k={k}$ ({npieces} đoạn)")
ax[0].set_xscale("log", base=2); ax[0].set_yscale("log")
ax[0].set_xlabel("bề rộng của mạng MỘT lớp ẩn"); ax[0].set_ylabel("MSE nhỏ nhất đạt được (log)")
ax[0].set_title("Sai số của mạng một lớp ẩn theo bề rộng")
ax[0].legend(frameon=False, fontsize=7.5)
ks = [c[0] for c in Crows]
ax[1].plot(ks, [c[2] for c in Crows], "-o", ms=5, color=C_MAIN, label="mạng sâu $k$ lớp")
ax[1].plot(ks, [c[3] * 3 + 2 for c in Crows], "-o", ms=5, color=C_BAD, label="mạng một lớp")
ax[1].set_yscale("log"); ax[1].set_xlabel("số lớp $k$ của mạng sâu")
ax[1].set_ylabel("số tham số cần (log)"); ax[1].set_xticks(ks)
ax[1].set_title("Tuyến tính theo $k$ so với hàm mũ theo $k$")
ax[1].legend(frameon=False, fontsize=8)
fig.tight_layout(); fig.savefig(OUT + "models05_depth.png"); plt.close(fig)


# =====================================================================
# (D) Hình 7 — Gradient tiêu biến, và ba cách chữa
# =====================================================================
head("(D) Gradient truyen nguoc qua 40 lop: do bang |dL/da| tung lop")

def layernorm_fwd(z, eps=1e-5):
    mu = z.mean(-1, keepdims=True)
    sd = np.sqrt(z.var(-1, keepdims=True) + eps)
    return (z - mu) / sd, sd

def layernorm_bwd(dy, y, sd):
    """Dao ham chinh xac cua LayerNorm theo truc dac trung."""
    n = y.shape[-1]
    return (dy - dy.mean(-1, keepdims=True) - y * (dy * y).mean(-1, keepdims=True)) / sd

def act_grad_profile(depth=40, width=128, gain=np.sqrt(2.0), mode="plain", seed=0):
    """Tra ve |dL/da_i| tai tung lop, chuan hoa theo lop tren cung."""
    r = np.random.default_rng(seed)
    Ws = [r.normal(0, gain / np.sqrt(width), (width, width)) for _ in range(depth)]
    a = r.normal(0, 1, (128, width))
    cache = []
    for W in Ws:
        z = a @ W
        y, sd = (None, None)
        if mode in ("layernorm", "prenorm"):
            z, sd = layernorm_fwd(z)
            y = z
        h = np.maximum(z, 0)
        if mode in ("residual", "prenorm"):
            h = h + a
        cache.append((a, z, y, sd)); a = h
    g = r.normal(0, 1, a.shape); g /= np.linalg.norm(g)
    norms = [np.linalg.norm(g)]
    for i in range(depth - 1, -1, -1):
        a_in, z, y, sd = cache[i]
        g_skip = g if mode in ("residual", "prenorm") else None
        gz = g * (z > 0)
        if mode in ("layernorm", "prenorm"):
            gz = layernorm_bwd(gz, y, sd)
        g = gz @ Ws[i].T
        if g_skip is not None:
            g = g + g_skip
        norms.append(np.linalg.norm(g))
    return np.array(norms[::-1]) / norms[0]

cases = [("gain 0,5 — khởi tạo quá nhỏ", dict(gain=0.5, mode="plain")),
         ("gain √2 — khởi tạo He", dict(gain=np.sqrt(2.0), mode="plain")),
         ("gain 2,0 — khởi tạo quá lớn", dict(gain=2.0, mode="plain")),
         ("He + LayerNorm", dict(gain=np.sqrt(2.0), mode="layernorm")),
         ("He + kết nối tắt, KHÔNG chuẩn hoá", dict(gain=np.sqrt(2.0), mode="residual")),
         ("He + chuẩn hoá + kết nối tắt (pre-LN)", dict(gain=np.sqrt(2.0), mode="prenorm"))]
print(f"{'cách dựng lớp':<40}{'|∇a| lớp 40':>14}{'lớp 20':>13}{'lớp 1':>13}{'lớp1/lớp40':>14}")
Dprof = {}
for lab, kw in cases:
    p = np.mean([act_grad_profile(seed=s, **kw) for s in range(5)], axis=0)
    Dprof[lab] = p
    print(f"{lab:<40}{p[-1]:14.3e}{p[20]:13.3e}{p[0]:13.3e}{p[0]/p[-1]:14.3e}")
print("\nDoc bang: ti le lop1/lop40 << 1 la TIEU BIEN, >> 1 la BUNG NO, ~1 la on dinh.")

fig, ax = plt.subplots(figsize=(6.2, 3.2))
for lab, p in Dprof.items():
    ax.plot(np.arange(len(p)), p, lw=1.7, label=lab)
ax.set_yscale("log"); ax.set_xlabel("chỉ số lớp (0 = gần đầu vào, 40 = gần hàm mất mát)")
ax.set_ylabel("$\\|\\partial L/\\partial a\\|$ tương đối (log)")
ax.set_title("Cùng độ sâu 40, chỉ khác khởi tạo và cách dựng lớp")
ax.legend(frameon=False, fontsize=7.8)
fig.tight_layout(); fig.savefig(OUT + "models07_vanish.png"); plt.close(fig)


# =====================================================================
# (E) Hình 10 — Vì sao attention phải chia cho căn d_k
# =====================================================================
head("(E) Vi sao chia cho can d_k")

def softmax(z, axis=-1):
    z = z - z.max(axis=axis, keepdims=True)
    e = np.exp(z); return e / e.sum(axis=axis, keepdims=True)

print(f"{'d_k':>6}{'Var(q·k)':>12}{'Var lý thuyết':>15}{'entropy KHÔNG chia':>21}{'entropy CÓ chia':>18}{'max softmax':>13}")
dks = [4, 16, 64, 256, 1024]
E = []
for dk in dks:
    r = np.random.default_rng(3)
    q = r.normal(0, 1, (4000, dk)); k = r.normal(0, 1, (64, dk))
    s = q @ k.T
    ent_raw = -np.sum(softmax(s) * np.log(softmax(s) + 1e-12), axis=-1).mean()
    ps = softmax(s / np.sqrt(dk))
    ent_sc = -np.sum(ps * np.log(ps + 1e-12), axis=-1).mean()
    mx = softmax(s).max(axis=-1).mean()
    E.append((dk, s.var(), dk, ent_raw, ent_sc, mx))
    print(f"{dk:6d}{s.var():12.2f}{dk:15d}{ent_raw:21.4f}{ent_sc:18.4f}{mx:13.4f}")
print(f"\nEntropy toi da khi deu tren 64 khoa: ln(64) = {np.log(64):.4f}")
print("Khong chia: entropy sup ve 0 -> softmax bao hoa -> gradient gan bang 0.")

fig, ax = plt.subplots(1, 2, figsize=(8.4, 3.0))
ax[0].plot(dks, [e[1] for e in E], "-o", ms=4, color=C_MAIN, label="đo được")
ax[0].plot(dks, dks, ":", color=C_DIM, lw=1.4, label="lý thuyết $d_k$")
ax[0].set_xscale("log"); ax[0].set_yscale("log")
ax[0].set_xlabel("$d_k$"); ax[0].set_ylabel("phương sai của $q\\cdot k$")
ax[0].set_title("Phương sai của tích vô hướng theo $d_k$"); ax[0].legend(frameon=False, fontsize=8)
ax[1].plot(dks, [e[3] for e in E], "-o", ms=4, color=C_BAD, label="không chia")
ax[1].plot(dks, [e[4] for e in E], "-o", ms=4, color=C_MAIN, label="chia $\\sqrt{d_k}$")
ax[1].axhline(np.log(64), ls=":", color=C_DIM, lw=1.2)
ax[1].annotate("entropy tối đa ln 64", (dks[0], np.log(64) * 0.93), fontsize=7.5, color=C_DIM)
ax[1].set_xscale("log"); ax[1].set_xlabel("$d_k$"); ax[1].set_ylabel("entropy của softmax (nat)")
ax[1].set_title("Entropy của phân phối attention"); ax[1].legend(frameon=False, fontsize=8)
fig.tight_layout(); fig.savefig(OUT + "models10_scaling.png"); plt.close(fig)


# =====================================================================
# (F) Hình 9 — RNN quên, LSTM nhớ
# =====================================================================
head("(F) Gradient theo thoi gian: RNN so voi LSTM")

def rnn_grad(T=100, n=64, gain=1.0, seed=0):
    r = np.random.default_rng(seed)
    W = r.normal(0, gain / np.sqrt(n), (n, n))
    h = r.normal(0, 1, n) * 0.1
    hs, zs = [h], []
    for _ in range(T):
        z = W @ h; h = np.tanh(z); zs.append(z); hs.append(h)
    g = r.normal(0, 1, n); g /= np.linalg.norm(g)
    out = []
    for t in range(T - 1, -1, -1):
        g = (W.T @ (g * (1 - np.tanh(zs[t]) ** 2)))
        out.append(np.linalg.norm(g))
    return np.array(out[::-1])

def lstm_grad(T=100, n=64, f_bias=2.0, seed=0):
    """Chi theo duong trang thai o nho c_t: dc_{t-1} = dc_t * f_t."""
    r = np.random.default_rng(seed)
    g = 1.0; out = []
    for _ in range(T):
        f = 1 / (1 + np.exp(-(r.normal(0, 0.5) + f_bias)))   # cong quen mo san
        g = g * f; out.append(g)
    return np.array(out[::-1])

F = {}
for gain, lab in [(0.9, "RNN, gain 0,9"), (1.0, "RNN, gain 1,0"), (1.2, "RNN, gain 1,2")]:
    F[lab] = np.mean([rnn_grad(gain=gain, seed=s) for s in range(8)], axis=0)
for fb, lab in [(1.0, "LSTM, bias cổng quên = 1"), (4.0, "LSTM, bias cổng quên = 4")]:
    F[lab] = np.mean([lstm_grad(f_bias=fb, seed=s) for s in range(8)], axis=0)

print(f"{'mô hình':<26}{'|∇| sau 10 bước':>18}{'sau 50 bước':>14}{'sau 100 bước':>15}")
for lab, gs in F.items():
    g0 = gs[-1]
    print(f"{lab:<26}{gs[-10]/g0:18.3e}{gs[-50]/g0:14.3e}{gs[0]/g0:15.3e}")

fig, ax = plt.subplots(figsize=(6.0, 3.1))
for lab, gs in F.items():
    ax.plot(np.arange(len(gs))[::-1], gs / gs[-1], lw=1.6, label=lab)
ax.set_yscale("log"); ax.set_xlabel("số bước lùi về quá khứ")
ax.set_ylabel("chuẩn gradient tương đối (log)")
ax.set_title("Gradient truyền ngược qua thời gian")
ax.legend(frameon=False, fontsize=8)
fig.tight_layout(); fig.savefig(OUT + "models09_rnn.png"); plt.close(fig)


# =====================================================================
# (G) Hình 12 — RoPE chỉ phụ thuộc khoảng cách tương đối
# =====================================================================
head("(G) RoPE: tich vo huong chi phu thuoc m - n")

def rope(x, pos, base=10000.0):
    d = x.shape[-1]
    i = np.arange(0, d, 2)
    theta = base ** (-i / d)
    ang = pos * theta
    c, s = np.cos(ang), np.sin(ang)
    out = x.copy()
    out[..., 0::2] = x[..., 0::2] * c - x[..., 1::2] * s
    out[..., 1::2] = x[..., 0::2] * s + x[..., 1::2] * c
    return out

d = 64
r = np.random.default_rng(11)
q, k = r.normal(0, 1, d), r.normal(0, 1, d)
print(f"{'m':>5}{'n':>5}{'m-n':>6}{'<R_m q, R_n k>':>18}")
vals = {}
for m, n in [(5, 2), (105, 102), (500, 497), (7, 4), (9, 3), (109, 103)]:
    v = float(rope(q, m) @ rope(k, n))
    vals.setdefault(m - n, []).append(v)
    print(f"{m:5d}{n:5d}{m-n:6d}{v:18.6f}")
ok = all(np.allclose(v, v[0], atol=1e-9) for v in vals.values())
print(f"\nCung m-n thi tich vo huong giong nhau: {ok}")

# Suy giam theo khoang cach: do bang CUNG mot vector o hai vi tri, chuan hoa theo |q|^2.
dists = np.arange(0, 256)
sims = []
for D_ in dists:
    acc = []
    for _ in range(300):
        v = r.normal(0, 1, d)
        acc.append(float(rope(v, D_) @ rope(v, 0)) / float(v @ v))
    sims.append(np.mean(acc))
sims = np.array(sims)
print(f"<R_m q, R_0 q>/|q|^2 trung binh: khoang cach 0 -> {sims[0]:.3f}; "
      f"8 -> {sims[8]:.3f}; 64 -> {sims[64]:.3f}; 255 -> {sims[255]:.3f}")

fig, ax = plt.subplots(1, 2, figsize=(8.4, 3.0))
ds = sorted(vals)
ax[0].bar([str(x) for x in ds], [vals[x][0] for x in ds], color=C_MAIN)
ax[0].set_xlabel("khoảng cách tương đối $m-n$"); ax[0].set_ylabel("$\\langle R_m q, R_n k\\rangle$")
ax[0].set_title("Cùng $m-n$ cho cùng một giá trị")
ax[1].plot(dists, sims, lw=1.4, color=C_MAIN)
ax[1].axhline(0, lw=.8, color=C_DIM)
ax[1].set_xlabel("khoảng cách tương đối"); ax[1].set_ylabel(r"$\langle R_m q, R_0 q\rangle / |q|^2$")
ax[1].set_title("Suy giảm theo khoảng cách")
fig.tight_layout(); fig.savefig(OUT + "models12_rope.png"); plt.close(fig)


# =====================================================================
# (H) Hình 14 — Các chiến lược giải mã thay đổi phân phối thế nào
# =====================================================================
head("(H) Nhiet do, top-k, top-p")

V = 50000
r = np.random.default_rng(1)
logits = np.sort(r.normal(0, 2.2, V))[::-1]        # phan phoi dang Zipf thoai
p0 = softmax(logits)
print(f"Phan phoi goc: entropy {-(p0*np.log(p0+1e-12)).sum():.3f} nat, "
      f"top-1 {p0[0]:.3f}, so token phu 90% xac suat: {np.searchsorted(np.cumsum(p0), 0.9)+1}")

def apply_temp(lg, T): return softmax(lg / T)
def apply_topk(lg, k):
    out = np.full_like(lg, -np.inf); out[:k] = lg[:k]; return softmax(out)
def apply_topp(lg, p):
    pr = softmax(lg); c = np.cumsum(pr); n = int(np.searchsorted(c, p)) + 1
    out = np.full_like(lg, -np.inf); out[:n] = lg[:n]; return softmax(out)

print(f"\n{'chiến lược':<22}{'entropy (nat)':>15}{'số token phủ 90%':>20}{'top-1':>10}")
H = []
rows2 = [("gốc (T = 1)", p0), ("T = 0,7", apply_temp(logits, 0.7)),
         ("T = 1,3", apply_temp(logits, 1.3)), ("top-k = 40", apply_topk(logits, 40)),
         ("top-p = 0,9", apply_topp(logits, 0.9)), ("top-p = 0,5", apply_topp(logits, 0.5))]
for name, p in rows2:
    ent = -(p * np.log(p + 1e-12)).sum()
    n90 = int(np.searchsorted(np.cumsum(p), 0.9)) + 1
    H.append((name, ent, n90, p[0]))
    print(f"{name:<22}{ent:15.4f}{n90:20d}{p[0]:10.4f}")

fig, ax = plt.subplots(1, 2, figsize=(8.4, 3.0))
for name, p in rows2[:4]:
    ax[0].plot(np.arange(1, 201), p[:200], lw=1.4, label=name)
ax[0].set_yscale("log"); ax[0].set_xlabel("hạng của token"); ax[0].set_ylabel("xác suất (log)")
ax[0].set_title("Hình dạng phân phối"); ax[0].legend(frameon=False, fontsize=7.5)
ax[1].barh([h[0] for h in H], [h[2] for h in H], color=C_MAIN)
ax[1].set_xscale("log"); ax[1].set_xlabel("số token phủ 90% xác suất (log)")
ax[1].set_title("Số token phủ 90% xác suất")
fig.tight_layout(); fig.savefig(OUT + "models13_decode.png"); plt.close(fig)


# =====================================================================
# (I) Đếm tham số và FLOP, đối chiếu với GPT-2 đã công bố
# =====================================================================
head("(I) Dem tham so Transformer, doi chieu GPT-2")

def gpt2_params(n_layer, d_model, d_ff, vocab, n_ctx, tie=True):
    emb = vocab * d_model
    pos = n_ctx * d_model
    attn = n_layer * (d_model * 3 * d_model + 3 * d_model      # W_qkv + bias
                      + d_model * d_model + d_model)          # W_o + bias
    ffn = n_layer * (d_model * d_ff + d_ff + d_ff * d_model + d_model)
    ln = n_layer * 2 * 2 * d_model + 2 * d_model               # 2 LN/lop + LN cuoi, moi LN co gamma & beta
    total = emb + pos + attn + ffn + ln + (0 if tie else vocab * d_model)
    non_emb = 12 * n_layer * d_model ** 2                      # cong thuc Kaplan, bo bias va LN
    return dict(emb=emb, pos=pos, attn=attn, ffn=ffn, ln=ln, total=total, non_emb=non_emb)

specs = [("GPT-2 small", 12, 768, 3072, 50257, 1024, 124_439_808),
         ("GPT-2 medium", 24, 1024, 4096, 50257, 1024, 354_823_168),
         ("GPT-2 large", 36, 1280, 5120, 50257, 1024, 774_030_080)]
print(f"{'mô hình':<15}{'công thức':>16}{'đã công bố':>16}{'lệch':>8}{'phi-embedding':>16}")
for name, L, dm, dff, V_, ctx, published in specs:
    p = gpt2_params(L, dm, dff, V_, ctx)
    print(f"{name:<15}{p['total']:16,d}{published:16,d}{p['total']-published:8d}{p['non_emb']:16,d}")

p = gpt2_params(12, 768, 3072, 50257, 1024)
print(f"\nGPT-2 small tach nho: embedding {p['emb']:,} ({p['emb']/p['total']*100:.1f}%), "
      f"vi tri {p['pos']:,}, attention {p['attn']:,}, FFN {p['ffn']:,}")
print(f"Kiem tra 12*L*d^2 = {p['non_emb']:,} ; FFN chiem {p['ffn']/(p['attn']+p['ffn'])*100:.0f}% khoi Transformer")
print(f"FLOP suy luan moi token ~ 2N = {2*p['non_emb']:,} ; huan luyen ~ 6N = {6*p['non_emb']:,}")


# =====================================================================
# (J) Kiến trúc kiểu Llama: tham số, FLOP huấn luyện, và tác dụng của GQA
# =====================================================================
head("(J) Kien truc kieu Llama: RoPE, RMSNorm, SwiGLU, GQA")

def llama_params(n_layer, d_model, d_ff, vocab, n_kv_heads, n_heads):
    """RoPE nen khong co embedding vi tri; RMSNorm chi co gamma; khong do lech;
    FFN SwiGLU dung BA ma tran; lop ra khong dung chung trong so voi embedding."""
    d_head = d_model // n_heads
    attn = n_layer * (d_model * d_model                      # W_Q
                      + 2 * d_model * n_kv_heads * d_head    # W_K, W_V (GQA)
                      + d_model * d_model)                   # W_O
    ffn = n_layer * 3 * d_model * d_ff                       # SwiGLU: W, V, W_2
    norms = n_layer * 2 * d_model + d_model
    emb = vocab * d_model
    head_ = vocab * d_model
    return dict(emb=emb, head=head_, attn=attn, ffn=ffn, norms=norms,
                total=emb + head_ + attn + ffn + norms, non_emb=attn + ffn)

print(f"{'mô hình':<16}{'công thức':>18}{'đã công bố':>18}{'lệch':>8}")
for name, L, dm, dff, V_, nkv, nh, published in [
        ("Llama-2 7B", 32, 4096, 11008, 32000, 32, 32, 6_738_415_616),
        ("Llama-2 13B", 40, 5120, 13824, 32000, 40, 40, 13_015_864_320)]:
    q = llama_params(L, dm, dff, V_, nkv, nh)
    print(f"{name:<16}{q['total']:18,d}{published:18,d}{q['total']-published:8d}")

q = llama_params(32, 4096, 11008, 32000, 32, 32)
N = q["non_emb"]
print(f"  d_ff = 11008 so voi 8/3*d = {8/3*4096:.0f}  -> lam tron len boi cua 256")
print(f"  Llama-2 7B phi-embedding N = {N:,}")

D_tok = 2e12
C = 6 * N * D_tok
print(f"\nHuan luyen tren {D_tok:.0e} token: C = 6ND = {C:.4e} FLOP")
for mfu in (0.30, 0.376, 0.40):
    hours = C / (312e12 * mfu) / 3600
    print(f"  o {mfu*100:5.1f}% MFU tren A100 (312 TFLOPS bf16): {hours:9,.0f} gio-GPU")
print("  Bao cao cua Llama 2 cho ban 7B: 184.320 gio-GPU")

def kv_bytes(L, n_kv, d_head, T, B, bpe=2):
    return 2 * L * n_kv * d_head * T * B * bpe

mha = kv_bytes(80, 64, 128, 4096, 8)
gqa = kv_bytes(80, 8, 128, 4096, 8)
print(f"\nKV cache mo hinh 70B (80 lop, d_head 128), ngu canh 4096, batch 8, FP16:")
print(f"  MHA, 64 KV head: {mha / 2**30:7.2f} GiB")
print(f"  GQA,  8 KV head: {gqa / 2**30:7.2f} GiB   -> giam dung {mha / gqa:.0f} lan")
print(f"  moi token moi chuoi voi GQA: {kv_bytes(80, 8, 128, 1, 1) / 2**20:.4f} MiB")

# Khi nao attention chi phoi chi phi
print(f"\nTi le chi phi attention tren phan con lai = T / (6d):")
for d_, T_ in [(768, 1024), (4096, 4096), (4096, 32768), (4096, 131072)]:
    print(f"  d={d_:5d}, T={T_:6d}: {T_ / (6 * d_):7.2f}   "
          f"({'attention chi phoi' if T_ > 6 * d_ else 'phan con lai chi phoi'})")

print("\nDa luu 8 hinh vao " + OUT)
Kết quả chạy — experiments_output.txt
Kết quả chạy

==========================================================================
(A) Phan ra do chech - phuong sai cua da thuc
==========================================================================
 bậc     độ chệch²   phương sai    nhiễu²    tổng MSE
   1        0.4221       0.0231    0.1225      0.5677
   2        0.4223       0.0418    0.1225      0.5866
   3        0.4098       0.0857    0.1225      0.6180
   4        0.4191       0.1852    0.1225      0.7268
   5        0.0836       0.0767    0.1225      0.2828  <- nhỏ nhất
   6        0.1137       0.3914    0.1225      0.6276
   7        0.0058       0.3415    0.1225      0.4699
   8        0.0181       0.2455    0.1225      0.3861
   9        0.0024       1.2645    0.1225      1.3894
  10        0.0052       6.7742    0.1225      6.9019
  11        0.0251      25.4582    0.1225     25.6058
  12        0.0276      52.2156    0.1225     52.3657
  13        6.2261     871.8100    0.1225    878.1586
  14       35.1402    7392.2751    0.1225   7427.5379

Bac toi uu = 5; tai do do chech^2=0.0836, phuong sai=0.0767
Bac 1  : do chech^2 = 0.422 (chiem 74% MSE) -> UNDERFITTING
Bac 14: phuong sai  = 7392.275 (chiem 100% MSE) -> OVERFITTING
Sai so Monte Carlo cua uoc luong do chech^2 (~ phuong sai / 250): bac 12 ~ 0.21, bac 13 ~ 3.49, bac 14 ~ 29.57

==========================================================================
(B) Bagging va boosting cat vao thanh phan nao
==========================================================================
Một cây (sâu 8)      độ chệch²=0.0031  phương sai=0.1089  tổng=0.1120
Bagging 100 cây      độ chệch²=0.0016  phương sai=0.0534  tổng=0.0550
Boosting 100 gốc     độ chệch²=0.0032  phương sai=0.0371  tổng=0.0403

Bagging giam phuong sai 2.0 lan, do chech doi 0.52 lan
Boosting giam do chech 1.0 lan, phuong sai doi 0.34 lan

==========================================================================
(C) Sau mua duoc su gon gang (chung minh kien tao, khong phu thuoc toi uu)
==========================================================================
 k lớp sâu  đoạn 2^k  tham số sâu   sai số của mạng MỘT lớp theo bề rộng
                                            2        4        8       16       32       64      128      256
         2         4           12     7.9e-02  1.5e-02  1.8e-03  2.9e-04  4.0e-05  5.3e-06  6.8e-07  8.6e-08
         3         8           18     8.3e-02  8.3e-02  2.6e-02  2.1e-03  3.2e-04  4.2e-05  5.4e-06  6.9e-07
         4        16           24     8.3e-02  8.3e-02  8.3e-02  3.3e-02  2.2e-03  3.4e-04  4.3e-05  5.5e-06
         5        32           30     8.3e-02  8.3e-02  8.3e-02  8.3e-02  3.8e-02  2.3e-03  3.4e-04  4.4e-05
         6        64           36     8.3e-02  8.3e-02  8.3e-02  8.3e-02  8.3e-02  4.0e-02  2.3e-03  3.5e-04
         7       128           42     8.3e-02  8.3e-02  8.3e-02  8.3e-02  8.3e-02  8.3e-02  4.2e-02  2.4e-03

  k  đoạn 2^k   tham số mạng sâu   bề rộng 1 lớp cần   tham số 1 lớp    gấp
  2         4                 12                   4              14    1.2x
  3         8                 18                   8              26    1.4x
  4        16                 24                  16              50    2.1x
  5        32                 30                  32              98    3.3x
  6        64                 36                  64             194    5.4x
  7       128                 42                 128             386    9.2x

Be rong can thiet dung bang so DOAN TUYEN TINH = 2^k, tuc TANG THEO HAM MU cua do sau,
trong khi mang sau chi can TUYEN TINH theo k. Day la 'do sau mua duoc su gon gang'.

==========================================================================
(D) Gradient truyen nguoc qua 40 lop: do bang |dL/da| tung lop
==========================================================================
cách dựng lớp                              |∇a| lớp 40       lớp 20        lớp 1    lớp1/lớp40
gain 0,5 — khởi tạo quá nhỏ                  1.000e+00    1.015e-09    1.233e-18     1.233e-18
gain √2 — khởi tạo He                        1.000e+00    1.090e+00    1.421e+00     1.421e+00
gain 2,0 — khởi tạo quá lớn                  1.000e+00    1.116e+03    1.490e+06     1.490e+06
He + LayerNorm                               1.000e+00    1.081e+00    1.151e+00     1.151e+00
He + kết nối tắt, KHÔNG chuẩn hoá            1.000e+00    6.829e+03    2.446e+08     2.446e+08
He + chuẩn hoá + kết nối tắt (pre-LN)        1.000e+00    1.037e+00    2.222e+00     2.222e+00

Doc bang: ti le lop1/lop40 << 1 la TIEU BIEN, >> 1 la BUNG NO, ~1 la on dinh.

==========================================================================
(E) Vi sao chia cho can d_k
==========================================================================
   d_k    Var(q·k)  Var lý thuyết   entropy KHÔNG chia   entropy CÓ chia  max softmax
     4        3.63              4               2.8920            3.7391       0.2580
    16       15.99             16               1.3597            3.6854       0.5837
    64       64.66             64               0.5818            3.6803       0.7913
   256      251.67            256               0.2746            3.6923       0.8943
  1024     1021.97           1024               0.1179            3.6847       0.9533

Entropy toi da khi deu tren 64 khoa: ln(64) = 4.1589
Khong chia: entropy sup ve 0 -> softmax bao hoa -> gradient gan bang 0.

==========================================================================
(F) Gradient theo thoi gian: RNN so voi LSTM
==========================================================================
mô hình                      |∇| sau 10 bước   sau 50 bước   sau 100 bước
RNN, gain 0,9                      3.182e-01     6.815e-02      3.236e-02
RNN, gain 1,0                      4.180e-01     1.765e-01      1.089e+00
RNN, gain 1,2                      6.390e-01     3.780e-01      3.014e+00
LSTM, bias cổng quên = 1           5.032e-02     8.027e-08      2.972e-15
LSTM, bias cổng quên = 4           8.305e-01     3.597e-01      1.266e-01

==========================================================================
(G) RoPE: tich vo huong chi phu thuoc m - n
==========================================================================
    m    n   m-n    <R_m q, R_n k>
    5    2     3          6.851342
  105  102     3          6.851342
  500  497     3          6.851342
    7    4     3          6.851342
    9    3     6          5.861634
  109  103     6          5.861634

Cung m-n thi tich vo huong giong nhau: True
<R_m q, R_0 q>/|q|^2 trung binh: khoang cach 0 -> 1.000; 8 -> 0.708; 64 -> 0.439; 255 -> 0.319

==========================================================================
(H) Nhiet do, top-k, top-p
==========================================================================
Phan phoi goc: entropy 8.223 nat, top-1 0.027, so token phu 90% xac suat: 8362

chiến lược              entropy (nat)    số token phủ 90%     top-1
gốc (T = 1)                    8.2232                8362    0.0274
T = 0,7                        5.8857                1215    0.1279
T = 1,3                        9.3050               16766    0.0082
top-k = 40                     3.4488                  33    0.1386
top-p = 0,9                    7.6690                4141    0.0304
top-p = 0,5                    5.6727                 376    0.0547

==========================================================================
(I) Dem tham so Transformer, doi chieu GPT-2
==========================================================================
mô hình               công thức      đã công bố    lệch   phi-embedding
GPT-2 small         124,439,808     124,439,808       0      84,934,656
GPT-2 medium        354,823,168     354,823,168       0     301,989,888
GPT-2 large         774,030,080     774,030,080       0     707,788,800

GPT-2 small tach nho: embedding 38,597,376 (31.0%), vi tri 786,432, attention 28,348,416, FFN 56,669,184
Kiem tra 12*L*d^2 = 84,934,656 ; FFN chiem 67% khoi Transformer
FLOP suy luan moi token ~ 2N = 169,869,312 ; huan luyen ~ 6N = 509,607,936

==========================================================================
(J) Kien truc kieu Llama: RoPE, RMSNorm, SwiGLU, GQA
==========================================================================
mô hình                  công thức        đã công bố    lệch
Llama-2 7B           6,738,415,616     6,738,415,616       0
Llama-2 13B         13,015,864,320    13,015,864,320       0
  d_ff = 11008 so voi 8/3*d = 10923  -> lam tron len boi cua 256
  Llama-2 7B phi-embedding N = 6,476,005,376

Huan luyen tren 2e+12 token: C = 6ND = 7.7712e+22 FLOP
  o  30.0% MFU tren A100 (312 TFLOPS bf16):   230,627 gio-GPU
  o  37.6% MFU tren A100 (312 TFLOPS bf16):   184,011 gio-GPU
  o  40.0% MFU tren A100 (312 TFLOPS bf16):   172,970 gio-GPU
  Bao cao cua Llama 2 cho ban 7B: 184.320 gio-GPU

KV cache mo hinh 70B (80 lop, d_head 128), ngu canh 4096, batch 8, FP16:
  MHA, 64 KV head:   80.00 GiB
  GQA,  8 KV head:   10.00 GiB   -> giam dung 8 lan
  moi token moi chuoi voi GQA: 0.3125 MiB

Ti le chi phi attention tren phan con lai = T / (6d):
  d=  768, T=  1024:    0.22   (phan con lai chi phoi)
  d= 4096, T=  4096:    0.17   (phan con lai chi phoi)
  d= 4096, T= 32768:    1.33   (attention chi phoi)
  d= 4096, T=131072:    5.33   (attention chi phoi)

Da luu 8 hinh vao figs/

code/models/thi_giac.py

viết thêm cho repo này215 dòngTải về

Sinh Hình 9 và các số liệu của Mục 7.5–7.6: IoU của các cặp hộp, NMS từng bước trên sáu hộp, đường precision–recall và AP của một bộ phát hiện mô phỏng trên 300 ảnh (ở các ngưỡng IoU 0,5 tới 0,95, có và không có NMS), và liên hệ giữa Dice với IoU.

Xem mã nguồn
Python
"""Số liệu và hình cho Mục 7.5–7.6 của giáo trình "Học sâu": IoU, NMS, AP và Dice.

Không huấn luyện mô hình nào: một bộ phát hiện được mô phỏng bằng cách làm nhiễu
hộp thật, để cô lập đúng phần cần học là cách ghép hộp, cách lọc trùng và cách tính AP.
Hạt giống cố định nên chạy lại cho kết quả y hệt.

    python code/models/thi_giac.py
"""

import numpy as np
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle

OUT = "figs/"
plt.rcParams.update({
    "figure.dpi": 150, "font.size": 9, "axes.grid": True,
    "grid.alpha": .25, "axes.spines.top": False, "axes.spines.right": False,
})
C_MAIN, C_WARN, C_BAD, C_DIM = "#1f6f68", "#b8860b", "#b0413e", "#8a857c"


def head(t):
    print("\n" + "=" * 74 + "\n" + t + "\n" + "=" * 74)


def iou(a, b):
    """Hộp dạng (x1, y1, x2, y2)."""
    w = max(0.0, min(a[2], b[2]) - max(a[0], b[0]))
    h = max(0.0, min(a[3], b[3]) - max(a[1], b[1]))
    inter = w * h
    union = (a[2] - a[0]) * (a[3] - a[1]) + (b[2] - b[0]) * (b[3] - b[1]) - inter
    return inter / union if union > 0 else 0.0


def nms(boxes, scores, nguong):
    """Giữ hộp điểm cao nhất, bỏ mọi hộp còn lại trùng với nó quá ngưỡng, lặp lại."""
    thu_tu = list(np.argsort(-np.asarray(scores)))
    giu = []
    while thu_tu:
        i = thu_tu.pop(0)
        giu.append(i)
        thu_tu = [j for j in thu_tu if iou(boxes[i], boxes[j]) <= nguong]
    return giu


# =====================================================================
# (A) IoU tính tay
# =====================================================================
head("(A) IoU cua hai cap hop")
for a, b in [((0, 0, 4, 4), (2, 2, 6, 6)), ((0, 0, 4, 4), (1, 0, 5, 4)), ((0, 0, 4, 4), (0, 0, 4, 2))]:
    print(f"  {a} va {b}: IoU = {iou(a, b):.4f}")


# =====================================================================
# (B) NMS từng bước trên sáu hộp
# =====================================================================
head("(B) NMS tren sau hop du doan, nguong IoU 0.5")
VD = {  # tên: (x1, y1, x2, y2, điểm)
    "A": (10, 10, 50, 50, 0.92), "B": (12, 8, 52, 48, 0.85), "C": (16, 16, 56, 56, 0.60),
    "D": (60, 60, 90, 95, 0.80), "E": (63, 58, 93, 92, 0.55), "F": (30, 30, 70, 70, 0.40),
}
ten = list(VD)
hop = [VD[k][:4] for k in ten]
diem = [VD[k][4] for k in ten]
print("IoU giua cac hop:")
print("     " + "".join(f"{k:>7}" for k in ten))
for i, k in enumerate(ten):
    print(f"  {k}  " + "".join(f"{iou(hop[i], hop[j]):7.3f}" for j in range(len(ten))))
giu = nms(hop, diem, 0.5)
print("Giu lai:", [ten[i] for i in giu])


# =====================================================================
# (C) AP trên một bộ dữ liệu mô phỏng
# =====================================================================
head("(C) AP cua mot bo phat hien mo phong: 300 anh, mot lop doi tuong")
rng = np.random.default_rng(1207)
N_ANH = 300
gt, det = [], []      # gt[i]: các hộp thật của ảnh i; det: (ảnh, hộp, điểm)
for i in range(N_ANH):
    hops = []
    for _ in range(rng.integers(1, 5)):
        w, h = rng.uniform(15, 45, 2)
        x, y = rng.uniform(0, 100 - w), rng.uniform(0, 100 - h)
        hops.append((x, y, x + w, y + h))
    gt.append(hops)
    for (x1, y1, x2, y2) in hops:
        s = np.array([x2 - x1, y2 - y1, x2 - x1, y2 - y1])
        if rng.random() < 0.9:                               # phát hiện được
            b = np.array([x1, y1, x2, y2]) + rng.normal(0, 0.06, 4) * s
            det.append((i, tuple(b), float(np.clip(rng.normal(0.75, 0.12), 0.01, 0.99))))
        if rng.random() < 0.3:                               # thêm một hộp trùng, lệch hơn
            b = np.array([x1, y1, x2, y2]) + rng.normal(0, 0.12, 4) * s
            det.append((i, tuple(b), float(np.clip(rng.normal(0.55, 0.15), 0.01, 0.99))))
    for _ in range(rng.poisson(0.6)):                        # báo nhầm ở chỗ không có gì
        w, h = rng.uniform(10, 40, 2)
        x, y = rng.uniform(0, 100 - w), rng.uniform(0, 100 - h)
        det.append((i, (x, y, x + w, y + h), float(np.clip(rng.normal(0.35, 0.15), 0.01, 0.99))))
n_gt = sum(len(g) for g in gt)


def qua_nms(dets, nguong=0.5):
    ra = []
    for i in range(N_ANH):
        cua = [d for d in dets if d[0] == i]
        if not cua:
            continue
        giu_i = nms([d[1] for d in cua], [d[2] for d in cua], nguong)
        ra.extend(cua[k] for k in giu_i)
    return ra


def duong_pr(dets, nguong_iou):
    """Ghép kiểu COCO: xét hộp theo điểm giảm dần; mỗi hộp ghép với hộp thật chưa ghép
    có IoU lớn nhất; đủ ngưỡng thì là dương tính thật, không thì là dương tính giả."""
    da_ghep = [np.zeros(len(g), bool) for g in gt]
    tp = []
    for (i, b, s) in sorted(dets, key=lambda d: -d[2]):
        best, j_best = 0.0, -1
        for j, g in enumerate(gt[i]):
            if not da_ghep[i][j]:
                v = iou(b, g)
                if v > best:
                    best, j_best = v, j
        if best >= nguong_iou:
            da_ghep[i][j_best] = True
            tp.append(1)
        else:
            tp.append(0)
    tp = np.array(tp)
    tp_cum, fp_cum = np.cumsum(tp), np.cumsum(1 - tp)
    return tp_cum / n_gt, tp_cum / np.maximum(tp_cum + fp_cum, 1)


def ap_moi_diem(rec, prec):
    """Diện tích dưới đường bao của precision (cách tính của PASCAL VOC từ năm 2010)."""
    r = np.r_[0, rec, 1]
    p = np.r_[0, prec, 0]
    for k in range(len(p) - 2, -1, -1):
        p[k] = max(p[k], p[k + 1])
    idx = np.flatnonzero(r[1:] != r[:-1])
    return float(np.sum((r[idx + 1] - r[idx]) * p[idx + 1]))


def ap_101(rec, prec):
    """Trung bình precision nội suy tại 101 mức recall 0; 0,01; ...; 1 (cách tính của COCO)."""
    p = prec.copy()
    for k in range(len(p) - 2, -1, -1):
        p[k] = max(p[k], p[k + 1])
    out = []
    for t in np.linspace(0, 1, 101):
        k = np.searchsorted(rec, t, side="left")
        out.append(p[k] if k < len(p) else 0.0)
    return float(np.mean(out))


sau_nms = qua_nms(det)
print(f"So hop that: {n_gt}; so hop du doan: {len(det)} truoc NMS, {len(sau_nms)} sau NMS (nguong 0,5)")
r0, p0 = duong_pr(det, 0.5)
r5, p5 = duong_pr(sau_nms, 0.5)
r75, p75 = duong_pr(sau_nms, 0.75)
print(f"{'':28}{'AP (moi diem)':>15}{'AP (101 diem)':>15}{'recall cuoi':>13}")
print(f"{'IoU 0,5, khong NMS':28}{ap_moi_diem(r0, p0):15.3f}{ap_101(r0, p0):15.3f}{r0[-1]:13.3f}")
print(f"{'IoU 0,5, co NMS':28}{ap_moi_diem(r5, p5):15.3f}{ap_101(r5, p5):15.3f}{r5[-1]:13.3f}")
print(f"{'IoU 0,75, co NMS':28}{ap_moi_diem(r75, p75):15.3f}{ap_101(r75, p75):15.3f}{r75[-1]:13.3f}")
aps = []
for t in np.arange(0.5, 0.96, 0.05):
    rt, pt = duong_pr(sau_nms, t)
    aps.append(ap_101(rt, pt))
print("AP 101 diem theo nguong IoU:", " ".join(f"{t:.2f}:{a:.3f}" for t, a in zip(np.arange(0.5, 0.96, 0.05), aps)))
print(f"AP@[0,5:0,95] (trung binh 10 nguong, kieu COCO) = {np.mean(aps):.3f}")


# =====================================================================
# (D) Dice và IoU
# =====================================================================
head("(D) Dice = 2 IoU / (1 + IoU)")
for v in [0.3, 0.5, 0.7, 0.9]:
    print(f"  IoU {v:.1f} -> Dice {2 * v / (1 + v):.3f}")


# =====================================================================
# Hình
# =====================================================================
fig, ax = plt.subplots(1, 2, figsize=(9.2, 3.6))
a0 = ax[0]
for i, k in enumerate(ten):
    x1, y1, x2, y2 = hop[i]
    giu_k = i in giu
    a0.add_patch(Rectangle((x1, y1), x2 - x1, y2 - y1, fill=False,
                           lw=2.0 if giu_k else 1.0, ls="-" if giu_k else "--",
                           ec=C_MAIN if giu_k else C_BAD))
    # vị trí nhãn chọn tay để các nhãn không đè lên nhau
    vt = {"A": (x1 + 1, y1 + 1, "left", "top"), "B": (x2 - 1, y1 - 1, "right", "bottom"),
          "C": (x2 - 1, y2 - 1, "right", "bottom"), "D": (x1 + 1, y1 + 1, "left", "top"),
          "E": (x2 - 1, y1 - 1, "right", "bottom"), "F": (x1 + 1, y2 - 1, "left", "bottom")}[k]
    a0.annotate(f"{k} {diem[i]:.2f}".replace(".", ","), vt[:2], fontsize=7.5,
                ha=vt[2], va=vt[3], color=C_MAIN if giu_k else C_BAD)
a0.set_xlim(0, 100); a0.set_ylim(100, 0); a0.set_aspect("equal")
a0.set_title("NMS, ngưỡng IoU 0,5: nét liền giữ lại, nét đứt bị loại", fontsize=8.5)
a0.grid(False)

a1 = ax[1]
a1.plot(r0, p0, color=C_DIM, lw=1.3, ls="--", label=f"IoU 0,5, không NMS (AP {ap_moi_diem(r0, p0):.2f})".replace(".", ",", 1))
a1.plot(r5, p5, color=C_MAIN, lw=1.8, label=f"IoU 0,5, có NMS (AP {ap_moi_diem(r5, p5):.2f})".replace(".", ",", 1))
a1.plot(r75, p75, color=C_BAD, lw=1.6, label=f"IoU 0,75, có NMS (AP {ap_moi_diem(r75, p75):.2f})".replace(".", ",", 1))
a1.set_xlim(0, 1); a1.set_ylim(0, 1.02)
a1.set_xlabel("recall"); a1.set_ylabel("precision")
a1.set_title("Đường precision–recall của bộ phát hiện mô phỏng", fontsize=8.5)
a1.legend(frameon=False, fontsize=7.5, loc="lower left")
fig.tight_layout(); fig.savefig(OUT + "models15_detect.png"); plt.close(fig)
head("Xong. Da luu figs/models15_detect.png")
Kết quả chạy — thi_giac_output.txt
Kết quả chạy

==========================================================================
(A) IoU cua hai cap hop
==========================================================================
  (0, 0, 4, 4) va (2, 2, 6, 6): IoU = 0.1429
  (0, 0, 4, 4) va (1, 0, 5, 4): IoU = 0.6000
  (0, 0, 4, 4) va (0, 0, 4, 2): IoU = 0.5000

==========================================================================
(B) NMS tren sau hop du doan, nguong IoU 0.5
==========================================================================
IoU giua cac hop:
           A      B      C      D      E      F
  A    1.000  0.822  0.566  0.000  0.000  0.143
  B    0.822  1.000  0.562  0.000  0.000  0.141
  C    0.566  0.562  1.000  0.000  0.000  0.268
  D    0.000  0.000  0.000  1.000  0.716  0.039
  E    0.000  0.000  0.000  0.716  1.000  0.033
  F    0.143  0.141  0.268  0.039  0.033  1.000
Giu lai: ['A', 'D', 'F']

==========================================================================
(C) AP cua mot bo phat hien mo phong: 300 anh, mot lop doi tuong
==========================================================================
So hop that: 765; so hop du doan: 1107 truoc NMS, 882 sau NMS (nguong 0,5)
                              AP (moi diem)  AP (101 diem)  recall cuoi
IoU 0,5, khong NMS                    0.885          0.879        0.928
IoU 0,5, co NMS                       0.913          0.908        0.916
IoU 0,75, co NMS                      0.708          0.708        0.792
AP 101 diem theo nguong IoU: 0.50:0.908 0.55:0.908 0.60:0.894 0.65:0.865 0.70:0.829 0.75:0.708 0.80:0.425 0.85:0.136 0.90:0.014 0.95:0.000
AP@[0,5:0,95] (trung binh 10 nguong, kieu COCO) = 0.569

==========================================================================
(D) Dice = 2 IoU / (1 + IoU)
==========================================================================
  IoU 0.3 -> Dice 0.462
  IoU 0.5 -> Dice 0.667
  IoU 0.7 -> Dice 0.824
  IoU 0.9 -> Dice 0.947

==========================================================================
Xong. Da luu figs/models15_detect.png
==========================================================================

code/models/fig_diagrams.py

viết thêm cho repo này264 dòngTải về

Sinh Hình 1, 3, 6, 8, 12, 13: bốn họ mô hình, một cây quyết định và các vùng nó chia, lượt xuôi và lượt ngược của lan truyền ngược, trường tiếp nhận của CNN, cấu trúc một khối Transformer, và bảng so sánh Transformer 2017 với các lựa chọn hiện nay. Các sơ đồ được vẽ bằng matplotlib để sửa được cùng với nội dung bài.

Xem mã nguồn
Python
"""Sinh Hình 1, 3, 6, 8, 11 và 12 của giáo trình "Học sâu": các sơ đồ.

Đây là hình vẽ tay bằng matplotlib chứ không phải kết quả thí nghiệm, nhưng vẫn để
trong mã nguồn để sửa được và để mọi chi tiết khớp với chữ trong bài.

    python code/models/fig_diagrams.py
"""

import numpy as np
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.patches import FancyBboxPatch, FancyArrowPatch, Rectangle, Circle

OUT = "figs/"
plt.rcParams.update({"figure.dpi": 150, "font.size": 9})

INK, DIM = "#1f1d1a", "#8a857c"
TEAL, TEAL_BG = "#1f6f68", "#e0efed"
AMBER, AMBER_BG = "#8a5300", "#fbf0db"
RED, RED_BG = "#a13328", "#fbe9e6"
BLUE, BLUE_BG = "#1a5aa8", "#dceafb"
GREEN, GREEN_BG = "#2c6b39", "#e6f2e8"
PURPLE, PURPLE_BG = "#6b3b8a", "#efe4f7"


def box(ax, x, y, w, h, text, fc="white", ec=INK, fs=8.5, weight="normal", lw=1.1, r=0.02):
    ax.add_patch(FancyBboxPatch((x, y), w, h, boxstyle=f"round,pad=0,rounding_size={r}",
                                fc=fc, ec=ec, lw=lw, zorder=2))
    if text:
        ax.text(x + w / 2, y + h / 2, text, ha="center", va="center", fontsize=fs,
                color=INK, weight=weight, zorder=3, linespacing=1.45)


def arrow(ax, p, q, color=INK, lw=1.2, ls="-", rad=0.0):
    ax.add_patch(FancyArrowPatch(p, q, arrowstyle="-|>", mutation_scale=10, color=color,
                                 lw=lw, linestyle=ls, shrinkA=2, shrinkB=2, zorder=4,
                                 connectionstyle=f"arc3,rad={rad}"))


def canvas(w, h, xlim=(0, 12), ylim=(0, 6)):
    fig, ax = plt.subplots(figsize=(w, h))
    ax.set_xlim(*xlim); ax.set_ylim(*ylim); ax.axis("off")
    return fig, ax


# ==================================================== Hình 1 — bản đồ họ mô hình
fig, ax = canvas(8.6, 4.0, (0, 12), (0, 7.4))
ax.text(6, 7.1, "Bốn câu hỏi thiết kế, và các họ mô hình trả lời chúng khác nhau",
        ha="center", fontsize=9.5, weight="bold", color=INK)
cols = [
    ("Mô hình tuyến tính", BLUE_BG, BLUE, 0.2,
     ["giả thiết: quan hệ tuyến tính", "tham số: ít, giải thích được",
      "mạnh: dữ liệu ít, cần diễn giải", "yếu: quan hệ phi tuyến"]),
    ("Cây và tập hợp", GREEN_BG, GREEN, 3.15,
     ["giả thiết: chia trục toạ độ", "tham số: nhiều, phi tham số",
      "mạnh: dữ liệu bảng, đặc trưng lẫn lộn", "yếu: ngoại suy, dữ liệu thô"]),
    ("Mạng nơ-ron", AMBER_BG, AMBER, 6.1,
     ["giả thiết: hợp thành nhiều tầng", "tham số: rất nhiều",
      "mạnh: dữ liệu thô có cấu trúc", "yếu: cần nhiều dữ liệu"]),
    ("Transformer", PURPLE_BG, PURPLE, 9.05,
     ["giả thiết: quan hệ cặp giữa vị trí", "tham số: rất nhiều, song song tốt",
      "mạnh: chuỗi dài, mọi phương thức", "yếu: chi phí bậc hai theo độ dài"]),
]
for name, fc, ec, x, items in cols:
    box(ax, x, 0.5, 2.75, 5.9, "", fc=fc, ec=ec, lw=1.3)
    ax.text(x + 1.375, 5.95, name, ha="center", fontsize=9, weight="bold", color=ec)
    for i, it in enumerate(items):
        ax.text(x + 0.18, 5.25 - i * 1.28, "•", fontsize=9, color=ec, va="top")
        ax.text(x + 0.45, 5.25 - i * 1.28, it, fontsize=7.8, va="top", color=INK,
                linespacing=1.4, wrap=True)
for x in (2.98, 5.93, 8.88):
    arrow(ax, (x, 3.4), (x + 0.15, 3.4), color=DIM)
ax.text(6, 0.1, "Trục chung: càng sang phải thì càng ít giả thiết về dạng hàm, "
                "càng cần nhiều dữ liệu và càng khó diễn giải.",
        ha="center", fontsize=8, color=DIM)
fig.tight_layout(); fig.savefig(OUT + "models01_families.png"); plt.close(fig)


# ============================================ Hình 3 — cây chia không gian
fig, axes = plt.subplots(1, 2, figsize=(8.2, 3.2))
rng = np.random.default_rng(4)
X = rng.uniform(0, 1, (260, 2))
y = ((X[:, 0] > 0.45) & (X[:, 1] > 0.55)) | ((X[:, 0] <= 0.45) & (X[:, 1] > 0.25))
ax = axes[0]
ax.scatter(X[~y, 0], X[~y, 1], s=11, color=RED, alpha=.7, label="lớp 0")
ax.scatter(X[y, 0], X[y, 1], s=11, color=TEAL, alpha=.7, label="lớp 1")
ax.axvline(0.45, color=INK, lw=1.4)
ax.plot([0, 0.45], [0.25, 0.25], color=INK, lw=1.4)
ax.plot([0.45, 1], [0.55, 0.55], color=INK, lw=1.4)
ax.set_xlim(0, 1); ax.set_ylim(0, 1); ax.set_xlabel("$x_1$"); ax.set_ylabel("$x_2$")
ax.set_title("Cây chia không gian bằng các nhát cắt\nvuông góc với trục", fontsize=9)
ax.legend(frameon=False, fontsize=8, loc="lower left")

ax = axes[1]; ax.axis("off"); ax.set_xlim(0, 10); ax.set_ylim(0, 10)
box(ax, 3.4, 8.2, 3.2, 1.2, "$x_1 \\leq 0{,}45$ ?", fc=BLUE_BG, ec=BLUE, fs=9)
box(ax, 0.6, 5.4, 3.0, 1.2, "$x_2 \\leq 0{,}25$ ?", fc=BLUE_BG, ec=BLUE, fs=9)
box(ax, 6.4, 5.4, 3.0, 1.2, "$x_2 \\leq 0{,}55$ ?", fc=BLUE_BG, ec=BLUE, fs=9)
for x, lab, col, bg in [(0.2, "lớp 0", RED, RED_BG), (2.4, "lớp 1", TEAL, TEAL_BG),
                        (5.9, "lớp 0", RED, RED_BG), (8.1, "lớp 1", TEAL, TEAL_BG)]:
    box(ax, x, 2.9, 1.8, 1.0, lab, fc=bg, ec=col, fs=8.5)
for a, b, lab in [((4.4, 8.2), (2.1, 6.6), "đúng"), ((5.6, 8.2), (7.9, 6.6), "sai"),
                  ((1.4, 5.4), (1.1, 3.9), "đúng"), ((2.8, 5.4), (3.3, 3.9), "sai"),
                  ((7.2, 5.4), (6.8, 3.9), "đúng"), ((8.6, 5.4), (9.0, 3.9), "sai")]:
    arrow(ax, a, b, color=DIM)
    ax.text((a[0] + b[0]) / 2 - 0.35, (a[1] + b[1]) / 2, lab, fontsize=7, color=DIM)
ax.set_title("Cùng một mô hình, viết dưới dạng cây", fontsize=9)
fig.tight_layout(); fig.savefig(OUT + "models03_tree.png"); plt.close(fig)


# =============================== Hình 6 — đồ thị tính toán và lan truyền ngược
fig, ax = canvas(8.6, 3.4, (0, 12), (0, 6.2))
ax.text(6, 5.9, "Lan truyền ngược là quy tắc dây chuyền áp dụng theo thứ tự ngược",
        ha="center", fontsize=9.5, weight="bold", color=INK)
# (nhãn, toạ độ trái, độ rộng): độ rộng ghi rõ để hộp ŷ không đè lên hộp hàm mất mát
nodes = [("$x$", 0.3, 1.0), ("$z_1 = W_1 x$", 2.0, 2.1), ("$a_1 = \\sigma(z_1)$", 4.5, 2.1),
         ("$z_2 = W_2 a_1$", 7.0, 2.1), ("$\\hat{y}$", 9.5, 1.0), ("$\\mathcal{L}$", 11.0, 0.9)]
for t, x, w in nodes:
    box(ax, x, 3.3, w, 1.0, t, fc=TEAL_BG, ec=TEAL, fs=8.5)
for (_, x0, w0), (_, x1, _) in zip(nodes[:-1], nodes[1:]):
    arrow(ax, (x0 + w0, 3.8), (x1, 3.8), color=TEAL, lw=1.4)
ax.text(0.3, 4.6, "lượt xuôi: tính và ghi lại mọi giá trị trung gian", fontsize=8, color=TEAL)

for t, x, w in [("$\\frac{\\partial \\mathcal{L}}{\\partial x}$", 0.3, 1.0),
                ("$\\frac{\\partial \\mathcal{L}}{\\partial z_1}$", 2.0, 2.1),
                ("$\\frac{\\partial \\mathcal{L}}{\\partial a_1}$", 4.5, 2.1),
                ("$\\frac{\\partial \\mathcal{L}}{\\partial z_2}$", 7.0, 2.1),
                ("$\\frac{\\partial \\mathcal{L}}{\\partial \\hat{y}}$", 9.5, 1.0)]:
    box(ax, x, 1.2, w, 1.0, t, fc=RED_BG, ec=RED, fs=11)
for a, b in zip([2.0, 4.5, 7.0, 9.5], [0.3 + 1.0, 2.0 + 2.1, 4.5 + 2.1, 7.0 + 2.1]):
    arrow(ax, (a, 1.7), (b, 1.7), color=RED, lw=1.4)
arrow(ax, (11.45, 3.3), (10.5, 2.2), color=RED, lw=1.4, rad=-0.2)
ax.text(0.3, 0.55, "lượt ngược: nhân dần các đạo hàm cục bộ, "
                   "dùng lại đúng những giá trị đã ghi ở lượt xuôi", fontsize=8, color=RED)
fig.tight_layout(); fig.savefig(OUT + "models06_backprop.png"); plt.close(fig)


# ===================================== Hình 8 — tích chập và trường tiếp nhận
fig, ax = canvas(8.6, 3.4, (0, 12), (0, 6.4))
ax.text(6, 6.1, "Mỗi lớp tích chập 3×3 nới trường tiếp nhận thêm 2 ô",
        ha="center", fontsize=9.5, weight="bold", color=INK)
for li, (x0, rf, col, bg) in enumerate([(0.3, 1, BLUE, BLUE_BG), (4.2, 3, GREEN, GREEN_BG),
                                        (8.1, 5, AMBER, AMBER_BG)]):
    n = 7
    cell = 0.42
    ox, oy = x0 + 0.35, 1.5
    for i in range(n):
        for j in range(n):
            inside = abs(i - 3) <= rf // 2 and abs(j - 3) <= rf // 2
            ax.add_patch(Rectangle((ox + j * cell, oy + i * cell), cell * .92, cell * .92,
                                   fc=bg if inside else "white", ec=DIM, lw=.6, zorder=2))
    ax.add_patch(Rectangle((ox + 3 * cell, oy + 3 * cell), cell * .92, cell * .92,
                           fc=col, ec=col, lw=.6, zorder=3))
    ax.text(x0 + 1.8, oy - 0.45, f"sau {li} lớp: trường tiếp nhận {rf}×{rf}",
            ha="center", fontsize=8.2, color=col, weight="bold")
    ax.text(x0 + 1.8, oy + n * cell + 0.25, ["đầu vào", "một lớp 3×3", "hai lớp 3×3"][li],
            ha="center", fontsize=8.5, color=INK)
for x in (3.85, 7.75):
    arrow(ax, (x, 3.0), (x + 0.3, 3.0), color=DIM)
ax.text(6, 0.45, "Trường tiếp nhận lý thuyết tăng TUYẾN TÍNH theo số lớp: $r_L = 1 + L(k-1)$ với bước nhảy 1.\n"
                 "Muốn nó tăng nhanh hơn thì phải giảm độ phân giải (bước nhảy > 1, gộp) hoặc giãn hạt nhân.",
        ha="center", fontsize=8, color=DIM, linespacing=1.5)
fig.tight_layout(); fig.savefig(OUT + "models08_cnn.png"); plt.close(fig)


# ======================================= Hình 11 — một khối Transformer đầy đủ
fig, ax = canvas(7.6, 6.4, (0, 12), (0, 14.0))
ax.text(5.6, 13.65, "Một khối Transformer: hai khối con, mỗi khối con có kết nối tắt",
        ha="center", fontsize=9.5, weight="bold", color=INK)

CX, CW = 2.4, 6.6            # cot chinh
RES = 9.9                     # duong tat ben phai
mid = CX + CW / 2

def up(y0, y1, color=INK, lw=1.3, x=None):
    arrow(ax, (x if x else mid, y0), (x if x else mid, y1), color=color, lw=lw)

# --- dau vao
box(ax, CX + 1.5, 0.3, 3.6, 0.8, "$x \\in \\mathbb{R}^{T \\times d}$", fc="white", ec=DIM, fs=9)

# --- khoi con 1 (doc tu duoi len)
box(ax, CX - 0.45, 1.9, CW + 0.9, 5.3, "", fc=TEAL_BG, ec=TEAL, lw=1.3)
ax.text(CX - 0.25, 6.95, "Khối con 1 — trộn thông tin GIỮA các vị trí",
        fontsize=8.2, color=TEAL, weight="bold")
box(ax, CX + 1.9, 2.15, 2.8, 0.7, "LayerNorm", fc="white", ec=TEAL, fs=8.2)
for lab, x in [("$W_Q$", CX + 0.1), ("$W_K$", CX + 2.4), ("$W_V$", CX + 4.7)]:
    box(ax, x, 3.45, 1.8, 0.65, lab, fc="white", ec=TEAL, fs=8.5)
    arrow(ax, (mid, 2.85), (x + 0.9, 3.45), color=TEAL, rad=-0.06)
    up(4.1, 4.55, color=TEAL, x=x + 0.9)
box(ax, CX, 4.55, CW, 0.95,
    "softmax$\\left(\\frac{QK^\\top}{\\sqrt{d_k}} + M\\right)V$   ·   $h$ đầu song song",
    fc="white", ec=TEAL, fs=8.5)
box(ax, CX + 1.9, 6.0, 2.8, 0.7, "$W_O$", fc="white", ec=TEAL, fs=8.5)
up(5.5, 6.0, color=TEAL)
up(1.1, 2.15, color=DIM)                      # x -> LayerNorm

# --- cong tat 1
ax.add_patch(Circle((mid, 8.0), 0.34, fc="white", ec=INK, lw=1.3, zorder=5))
ax.text(mid, 8.0, "+", ha="center", va="center", fontsize=13, color=INK, zorder=6)
up(6.7, 7.66, color=TEAL)

arrow(ax, (mid + 1.9, 0.7), (RES, 0.7), color=DIM, ls=(0, (4, 2)))
arrow(ax, (RES, 0.7), (RES, 8.0), color=DIM, ls=(0, (4, 2)))
arrow(ax, (RES, 8.0), (mid + 0.36, 8.0), color=DIM)

# --- khoi con 2 (doc tu duoi len)
box(ax, CX - 0.45, 9.1, CW + 0.9, 2.9, "", fc=AMBER_BG, ec=AMBER, lw=1.3)
ax.text(CX - 0.25, 11.75, "Khối con 2 — biến đổi TỪNG vị trí, độc lập nhau",
        fontsize=8.2, color=AMBER, weight="bold")
box(ax, CX + 1.9, 9.35, 2.8, 0.7, "LayerNorm", fc="white", ec=AMBER, fs=8.2)
box(ax, CX, 10.45, CW, 0.95,
    "$W_2\\,\\phi(W_1 x)$   ·   $d_{\\mathrm{ff}} = 4d$   ·   $\\phi$ = ReLU / GELU / SwiGLU",
    fc="white", ec=AMBER, fs=8.2)
up(10.05, 10.45, color=AMBER)
up(8.34, 9.35, color=DIM)

# --- cong tat 2
ax.add_patch(Circle((mid, 12.6), 0.34, fc="white", ec=INK, lw=1.3, zorder=5))
ax.text(mid, 12.6, "+", ha="center", va="center", fontsize=13, color=INK, zorder=6)
up(11.4, 12.26, color=AMBER)
arrow(ax, (mid + 1.2, 8.55), (RES, 8.55), color=DIM, ls=(0, (4, 2)))
arrow(ax, (RES, 8.55), (RES, 12.6), color=DIM, ls=(0, (4, 2)))
arrow(ax, (RES, 12.6), (mid + 0.36, 12.6), color=DIM)
ax.text(RES + 0.2, 6.0, "kết nối tắt", fontsize=7.8, color=DIM, rotation=90, va="center")

box(ax, CX + 1.5, 12.95, 3.6, 0.55, "sang khối tiếp theo", fc="white", ec=DIM, fs=8.2)
up(12.94, 12.95, color=INK)

ax.text(0.05, 3.6,
        "Thứ tự ở đây\nlà pre-LN:\nchuẩn hoá TRƯỚC\nkhối con, nên\nđường tắt đi\nthẳng từ đầu vào\ntới đầu ra mà\nkhông qua phép\nchuẩn hoá nào.",
        fontsize=7.4, color=INK, va="center", linespacing=1.7)
fig.tight_layout(); fig.savefig(OUT + "models11_block.png"); plt.close(fig)


# ============================ Hình 14 — Transformer 2017 so với Transformer nay
fig, ax = canvas(8.6, 4.2, (0, 12), (0, 8.0))
ax.text(6, 7.7, "Cùng một bộ khung, năm chi tiết đã đổi", ha="center",
        fontsize=9.5, weight="bold", color=INK)
rows = [
    ("Vị trí trong khối", "Post-LN: $x + \\mathrm{Sublayer}(x)$ rồi mới LN",
     "Pre-LN: LN trước, tắt đi thẳng", "huấn luyện ổn định hơn, ít cần warmup"),
    ("Chuẩn hoá", "LayerNorm (trừ trung bình, chia độ lệch chuẩn)",
     "RMSNorm (chỉ chia chuẩn bậc hai)", "rẻ hơn, gần như không mất chất lượng"),
    ("Vị trí token", "Sin/cos cộng vào embedding",
     "RoPE: xoay $Q$, $K$ theo vị trí", "ngoại suy độ dài tốt hơn, quan hệ tương đối"),
    ("Đầu attention", "MHA: mỗi đầu một cặp $K$, $V$",
     "GQA/MQA: nhiều đầu dùng chung $K$, $V$", "KV cache nhỏ đi theo đúng tỉ lệ nhóm"),
    ("Hàm kích hoạt FFN", "ReLU, $d_{\\mathrm{ff}} = 4d$",
     "SwiGLU, $d_{\\mathrm{ff}} \\approx \\frac{2}{3}\\cdot 4d$", "ba ma trận thay vì hai, bù bằng bề rộng nhỏ hơn"),
]
box(ax, 0.2, 6.3, 2.6, 0.85, "Thành phần", fc="#f4f2ec", ec=DIM, fs=8.5, weight="bold")
box(ax, 2.9, 6.3, 3.4, 0.85, "Transformer 2017", fc=RED_BG, ec=RED, fs=8.5, weight="bold")
box(ax, 6.4, 6.3, 3.4, 0.85, "Phổ biến hiện nay", fc=GREEN_BG, ec=GREEN, fs=8.5, weight="bold")
box(ax, 9.9, 6.3, 1.9, 0.85, "Vì sao đổi", fc="#f4f2ec", ec=DIM, fs=8.5, weight="bold")
for i, (comp, old, new, why) in enumerate(rows):
    y = 5.1 - i * 1.12
    box(ax, 0.2, y, 2.6, 1.0, comp, fc="white", ec=DIM, fs=7.8)
    box(ax, 2.9, y, 3.4, 1.0, old, fc=RED_BG, ec=RED, fs=7.6)
    box(ax, 6.4, y, 3.4, 1.0, new, fc=GREEN_BG, ec=GREEN, fs=7.6)
    box(ax, 9.9, y, 1.9, 1.0, why, fc="white", ec=DIM, fs=7.0)
fig.tight_layout(); fig.savefig(OUT + "models14_modern.png"); plt.close(fig)

print("Đã lưu Hình 1, 3, 6, 8, 11 và 12 vào " + OUT)