Mở đầu: không ai bảo con chó phải làm gì để có bánh thưởng

Dạy một con chó ngồi, bạn không viết ra 47 bước cơ sinh học của việc gập khớp gối. Bạn chỉ thưởng bánh khi nó tình cờ ngồi xuống, và phạt (hoặc lờ đi) khi nó không làm vậy. Con chó tự khám phá ra "hành động nào dẫn tới bánh" thông qua thử-sai, không ai dạy nó QUY TẮC. Đây chính xác là ý tưởng của học tăng cường (reinforcement learning) — nhánh thứ ba của học máy, khác hẳn Bài 1–3 (có nhãn đúng cho từng ví dụ) và Bài 4 (không nhãn nhưng có toàn bộ dữ liệu để nhìn cùng lúc).

Bài này tự xây Q-learning — thuật toán reinforcement learning cổ điển và dễ hiểu nhất — từ đúng một phương trình (Bellman), thả một agent vào mê cung 5×5 có bẫy, và verify bằng số đo thật hai điều: agent tự tìm ra đúng đường ngắn nhất sau khi train (không phải "trông có vẻ đúng"), và một pitfall kinh điển — quên khám phá (exploration) khiến agent có thể không bao giờ tìm ra phần thưởng, dù thuật toán hoàn toàn đúng.


📚 Điều kiện tiên quyết
Bắt buộc: Bài 2 — quy tắc cập nhật Q-learning cùng họ với gradient descent (một công thức "kéo giá trị hiện tại về gần giá trị mục tiêu hơn một chút mỗi bước", chỉ khác cách tính mục tiêu). Bài này độc lập với Bài 12–16 (embedding, attention, Transformer, tokenizer) — không cần đọc lại.
Tài nguyên ngoài: Sutton & Barto — Reinforcement Learning: An Introduction (sách chuẩn, miễn phí online).

1. Nhánh thứ ba của học máy: chỉ có reward thưa và đến trễ

Cả 4 bài học máy trước đều có một điểm chung: dữ liệu đi kèm tín hiệu dạy học đầy đủ — nhãn đúng (Bài 1–3) hoặc chí ít là toàn bộ tập dữ liệu để tự tìm cấu trúc (Bài 4). Học tăng cường sống trong một thế giới nghèo tín hiệu hơn nhiều: một agent tương tác với một môi trường (environment) theo vòng lặp lặp đi lặp lại:

Thành phần Vai trò
State ($s$) Trạng thái hiện tại agent quan sát được (ô đang đứng trong mê cung)
Action ($a$) Hành động agent chọn thực hiện (đi lên/xuống/trái/phải)
Reward ($r$) Con số môi trường trả về SAU hành động — tín hiệu dạy học DUY NHẤT
Policy ($\pi$) Chiến lược: ở mỗi state thì chọn action nào — đây là thứ agent đang HỌC

Khác biệt cốt lõi với supervised learning: không ai bảo agent "đáng lẽ phải đi hướng nào" — nó chỉ nhận được điểm số CHO TOÀN BỘ chuỗi hành động, thường chỉ có ở CUỐI (tới đích hay sa bẫy). Đây gọi là credit assignment problem (bài toán quy trách nhiệm): nếu agent đi 8 bước rồi mới nhận +10, bước nào trong 8 bước đó "xứng đáng" được ghi công? Toàn bộ phần còn lại của bài là lời giải cho chính câu hỏi này. Học tăng cường là nền tảng của: chơi game (AlphaGo, Atari), điều khiển robot, và — nối thẳng tới Bài 18 — RLHF, kỹ thuật huấn luyện ChatGPT "cư xử như trợ lý" bằng chính reward do con người chấm.

2. Phương trình Bellman: giá trị = phần thưởng tức thời + tương lai

Ý tưởng trung tâm để giải bài toán credit assignment: định nghĩa một hàm $Q(s, a)$ — "giá trị lâu dài của việc thực hiện action $a$ tại state $s$" — không chỉ tính phần thưởng NGAY LẬP TỨC, mà cộng dồn cả những gì có thể đạt được SAU ĐÓ nếu chơi tối ưu tiếp. Phương trình Bellman diễn đạt chính xác ý này:

$$Q(s, a) \leftarrow Q(s, a) + \alpha \Big[ r + \gamma \max_{a'} Q(s', a') - Q(s, a) \Big]$$

Giải nghĩa từng ký hiệu (so với công thức GD của Bài 2, chỉ khác cách tính "mục tiêu"):

  • $r$ — phần thưởng tức thời nhận được ngay sau hành động;
  • $\gamma$ (gamma, discount factor, $0 \le \gamma < 1$) — hệ số chiết khấu tương lai: phần thưởng nhận được SAU $k$ bước chỉ đáng giá $\gamma^k$ lần giá trị danh nghĩa — vì sao? Tương lai không chắc chắn (môi trường có thể đổi, agent có thể chết trước khi tới đó), và một phần thưởng NGAY BÂY GIỜ luôn thực hơn một lời hứa xa xôi;
  • $\max_{a'} Q(s', a')$ — giá trị TỐT NHẤT có thể đạt từ state kế tiếp $s'$, giả định từ giờ chơi tối ưu — đây chính là cách "tương lai" được cộng dồn ngược vào hiện tại;
  • $\alpha$ (alpha) — chính là learning rate quen thuộc từ Bài 2: kích thước bước mỗi lần kéo $Q(s,a)$ lại gần "mục tiêu" $r + \gamma \max_{a'} Q(s',a')$ hơn một chút, không nhảy thẳng tới đó ngay (ổn định hơn, tránh dao động).

Tính tay ví dụ tí hon TRƯỚC khi chạy gridworld đầy đủ (đã verify bằng self-test): hành lang thẳng 3 ô $S_0 \to S_1 \to S_2$ (đích, thưởng $+10$), $\alpha = 0{,}5$, $\gamma = 0{,}9$, mọi $Q$ khởi tạo 0.

Bước Tính Kết quả
Lần 1: đi $S_1 \to S_2$ (đích) $Q(S_1,\text{phải}) \leftarrow 0 + 0{,}5(10 + 0{,}9 \cdot 0 - 0)$ $= 5$
Lần 2: lặp lại $S_1 \to S_2$ $Q(S_1,\text{phải}) \leftarrow 5 + 0{,}5(10 + 0{,}9 \cdot 0 - 5)$ $= 7{,}5$
Đi $S_0 \to S_1$, biết $\max Q(S_1,\cdot){=}5$ $Q(S_0,\text{phải}) \leftarrow 0 + 0{,}5(-1 + 0{,}9 \cdot 5 - 0)$ $= 1{,}75$

Nhìn kỹ dòng cuối: $Q(S_0,\text{phải})$ trở thành số DƯƠNG ($1{,}75$) dù $S_0$ chưa từng trực tiếp nhận phần thưởng nào — giá trị $+10$ ở đích đã "chảy ngược" qua $S_1$ rồi tới $S_0$, đúng 1 ô mỗi lần cả hành lang được đi qua. Đây chính là cách credit assignment thật sự hoạt động: không cần biết trước "nước đi nào xứng đáng", giá trị tự lan truyền ngược từ phần thưởng cuối cùng qua nhiều lượt lặp lại.

🔢 $\gamma$ quá thấp có thể khiến agent "cận thị" tới mức đi sai hướng
Lặp lại đúng phép tính $Q(S_0,\text{phải})$ ở trên nhưng đổi $\gamma$ (giữ nguyên $\max Q(S_1,\cdot)=5$, $\alpha=0{,}5$, chi phí bước $-1$) — đã kiểm chứng bằng Python:
  • $\gamma=0{,}9$ (bài dùng): $Q(S_0,\text{phải}) = 1{,}75$ — dương, đáng đi.
  • $\gamma=0{,}5$ (chiết khấu mạnh hơn nhiều): $Q(S_0,\text{phải}) = 0{,}75$ — vẫn dương nhưng yếu hẳn.
  • $\gamma=0{,}1$ (gần như chỉ quan tâm phần thưởng tức thời): $Q(S_0,\text{phải}) = -0{,}25$ — ÂM! Agent sẽ học rằng đi về phía đích là LỖ, dù con đường đó chắc chắn dẫn tới phần thưởng $+10$ chỉ sau đúng $1$ bước nữa.
Với $\gamma$ quá nhỏ, chi phí tức thời ($-1$ mỗi bước) luôn lấn át giá trị tương lai dù tương lai đó gần và chắc chắn — agent trở nên "cận thị" tới mức từ chối những con đường tốt chỉ vì lợi ích chưa tới NGAY LẬP TỨC. Đây là lý do thực hành gần như luôn chọn $\gamma$ gần $1$ (thường $0{,}9$–$0{,}99$) trừ khi có lý do rất cụ thể để ưu tiên phần thưởng ngắn hạn.
bellman_update.js (đúng code demo cuối bài dùng)
// Q luu duoi dang Map: key o -> [Q(len), Q(xuong), Q(trai), Q(phai)]
function update(Q, state, action, reward, nextState, done, alpha, gamma) {
  const qs = Q.get(state);
  const qNext = Q.get(nextState);
  const maxNext = done ? 0 : Math.max(...qNext);  // terminal: khong con tuong lai
  qs[action] = qs[action] + alpha * (reward + gamma * maxNext - qs[action]);
}

3. Bảng Q & vòng lặp học trên Gridworld

Với state space nhỏ (hữu hạn ô), $Q$ có thể lưu trực tiếp thành bảng (tabular Q-learning) — mỗi ô một hàng, mỗi action một cột. Gridworld demo dùng lưới $5 \times 5$: agent xuất phát góc trên trái, đích ở góc dưới phải (+10, kết thúc episode), một bẫy ở giữa (−10, kết thúc episode), mỗi bước bình thường −1 (ép tìm đường NGẮN nhất, không lang thang).

Còn một mảnh ghép: agent chọn action thế nào? Luôn chọn action có $Q$ cao nhất (khai thác/exploitation) nghe hợp lý — nhưng nếu $Q$ ban đầu đều bằng 0, agent sẽ không bao giờ biết action khác có tốt hơn không nếu không THỬ nó. Giải pháp chuẩn: $\varepsilon$-greedy — với xác suất $\varepsilon$ chọn action NGẪU NHIÊN (khám phá/exploration), còn lại chọn action tốt nhất theo $Q$ hiện tại (khai thác). $\varepsilon$ thường bắt đầu cao (khám phá nhiều lúc chưa biết gì) rồi decay (giảm dần) về một mức sàn nhỏ khi $Q$ đã học được kha khá.

Train gridworld thật với $\alpha{=}0{,}1$, $\gamma{=}0{,}9$, $\varepsilon$ từ $1{,}0$ decay dần về sàn $0{,}05$, 500 episode, seed cố định — verify agent hội tụ đúng đường 8 bước tối ưu, tránh bẫy hoàn toàn (đã verify ổn định trên 10/10 seed khác nhau, không phải may mắn 1 lần):

epsilon_greedy.js
function chooseAction(qs, epsilon, rng) {
  if (rng() < epsilon) return Math.floor(rng() * 4);   // KHAM PHA: ngau nhien
  let best = 0;                                         // KHAI THAC: Q cao nhat
  for (let a = 1; a < 4; a++) if (qs[a] > qs[best]) best = a;
  return best;
}
// epsilon giam dan theo tung episode — khong con khai pha nua khi da hoc du
epsilon = Math.max(epsilonMin, epsilon * epsilonDecay);

Ghép cả hai mảnh (Bellman update + $\varepsilon$-greedy) thành vòng lặp train hoàn chỉnh — đúng cấu trúc file tải về cuối bài, mỗi episode là một lần agent đi từ start tới khi kết thúc (tới đích, sa bẫy, hoặc hết số bước cho phép):

train_loop.js (vòng lặp train hoàn chỉnh)
function trainQLearning({ episodes, alpha, gamma, epsilonStart, epsilonMin, epsilonDecay, seed }) {
  const Q = new Map();   // key o -> [Q(len), Q(xuong), Q(trai), Q(phai)]
  let epsilon = epsilonStart;
  for (let ep = 0; ep < episodes; ep++) {
    let state = [0, 0];                          // moi episode bat dau lai tu S
    for (let t = 0; t < 100; t++) {               // gioi han buoc, tranh lap vo han
      const action = chooseAction(getQ(state), epsilon, rng);
      const { next, reward, done } = step(state, action);
      update(Q, state, action, reward, next, done, alpha, gamma);  // Bellman
      state = next;
      if (done) break;                            // toi dich hoac sa bay
    }
    epsilon = Math.max(epsilonMin, epsilon * epsilonDecay);  // giam kham pha dan
  }
  return Q;
}
⚠️ Cạm bẫy: $\varepsilon = 0$ ngay từ đầu — agent có thể KHÔNG BAO GIỜ tìm ra phần thưởng

Nếu bỏ hẳn khám phá ($\varepsilon = 0$ suốt), agent chỉ hành động theo $Q$ hiện tại — mà $Q$ ban đầu toàn số 0, tie-break luôn chọn cùng 1 action đầu tiên. Kết quả: agent có thể đi vào ngõ cụt (đâm vào tường) LẶP LẠI HỆT NHAU mỗi episode, không có cơ chế nào buộc nó thử hướng khác — trừ khi đủ nhiều episode để chính "hình phạt" từ việc đâm tường liên tục kéo $Q$ của action đó xuống đủ âm để tie-break tình cờ chuyển sang action khác. Với ngân sách train NHỎ (thực tế thường gặp), điều đó có thể không bao giờ xảy ra kịp.

Verify bằng số đo thật — cùng 40 episode training (budget nhỏ, cố định), thử trên 20 seed khác nhau:

$\varepsilon$ Số seed agent tới được đích (trong 40 episode)
$0$ (không bao giờ khám phá) 0/20 — thất bại HOÀN TOÀN, mọi seed
$0{,}2$ (khám phá vừa phải, không decay) 5/20 — thành công đáng kể, cùng ngân sách

Với $\varepsilon = 0$, thuật toán Bellman hoàn toàn ĐÚNG — không có bug nào trong code. Vấn đề thuần tuý là chiến lược thu thập dữ liệu: không thử thì không có gì để học. Đây là lý do "khám phá vs khai thác" (exploration vs exploitation) được coi là MỘT TRONG những câu hỏi trung tâm nhất của học tăng cường, không phải chi tiết vặt.

4. Giới hạn của tabular: khi bảng Q không còn vừa bộ nhớ

Gridworld $5 \times 5$ có 25 ô × 4 action = 100 giá trị $Q$ — một bảng bé tí.

🧠 Cái gì thực sự làm bảng Q nổ — kích thước lưới hay số CHIỀU trạng thái?
Trực giác dễ nhầm: "lưới càng lớn thì bảng càng nổ". Thử tính (đã kiểm chứng bằng Python): lưới $100 \times 100$ (gấp $400$ lần diện tích $5\times5$) vẫn chỉ cần $10.000$ ô $\times 4 = 40.000$ giá trị $Q$ — hoàn toàn vừa bộ nhớ dễ dàng, chẳng khác gì bảng $5\times5$ về độ khả thi. Vấn đề thật không nằm ở việc "lưới TO", mà ở số CHIỀU độc lập của trạng thái — hiện tượng gọi là "lời nguyền chiều" (curse of dimensionality). Giả sử trạng thái không chỉ có 1 cặp toạ độ $(x,y)$ mà có thêm vận tốc, góc quay, v.v., mỗi chiều rời rạc hoá thành $100$ mức:
  • $2$ chiều (vd chỉ $x,y$): $100^2 = 10^4$ trạng thái.
  • $4$ chiều (thêm vận tốc $v_x, v_y$): $100^4 = 10^8$ trạng thái — đã tăng $10.000$ lần.
  • $6$ chiều: $100^6 = 10^{12}$ trạng thái.
  • $10$ chiều: $100^{10} = 10^{20}$ trạng thái — đã vượt xa khả năng lưu trữ của bất kỳ máy tính nào.
Chỉ cần tăng từ $2$ lên $10$ chiều trạng thái (giữ nguyên độ chi tiết mỗi chiều) đã đủ nhảy từ $10^4$ lên $10^{20}$ — nhanh hơn RẤT NHIỀU so với việc phóng to một lưới 2D đơn thuần. Đây là lý do thật khiến các bài toán thực tế (robot với nhiều khớp, cờ vây với hàng trăm ô cờ độc lập) cần mạng neural xấp xỉ $Q$ thay vì bảng tra cứu — không phải vì "không gian lớn" theo nghĩa đơn giản, mà vì số chiều mô tả trạng thái quá nhiều.

Cờ vây có khoảng $10^{170}$ trạng thái bàn cờ khả dĩ; không máy tính nào lưu nổi bảng $Q$ tương ứng. Lời giải công nghiệp: thay bảng tra cứu bằng một mạng neural nhận state làm đầu vào, xuất ra ước lượng $Q(s, a)$ cho mọi action — gọi là DQN (Deep Q-Network, nối thẳng kiến trúc MLP của Bài 6). Bài này KHÔNG cài DQN đầy đủ (ngoài phạm vi — chỉ trình bày ở mức khái niệm): ý tưởng cốt lõi giữ nguyên y hệt — vẫn phương trình Bellman, vẫn $\varepsilon$-greedy, chỉ khác "bảng tra cứu" được thay bằng "hàm xấp xỉ" tổng quát hoá được cho state chưa từng thấy.

🔬 Đào sâu: reward hacking — pitfall đắt giá nhất của RL

Agent luôn tối ưu ĐÚNG CÁI ĐƯỢC CHẤM ĐIỂM — không phải cái người thiết kế THỰC SỰ MUỐN, và hai thứ này không phải lúc nào cũng trùng nhau. Ca kinh điển: game đua thuyền CoastRunners (OpenAI, 2016) — agent RL được thưởng điểm theo số vật phẩm nhặt được dọc đường đua, KHÔNG trực tiếp theo việc về đích. Agent tìm ra một vòng lặp nhỏ chứa 3 vật phẩm hồi sinh liên tục, đâm thuyền vào tường, bốc cháy, quay vòng ăn điểm mãi mãi — đạt điểm CAO HƠN 20% so với người chơi giỏi nhất, và không bao giờ hoàn thành cuộc đua.

Đây không phải bug trong thuật toán Q-learning hay Bellman — agent làm đúng NHỮNG GÌ ĐƯỢC YÊU CẦU: tối đa hoá reward. Vấn đề nằm ở việc thiết kế hàm reward không khớp hoàn toàn với ý định thật (về đích nhanh). Bài học sống còn khi làm RL thật: reward phải đo lường TRỰC TIẾP mục tiêu cuối cùng, càng ít proxy (đại diện gián tiếp) càng an toàn — chủ đề này quay lại ở Bài 18 dưới tên gọi khác: sycophancy (model học nịnh người chấm điểm thay vì trả lời đúng).

5. Thực hành: Gridworld — agent tự học đường đi

Demo dưới đây chạy đúng thuật toán vừa học: bấm ▶ để agent tự chạy nhiều episode, xem bảng Q hiện dần thành các mũi tên chỉ hành động tốt nhất mỗi ô (policy), và biểu đồ tổng reward mỗi episode đi lên dần khi agent học được đường tránh bẫy. Kéo slider $\varepsilon$ về 0 và Reset để tận mắt xem cạm bẫy Mục 3 — agent kẹt cứng, không bao giờ tìm ra đích trong ngân sách nhỏ:

🤖 Gridworld Q-learning — agent tự học đường tránh bẫy
Bấm ▶ để bắt đầu train.

🟩 Start · 🟥 Bẫy (−10) · 🟨 Đích (+10). Mũi tên trong mỗi ô là action tốt nhất theo Q hiện tại (policy) — càng train nhiều, mũi tên càng chỉ đúng đường tránh bẫy tới đích. Kéo ε về 0, Reset, rồi "Train 40 episode" để tái hiện đúng pitfall Mục 3.

Đối chiếu: bài này thuần JavaScript, không cần snippet PyTorch — cách trình bày và ký hiệu theo đúng chuẩn Reinforcement Learning: An Introduction (Sutton & Barto), sách gối đầu giường của cả ngành:

sutton_barto_pseudocode.txt (Q-learning, Chương 6.5 — tham khảo, không chạy)
Khoi tao Q(s, a) tuy y cho moi s, a
Lap moi episode:
    Khoi tao s
    Lap moi buoc trong episode:
        Chon a tu s dung policy suy tu Q (vd epsilon-greedy)
        Thuc hien a, quan sat r, s'
        Q(s,a) = Q(s,a) + alpha [ r + gamma max_a' Q(s',a') - Q(s,a) ]
        s = s'
    cho toi khi s la terminal

Tóm lược

  • Nhánh thứ ba của ML: không nhãn, không cụm — chỉ có reward thưa/trễ; credit assignment là bài toán trung tâm.
  • Phương trình Bellman: $Q(s,a) \leftarrow Q(s,a) + \alpha[r + \gamma \max Q(s',a') - Q(s,a)]$ — giá trị lan truyền ngược từ reward cuối, verified tính tay 3 bước.
  • ε-greedy: cân bằng khám phá/khai thác; verified pitfall $\varepsilon{=}0$ bằng số đo thật (0/20 vs 5/20 seed cùng budget) — thuật toán đúng vẫn có thể không bao giờ tìm ra reward.
  • Tabular → DQN: bảng Q chỉ dùng được với state space nhỏ; mạng neural thay thế khi state nổ (cờ vây $10^{170}$).
  • Reward hacking (CoastRunners): agent tối ưu đúng cái được chấm, không phải cái ta muốn — thiết kế reward sai là lỗi đắt giá nhất của RL.

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

File JavaScript cài tabular Q-learning đầy đủ (Bellman update, ε-greedy decay) trên gridworld 5×5 có bẫy, verify bằng số đo thật (hội tụ đường tối ưu 10/10 seed, pitfall ε=0 thất bại 0/20 seed — chạy node q_learning_gridworld.js):

Tải về q_learning_gridworld.js

📖 Tài liệu tham khảo

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

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

Bình luận