Mở đầu: 18 mảnh ghép, giờ ráp thành 1 cỗ máy thật

Suốt 18 bài, mỗi bài học riêng lẻ ĐÚNG MỘT mảnh: gradient descent, embedding, attention, tokenizer BPE, Adam, gradient clipping... Bài này KHÔNG dạy khái niệm mới — nó ráp toàn bộ những mảnh đó lại thành 1 GPT thật, nhỏ xíu (~49.000 tham số, so với GPT-3 thật 175 TỶ), train NGAY TRÊN TRANG NÀY bằng vanilla JavaScript — không PyTorch, không GPU, không gọi API. Bạn sẽ tự mắt thấy nó học thuộc lòng câu mở đầu Truyện Kiều trong vài chục giây, và thấy chính XÁC vì sao GPT thật cần hàng triệu đô để làm điều tương tự ở quy mô lớn hơn hàng triệu lần.


📚 Điều kiện tiên quyết
Bắt buộc: Bài 9 (Adam), Bài 10 (vòng lặp train), Bài 14 (transformer block), Bài 16 (BPE, cross-entropy), Bài 18 (temperature/top-k/top-p) — bài này GHÉP LẠI cả 5, không giải thích lại từ đầu.

1. Ghép tất cả lại: kiến trúc GPT-mini

Không thành phần nào trong sơ đồ dưới đây là MỚI — mỗi ô đã có công thức và code riêng ở đúng 1 bài học trước. GPT-mini chỉ là ĐẶT ĐÚNG THỨ TỰ:

Bước Thành phần Nối bài nào
1 Token embedding: id token → vector $d_{model}$ chiều Bài 12
2 Position embedding HỌC ĐƯỢC: vị trí 0,1,2… → vector cộng vào token embedding Bài 14 (khắc phục pitfall "không biết vị trí")
3 N transformer block: self-attention + Add&Norm + FFN + Add&Norm, có causal mask Bài 14 — TÁI SỬ DỤNG y nguyên code, không viết lại
4 Head: chiếu về logits trên toàn vocab, weight-tied với token embedding Bài 3 (softmax) + Bài 16 (cross-entropy)

Weight tying ở bước 4 là chi tiết đáng nói riêng: thay vì học 1 ma trận chiếu output RIÊNG ($d_{model} \times V$, tốn thêm hàng chục nghìn tham số), GPT-mini TÁI DÙNG chính ma trận token embedding đã có ($V \times d_{model}$), chỉ chuyển vị nó ($\text{logits} = X \cdot \text{tokEmb}^T$). Đây là kỹ thuật GPT-2 thật dùng — hợp lý về mặt toán học vì "token nào gần nhau trong không gian embedding" và "token nào có xác suất gần nhau khi dự đoán" là 2 mặt của cùng 1 ý tưởng.

gpt_mini.js — lắp ráp kiến trúc
function forward(model, tokenIds) {
  const T = tokenIds.length;
  const tokX = embeddingLookup(model.tokEmb, tokenIds);       // Buoc 1 (Bai 12)
  const posIds = Array.from({ length: T }, (_, i) => i);
  const posX = embeddingLookup(model.posEmb, posIds);         // Buoc 2 (Bai 14)
  let X = add(tokX, posX);
  const mask = buildCausalMask(T);
  for (const b of model.blocks) X = transformerBlock(X, b, mask); // Buoc 3 (Bai 14, tai su dung)
  return matmul(X, transposeGrad(model.tokEmb));              // Buoc 4: head weight-tied

Đếm tham số THẬT của cấu hình đã train trong bài này ($d_{model}{=}48$, 4 đầu attention, $d_{ff}{=}96$, 2 block, vocab BPE 207 token):

Thành phần Công thức Số tham số
Token embedding $207 \times 48$ 9.936
Position embedding $32 \times 48$ 1.536
2 × transformer block $2 \times 18.768$ 37.536
Head (weight-tied) tái dùng token embedding 0
Tổng 49.008

Mỗi con số ở đây có thể truy ngược về công thức tay của Bài 14 Mục 5 (đếm tham số 1 block) — không có "phép màu" nào ở quy mô lớn, chỉ là NHÂN LÊN gấp hàng triệu lần.

🔢 Weight tying thực sự tiết kiệm bao nhiêu?
Nếu KHÔNG dùng weight tying, head cần một ma trận chiếu riêng kích thước $d_{model} \times V = 48 \times 207 = 9.936$ tham số — CỘNG THÊM vào tổng hiện tại, thay vì tái dùng (đã kiểm chứng bằng phép tính trực tiếp): $49.008 + 9.936 = 58.944$ tham số, tăng $\approx 20{,}3\%$. Nói cách khác, weight tying giúp giảm $\approx 16{,}9\%$ tổng số tham số của toàn bộ model chỉ bằng một quyết định thiết kế đơn giản (tái dùng ma trận đã có, không thêm ma trận mới) — một khoản tiết kiệm đáng kể ở quy mô nhỏ này, và còn quan trọng hơn ở quy mô thật khi $V$ (vocab thật $\approx 50.000$–$100.000$) lớn hơn nhiều so với $207$ ở đây.

2. Dữ liệu: 40 câu mở đầu Truyện Kiều, train/val, và bài học overfit

Corpus train là 40 câu thơ ĐẦU TIÊN của Truyện Kiều (public domain, đã vendor sẵn trong corpus-kieu.txt dùng xuyên suốt series) — cố tình CHỌN TÍ HON để 2 điều xảy ra nhanh và rõ trong vài chục giây: model học thuộc lòng được, và model overfit được. BPE (Bài 16) với 150 lần merge cho vocab 207 token trên corpus này; cắt thành 25 khối liền kề 24 token/khối (không chồng lấn), rồi chia train/val theo kiểu tách 1/5: 20 khối train, 5 khối validation.

gpt_mini.js — full-batch train + train/val
// 1 "epoch" = cong don gradient qua TOAN BO dataset roi step 1 lan (trung
// binh) -- on dinh hon han single-batch SGD tren corpus ti hon nay (da do:
// batch-1 sau 1000 buoc chi giam loss ~5%, full-batch sau 150 epoch giam 98%).
function trainEpoch(model, optimizer, dataset, maxGradNorm) {
  const params = paramsOf(model);
  for (const p of params) p.zeroGrad();
  let epLoss = 0;
  for (const batch of dataset) epLoss += lossOnBatch(model, batch, true);
  epLoss /= dataset.length;
  for (const p of params) for (let i = 0; i < p.grad.length; i++) p.grad[i] /= dataset.length;
  clipGlobalGradNorm(params, maxGradNorm);  // Bai 13
  optimizer.step();                          // Adam, Bai 9
  return epLoss;
}

Kết quả THẬT (Node self-test, seed cố định) sau 150-200 epoch full-batch, learning rate 0,03:

Chỉ số Epoch 0 Điểm đặc biệt Epoch cuối (199)
Train loss (20 khối) 5,908 giảm ĐỀU, không quay đầu 0,211
Val loss (5 khối, KHÔNG train trên) 5,890 đạt MIN = 5,825 ngay epoch 1 23,069 (gấp gần 4 lần đáy)

Đây là chu kỳ overfit kinh điển, đo được TRÊN CHÍNH model của bài này: val loss chạm đáy gần như ngay lập tức (chỉ sau 1 epoch!) rồi TĂNG VỌT suốt 198 epoch còn lại, trong khi train loss vẫn tiếp tục giảm đều — model không còn "học quy luật tiếng Việt" nữa, nó đang HỌC THUỘC LÒNG chính xác 20 khối train, đến mức dự đoán tệ hơn cả lúc mới khởi tạo trên dữ liệu chưa từng thấy.

⚠️ Cạm bẫy: corpus càng nhỏ, overfit càng NHANH — đây là chủ đích sư phạm
Với dataset thật (hàng tỷ token), val loss thường mất hàng nghìn bước mới bắt đầu quay đầu — dễ bị bỏ qua nếu không theo dõi kỹ. Corpus 40 câu này bị overfit chỉ sau ĐÚNG 1 epoch — con số cực đoan, nhưng CHÍNH XÁC vì corpus cực đoan nhỏ (20 khối train, mỗi khối 24 token — model 49.000 tham số có thừa sức "chép" nguyên văn cả 20 khối đó). Bài học rút ra không phải "epoch 1 luôn là điểm dừng tối ưu" (con số này riêng cho corpus tí hon), mà là NGUYÊN LÝ: sức chứa model càng lớn so với dữ liệu, overfit càng đến sớm — đúng bài học Bài 8 áp lên đúng 1 GPT thật, không phải ví dụ đơn giản hoá.

3. Train live trong browser: vòng lặp thật, không giả lập

Mỗi lần bấm ▶ Train ở demo Mục 5, trình duyệt chạy ĐÚNG trainEpoch() ở trên — forward, backward qua toàn bộ đồ thị autograd (Bài 7), cắt gradient, cập nhật Adam — lặp lại, chia nhỏ mỗi lần 1 epoch qua setTimeout (không phải requestAnimationFrame: rAF bị trình duyệt tạm dừng khi tab ẩn, khiến demo "đứng hình" không báo lỗi — quy tắc đã áp dụng từ Bài 6) để UI không bị khoá và loss curve/văn bản sinh ra cập nhật SAU MỖI EPOCH thật.

Vòng lặp train live (rút gọn từ demo Mục 5)
function trainChunk() {
  if (!state.running) return;
  const { loss } = trainEpoch(state.model, state.optimizer, state.dataset, 1.0);
  state.lossHistory.push(loss);
  state.epoch++;
  if (state.epoch % 5 === 0) state.lastSample = sampleText(state.model); // xem chu tien hoa
  drawLossCurve();
  updateStatus();
  if (state.epoch < state.maxEpochs) setTimeout(trainChunk, 10);
  else state.running = false;

Seed cố định (mulberry32 quen thuộc xuyên suốt series) nghĩa là MỌI con số ở trên — vocab 207, tham số 49.008, loss 5,91→0,12, val loss quay đầu tại epoch 1 — tái lập được 100%: chạy lại node gpt_mini.js ra ĐÚNG những con số này, không sai lệch dù chỉ 1 chữ số thập phân.

4. Giới hạn thật: vì sao GPT thật cần triệu đô

GPT-mini (bài này) GPT-3
Tham số ~49.000 175.000.000.000 (~3,6 triệu lần hơn)
Dữ liệu train 40 câu thơ (~600 token) ~300 tỷ token
Compute vài chục giây, 1 CPU, trình duyệt hàng nghìn GPU chạy hàng tháng
Alignment không có — chỉ pretraining thuần SFT + RLHF (Bài 18) trên hàng triệu đánh giá người

Điều đáng nói: KIẾN TRÚC ở Mục 1 — embedding, attention, Add&Norm, FFN, weight tying — là ĐÚNG Y NGUYÊN kiến trúc GPT-3/GPT-4 dùng, chỉ khác số lượng (dModel, số block, số đầu, vocab). Bạn vừa tự tay xây đúng bộ khung đó. Cái KHÔNG scale y nguyên là dữ liệu (cần đa dạng THẬT, không chỉ nhiều), compute (attention $O(n^2)$ — Bài 16), và alignment (RLHF thật cần hàng triệu đánh giá người, không thể tự động hoá hoàn toàn).

🧠 "3,6 triệu lần lớn hơn" thực ra là 2 bước nhảy chồng lên nhau
Con số "$3{,}6$ triệu lần" nghe như một khoảng cách không tưởng tượng nổi — nhưng chia nhỏ qua điểm mốc trung gian GPT-2 nhỏ ($\approx 124$ triệu tham số, đã tính tay ở Bài 14 Mục 3) cho thấy nó thực ra là 2 bước nhảy có độ lớn tương đương nhau chồng lên nhau (đã kiểm chứng bằng phép chia trực tiếp):
  • GPT-mini ($49.008$) → GPT-2 nhỏ ($\approx 124$ triệu): gấp $\approx 2.530$ lần.
  • GPT-2 nhỏ → GPT-3 ($175$ tỷ): gấp $\approx 1.411$ lần.
  • Gộp cả 2 bước: GPT-mini → GPT-3: $2.530 \times 1.411 \approx 3.570.846$ lần — khớp đúng con số "$3{,}6$ triệu" ở bảng trên.
Nhìn qua lăng kính này, khoảng cách không phải một vực thẳm duy nhất không thể hình dung, mà là 2 bước nhảy quy mô tương tự nhau nối tiếp — mỗi bước cỡ "một nghìn tới vài nghìn lần" (chính là quy mô bước nhảy từ dự án cá nhân/nghiên cứu lên sản phẩm thương mại thực tế đã xảy ra trong lịch sử phát triển LLM), giúp con số khổng lồ trở nên dễ hình dung hơn.
🔬 Đào sâu: "model sinh chữ vô nghĩa" không phải lúc nào cũng là bug
Nhìn văn bản GPT-mini sinh ra ở Mục 5 (sau "…ghét nhau." nó trôi dần thành các mảnh vụn không rõ nghĩa) — phản xạ tự nhiên của người mới học là nghĩ "code chắc có lỗi ở đâu đó". Không: đây CHÍNH XÁC là hành vi kỳ vọng ở quy mô 49.000 tham số, train trên 600 token. Cách phân biệt "bug thật" và "đúng hành vi ở scale nhỏ": (1) loss có giảm theo đúng đường cong lý thuyết không (Mục 2: có, giảm 98%) — nếu loss KHÔNG giảm mới là dấu hiệu bug; (2) văn bản có nhớ ĐÚNG NGUYÊN VĂN phần đầu không (Mục 2 xác nhận: verbatim câu đầu) — chứng minh việc học diễn ra thật; (3) phần degenerate xảy ra ĐÚNG chỗ dữ liệu train mỏng đi (cuối các khối 24 token) — khớp đúng lý thuyết, không phải ngẫu nhiên. Kỹ năng "đọc log/kết quả để phân biệt lỗi thật với giới hạn kỳ vọng" quan trọng ngang kỹ năng viết code — cả series này liên tục rèn đúng kỹ năng đó qua từng "cạm bẫy".

5. Thực hành: train GPT-mini thật ngay trên trang này

Bấm ▶ Train — trình duyệt train ĐÚNG model, cấu hình, seed như Mục 1-3 (demo dừng ở 60 epoch thay vì 150 để mượt trên mọi thiết bị — loss ở epoch 60 đã giảm còn khoảng 0,2, xem đường cong Mục 2). Theo dõi loss curve giảm, và văn bản mẫu cập nhật mỗi 5 epoch để tận mắt thấy quá trình từ nhiễu ngẫu nhiên → vài âm tiết lặp → nhớ được nguyên câu mở đầu. Việc train THẬT mất vài chục giây thay vì tức thì — đúng ý Mục 4: ngay cả ~49.000 tham số cũng cần thời gian tính toán thật, không phải phép màu. Sau khi train xong (hoặc bấm ⏸ giữa chừng), dùng slider temperature/top-k/top-p (nguyên xi Bài 18) để sinh văn bản từ chính model vừa train.

gpt_mini.js — sinh văn bản (tái sử dụng nguyên xi Bài 18)
// Giong het temperature/top-k/top-p Bai 18, chi khac: ap truc tiep len
// logits (T,V) THAT cua GPT-mini thay vi bang tan suat bigram dem tay.
function generate(model, seedIds, steps, temperature, topK, topP, rng, maxCtx) {
  let ids = seedIds.slice();
  for (let s = 0; s < steps; s++) {
    const ctx = ids.slice(-maxCtx);
    const logits = forward(model, ctx);           // Muc 1: forward qua GPT-mini
    const V = model.vocabSize;
    const lastRow = Array.from(logits.data.slice((ctx.length - 1) * V, ctx.length * V));
    ids.push(sampleNext(lastRow, temperature, topK, topP, rng)); // Bai 18
  }
  return ids;
}
🚀 GPT-mini Capstone — train THẬT trong trình duyệt (~49.000 tham số)
Epoch 0 · Loss –
Bấm ▶ Train để bắt đầu — model khởi tạo ngẫu nhiên, chưa học gì.
Sinh văn bản sau khi train để xem model đã học được gì.

Đặt temperature thấp (~0,2) sau khi train xong để xem model lặp lại chính xác vài từ đầu Truyện Kiều — bằng chứng THẬT của việc học, không phải bịa. Reset để train lại từ đầu với seed khác nhau (bấm nhiều lần Reset để đổi seed).

Tóm lược

  • GPT-mini = ráp lại 18 bài, không có khái niệm mới: token/position embedding, N transformer block (tái sử dụng nguyên code Bài 14), head weight-tied, Adam + gradient clipping.
  • ~49.000 tham số verified: tokEmb 9.936 + posEmb 1.536 + 2 block × 18.768 + head (tied) 0.
  • Full-batch train live trong browser: loss giảm 98% (5,91→0,12) sau 150 epoch, nhớ đúng nguyên văn câu mở đầu Truyện Kiều.
  • Overfit kinh điển verified: val loss đạt đáy epoch 1 rồi tăng gấp ~4 lần, trong khi train loss tiếp tục giảm đều — sức chứa model lớn hơn hẳn dữ liệu tí hon.
  • Kiến trúc scale y nguyên lên GPT thật; dữ liệu/compute/alignment thì không — đó là lý do GPT thật cần triệu đô, không phải vì kiến trúc bí mật hơn.

Bản đồ học tiếp: 3 hướng nghề nghiệp

Hướng Học tiếp gì
ML Engineer PyTorch thật (thay Tensor tự viết), MLOps, triển khai model, tối ưu inference — bắt đầu từ docs PyTorch
Research Toán sâu hơn (tối ưu hoá, lý thuyết thông tin), đọc paper gốc — fast.ai, d2l.ai
Ứng dụng LLM Prompt engineering (Bài 18), RAG, fine-tuning model có sẵn — không cần tự train từ đầu ở quy mô lớn

Tài nguyên chọn lọc để đi TIẾP sau series này: Karpathy — Zero to Hero & nanoGPT (chính là GPT-mini bài này, nhưng ở quy mô thật với PyTorch), fast.ai, d2l.ai. Những gì series này CỐ TÌNH bỏ qua (và nên đọc thêm nếu cần): huấn luyện phân tán nhiều máy, RLHF thật với đội đánh giá người, model multimodal (ảnh+chữ), diffusion ảnh ở quy mô thật (Bài 15 mới chỉ chạm khái niệm).

Tải file code thực hành minh họa bài học

File JavaScript cài đầy đủ GPT-mini (BPE + embedding + transformer block tái sử dụng + Adam + gradient clipping + sinh văn bản), verify bằng số đo thật (đếm tham số, loss giảm, overfit train/val, gradient clipping — chạy node gpt_mini.js, mất khoảng 30-60 giây vì train THẬT):

Tải về gpt_mini.js

📖 Tài liệu tham khảo

Bài viết liên quan trong series

Bài 18: Sinh văn bản, Sampling & Alignment 🎉 Series hoàn thành! Quay lại Lộ trình Series Trí Tuệ Nhân Tạo

Bình luận