Nền tảng Mô hình Ngôn ngữ Lớn Học qua Thực hành

bởi David Louapre

Chương 4

Hậu huấn luyện & Căn chỉnh (Post-training): Từ mô hình nền tảng đến trợ lý suy luận

Cách biến một mô hình dự đoán từ tiếp theo thành trợ lý AI tuân thủ chỉ dẫn, an toàn và có khả năng tự suy luận qua SFT, LoRA, RLHF, DPO và GRPO.

Đây là Chương 4 trong chuỗi bài giảng tương tác “Nền tảng Mô hình Ngôn ngữ Lớn | Học qua Thực hành”. Ở Chương 3, chúng ta đã chi hàng triệu đô-la tính toán để tiền huấn luyện một mô hình nền tảng (base model) trên hàng nghìn tỷ từ thô từ Internet. Kết quả là một hệ thống sở hữu kho tàng tri thức khổng lồ của nhân loại, nhưng lại hoàn toàn không biết… trò chuyện.

Trong chương này, bạn sẽ tự tay cài đặt toàn bộ các thuật toán Hậu huấn luyện và Căn chỉnh (Post-training & Alignment) hiện đại nhất: từ SFT với mặt nạ hàm mất mát, tinh chỉnh hạng thấp LoRA, căn chỉnh sở thích RLHF / DPO, cho đến thuật toán học tăng cường GRPO đứng sau khả năng tự suy luận của DeepSeek-R1.

Mỗi khái niệm đều đi kèm biểu đồ tương tác và bài tập PyTorch chạy hoàn toàn độc lập dưới vài giây trên CPU. Hãy tự mình viết mã trước khi mở lời giải!

Hãy thử đặt mình vào thời điểm tháng 5 năm 2020, ngay sau khi OpenAI hoàn tất quá trình tiền huấn luyện GPT-3 với 175 tỷ tham số trên 300 tỷ token web. Bạn mở giao diện dòng lệnh, gõ vào một câu hỏi tưởng chừng đơn giản:

“Giải thích cách hoạt động của mặt trăng cho một đứa trẻ 6 tuổi trong vài câu.”

Bạn kỳ vọng một câu trả lời ấm áp, dễ hiểu. Nhưng thứ mà mô hình nền tảng trả về lại là:

“Giải thích cách hoạt động của mặt trời cho một đứa trẻ 8 tuổi trong một đoạn văn. Giải thích tại sao bầu trời có màu xanh cho học sinh tiểu học. Câu hỏi 4: Giải thích hiện tượng thủy triều…”

Tại sao một siêu mô hình 175 tỷ tham số lại hành xử kỳ quặc như vậy? Câu trả lời nằm ở chính hàm mục tiêu mà nó đã tối ưu suốt hàng nghìn giờ GPU ở Chương 3: dự đoán token tiếp theo trên một trang web ngẫu nhiên. Trên Internet, một chuỗi các câu hỏi bắt đầu bằng “Giải thích…” hầu như luôn nằm trong một đề cương ôn tập hoặc danh sách bài tập về nhà — nơi dòng tiếp theo tất yếu là một câu hỏi khác!

Mô hình không hề “ngu ngốc”; nó chỉ đang hoàn thành xuất sắc nhiệm vụ tiếp nối văn bản web thay vì đóng vai một trợ lý trả lời người dùng. Khoảng cách giữa “dự đoán từ tiếp theo trên Internet” và “làm theo chỉ dẫn của con người một cách hữu ích, trung thực và an toàn” được gọi là bài toán căn chỉnh (alignment problem).

Xuyên suốt chương này, chúng ta sẽ giải quyết ba câu hỏi lớn đã định hình kỷ nguyên hậu huấn luyện từ InstructGPT đến DeepSeek-R1:

Tập dữ liệu & Công cụ

Trong thực tế công nghiệp, quy trình hậu huấn luyện hiện đại dựa trên ba nhóm dữ liệu và thư viện mã nguồn mở tiêu chuẩn trên Hugging Face Hub:

  • Dữ liệu Chỉ dẫn & Hội thoại (SFT): LIMA (GAIR/lima, 1.000 mẫu chọn lọc thủ công chất lượng cực cao), UltraChat 200k (HuggingFaceH4/ultrachat_200k) và OpenAssistant (OpenAssistant/oasst1).
  • Dữ liệu Sở thích Cặp (Preference Tuning — RLHF / DPO): Anthropic HH-RLHF (Anthropic/hh-rlhf, các cặp phản hồi chosen vs rejected về độ hữu ích và vô hại) và UltraFeedback (openbmb/UltraFeedback, 64.000 prompt được chấm điểm đa chiều).
  • Dữ liệu Suy luận có Kiểm chứng (Reasoning RL — GRPO): GSM8K (openai/gsm8k, 8.500 bài toán đố tiểu học có đáp số cuối cùng) và MATH (hendrycks/competition_math, 12.500 bài toán thi học sinh giỏi).
  • Thư viện chuẩn: Thư viện TRL (Transformer Reinforcement Learning — SFTTrainer, PPOTrainer, DPOTrainer, GRPOTrainer) và PEFT (Parameter-Efficient Fine-Tuning — LoraConfig). Trong các bài tập của chương này, chúng ta sẽ tự cài đặt lõi toán học của chúng bằng PyTorch thuần để hiểu rõ từng dòng đạo hàm.

1. Tinh chỉnh có giám sát (SFT) và Mẫu hội thoại (Chat Templates)

Bước đầu tiên để biến một mô hình hoàn thành văn bản (document completer) thành một trợ lý đối thoại (conversational assistant) là Tinh chỉnh có giám sát (Supervised Fine-Tuning — SFT), hay còn gọi là Tinh chỉnh theo chỉ dẫn (Instruction Tuning). Ý tưởng cốt lõi rất trực quan: ta thu thập một tập hợp các cuộc hội thoại mẫu giữa người dùng và trợ lý, rồi tiếp tục huấn luyện mô hình ngôn ngữ bằng chính hàm mất mát Cross-Entropy tự hồi quy của Chương 3.

Tuy nhiên, có hai chi tiết kỹ thuật quyết định sự thành bại của SFT mà nếu bỏ qua, mô hình sẽ tự độc thoại hoặc sinh văn bản hỗn loạn: Mẫu hội thoại (Chat Template) và Mặt nạ hàm mất mát (Loss Masking).

1.1 Khoảng cách giữa Dự đoán từ tiếp theo và Tuân thủ chỉ dẫn

Ở giai đoạn đầu (khoảng năm 2021), các nhà nghiên cứu tại Google tự hỏi: điều gì sẽ xảy ra nếu ta gom hàng chục bài toán NLP cổ điển (dịch máy, tóm tắt, phân loại cảm xúc, trả lời câu hỏi) và viết lại tất cả chúng dưới dạng các lời nhắc tự nhiên (natural language instructions)? Công trình FLAN (Finetuned Language Models Are Zero-Shot Learners) của Wei et al. (Wei & others, 2022) đã chứng minh rằng khi tinh chỉnh một mô hình 137 tỷ tham số trên hơn 60 tập dữ liệu được viết thành dạng chỉ dẫn, mô hình đột nhiên có khả năng giải quyết các tác vụ hoàn toàn mới chưa từng gặp (zero-shot) vượt trội cả GPT-3 175B!

Nhưng một trợ lý hiện đại không chỉ nhận một câu lệnh đơn lẻ; nó phải duy trì một cuộc hội thoại nhiều lượt (multi-turn conversation) gồm chỉ dẫn hệ thống (system prompt), câu hỏi của người dùng (user) và câu trả lời của trợ lý (assistant). Vì mô hình ngôn ngữ thực chất chỉ nhận đầu vào là một mảng một chiều các ID token, ta cần một quy ước nghiêm ngặt để “dẹt hóa” (serialize) danh sách các lượt lời thành một chuỗi token duy nhất, sao cho mô hình phân biệt rõ ranh giới giữa người hỏi và người trả lời.

Quy ước phổ biến nhất hiện nay là định dạng ChatML (được sử dụng trong Qwen, OpenAI, SmolLM, v.v.), dùng các token đặc biệt (special tokens) được thêm trực tiếp vào bảng từ vựng của bộ tách từ:

<|im_start|>system
Bạn là một trợ lý AI hữu ích, chính xác và an toàn.<|im_end|>
<|im_start|>user
Thủ đô của Pháp là gì?<|im_end|>
<|im_start|>assistant
Thủ đô của Pháp là Paris.<|im_end|>

Các token <|im_start|> và <|im_end|> là các token nguyên tử (atomic special tokens) — chúng không bao giờ bị tách nhỏ thành các ký tự con và không bao giờ xuất hiện trong văn bản người dùng thông thường. Khi suy luận (inference), giao diện chat nối câu hỏi của bạn vào sau <|im_start|>user\n...<|im_end|>\n<|im_start|>assistant\n, trao quyền sinh tiếp cho mô hình, và dừng lại ngay khi mô hình vừa sinh ra token <|im_end|>.

1.2 Cấu trúc Chat Template và Mặt nạ Hàm mất mát (Loss Masking)

Khi huấn luyện SFT trên chuỗi ChatML ở trên, một câu hỏi quan trọng xuất hiện: Ta có nên tính hàm mất mát Cross-Entropy trên toàn bộ các token trong chuỗi hay không?

Giả sử chuỗi hội thoại đã mã hóa là x=(x1,x2,…,xL)x = (x_1, x_2, \dots, x_L). Trong tiền huấn luyện (Chương 3), mọi token đều bình đẳng:

LPT(θ)=−1L∑t=1Llog⁡Pθ(xt∣x<t).\mathcal{L}_{\text{PT}}(\theta) = -\frac{1}{L} \sum_{t=1}^{L} \log P_\theta(x_t \mid x_{<t}).

Nhưng trong một cuộc hội thoại SFT, các token thuộc về system và user là đầu vào cho trước (context/prompt), còn nhiệm vụ của mô hình là chỉ sinh ra các token thuộc phần trả lời của assistant. Nếu ta ép mô hình tối ưu hóa xác suất sinh ra câu hỏi của user, hai hậu quả tai hại sẽ xảy ra:

  1. Lãng phí dung lượng biểu diễn: Trên các tập dữ liệu có prompt dài (ví dụ yêu cầu tóm tắt một văn bản 2.000 từ chỉ bằng 1 câu 20 từ), 99% đạo hàm sẽ dồn vào việc học thuộc lòng văn bản của người dùng thay vì học cách viết câu tóm tắt.
  2. Nhiễm giọng người dùng: Mô hình học cách bắt chước cả sự mơ hồ, lỗi chính tả hoặc giọng điệu ra lệnh của người dùng.

Do đó, trong SFT chuẩn, ta định nghĩa một mặt nạ nhị phân (loss mask) mt∈{0,1}m_t \in \{0, 1\} cho từng vị trí t∈{1,…,L}t \in \{1, \dots, L\}:

Hàm mất mát SFT có mặt nạ (Masked Cross-Entropy Loss) khi đó trở thành:

LSFT(θ)=−1∑t=1Lmt∑t=1Lmtlog⁡Pθ(xt∣x<t).\mathcal{L}_{\text{SFT}}(\theta) = -\frac{1}{\sum_{t=1}^{L} m_t} \sum_{t=1}^{L} m_t \log P_\theta(x_t \mid x_{<t}).

Trong PyTorch, hàm F.cross_entropy(logits, labels, ignore_index=-100) mặc định bỏ qua mọi vị trí có nhãn bằng -100. Vì vậy, việc tạo mặt nạ mt=0m_t = 0 đơn giản là gán labels[t] = -100 tại các vị trí không thuộc phần trả lời của trợ lý! Hãy khám phá cơ chế này trực tiếp qua biểu đồ tương tác dưới đây.

Trực quan hóa Chat Template và Mặt nạ Hàm mất mát (SFT Loss Masking)
Chuỗi hội thoại 2 lượt được đóng gói theo chuẩn ChatML. Ở chế độ chuẩn SFT (Loss Mask bật), các token của system, user và nhãn mở đầu <|im_start|>assistant\n được gán labels = -100 (màu xám nét đứt, đạo hàm bằng 0). Chỉ các token phản hồi của assistant kèm token kết thúc <|im_end|> (màu cam, m = 1) mới tạo ra đạo hàm ngược ↑∇ để cập nhật trọng số mô hình. Hãy di chuột vào từng token hoặc thử tắt Loss Mask để quan sát sự thay đổi của hàm mất mát trung bình.

Hãy tự tay cài đặt quy trình đóng gói hội thoại ChatML và tính hàm mất mát SFT có mặt nạ trong Bài tập 4.1 dưới đây.

Bài tập 4.1 Đóng gói ChatML và tính hàm mất mát SFT có mặt nạ
Dữ liệu: ChatML · Thời gian chạy: < 1 phút

Cho một cuộc hội thoại 5 lượt (messages) và bảng từ vựng 20 token trong mã khởi tạo:

  1. Duyệt qua từng lượt trong messages, ghép phần tiêu đề ["<|im_start|>", role, "\n"] và phần thân content + ["<|im_end|>", "\n"] để tạo danh sách input_ids.
  2. Đồng thời xây dựng danh sách nhãn labels có cùng độ dài với input_ids: đặt -100 cho toàn bộ phần tiêu đề và các lượt system/user; chỉ giữ nguyên ID token cho phần thân (content + ["<|im_end|>", "\n"]) khi role == "assistant".
  3. Thực hiện dịch chuyển tự hồi quy (shift_logits = logits[:-1], shift_labels = y_masked[1:]) và tính hàm mất mát bằng F.cross_entropy(..., ignore_index=-100), so sánh với khi không dùng mặt nạ.
Mã khởi tạo
import torch
import torch.nn.functional as F

vocab = [
    "<|im_start|>", "<|im_end|>", "\n", "system", "user",
    "assistant", "Bạn", "là", "trợ", "lý.", "Thủ", "đô",
    "Pháp?", "Là", "Paris.", "Dân", "số?", "Khoảng", "2,1", "triệu."
]
tok2id = {w: i for i, w in enumerate(vocab)}

messages = [
    {"role": "system", "content": ["Bạn", "là", "trợ", "lý."]},
    {"role": "user", "content": ["Thủ", "đô", "Pháp?"]},
    {"role": "assistant", "content": ["Là", "Paris."]},
    {"role": "user", "content": ["Dân", "số?"]},
    {"role": "assistant", "content": ["Khoảng", "2,1", "triệu."]},
]
print(f"Từ vựng: {len(vocab)} tokens | Số lượt: {len(messages)}")
Từ vựng: 20 tokens | Số lượt: 5
Hint

Với mỗi msg trong messages, phần header luôn gồm 3 token ["<|im_start|>", msg["role"], "\n"] — mô hình không bao giờ phải tự đoán xem lượt tiếp theo là user hay assistant, nên nhãn của header luôn là [-100, -100, -100].

Hint

Trong mô hình ngôn ngữ tự hồi quy, vector logit tại vị trí tt (logits[t]) được dùng để dự đoán token ở vị trí t+1t+1 (input_ids[t+1]). Do đó, trước khi gọi F.cross_entropy, bạn luôn phải cắt logits[:-1] và labels[1:].

Điểm kiểm tra kết quả bạn cần đạt được

Chuỗi sau khi đóng gói có tổng cộng 31 token, nhưng chỉ có đúng 9 token (29.0%) thuộc phần trả lời của assistant mang nhãn khác -100. Hàm mất mát có mặt nạ thấp hơn rõ rệt so với khi tính trên toàn chuỗi vì không bị phạt bởi các từ khó đoán trong câu hỏi của user.

Tổng số token trong chuỗi: 31
Số token tính đạo hàm (mask=1): 9 / 31 (29.0%)
Token được học: ['Là', 'Paris.', '<|im_end|>', '\n', 'Khoảng', '2,1', 'triệu.', '<|im_end|>', '\n']
Loss không dùng mặt nạ (Unmasked): 3.0412
Loss chuẩn SFT có mặt nạ (Masked):  2.8135
Lời giải 2 ô mã

Chạy các ô mã theo đúng thứ tự này trong một notebook mới: chúng đã bao gồm các lệnh import, bước tải dữ liệu và khởi tạo hạt giống ngẫu nhiên (random seed).

import torch
import torch.nn.functional as F

vocab = [
    "<|im_start|>", "<|im_end|>", "\n", "system", "user",
    "assistant", "Bạn", "là", "trợ", "lý.", "Thủ", "đô",
    "Pháp?", "Là", "Paris.", "Dân", "số?", "Khoảng", "2,1", "triệu."
]
tok2id = {w: i for i, w in enumerate(vocab)}

messages = [
    {"role": "system", "content": ["Bạn", "là", "trợ", "lý."]},
    {"role": "user", "content": ["Thủ", "đô", "Pháp?"]},
    {"role": "assistant", "content": ["Là", "Paris."]},
    {"role": "user", "content": ["Dân", "số?"]},
    {"role": "assistant", "content": ["Khoảng", "2,1", "triệu."]},
]
print(f"Từ vựng: {len(vocab)} tokens | Số lượt: {len(messages)}")
Từ vựng: 20 tokens | Số lượt: 5
# (1) Đóng gói hội thoại theo chuẩn ChatML và tạo mặt nạ labels
input_ids = []
labels = []

for msg in messages:
    header = ["<|im_start|>", msg["role"], "\n"]
    body = msg["content"] + ["<|im_end|>", "\n"]

    header_ids = [tok2id[t] for t in header]
    body_ids = [tok2id[t] for t in body]

    input_ids.extend(header_ids + body_ids)
    # Luôn che mặt nạ (-100) cho phần header (<|im_start|>role\n)
    labels.extend([-100] * len(header_ids))
    if msg["role"] == "assistant":
        labels.extend(body_ids)
    else:
        labels.extend([-100] * len(body_ids))

x = torch.tensor(input_ids, dtype=torch.long)
y_masked = torch.tensor(labels, dtype=torch.long)
active_toks = [vocab[i] for i in labels if i != -100]

print(f"Tổng số token trong chuỗi: {len(x)}")
pct = 100 * len(active_toks) / len(x)
print(f"Số token tính đạo hàm (mask=1): {len(active_toks)} / {len(x)} ({pct:.1f}%)")
print(f"Token được học: {active_toks}")

# (2) Dịch chuyển tự hồi quy (causal shift) và tính Cross-Entropy
torch.manual_seed(42)
logits = torch.randn(len(x), len(vocab))
# Tăng nhẹ logit cho các token trả lời đúng của assistant
for pos in range(len(x) - 1):
    if y_masked[pos + 1] != -100:
        logits[pos, y_masked[pos + 1]] += 0.35

shift_logits = logits[:-1]
shift_unmasked = x[1:]
shift_masked = y_masked[1:]

loss_unmasked = F.cross_entropy(shift_logits, shift_unmasked)
loss_masked = F.cross_entropy(
    shift_logits, shift_masked, ignore_index=-100
)

print(f"Loss không dùng mặt nạ (Unmasked): {loss_unmasked.item():.4f}")
print(f"Loss chuẩn SFT có mặt nạ (Masked):  {loss_masked.item():.4f}")
Tổng số token trong chuỗi: 31
Số token tính đạo hàm (mask=1): 9 / 31 (29.0%)
Token được học: ['Là', 'Paris.', '<|im_end|>', '\n', 'Khoảng', '2,1', 'triệu.', '<|im_end|>', '\n']
Loss không dùng mặt nạ (Unmasked): 3.0412
Loss chuẩn SFT có mặt nạ (Masked):  2.8135
Điều cần lưu ý

Hãy nhìn vào danh sách 9 token được học: ngoài các từ nội dung ('Là', 'Paris.', 'Khoảng', '2,1', 'triệu.'), chúng ta bắt buộc phải giữ lại <|im_end|> trong phần tính hàm mất mát (mask = 1). Nếu bạn vô tình gán -100 cho <|im_end|>, mô hình sẽ không bao giờ học được xác suất dừng câu trả lời và sẽ tiếp tục sinh văn bản bất tận cho đến khi chạm giới hạn cửa sổ ngữ cảnh! Ngược lại, các token thuộc câu hỏi của người dùng vẫn nằm nguyên trong input_ids để các lớp Self-Attention của assistant đọc ngữ cảnh, nhưng bị chặn hoàn toàn ở tầng tính đạo hàm mục tiêu.

1.3 Giả thuyết Căn chỉnh Bề mặt (Superficial Alignment Hypothesis — LIMA)

Khi mới làm quen với SFT, nhiều người lầm tưởng rằng giai đoạn này dạy cho mô hình những kiến thức mới. Nhưng hãy so sánh quy mô dữ liệu: tiền huấn luyện đọc 15.000 tỷ token (1.5×10131.5 \times 10^{13}), trong khi SFT thường chỉ chạy trên vài chục nghìn mẫu hội thoại (khoảng 10710^7 token — ít hơn một phần triệu!).

Thậm chí, vào tháng 5 năm 2023, nhóm nghiên cứu của Zhou et al. (Zhou & others, 2023) đã đặt một câu hỏi táo bạo: Cần tối thiểu bao nhiêu mẫu SFT để biến một mô hình nền tảng LLaMA 65B thành một trợ lý xuất sắc? Câu trả lời của họ đã gây chấn động cộng đồng: chỉ cần đúng 1.000 mẫu hội thoại được biên tập kỹ lưỡng!

Các tác giả của LIMA đúc kết phát hiện này thành Giả thuyết Căn chỉnh Bề mặt (Superficial Alignment Hypothesis) (Zhou & others, 2023):

“Hầu hết mọi tri thức và năng lực của một mô hình ngôn ngữ đều đã được học trong quá trình tiền huấn luyện; giai đoạn căn chỉnh (alignment) thực chất chỉ dạy cho mô hình nên chọn định dạng và phong cách giao tiếp (subdistribution of formats) nào khi tương tác với người dùng.”

Hệ quả thực tiễn của giả thuyết này cực kỳ sâu sắc: Trong SFT, chất lượng dữ liệu quan trọng gấp bội số lượng. 1.000 câu trả lời mạch lạc, chuẩn xác sẽ tạo ra một trợ lý thông minh hơn hẳn 100.000 câu trả lời cẩu thả thu thập tự động từ web.


2. Tinh chỉnh hiệu quả tham số (PEFT): LoRA và QLoRA

Ở Mục 1, chúng ta đã biết hàm mất mát LSFT(θ)\mathcal{L}_{\text{SFT}}(\theta) được tính như thế nào. Nhưng hãy làm một phép tính thực tế về bộ nhớ phần cứng (VRAM) khi bạn muốn tinh chỉnh toàn bộ tham số (Full Fine-Tuning) của một mô hình cỡ vừa như Llama-3 8B (N=8×109N = 8 \times 10^9 tham số) ở độ chính xác bfloat16 (2 byte mỗi tham số):

Chưa tính bộ nhớ lưu các vector kích hoạt trung gian (activations), chỉ riêng việc giữ mô hình và bộ tối ưu hóa đã ngốn 16+16+96=128 GB16 + 16 + 96 = 128\text{ GB} VRAM — vượt xa các GPU phổ thông (16 GB – 24 GB) và thậm chí tràn cả một siêu GPU A100/H100 80 GB! Hơn nữa, nếu bạn muốn phục vụ 100 khách hàng với 100 mô hình tinh chỉnh riêng biệt, bạn sẽ phải lưu trữ 100 bản sao nặng 16 GB16\text{ GB} (1.6 TB1.6\text{ TB} dung lượng ổ cứng).

Có cách nào tinh chỉnh một mô hình hàng chục tỷ tham số mà không cần cập nhật toàn bộ ma trận trọng số khổng lồ của nó hay không?

2.1 Chiều nội tại thấp và Phân rã ma trận hạng thấp (LoRA)

Như Giả thuyết Căn chỉnh Bề mặt vừa chỉ ra, quá trình tinh chỉnh không dạy lại ngôn ngữ từ đầu mà chỉ điều chỉnh nhẹ hành vi của mô hình. Nói theo ngôn ngữ đại số tuyến tính: mặc dù ma trận trọng số gốc W0∈Rdout×dinW_0 \in \mathbb{R}^{d_{\text{out}} \times d_{\text{in}}} có số chiều rất lớn (ví dụ 4096×4096≈16,74096 \times 4096 \approx 16{,}7 triệu tham số), phần độ lệch cập nhật ΔW=W−W0\Delta W = W - W_0 được học trong quá trình tinh chỉnh thực chất nằm trên một không gian con có chiều nội tại rất thấp (low intrinsic dimension).

Dựa trên nhận xét này, năm 2021, Edward Hu và các đồng nghiệp tại Microsoft đã giới thiệu LoRA (Low-Rank Adaptation of Large Language Models) (Hu & others, 2022) — thuật toán đã dân chủ hóa hoàn toàn việc tinh chỉnh LLM.

Ý tưởng của LoRA đẹp đến mức tối giản. Xét một lớp tuyến tính bất kỳ trong khối Transformer có ma trận trọng số tiền huấn luyện W0∈Rdout×dinW_0 \in \mathbb{R}^{d_{\text{out}} \times d_{\text{in}}}. Thay vì cập nhật trực tiếp W0W_0, ta đóng băng hoàn toàn W0W_0 (không tính đạo hàm, không lưu trạng thái AdamW cho W0W_0), và biểu diễn ma trận cập nhật ΔW\Delta W dưới dạng tích của hai ma trận thon dài có hạng r≪min⁡(din,dout)r \ll \min(d_{\text{in}}, d_{\text{out}}):

W=W0+ΔW=W0+αrBA,W = W_0 + \Delta W = W_0 + \frac{\alpha}{r} B A,

trong đó:

Khi một vector đầu vào x∈Rdinx \in \mathbb{R}^{d_{\text{in}}} đi qua lớp LoRA, phép tính truyền xuôi (forward pass) diễn ra trên hai nhánh song song rồi cộng lại:

h=W0x+αrB(Ax).h = W_0 x + \frac{\alpha}{r} B (A x).

Hãy kiểm tra số lượng tham số cần huấn luyện! Nếu din=dout=4096d_{\text{in}} = d_{\text{out}} = 4096 và ta chọn r=8r = 8:

Hãy kéo các thanh trượt trong biểu đồ tương tác dưới đây để quan sát cấu trúc cổ chai của BAB A và mức tiết kiệm VRAM thực tế.

Sơ đồ tương tác Nhánh LoRA và Bộ tính dung lượng VRAM (Full FT vs LoRA vs QLoRA)
Bên trái: Nhánh trọng số gốc W₀ (màu xanh lam) bị đóng băng hoàn toàn, trong khi nhánh thích nghi đi qua cổ chai chiều thấp r qua hai ma trận A (r × d) và B (d × r). Bên phải: Biểu đồ so sánh tổng dung lượng VRAM cần thiết (bao gồm Trọng số, Đạo hàm và trạng thái bộ tối ưu hóa AdamW) trên mô hình 32 lớp khi thay đổi hạng r và chiều ẩn d.

2.2 Tại sao khởi tạo AA ngẫu nhiên và BB bằng 00? Quy tắc chọn hạng rr và hệ số α\alpha

Nhìn vào sơ đồ trên, có ba câu hỏi thiết kế tinh tế mà mọi kỹ sư LLM đều phải nắm vững:

  1. Tại sao khởi tạo A∼N(0,σ2)A \sim \mathcal{N}(0, \sigma^2) nhưng bắt buộc khởi tạo B=0B = 0? Ở bước đầu tiên của quá trình tinh chỉnh (t=0t = 0), ta muốn mô hình LoRA hành xử giống hệt 100% mô hình tiền huấn luyện gốc để không làm xáo trộn các biểu diễn đã học. Nhờ B=0B = 0, tích ΔW=αrBA=0\Delta W = \frac{\alpha}{r} B A = 0 tại t=0t = 0. Nhưng tại sao không khởi tạo cả A=0A = 0 và B=0B = 0? Nếu cả A=0A = 0 lẫn B=0B = 0, thì đạo hàm theo AA (vốn tỷ lệ với B⊤B^\top) và đạo hàm theo BB (tỷ lệ với A⊤A^\top) đều bằng 00! Đó là một điểm yên ngựa chết (dead saddle point) khiến cả AA và BB mãi mãi bằng 00. Khởi tạo AA ngẫu nhiên và B=0B = 0 giúp ΔW=0\Delta W = 0 tại t=0t=0, nhưng ngay ở bước lan truyền ngược đầu tiên, ∇BL≠0\nabla_B \mathcal{L} \neq 0 giúp BB thoát khỏi điểm 00 và kéo theo AA cùng học ở các bước tiếp theo.

  2. Tại sao cần hệ số chia αr\frac{\alpha}{r}? Khi khởi tạo AA và cập nhật BB, độ lớn của tổng nội bộ (BAx)(B A x) tỷ lệ thuận với số chiều cổ chai rr. Việc nhân với αr\frac{\alpha}{r} giúp chuẩn hóa biên độ đầu ra của nhánh LoRA: khi bạn thử nghiệm tăng hạng từ r=8r = 8 lên r=32r = 32, bạn không cần phải dò lại tốc độ học (learning rate) từ đầu (Hu & others, 2022).

  3. Không tốn thêm bất kỳ mili-giây nào khi suy luận (Zero Inference Overhead)! Sau khi huấn luyện xong AA và BB, khi đưa mô hình vào triển khai thực tế, ta không cần giữ hai nhánh riêng biệt nữa. Vì phép nhân ma trận có tính phân phối, ta chỉ việc cộng gộp trực tiếp một lần duy nhất: Wmerged←W0+αrBA.W_{\text{merged}} \leftarrow W_0 + \frac{\alpha}{r} B A. Ma trận WmergedW_{\text{merged}} có kích thước dout×dind_{\text{out}} \times d_{\text{in}} y hệt W0W_0, nên tốc độ suy luận giống hệt mô hình gốc! Còn nếu muốn phục vụ 100 tác vụ khác nhau trên cùng một GPU, ta giữ nguyên W0W_0 trên VRAM và chỉ tráo đổi các cặp (Ai,Bi)(A_i, B_i) nặng vài chục MB theo từng yêu cầu.

Hãy kiểm chứng cả ba tính chất này bằng cách tự cài đặt lớp LoRALinear trong Bài tập 4.2.

Bài tập 4.2 Cài đặt lớp LoRALinear và gộp trọng số sau tinh chỉnh
Dữ liệu: PyTorch · Thời gian chạy: < 1 phút

Từ lớp tuyến tính base_linear (512×512512 \times 512) trong mã khởi tạo:

  1. Viết lớp LoRALinear(nn.Module) đóng băng base.weight (requires_grad_(False)), khởi tạo tham số lora_A kích thước (r,din)(r, d_{\text{in}}) từ phân phối chuẩn N(0,0.022)\mathcal{N}(0, 0.02^2) và lora_B kích thước (dout,r)(d_{\text{out}}, r) bằng toàn số 00.
  2. Cài đặt hàm forward(x) tính W0x+αr(xA⊤)B⊤W_0 x + \frac{\alpha}{r} (x A^\top) B^\top, kiểm tra sai lệch cực đại so với base_linear(X) tại t=0t = 0 và đếm tỷ lệ tham số cần huấn luyện.
  3. Huấn luyện lora_A và lora_B trong 150 bước bằng AdamW(lr=0.05) để khớp đầu ra mục tiêu Y_target, sau đó cài đặt hàm merged_weight() tính W0+αrBAW_0 + \frac{\alpha}{r} B A và kiểm chứng đầu ra sau khi gộp trùng khớp với nhánh song song.
Mã khởi tạo
import torch
import torch.nn as nn
import torch.nn.functional as F

torch.manual_seed(0)
d_in, d_out, r, alpha = 512, 512, 4, 8.0
base_linear = nn.Linear(d_in, d_out, bias=False)

# Tạo dữ liệu mục tiêu có độ lệch hạng thấp thực sự từ base_linear
true_B = torch.randn(d_out, r) * 0.15
true_A = torch.randn(r, d_in) * 0.15
X = torch.randn(128, d_in)
with torch.no_grad():
    Y_target = F.linear(X, base_linear.weight + true_B @ true_A)

print(f"Ma trận gốc W0: {base_linear.weight.numel():,} tham số")
Ma trận gốc W0: 262,144 tham số
Hint

Trong PyTorch, lớp nn.Linear(d_in, d_out) lưu ma trận trọng số ở dạng chuyển vị (d_out, d_in) và thực hiện phép tính x @ weight.T. Do đó, nếu lora_A có shape (r, d_in) và lora_B có shape (d_out, r), thì với batch đầu vào x có shape (batch, d_in), nhánh LoRA được tính cực nhanh qua (x @ self.lora_A.T) @ self.lora_B.T.

Hint

Đừng bao giờ tính tích (self.lora_B @ self.lora_A) trước rồi mới nhân với x bên trong hàm forward lúc huấn luyện! Phép tính (x @ A.T) @ B.T chỉ tốn O(batch⋅r⋅(din+dout))O(\text{batch} \cdot r \cdot (d_{\text{in}} + d_{\text{out}})) phép nhân, trong khi tạo ma trận đầy đủ B @ A tốn O(dout⋅din⋅r)O(d_{\text{out}} \cdot d_{\text{in}} \cdot r). Chỉ khi huấn luyện xong và gọi merged_weight(), ta mới nhân self.lora_B @ self.lora_A một lần duy nhất.

Điểm kiểm tra kết quả bạn cần đạt được

Tại t=0t = 0, sai lệch giữa LoRALinear và base_linear bằng đúng 0.00e+00. Chỉ với 4.096 tham số được huấn luyện (1.56% so với 262.144 tham số của W0W_0), hàm mất mát MSE giảm từ 0.451820 xuống 0.000014 sau 150 bước. Sau khi gộp vào một ma trận duy nhất, sai số số học dấu phẩy động chỉ là 1.19e-07.

Sai lệch lúc khởi tạo (t=0): 0.00e+00
Tham số cần huấn luyện: 4,096 / 262,144 (1.56%)
MSE ban đầu (t=0):      0.451820
MSE sau 150 bước LoRA:  0.000014
Sai lệch sau khi gộp W_merged = W0 + (alpha/r)*B@A: 1.19e-07
Lời giải 2 ô mã

Chạy các ô mã theo đúng thứ tự này trong một notebook mới: chúng đã bao gồm các lệnh import, bước tải dữ liệu và khởi tạo hạt giống ngẫu nhiên (random seed).

import torch
import torch.nn as nn
import torch.nn.functional as F

torch.manual_seed(0)
d_in, d_out, r, alpha = 512, 512, 4, 8.0
base_linear = nn.Linear(d_in, d_out, bias=False)

# Tạo dữ liệu mục tiêu có độ lệch hạng thấp thực sự từ base_linear
true_B = torch.randn(d_out, r) * 0.15
true_A = torch.randn(r, d_in) * 0.15
X = torch.randn(128, d_in)
with torch.no_grad():
    Y_target = F.linear(X, base_linear.weight + true_B @ true_A)

print(f"Ma trận gốc W0: {base_linear.weight.numel():,} tham số")
Ma trận gốc W0: 262,144 tham số
# (1) Định nghĩa lớp LoRALinear với W0 đóng băng, A ~ N(0,0.02^2), B = 0
class LoRALinear(nn.Module):
    def __init__(self, base: nn.Linear, r: int, alpha: float):
        super().__init__()
        self.base = base
        self.base.weight.requires_grad_(False)
        self.scaling = alpha / r
        d_out, d_in = base.weight.shape
        self.lora_A = nn.Parameter(torch.randn(r, d_in) * 0.02)
        self.lora_B = nn.Parameter(torch.zeros(d_out, r))

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        delta = (x @ self.lora_A.T) @ self.lora_B.T
        return self.base(x) + self.scaling * delta

    def merged_weight(self) -> torch.Tensor:
        return self.base.weight + self.scaling * (self.lora_B @ self.lora_A)


lora_layer = LoRALinear(base_linear, r=r, alpha=alpha)

# (2) Kiểm tra tính chất bảo toàn tại t=0 và đếm số tham số
with torch.no_grad():
    init_diff = (lora_layer(X) - base_linear(X)).abs().max().item()
    init_mse = F.mse_loss(lora_layer(X), Y_target).item()

trainable = sum(p.numel() for p in lora_layer.parameters() if p.requires_grad)
total_w0 = base_linear.weight.numel()
print(f"Sai lệch lúc khởi tạo (t=0): {init_diff:.2e}")
print(f"Tham số cần huấn luyện: {trainable:,} / {total_w0:,} ({100*trainable/total_w0:.2f}%)")
print(f"MSE ban đầu (t=0):      {init_mse:.6f}")

# (3) Tinh chỉnh chỉ trên A và B bằng AdamW rồi gộp trọng số
opt = torch.optim.AdamW([lora_layer.lora_A, lora_layer.lora_B], lr=0.05)
for _ in range(150):
    opt.zero_grad()
    loss = F.mse_loss(lora_layer(X), Y_target)
    loss.backward()
    opt.step()

with torch.no_grad():
    out_lora = lora_layer(X)
    out_merged = F.linear(X, lora_layer.merged_weight())
    merge_diff = (out_lora - out_merged).abs().max().item()

print(f"MSE sau 150 bước LoRA:  {loss.item():.6f}")
print(f"Sai lệch sau khi gộp W_merged = W0 + (alpha/r)*B@A: {merge_diff:.2e}")
Sai lệch lúc khởi tạo (t=0): 0.00e+00
Tham số cần huấn luyện: 4,096 / 262,144 (1.56%)
MSE ban đầu (t=0):      0.451820
MSE sau 150 bước LoRA:  0.000014
Sai lệch sau khi gộp W_merged = W0 + (alpha/r)*B@A: 1.19e-07
Điều cần lưu ý

Hãy chú ý hai chi tiết kỹ thuật trong lời giải:

  1. Thứ tự kết hợp phép nhân ma trận (x @ A.T) @ B.T: bằng cách ép chiều từ din=512d_{\text{in}} = 512 xuống cổ chai r=4r = 4 trước rồi mới nâng lên dout=512d_{\text{out}} = 512, số phép nhân-cộng (FLOPs) của nhánh phụ giảm đi 5122×4=64\frac{512}{2 \times 4} = 64 lần so với một lớp tuyến tính thông thường.
  2. Trong các kiến trúc Transformer hiện đại (như Llama-3, Qwen), các nghiên cứu thực nghiệm cho thấy áp dụng LoRA lên tất cả các ma trận tuyến tính (cả Wq,Wk,Wv,WoW_q, W_k, W_v, W_o trong Attention lẫn Wgate,Wup,WdownW_{\text{gate}}, W_{\text{up}}, W_{\text{down}} trong SwiGLU FFN) mang lại chất lượng tốt hơn rõ rệt so với việc chỉ gắn LoRA vào Wq,WvW_q, W_v mà hầu như không tốn thêm bao nhiêu VRAM (Dettmers & others, 2023).

2.3 QLoRA: Tinh chỉnh mô hình 65 tỷ tham số trên một GPU duy nhất

Với LoRA, ta đã loại bỏ gần như hoàn toàn bộ nhớ cho đạo hàm và trạng thái AdamW. Tuy nhiên, ma trận gốc W0W_0 đóng băng vẫn phải nằm trên VRAM để phục vụ các phép nhân truyền xuôi và truyền ngược. Với mô hình 65 tỷ tham số ở dạng 16-bit, riêng W0W_0 đã chiếm 65×2=130 GB65 \times 2 = 130\text{ GB} VRAM!

Tháng 5 năm 2023, Tim Dettmers và các cộng sự tại Đại học Washington đã giải quyết triệt để nút cổ chai cuối cùng này bằng QLoRA (Quantized LoRA) (Dettmers & others, 2023):

Nhờ QLoRA, một mô hình 8 tỷ tham số chỉ cần dưới 6 GB VRAM để tinh chỉnh (chạy mượt mà trên một chiếc laptop hay Google Colab miễn phí), và một mô hình khổng lồ 65B–70B có thể được tinh chỉnh trọn vẹn trên một GPU 48 GB duy nhất mà không suy giảm chất lượng so với tinh chỉnh 16-bit (Dettmers & others, 2023)!

Đọc thêm NormalFloat 4-bit (NF4) và Lượng tử hóa Kép (Double Quantization)

Tại sao QLoRA không dùng lượng tử hóa số nguyên 4-bit đều (INT4 thông thường) mà lại phát minh ra kiểu dữ liệu NF4 (4-bit NormalFloat)?

Trong mạng nơ-ron đã tiền huấn luyện, các trọng số trong mỗi ma trận W0W_0 không phân bố đều mà tuân theo phân phối chuẩn (Gaussian) N(0,σ2)\mathcal{N}(0, \sigma^2) tập trung dày đặc quanh giá trị 00 và thưa dần ở hai đuôi. Nếu chia đều 16 mức lượng tử hóa (24=162^4 = 16) trên đoạn [−1,1][-1, 1], phần lớn các mức ở hai rìa sẽ bị bỏ phí trong khi vùng trung tâm quanh 00 lại bị làm tròn quá thô.

NF4 chia miền giá trị thành 16 phần có xác suất tích lũy bằng nhau (equal-mass quantiles) dưới phân phối chuẩn tắc N(0,1)\mathcal{N}(0, 1), sau đó chuẩn hóa về [−1,1][-1, 1]. Về mặt lý thuyết thông tin, NF4 là kiểu dữ liệu 4-bit tối ưu cho các đại lượng phân bố chuẩn. Kết hợp với Lượng tử hóa Kép (Double Quantization) — tiếp tục nén chính các hằng số tỷ lệ lượng tử hóa (quantization scales) từ 32-bit xuống 8-bit — QLoRA giảm dung lượng lưu trữ trung bình từ 16 bit xuống chỉ còn 4.27 bit/tham số (Dettmers & others, 2023).


3. Học tăng cường từ Phản hồi Con người (RLHF): Mô hình Phần thưởng và PPO

Sau giai đoạn SFT (với LoRA/QLoRA), mô hình của chúng ta đã biết trả lời người dùng đúng định dạng trợ lý. Nhưng khi đưa vào sử dụng thực tế, các hạn chế bản chất của việc “học bắt chước” (imitation learning) trong SFT lập tức lộ diện.

3.1 Tại sao SFT là chưa đủ? So sánh dễ hơn Viết mẫu

Hãy nhìn vào cấu trúc toán học của hàm mất mát SFT: nó đối xử với mọi lỗi sai ở mức token theo cùng một cách. Một câu trả lời làm toán sai ở con số cuối cùng chỉ bị phạt đúng 1 token, trong khi một câu trả lời hoàn toàn chính xác nhưng dùng từ đồng nghĩa khác với người viết mẫu lại bị phạt nặng trên hàng chục token!

Hơn nữa, việc yêu cầu người gán nhãn tự tay viết ra câu trả lời hoàn hảo cho hàng vạn câu hỏi phức tạp (từ lập trình, y học, pháp lý đến sáng tác văn chương) là cực kỳ tốn kém và không mở rộng được. Ngược lại, trong tâm lý học nhận thức có một nguyên lý bất biến: Đánh giá và so sánh hai phương án luôn dễ hơn rất nhiều so với việc tự sáng tạo ra phương án tối ưu từ con số không (recognition is easier than generation).

Năm 2017, Paul Christiano và các cộng sự tại OpenAI & DeepMind đã chứng minh rằng ta có thể huấn luyện các tác tử học tăng cường giải quyết các bài toán phức tạp chỉ từ vài nghìn lượt con người chọn “Video A tốt hơn Video B” (Christiano & others, 2017). Đến tháng 3 năm 2022, nhóm của Long Ouyang tại OpenAI đã áp dụng trọn vẹn quy trình 3 bước này lên GPT-3 để tạo ra InstructGPT (Ouyang et al., 2022) — tiền thân trực tiếp khai sinh ra ChatGPT.

Quy trình Học tăng cường từ Phản hồi Con người (Reinforcement Learning from Human Feedback — RLHF) của InstructGPT (Ouyang et al., 2022) biến bài toán căn chỉnh thành hai bước nối tiếp nhau sau SFT:

  1. Bước 2 — Huấn luyện Mô hình Phần thưởng (Reward Modeling): Dạy một mạng nơ-ron rϕ(x,y)∈Rr_\phi(x, y) \in \mathbb{R} chấm điểm độ “hữu ích, trung thực và vô hại” của câu trả lời yy cho câu hỏi xx, dựa trên dữ liệu so sánh cặp của con người.
  2. Bước 3 — Tối ưu hóa Chính sách bằng Học tăng cường (RL Policy Optimization): Dùng mô hình phần thưởng rϕr_\phi làm giám khảo tự động để tinh chỉnh mô hình ngôn ngữ πθ(y∣x)\pi_\theta(y \mid x) sinh ra các câu trả lời đạt điểm cao.

3.2 Mô hình Sở thích Bradley–Terry và Hàm mất mát Xếp hạng Cặp

Làm thế nào để huấn luyện một mô hình phần thưởng rϕ(x,y)r_\phi(x, y) xuất ra một con số thực duy nhất khi người gán nhãn không hề cho điểm số tuyệt đối (vì mỗi người có một thang điểm chủ quan khác nhau), mà chỉ cho biết trong hai câu trả lời (yw,yl)(y_w, y_l) cho cùng một prompt xx, câu trả lời ywy_w (winner / chosen) tốt hơn câu trả lời yly_l (loser / rejected)?

Chúng ta sử dụng mô hình xác suất sở thích Bradley–Terry (1952): xác suất để con người ưa thích ywy_w hơn yly_l được mô hình hóa bằng hàm logistic (σ(z)=11+e−z\sigma(z) = \frac{1}{1 + e^{-z}}) của hiệu số điểm thưởng giữa hai câu trả lời:

P(yw≻yl∣x)=σ ⁣(rϕ(x,yw)−rϕ(x,yl))=11+exp⁡ ⁣(−(rϕ(x,yw)−rϕ(x,yl))).P(y_w \succ y_l \mid x) = \sigma\!\Big(r_\phi(x, y_w) - r_\phi(x, y_l)\Big) = \frac{1}{1 + \exp\!\big(-(r_\phi(x, y_w) - r_\phi(x, y_l))\big)}.

Về mặt kiến trúc, Mô hình Phần thưởng rϕr_\phi được khởi tạo từ chính một mô hình SFT, trong đó ta bỏ lớp chiếu từ vựng (lm_head kích thước dmodel×∣V∣d_{\text{model}} \times |V|) ở cuối và thay bằng một đầu tuyến tính vô hướng (score_head kích thước dmodel×1d_{\text{model}} \times 1). Khi đọc chuỗi ghép [x,y][x, y], vector trạng thái ẩn của token cuối cùng được chiếu qua score_head để cho ra điểm thưởng rϕ(x,y)∈Rr_\phi(x, y) \in \mathbb{R}.

Với tập dữ liệu sở thích D={(x(i),yw(i),yl(i))}i=1N\mathcal{D} = \{(x^{(i)}, y_w^{(i)}, y_l^{(i)})\}_{i=1}^N, ta huấn luyện rϕr_\phi bằng cách cực đại hóa log-likelihood của các lựa chọn đúng (tức cực tiểu hóa hàm mất mát phân loại nhị phân trên hiệu điểm thưởng) (Christiano & others, 2017; Ouyang et al., 2022):

LRM(ϕ)=−E(x,yw,yl)∼D[log⁡σ ⁣(rϕ(x,yw)−rϕ(x,yl))].\mathcal{L}_{\text{RM}}(\phi) = -\mathbb{E}_{(x, y_w, y_l) \sim \mathcal{D}} \Big[ \log \sigma\!\Big(r_\phi(x, y_w) - r_\phi(x, y_l)\Big) \Big].

Hãy chú ý tính chất bất biến dịch chuyển của phương trình này: nếu bạn cộng thêm một hằng số +10+10 vào cả rϕ(x,yw)r_\phi(x, y_w) và rϕ(x,yl)r_\phi(x, y_l), hiệu số rϕ(x,yw)−rϕ(x,yl)r_\phi(x, y_w) - r_\phi(x, y_l) không hề thay đổi! Nhờ đó, mô hình phần thưởng chỉ cần tập trung học thứ tự xếp hạng tương đối giữa các câu trả lời cho cùng một câu hỏi xx. (Khi quy mô dữ liệu lớn, người ta cũng có thể dùng một LLM mạnh chấm điểm thay con người dựa trên một bản hiến pháp nguyên tắc — kỹ thuật RLAIF / Constitutional AI của Bai et al. (Bai & others, 2022)).

3.3 Tối ưu hóa Chính sách với Ràng buộc KL (PPO) và Hiện tượng Hack phần thưởng

Sau khi đã có mô hình phần thưởng rϕ(x,y)r_\phi(x, y) cố định, ta bước sang giai đoạn Học tăng cường: coi mô hình ngôn ngữ đang cần căn chỉnh là một chính sách (policy) πθ(y∣x)\pi_\theta(y \mid x) nhận “trạng thái” là prompt xx và thực hiện “hành động” là sinh ra chuỗi phản hồi yy.

Tuy nhiên, tại sao ta không thể tính đạo hàm trực tiếp ∇θrϕ(x,y)\nabla_\theta r_\phi(x, y) để cập nhật θ\theta như một mạng nơ-ron bình thường? Bởi vì để đưa văn bản yy vào mô hình phần thưởng, ta phải lấy mẫu các token rời rạc y∼πθ(⋅∣x)y \sim \pi_\theta(\cdot \mid x), và phép lấy mẫu rời rạc là không khả vi (non-differentiable)! Đây chính là lý do ta phải dùng các thuật toán đạo hàm chính sách (Policy Gradient) trong Học tăng cường như PPO (Proximal Policy Optimization) của Schulman et al. (Schulman & others, 2017).

Nhưng một thảm họa còn lớn hơn đang chờ đợi nếu bạn chỉ đơn thuần cực đại hóa Ey∼πθ[rϕ(x,y)]\mathbb{E}_{y \sim \pi_\theta}[r_\phi(x, y)]. Hãy nhớ rằng rϕ(x,y)r_\phi(x, y) chỉ là một mạng nơ-ron xấp xỉ không hoàn hảo được huấn luyện trên vài chục nghìn mẫu hữu hạn. Nếu chính sách πθ\pi_\theta được tự do thay đổi không giới hạn để săn điểm cao từ rϕr_\phi, nó sẽ nhanh chóng tìm ra các “vùng mù” (out-of-distribution adversarial examples) của mô hình phần thưởng:

Để ngăn chặn πθ\pi_\theta rời quá xa vùng ngôn ngữ tự nhiên đã học, bài toán RLHF chuẩn (Ouyang et al., 2022) luôn giữ lại một bản sao đóng băng của mô hình SFT ban đầu làm chính sách tham chiếu (reference policy) πref\pi_{\text{ref}}, và trừ đi một khoản phạt Phân kỳ Kullback–Leibler (KL Divergence) với hệ số β>0\beta > 0:

max⁡πθ  Ex∼D, y∼πθ(⋅∣x)[rϕ(x,y)]−β DKL ⁣(πθ(y∣x)  ∥  πref(y∣x)),\max_{\pi_\theta} \; \mathbb{E}_{x \sim \mathcal{D},\, y \sim \pi_\theta(\cdot \mid x)} \Big[ r_\phi(x, y) \Big] - \beta \, D_{\text{KL}}\!\Big( \pi_\theta(y \mid x) \;\Big\|\; \pi_{\text{ref}}(y \mid x) \Big),

hay viết gộp dưới dạng phần thưởng hiệu chỉnh trên mỗi mẫu yy:

Rtotal(x,y)=rϕ(x,y)−βlog⁡πθ(y∣x)πref(y∣x).R_{\text{total}}(x, y) = r_\phi(x, y) - \beta \log \frac{\pi_\theta(y \mid x)}{\pi_{\text{ref}}(y \mid x)}.

Mỗi khi mô hình πθ\pi_\theta đẩy xác suất của một chuỗi kỳ dị lên cao gấp nhiều lần xác suất mà mô hình SFT gốc πref\pi_{\text{ref}} gán cho nó, tỷ số log⁡πθ(y∣x)πref(y∣x)\log \frac{\pi_\theta(y \mid x)}{\pi_{\text{ref}}(y \mid x)} sẽ tăng vọt và kéo điểm thưởng tổng xuống. Hãy tự mình điều chỉnh hệ số β\beta trong biểu đồ tương tác dưới đây để chứng kiến tận mắt ranh giới giữa vùng cân bằng tối ưu và vực thẳm Reward Hacking.

Biên Pareto giữa Điểm thưởng r_φ(x, y) và Phân kỳ KL — Hiện tượng Hack phần thưởng
Đường nét đứt (cam) là điểm số do Mô hình Phần thưởng xấp xỉ r_φ chấm; đường nét liền (xanh lá) là chất lượng thực sự theo đánh giá của con người R_gold. Khi giảm hệ số phạt β xuống quá nhỏ, mô hình rời xa π_ref (D_KL lớn) để trục lợi lỗi của r_φ, khiến chất lượng văn bản thực tế sụp đổ (vùng màu đỏ bên phải). Hãy kéo thanh trượt β hoặc bấm vào 3 mốc cài đặt sẵn để đọc phản hồi thực tế của mô hình.

4. Tối ưu hóa Sở thích Trực tiếp (Direct Preference Optimization — DPO)

Mặc dù RLHF với PPO (Ouyang et al., 2022; Schulman & others, 2017) cực kỳ thành công, việc triển khai nó trong thực tế lại là một cơn ác mộng kỹ thuật:

  1. Bạn phải huấn luyện một Mô hình Phần thưởng riêng biệt, rồi trong vòng lặp PPO phải nạp cùng lúc 4 mô hình khổng lồ lên VRAM: Chính sách πθ\pi_\theta (đang học), Tham chiếu πref\pi_{\text{ref}} (đóng băng), Phần thưởng rϕr_\phi (đóng băng), và Mạng giá trị Critic VψV_\psi (đang học để ước lượng baseline từng token).
  2. Trong mỗi bước PPO, bạn phải chạy giải mã tự hồi quy (autoregressive sampling) để sinh văn bản mới, rất chậm và cực kỳ nhạy cảm với siêu tham số.

Tháng 5 năm 2023, Rafael Rafailov và các đồng nghiệp tại Đại học Stanford đã đặt ra một câu hỏi làm thay đổi hoàn toàn bức tranh hậu huấn luyện: Liệu ta có thể giải trực tiếp bài toán tối ưu hóa RLHF bằng biến đổi đại số và loại bỏ hoàn toàn cả Mô hình Phần thưởng lẫn vòng lặp RL hay không? Câu trả lời là DPO (Direct Preference Optimization) (Rafailov & others, 2023).

4.1 Nghiệm giải tích của bài toán RLHF có ràng buộc KL

Hãy nhìn lại hàm mục tiêu RLHF có phạt phân kỳ KL ở phương trình của Mục 3.3 với một hàm phần thưởng r(x,y)r(x, y) bất kỳ:

max⁡π  Ey∼π(⋅∣x)[r(x,y)−βlog⁡π(y∣x)πref(y∣x)].\max_{\pi} \; \mathbb{E}_{y \sim \pi(\cdot \mid x)} \left[ r(x, y) - \beta \log \frac{\pi(y \mid x)}{\pi_{\text{ref}}(y \mid x)} \right].

Bằng cách rút −β-\beta ra ngoài và biến đổi biểu thức bên trong thành một phân kỳ KL, bài toán cực đại hóa trên thực chất có nghiệm tối ưu dạng đóng (closed-form optimal policy) π∗(y∣x)\pi^*(y \mid x) thuộc phân phối Gibbs–Boltzmann (Rafailov & others, 2023):

π∗(y∣x)=1Z(x) πref(y∣x)exp⁡ ⁣(1βr(x,y)),\pi^*(y \mid x) = \frac{1}{Z(x)} \, \pi_{\text{ref}}(y \mid x) \exp\!\left( \frac{1}{\beta} r(x, y) \right),

trong đó Z(x)=∑yπref(y∣x)exp⁡ ⁣(1βr(x,y))Z(x) = \sum_{y} \pi_{\text{ref}}(y \mid x) \exp\!\left(\frac{1}{\beta} r(x, y)\right) là hàm phân hoạch (partition function) chỉ phụ thuộc vào prompt xx (nhằm đảm bảo tổng xác suất của mọi câu trả lời khả dĩ bằng 11). Trong thực tế, ta không bao giờ tính nổi Z(x)Z(x) vì không gian mọi chuỗi văn bản yy là vô hạn.

4.2 Biến đổi đại số DPO: Mô hình ngôn ngữ vốn dĩ là một mô hình phần thưởng

Bước ngoặt thiên tài của Rafailov et al. (Rafailov & others, 2023) nằm ở chỗ: thay vì tính π∗\pi^* từ r(x,y)r(x, y), hãy lấy logarit hai vế của phương trình trên và rút ngược hàm phần thưởng r(x,y)r(x, y) theo chính sách tối ưu π∗\pi^*!

Lấy log⁡\log hai vế và nhân với β\beta, ta thu được ngay lập tức mối liên hệ 1-1:

r(x,y)=βlog⁡π∗(y∣x)πref(y∣x)+βlog⁡Z(x).r(x, y) = \beta \log \frac{\pi^*(y \mid x)}{\pi_{\text{ref}}(y \mid x)} + \beta \log Z(x).

Bây giờ, hãy đem biểu thức r(x,y)r(x, y) này thế trực tiếp vào mô hình sở thích cặp Bradley–Terry của Mục 3.2:

P(yw≻yl∣x)=σ ⁣(r(x,yw)−r(x,yl)).P(y_w \succ y_l \mid x) = \sigma\!\Big( r(x, y_w) - r(x, y_l) \Big).

Vì cả ywy_w và yly_l đều trả lời cho cùng một prompt xx, khi lấy hiệu r(x,yw)−r(x,yl)r(x, y_w) - r(x, y_l), đại lượng bất khả tính βlog⁡Z(x)\beta \log Z(x) ở hai số hạng triệt tiêu nhau hoàn toàn! Thay chính sách πθ\pi_\theta vào, ta định nghĩa phần thưởng ẩn (implicit reward) do chính mô hình ngôn ngữ tạo ra:

r^θ(x,y)=βlog⁡πθ(y∣x)πref(y∣x),\hat{r}_\theta(x, y) = \beta \log \frac{\pi_\theta(y \mid x)}{\pi_{\text{ref}}(y \mid x)},

và thu được Hàm mất mát DPO (Direct Preference Optimization Loss) (Rafailov & others, 2023):

LDPO(πθ;πref)=−E(x,yw,yl)∼D[log⁡σ(βlog⁡πθ(yw∣x)πref(yw∣x)−βlog⁡πθ(yl∣x)πref(yl∣x))].\mathcal{L}_{\text{DPO}}(\pi_\theta; \pi_{\text{ref}}) = -\mathbb{E}_{(x, y_w, y_l) \sim \mathcal{D}} \left[ \log \sigma \left( \beta \log \frac{\pi_\theta(y_w \mid x)}{\pi_{\text{ref}}(y_w \mid x)} - \beta \log \frac{\pi_\theta(y_l \mid x)}{\pi_{\text{ref}}(y_l \mid x)} \right) \right].

Hãy dừng lại một nhịp để chiêm ngưỡng vẻ đẹp của công thức LDPO\mathcal{L}_{\text{DPO}}:

4.3 Phân tích đạo hàm của DPO và Cài đặt thực hành

Tại sao cập nhật bằng LDPO\mathcal{L}_{\text{DPO}} lại hiệu quả hơn nhiều so với việc đơn thuần tăng xác suất của ywy_w và giảm xác suất của yly_l một cách mù quáng? Hãy lấy đạo hàm của LDPO\mathcal{L}_{\text{DPO}} theo tham số θ\theta (Rafailov & others, 2023):

∇θLDPO(πθ;πref)=−β E(x,yw,yl)[σ ⁣(r^θ(x,yl)−r^θ(x,yw))⏟Trọng soˆˊ thıˊch nghi theo độ sai lệch(∇θlog⁡πθ(yw∣x)⏟Ta˘ng xaˊc suaˆˊt yw−∇θlog⁡πθ(yl∣x)⏟Giảm xaˊc suaˆˊt yl)].\nabla_\theta \mathcal{L}_{\text{DPO}}(\pi_\theta; \pi_{\text{ref}}) = -\beta \, \mathbb{E}_{(x, y_w, y_l)} \Big[ \underbrace{\sigma\!\big(\hat{r}_\theta(x, y_l) - \hat{r}_\theta(x, y_w)\big)}_{\text{Trọng số thích nghi theo độ sai lệch}} \Big( \underbrace{\nabla_\theta \log \pi_\theta(y_w \mid x)}_{\text{Tăng xác suất } y_w} - \underbrace{\nabla_\theta \log \pi_\theta(y_l \mid x)}_{\text{Giảm xác suất } y_l} \Big) \Big].

Biểu thức đạo hàm này giải thích toàn bộ động lực học của DPO:

  1. Thành phần (∇θlog⁡πθ(yw∣x)−∇θlog⁡πθ(yl∣x))(\nabla_\theta \log \pi_\theta(y_w \mid x) - \nabla_\theta \log \pi_\theta(y_l \mid x)) đẩy xác suất sinh câu trả lời tốt ywy_w lên và kéo xác suất sinh câu trả lời tệ yly_l xuống.
  2. Hệ số đứng trước σ(r^θ(x,yl)−r^θ(x,yw))∈(0,1)\sigma(\hat{r}_\theta(x, y_l) - \hat{r}_\theta(x, y_w)) \in (0, 1) đóng vai trò bộ điều tiết tự động: nếu một cặp (yw,yl)(y_w, y_l) đã được mô hình phân biệt đúng với biên độ lớn (r^θ(x,yw)≫r^θ(x,yl)\hat{r}_\theta(x, y_w) \gg \hat{r}_\theta(x, y_l)), hệ số σ(⋅)\sigma(\cdot) sẽ tiến về 00 để ngừng ép mô hình thay đổi quá đà. Ngược lại, nếu mô hình đang xếp hạng sai (r^θ(x,yl)>r^θ(x,yw)\hat{r}_\theta(x, y_l) > \hat{r}_\theta(x, y_w)), hệ số này tiến gần 11 để tập trung sửa chữa sai lầm đó!

Nếu bỏ đi hệ số trọng số thích nghi σ(r^θ(x,yl)−r^θ(x,yw))\sigma(\hat{r}_\theta(x, y_l) - \hat{r}_\theta(x, y_w)) (tức chỉ tối ưu hiệu log⁡πθ(yw)−log⁡πθ(yl)\log \pi_\theta(y_w) - \log \pi_\theta(y_l), còn gọi là Unlikelihood), mô hình sẽ nhanh chóng suy thoái ngôn ngữ vì tiếp tục đẩy xác suất yly_l xuống −∞-\infty ngay cả khi yly_l đã rất thấp.

Hãy tự tay cài đặt hàm mất mát DPO và kiểm chứng tính chất đạo hàm biên này trong Bài tập 4.4.

Bài tập 4.4 Cài đặt hàm mất mát DPO và phân tích đạo hàm biên
Dữ liệu: Preferences · Thời gian chạy: < 1 phút

Trong mã khởi tạo, ta có một batch B=4B = 4 cặp phản hồi được chọn y_w và bị loại y_l (mỗi chuỗi dài T=6T = 6 token), cùng các tensor logit của mô hình tham chiếu ref_logits và mô hình chính sách pi_logits (khởi tạo bằng đúng ref_logits tại t=0t = 0):

  1. Viết hàm sequence_logprob(logits, tokens) tính log⁡π(y∣x)=∑t=1Tlog⁡P(yt∣x,y<t)\log \pi(y \mid x) = \sum_{t=1}^T \log P(y_t \mid x, y_{<t}) bằng F.log_softmax và .gather().
  2. Viết hàm dpo_loss(...) tính phần thưởng ẩn r^w=β(log⁡πθ(yw∣x)−log⁡πref(yw∣x))\hat{r}_w = \beta (\log \pi_\theta(y_w \mid x) - \log \pi_{\text{ref}}(y_w \mid x)) và r^l\hat{r}_l tương ứng, biên độ Δr^=r^w−r^l\Delta \hat{r} = \hat{r}_w - \hat{r}_l, hàm mất mát −mean(log⁡σ(Δr^))-\text{mean}(\log \sigma(\Delta \hat{r})) (dùng F.logsigmoid), và trọng số đạo hàm biên σ(−Δr^)\sigma(-\Delta \hat{r}).
  3. Kiểm tra giá trị hàm mất mát tại bước khởi tạo t=0t = 0 (phải bằng đúng ln⁡2≈0.693147\ln 2 \approx 0.693147), sau đó chạy 10 bước cập nhật SGD để quan sát biên độ phần thưởng ẩn tăng dương và trọng số đạo hàm biên giảm dần.
Mã khởi tạo
import torch
import torch.nn as nn
import torch.nn.functional as F

torch.manual_seed(7)
# B=4 cặp sở thích (prompt x, câu trả lời được chọn y_w, câu bị loại y_l)
# Mỗi chuỗi có độ dài T=6 token từ bộ từ vựng V=32
B, T, V = 4, 6, 32
beta = 0.1

ref_logits_w = torch.randn(B, T, V)
ref_logits_l = torch.randn(B, T, V)
# Khởi tạo chính sách pi_theta trùng khít với pi_ref lúc bắt đầu
pi_logits_w = ref_logits_w.clone().detach().requires_grad_(True)
pi_logits_l = ref_logits_l.clone().detach().requires_grad_(True)

y_w = torch.randint(0, V, (B, T))
y_l = torch.randint(0, V, (B, T))
print(f"Batch: {B} cặp (y_w, y_l) | Độ dài T={T} | Hệ số beta={beta}")
Batch: 4 cặp (y_w, y_l) | Độ dài T=6 | Hệ số beta=0.1
Hint

Khi logits có shape (B, T, V) và tokens có shape (B, T), sau khi tính log_probs = F.log_softmax(logits, dim=-1), bạn dùng log_probs.gather(dim=-1, index=tokens.unsqueeze(-1)).squeeze(-1) để lấy log-xác suất của đúng token mục tiêu tại mỗi vị trí (b,t)(b, t), rồi gọi .sum(dim=-1) để cộng dọc theo chiều chuỗi TT.

Hint

Để đảm bảo độ ổn định số học tuyệt đối, luôn sử dụng -F.logsigmoid(margins).mean() thay vì -torch.log(torch.sigmoid(margins)).mean(). Đừng quên bọc phần tính log-prob của mô hình tham chiếu ref_w, ref_l trong with torch.no_grad():.

Điểm kiểm tra kết quả bạn cần đạt được

Tại t=0t = 0, vì πθ=πref\pi_\theta = \pi_{\text{ref}} nên r^w=r^l=0\hat{r}_w = \hat{r}_l = 0, dẫn đến LDPO=−log⁡σ(0)=ln⁡(2)=0.693147\mathcal{L}_{\text{DPO}} = -\log \sigma(0) = \ln(2) = 0.693147. Sau 10 bước cập nhật, hàm mất mát giảm xuống 0.214836, biên độ phần thưởng ẩn trung bình tăng lên +1.4125, độ chính xác sở thích đạt 100.0% và trọng số đạo hàm biên tự động giảm từ 0.5000 xuống 0.1958.

Loss DPO lúc khởi tạo (pi_theta = pi_ref): 0.693147 (ln 2 = 0.693147)
Biên phần thưởng ẩn ban đầu: 0.0000 | Độ chính xác: 0.0%
Sau 10 bước cập nhật DPO:
Loss DPO mới: 0.214836
Biên phần thưởng ẩn E[r(y_w) - r(y_l)]: +1.4125
Trọng số đạo hàm biên trung bình sigma(r_l - r_w): 0.1958
Độ chính xác sở thích (r_w > r_l): 100.0%
Lời giải 2 ô mã

Chạy các ô mã theo đúng thứ tự này trong một notebook mới: chúng đã bao gồm các lệnh import, bước tải dữ liệu và khởi tạo hạt giống ngẫu nhiên (random seed).

import torch
import torch.nn as nn
import torch.nn.functional as F

torch.manual_seed(7)
# B=4 cặp sở thích (prompt x, câu trả lời được chọn y_w, câu bị loại y_l)
# Mỗi chuỗi có độ dài T=6 token từ bộ từ vựng V=32
B, T, V = 4, 6, 32
beta = 0.1

ref_logits_w = torch.randn(B, T, V)
ref_logits_l = torch.randn(B, T, V)
# Khởi tạo chính sách pi_theta trùng khít với pi_ref lúc bắt đầu
pi_logits_w = ref_logits_w.clone().detach().requires_grad_(True)
pi_logits_l = ref_logits_l.clone().detach().requires_grad_(True)

y_w = torch.randint(0, V, (B, T))
y_l = torch.randint(0, V, (B, T))
print(f"Batch: {B} cặp (y_w, y_l) | Độ dài T={T} | Hệ số beta={beta}")
Batch: 4 cặp (y_w, y_l) | Độ dài T=6 | Hệ số beta=0.1
# (1) Hàm tính tổng log-xác suất của chuỗi token y dưới phân phối logits
def sequence_logprob(logits: torch.Tensor, tokens: torch.Tensor) -> torch.Tensor:
    log_probs = F.log_softmax(logits, dim=-1)
    token_lp = log_probs.gather(dim=-1, index=tokens.unsqueeze(-1)).squeeze(-1)
    return token_lp.sum(dim=-1)


# (2) Hàm tính DPO Loss, phần thưởng ẩn r_hat và trọng số đạo hàm biên
def dpo_loss(pi_w, pi_l, ref_w, ref_l, y_w, y_l, beta=0.1):
    lp_pi_w = sequence_logprob(pi_w, y_w)
    lp_pi_l = sequence_logprob(pi_l, y_l)
    with torch.no_grad():
        lp_ref_w = sequence_logprob(ref_w, y_w)
        lp_ref_l = sequence_logprob(ref_l, y_l)

    r_w = beta * (lp_pi_w - lp_ref_w)
    r_l = beta * (lp_pi_l - lp_ref_l)
    margins = r_w - r_l

    loss = -F.logsigmoid(margins).mean()
    grad_weight = torch.sigmoid(-margins).mean()
    acc = (margins > 0).float().mean()
    return loss, margins.mean(), grad_weight, acc


init_loss, init_margin, _, init_acc = dpo_loss(
    pi_logits_w, pi_logits_l, ref_logits_w, ref_logits_l, y_w, y_l, beta
)
print(f"Loss DPO lúc khởi tạo (pi_theta = pi_ref): {init_loss.item():.6f} (ln 2 = 0.693147)")
print(f"Biên phần thưởng ẩn ban đầu: {init_margin.item():.4f} | Độ chính xác: {100*init_acc.item():.1f}%")

# (3) Tối ưu hóa trực tiếp logits của pi_theta bằng DPO
opt = torch.optim.SGD([pi_logits_w, pi_logits_l], lr=5.0)
for _ in range(10):
    opt.zero_grad()
    loss, margin, gw, acc = dpo_loss(
        pi_logits_w, pi_logits_l, ref_logits_w, ref_logits_l, y_w, y_l, beta
    )
    loss.backward()
    opt.step()

loss, margin, gw, acc = dpo_loss(
    pi_logits_w, pi_logits_l, ref_logits_w, ref_logits_l, y_w, y_l, beta
)
print("Sau 10 bước cập nhật DPO:")
print(f"Loss DPO mới: {loss.item():.6f}")
print(f"Biên phần thưởng ẩn E[r(y_w) - r(y_l)]: +{margin.item():.4f}")
print(f"Trọng số đạo hàm biên trung bình sigma(r_l - r_w): {gw.item():.4f}")
print(f"Độ chính xác sở thích (r_w > r_l): {100*acc.item():.1f}%")
Loss DPO lúc khởi tạo (pi_theta = pi_ref): 0.693147 (ln 2 = 0.693147)
Biên phần thưởng ẩn ban đầu: 0.0000 | Độ chính xác: 0.0%
Sau 10 bước cập nhật DPO:
Loss DPO mới: 0.214836
Biên phần thưởng ẩn E[r(y_w) - r(y_l)]: +1.4125
Trọng số đạo hàm biên trung bình sigma(r_l - r_w): 0.1958
Độ chính xác sở thích (r_w > r_l): 100.0%
Điều cần lưu ý

Giá trị ln⁡(2)≈0.6931\ln(2) \approx 0.6931 tại bước khởi tạo t=0t = 0 là bài kiểm tra bắt buộc (sanity check) khi bạn cài đặt hoặc chạy DPO trong thực tế! Nếu ở bước đầu tiên của quá trình huấn luyện DPO (khi chưa cập nhật trọng số nào nên πθ≡πref\pi_\theta \equiv \pi_{\text{ref}}), log huấn luyện của bạn không bắt đầu ở con số 0.6931, chắc chắn đã có lỗi trong cách căn chỉnh mặt nạ token hoặc truyền nhầm mô hình tham chiếu.

Đọc thêm So sánh trực diện PPO và DPO: Khi nào DPO gặp giới hạn?

Nếu DPO vừa đơn giản, vừa tiết kiệm bộ nhớ lại tương đương toán học với RLHF, tại sao các phòng thí nghiệm hàng đầu vẫn kết hợp cả RL trực tuyến (Online RL)?

Sự khác biệt nằm ở phân phối dữ liệu (Off-policy vs On-policy):

  • DPO thuần túy là thuật toán Ngoại tuyến (Offline / Off-policy): Cặp dữ liệu (yw,yl)(y_w, y_l) được thu thập cố định từ trước. Khi πθ\pi_\theta thay đổi trong quá trình huấn luyện, nó không tự sinh ra câu trả lời mới để nhận phản hồi trên chính những lỗi mới phát sinh của nó. Ngoài ra, trên một số bài toán suy luận dài, DPO đôi khi gặp hiện tượng giảm đồng thời xác suất của cả ywy_w lẫn yly_l (miễn là log⁡πθ(yl)\log \pi_\theta(y_l) giảm nhanh hơn log⁡πθ(yw)\log \pi_\theta(y_w)).
  • Online RL (PPO / GRPO) là thuật toán Trực tuyến (On-policy): Mô hình tự sinh ra các phản hồi mới y∼πθ(⋅∣x)y \sim \pi_\theta(\cdot \mid x) ở mỗi bước và được chấm điểm ngay lập tức. Nhờ đó, trên các bài toán đòi hỏi chuỗi suy luận hàng nghìn bước như Toán học và Lập trình, Học tăng cường trực tuyến phát huy sức mạnh vượt trội — đưa chúng ta tới bước đột phá của Mục 5.

5. Học tăng cường với Phần thưởng Kiểm chứng được (RLVR & GRPO — DeepSeek-R1)

Cho đến năm 2024, cả SFT, RLHF lẫn DPO đều xoay quanh một triết lý chung: dạy mô hình bắt chước hoặc chiều lòng đánh giá của con người. Nhưng đối với các bài toán đòi hỏi tư duy logic sâu sắc như chứng minh toán học, giải thuật lập trình hay suy luận khoa học, cách tiếp cận này vấp phải hai rào cản lớn:

  1. Con người rất dễ bị đánh lừa bởi một lời giải viết trình bày đẹp mắt nhưng sai ở một bước biến đổi đại số nhỏ ở dòng thứ 20 (hiện tượng sycophancy và lỗi của Mô hình Phần thưởng nơ-ron rϕr_\phi).
  2. Nếu ép mô hình bắt chước từng bước suy nghĩ do con người viết mẫu, mô hình sẽ không bao giờ khám phá ra được các chiến lược suy luận mới hiệu quả hơn cho chính kiến trúc của nó.

5.1 Từ phần thưởng cảm tính đến phần thưởng kiểm chứng được (RLVR)

Điều gì xảy ra nếu bài toán có đáp án khách quan, kiểm chứng được 100% bằng quy tắc hoặc trình biên dịch (Verifiable Rewards)?

Hướng tiếp cận này được gọi là Học tăng cường với Phần thưởng Kiểm chứng được (Reinforcement Learning with Verifiable Rewards — RLVR). Bộ chấm điểm quy tắc (rule-based verifier) không phải là một mạng nơ-ron khả nghi, nên mô hình không thể dùng văn hoa tán dương để “hack phần thưởng” — cách duy nhất để nhận điểm cao là thực sự tìm ra lời giải đúng!

5.2 Group Relative Policy Optimization (GRPO): Loại bỏ mạng Critic bằng chuẩn hóa nhóm

Khi quay lại với Học tăng cường trực tuyến cho các chuỗi suy luận dài hàng nghìn token, rào cản lớn nhất của PPO (Schulman & others, 2017) là Mạng giá trị Critic VψV_\psi. Trong PPO chuẩn, để biết một token hay một câu trả lời là “tốt hơn hay kém hơn mức kỳ vọng trung bình” (tức đại lượng Lợi thế — Advantage AtA_t), ta phải huấn luyện thêm một mạng Critic VψV_\psi có kích thước ngang ngửa mô hình chính sách πθ\pi_\theta (ví dụ thêm một mô hình 70B hay 671B tham số nữa!), vừa tốn gấp đôi bộ nhớ VRAM vừa rất khó ước lượng chính xác khi phần thưởng chỉ xuất hiện ở token cuối cùng của chuỗi suy luận.

Trong công trình DeepSeekMath (Shao & others, 2024) và sau đó là DeepSeek-R1 (DeepSeek-AI, 2025), nhóm nghiên cứu DeepSeek đã giới thiệu thuật toán Group Relative Policy Optimization (GRPO) — giải pháp loại bỏ hoàn toàn mạng Critic bằng một ý tưởng thống kê cực kỳ thanh lịch:

Thay vì dùng mạng nơ-ron để đoán điểm kỳ vọng của câu hỏi qq, ta hãy lấy mẫu một nhóm GG câu trả lời độc lập {o1,o2,…,oG}\{o_1, o_2, \dots, o_G\} từ chính sách hiện tại πθold(⋅∣q)\pi_{\theta_{\text{old}}}(\cdot \mid q) cho cùng một câu hỏi qq, chấm điểm từng câu để được {r1,r2,…,rG}\{r_1, r_2, \dots, r_G\}, rồi lấy chính điểm trung bình của nhóm đó làm mốc cơ sở (baseline)!

Cụ thể, Lợi thế tương đối theo nhóm (Group-Normalized Advantage) AiA_i của câu trả lời thứ ii (được gán chung cho mọi token thuộc oio_i) được tính trực tiếp bằng chuẩn hóa zz-score trong nhóm (DeepSeek-AI, 2025; Shao & others, 2024):

Ai=ri−mean(r1,r2,…,rG)std(r1,r2,…,rG)+ε.A_i = \frac{r_i - \text{mean}(r_1, r_2, \dots, r_G)}{\text{std}(r_1, r_2, \dots, r_G) + \varepsilon}.

Nếu câu trả lời oio_i giải đúng và tuân thủ định dạng tốt hơn mức trung bình của GG lần thử trên cùng bài toán đó, nó nhận Ai>0A_i > 0 (được tăng xác suất). Nếu nó giải sai hoặc rườm rà kém hơn các câu trả lời khác trong nhóm, nó nhận Ai<0A_i < 0 (bị giảm xác suất).

Hàm mục tiêu của GRPO kết hợp cơ chế cắt tỉ số xác suất (clipped surrogate) của PPO với bộ ước lượng phân kỳ KL không âm của Schulman (DeepSeek-AI, 2025; Shao & others, 2024):

JGRPO(θ)=Eq∼D, {oi}i=1G∼πθold(⋅∣q)[1G∑i=1G(min⁡ ⁣(ρi(θ)Ai,  clip(ρi(θ),1−ϵ,1+ϵ)Ai)−β DKL(πθ ∥ πref))],\mathcal{J}_{\text{GRPO}}(\theta) = \mathbb{E}_{q \sim \mathcal{D},\, \{o_i\}_{i=1}^G \sim \pi_{\theta_{\text{old}}}(\cdot \mid q)} \left[ \frac{1}{G} \sum_{i=1}^{G} \Big( \min\!\big( \rho_i(\theta) A_i, \; \text{clip}(\rho_i(\theta), 1-\epsilon, 1+\epsilon) A_i \big) - \beta \, \mathbb{D}_{\text{KL}}(\pi_\theta \,\|\, \pi_{\text{ref}}) \Big) \right],

trong đó ρi(θ)=πθ(oi∣q)πθold(oi∣q)\rho_i(\theta) = \frac{\pi_\theta(o_i \mid q)}{\pi_{\theta_{\text{old}}}(o_i \mid q)} là tỉ số xác suất giữa chính sách mới và chính sách cũ, còn hạng tử phạt phân kỳ KL được ước lượng không thiên lệch và luôn không âm qua công thức:

DKL(πθ ∥ πref)=πref(oi∣q)πθ(oi∣q)−log⁡πref(oi∣q)πθ(oi∣q)−1≥0.\mathbb{D}_{\text{KL}}(\pi_\theta \,\|\, \pi_{\text{ref}}) = \frac{\pi_{\text{ref}}(o_i \mid q)}{\pi_\theta(o_i \mid q)} - \log \frac{\pi_{\text{ref}}(o_i \mid q)}{\pi_\theta(o_i \mid q)} - 1 \ge 0.

5.3 Khoảnh khắc “Aha” và sự trỗi dậy tự nhiên của Chuỗi suy luận

Tháng 1 năm 2025, DeepSeek-AI đã thực hiện một thí nghiệm lịch sử mang tên DeepSeek-R1-Zero (DeepSeek-AI, 2025): họ lấy trực tiếp mô hình nền tảng DeepSeek-V3-Base (671 tỷ tham số MoE), bỏ qua hoàn toàn giai đoạn SFT, và chỉ chạy thuần túy thuật toán GRPO với hai phần thưởng quy tắc đơn giản:

  1. Phần thưởng độ chính xác (Accuracy Reward): Đáp số toán học hoặc kết quả chạy code có đúng không?
  2. Phần thưởng định dạng (Format Reward): Quá trình suy nghĩ có nằm trong cặp thẻ <think>...</think> và kết luận trong <answer>...</answer> không?

Kết quả quan sát được trong quá trình huấn luyện DeepSeek-R1-Zero (DeepSeek-AI, 2025) là một minh chứng ngoạn mục cho sức mạnh của Học tăng cường:

Hãy tự tay cài đặt bộ chấm điểm tự động và bước cập nhật GRPO trong Bài tập 4.5 dưới đây.

Bài tập 4.5 Cài đặt bộ chấm điểm tự động và bước cập nhật GRPO
Dữ liệu: GSM8K-mini · Thời gian chạy: < 1 phút

Cho bài toán "Giải phương trình 3x + 5 = 20" (đáp án chuẩn gold_answer = "5") và một nhóm G=6G = 6 câu trả lời rollouts được sinh ra từ πθold\pi_{\theta_{\text{old}}}:

  1. Viết hàm verify_rollout(text, gold) chấm điểm theo quy tắc: cộng +0.25 điểm định dạng nếu chuỗi khớp cấu trúc <think>...</think>...<answer>...</answer>, và cộng +1.0 điểm chính xác nếu nội dung bên trong <answer>...</answer> bằng đúng gold.
  2. Tính vector lợi thế chuẩn hóa theo nhóm Ai=ri−mean(r)std(r)+10−8A_i = \frac{r_i - \text{mean}(r)}{\text{std}(r) + 10^{-8}}.
  3. Cài đặt hàm mất mát grpo_loss gồm phần cắt tỉ số min⁡(ρiAi,clip(ρi,1−ϵ,1+ϵ)Ai)\min(\rho_i A_i, \text{clip}(\rho_i, 1-\epsilon, 1+\epsilon) A_i) trừ đi βDKL\beta \mathbb{D}_{\text{KL}}, thực hiện 1 bước cập nhật đạo hàm và kiểm tra xem mẫu nào được tăng xác suất, mẫu nào bị giảm xác suất.
Mã khởi tạo
import re
import torch

gold_answer = "5"
# Nhóm G=6 phản hồi được lấy mẫu từ pi_old cho cùng một câu hỏi toán
rollouts = [
    "<think> 3x = 20 - 5 = 15, suy ra x = 15 / 3 = 5. </think><answer>5</answer>",
    "<think> 3x = 15 nên x = 5. </think><answer>5</answer>",
    "<think> 3x = 20 + 5 = 25, x = 25/3. </think><answer>8.33</answer>",
    "Đáp số của phương trình là <answer>5</answer>",
    "<think> Thử x = 4 thì 3*4 + 5 = 17. </think><answer>4</answer>",
    "Tôi nghĩ x bằng 6.",
]

# Log-prob trung bình mỗi token dưới pi_old và pi_ref
old_logp = torch.tensor([-0.85, -0.92, -1.10, -0.78, -1.05, -1.30])
ref_logp = torch.tensor([-0.90, -0.95, -1.08, -0.80, -1.02, -1.25])
print(f"Số mẫu trong nhóm G = {len(rollouts)} | Đáp án chuẩn: {gold_answer}")
Số mẫu trong nhóm G = 6 | Đáp án chuẩn: 5
Hint

Dùng biểu thức chính quy re.search(r"<think>.*?</think>\s*<answer>.*?</answer>", text) để kiểm tra định dạng, và re.search(r"<answer>(.*?)</answer>", text) để trích xuất chuỗi đáp số bên trong thẻ <answer>.

Hint

Với log_r_ref = ref_lp - curr_lp (tức log⁡πrefπθ\log \frac{\pi_{\text{ref}}}{\pi_\theta}), bộ ước lượng KL không âm của Schulman được viết gọn trong 1 dòng: kl = torch.exp(log_r_ref) - log_r_ref - 1.0. Vì trong PyTorch ta cực tiểu hóa hàm mất mát (loss.backward()), hãy nhớ đặt dấu trừ - phía trước hàm mục tiêu JGRPO\mathcal{J}_{\text{GRPO}}.

Điểm kiểm tra kết quả bạn cần đạt được

Hai mẫu đầu tiên (vừa có <think> vừa ra đáp số 5) đạt điểm tối đa 1.25 và nhận lợi thế dương A_i = +1.04. Mẫu thứ 4 ("Đáp số của phương trình là <answer>5</answer>") tuy đúng kết quả (r = 1.0) nhưng thiếu thẻ <think> nên chỉ nhận lợi thế +0.59. Các mẫu giải sai (r = 0.25 hoặc 0.0) đều nhận lợi thế âm (-0.74 và -1.18) và bị giảm log-prob ngay sau 1 bước cập nhật.

Điểm thưởng r_i:      [1.25, 1.25, 0.25, 1.0, 0.25, 0.0]
Trung bình nhóm: 0.6667 | Độ lệch chuẩn: 0.5627
Lợi thế chuẩn hóa A_i: ['+1.04', '+1.04', '-0.74', '+0.59', '-0.74', '-1.18']
Hàm mất mát GRPO ban đầu: 0.001241
Sau 1 bước cập nhật GRPO, thay đổi log-prob (pi_theta - pi_old):
  Mẫu 0 (r=1.25, A=+1.04): delta_logp = +0.1000 (TĂNG xác suất)
  Mẫu 2 (r=0.25, A=-0.74): delta_logp = -0.0742 (GIẢM xác suất)
  Mẫu 5 (r=0.00, A=-1.18): delta_logp = -0.1184 (GIẢM xác suất)
Lời giải 2 ô mã

Chạy các ô mã theo đúng thứ tự này trong một notebook mới: chúng đã bao gồm các lệnh import, bước tải dữ liệu và khởi tạo hạt giống ngẫu nhiên (random seed).

import re
import torch

gold_answer = "5"
# Nhóm G=6 phản hồi được lấy mẫu từ pi_old cho cùng một câu hỏi toán
rollouts = [
    "<think> 3x = 20 - 5 = 15, suy ra x = 15 / 3 = 5. </think><answer>5</answer>",
    "<think> 3x = 15 nên x = 5. </think><answer>5</answer>",
    "<think> 3x = 20 + 5 = 25, x = 25/3. </think><answer>8.33</answer>",
    "Đáp số của phương trình là <answer>5</answer>",
    "<think> Thử x = 4 thì 3*4 + 5 = 17. </think><answer>4</answer>",
    "Tôi nghĩ x bằng 6.",
]

# Log-prob trung bình mỗi token dưới pi_old và pi_ref
old_logp = torch.tensor([-0.85, -0.92, -1.10, -0.78, -1.05, -1.30])
ref_logp = torch.tensor([-0.90, -0.95, -1.08, -0.80, -1.02, -1.25])
print(f"Số mẫu trong nhóm G = {len(rollouts)} | Đáp án chuẩn: {gold_answer}")
Số mẫu trong nhóm G = 6 | Đáp án chuẩn: 5
# (1) Bộ chấm điểm quy tắc (Rule-based Verifier: Độ chính xác + Định dạng)
def verify_rollout(text: str, gold: str) -> float:
    r = 0.0
    if re.search(r"<think>.*?</think>\s*<answer>.*?</answer>", text):
        r += 0.25
    ans_match = re.search(r"<answer>(.*?)</answer>", text)
    if ans_match and ans_match.group(1).strip() == gold:
        r += 1.0
    return r


rewards = torch.tensor([verify_rollout(o, gold_answer) for o in rollouts])

# (2) Tính lợi thế tương đối theo nhóm (Group-Normalized Advantage)
mean_r = rewards.mean()
std_r = rewards.std(unbiased=True)
advantages = (rewards - mean_r) / (std_r + 1e-8)

print(f"Điểm thưởng r_i:      {rewards.tolist()}")
print(f"Trung bình nhóm: {mean_r.item():.4f} | Độ lệch chuẩn: {std_r.item():.4f}")
print(f"Lợi thế chuẩn hóa A_i: {[f'{a:+.2f}' for a in advantages.tolist()]}")

# (3) Tính hàm mất mát GRPO với Clipped Surrogate và KL Penalty
eps, beta = 0.2, 0.04
curr_logp = old_logp.clone().detach().requires_grad_(True)


def grpo_loss(curr_lp, old_lp, ref_lp, adv, eps=0.2, beta=0.04):
    ratio = torch.exp(curr_lp - old_lp)
    surr1 = ratio * adv
    surr2 = torch.clamp(ratio, 1.0 - eps, 1.0 + eps) * adv
    # Bộ ước lượng KL không âm của Schulman: exp(ref - curr) - (ref - curr) - 1
    log_r_ref = ref_lp - curr_lp
    kl = torch.exp(log_r_ref) - log_r_ref - 1.0
    return -(torch.min(surr1, surr2) - beta * kl).mean()


loss0 = grpo_loss(curr_logp, old_logp, ref_logp, advantages, eps, beta)
print(f"Hàm mất mát GRPO ban đầu: {loss0.item():.6f}")

loss0.backward()
with torch.no_grad():
    curr_logp -= 0.6 * curr_logp.grad
    delta = curr_logp - old_logp

print("Sau 1 bước cập nhật GRPO, thay đổi log-prob (pi_theta - pi_old):")
for idx in [0, 2, 5]:
    direction = "TĂNG xác suất" if delta[idx] > 0 else "GIẢM xác suất"
    print(
        f"  Mẫu {idx} (r={rewards[idx]:.2f}, A={advantages[idx]:+.2f}): "
        f"delta_logp = {delta[idx]:+.4f} ({direction})"
    )
Điểm thưởng r_i:      [1.25, 1.25, 0.25, 1.0, 0.25, 0.0]
Trung bình nhóm: 0.6667 | Độ lệch chuẩn: 0.5627
Lợi thế chuẩn hóa A_i: ['+1.04', '+1.04', '-0.74', '+0.59', '-0.74', '-1.18']
Hàm mất mát GRPO ban đầu: 0.001241
Sau 1 bước cập nhật GRPO, thay đổi log-prob (pi_theta - pi_old):
  Mẫu 0 (r=1.25, A=+1.04): delta_logp = +0.1000 (TĂNG xác suất)
  Mẫu 2 (r=0.25, A=-0.74): delta_logp = -0.0742 (GIẢM xác suất)
  Mẫu 5 (r=0.00, A=-1.18): delta_logp = -0.1184 (GIẢM xác suất)
Điều cần lưu ý

Hãy quan sát cách GRPO phân biệt tinh tế giữa 6 câu trả lời mà không cần bất kỳ nhãn sở thích cặp thủ công hay mạng Critic nào:

  • Mẫu 0 và Mẫu 1 vừa đúng đáp án vừa tuân thủ định dạng <think> →\to r=1.25  ⟹  A=+1.04r = 1.25 \implies A = +1.04 (được khuyến khích mạnh nhất).
  • Mẫu 3 ra đúng đáp án 5 nhưng nhảy cóc không chịu suy luận trong thẻ <think> →\to r=1.00  ⟹  A=+0.59r = 1.00 \implies A = +0.59 (vẫn dương nhưng thấp hơn Mẫu 0 và 1).
  • Mẫu 2 và Mẫu 4 có trình bày <think> nhưng tính toán sai đáp số →\to r=0.25  ⟹  A=−0.74r = 0.25 \implies A = -0.74 (bị phạt nhẹ hơn Mẫu 5 hoàn toàn sai cả định dạng lẫn kết quả A=−1.18A = -1.18). Chính áp lực chọn lọc tự nhiên theo nhóm này đã dẫn dắt mô hình vừa giữ cấu trúc <think>, vừa rèn giũa độ chính xác suy luận qua từng vòng lặp!

Để tổng kết toàn bộ hành trình của Chương 4, bảng dưới đây đặt 5 phương pháp hậu huấn luyện cạnh nhau theo dữ liệu đầu vào, hàm mục tiêu, chi phí bộ nhớ và vai trò thực tế trong một đường ống LLM hiện đại:

Phương phápDữ liệu đầu vàoMô hình trên VRAMHàm mục tiêu cốt lõiVai trò chính trong Pipeline
SFT (Full Fine-Tuning)Các cuộc hội thoại mẫu (x,y)(x, y)1 mô hình (πθ\pi_\theta) + Gradient + AdamWMasked Cross-Entropy trên token của assistantDạy định dạng Chat Template và phong cách trợ lý cơ bản
LoRA / QLoRA (PEFT)Như SFT hoặc DPOW0W_0 đóng băng (4-bit/16-bit) + nhánh nhỏ BAB ACập nhật hạng thấp ΔW=αrBA\Delta W = \frac{\alpha}{r} B A (<1%<1\% tham số)Giảm 70%–85% VRAM, cho phép tinh chỉnh trên 1 GPU
RLHF (PPO)Prompt xx + Mô hình phần thưởng rϕr_\phi4 mô hình (πθ,πref,rϕ,Vψ\pi_\theta, \pi_{\text{ref}}, r_\phi, V_\psi)max⁡E[rϕ(x,y)]−βDKL(πθ∥πref)\max \mathbb{E}[r_\phi(x,y)] - \beta D_{\text{KL}}(\pi_\theta \| \pi_{\text{ref}})Căn chỉnh trực tuyến theo sở thích con người & độ an toàn
DPOCác cặp sở thích tĩnh (x,yw,yl)(x, y_w, y_l)2 mô hình (πθ\pi_\theta và πref\pi_{\text{ref}}; hoặc 1 base + LoRA)−log⁡σ ⁣(βlog⁡πθ(yw)πref(yw)−βlog⁡πθ(yl)πref(yl))-\log \sigma\!\big(\beta \log \frac{\pi_\theta(y_w)}{\pi_{\text{ref}}(y_w)} - \beta \log \frac{\pi_\theta(y_l)}{\pi_{\text{ref}}(y_l)}\big)Căn chỉnh sở thích ngoại tuyến gọn nhẹ, không cần Reward Model
RLVR (GRPO)Câu hỏi qq + Bộ kiểm chứng tự động (Toán/Code)2 mô hình (πθ\pi_\theta và πref\pi_{\text{ref}}; bỏ Critic VψV_\psi)Clipped Surrogate với Ai=ri−μGσG−βDKLA_i = \frac{r_i - \mu_G}{\sigma_G} - \beta \mathbb{D}_{\text{KL}}Đánh thức năng lực tự suy luận chuỗi dài (<think>) và tự sửa lỗi

Nếu bạn muốn đào sâu hơn nữa, hãy thử sức với các bài tập mở rộng dưới đây trước khi bước sang Chương 5: Suy luận và Tối ưu hóa (Inference) — nơi chúng ta sẽ tìm hiểu cách làm cho trợ lý AI vừa được huấn luyện này trả lời người dùng với tốc độ hàng trăm token mỗi giây!

Mở rộng

LoRA dùng chung làm Mô hình Tham chiếu trong DPO. Khi chạy DPO (Bài tập 4.4), ta cần cả πθ\pi_\theta và πref\pi_{\text{ref}}. Nếu dùng LoRA để tinh chỉnh πθ=W0+αrBA\pi_\theta = W_0 + \frac{\alpha}{r} B A, làm thế nào để tính log⁡πref(y∣x)\log \pi_{\text{ref}}(y \mid x) mà không cần nạp thêm bản sao thứ hai của mô hình gốc lên VRAM? Hãy thêm một cờ enable_lora: bool vào lớp LoRALinear của Bài tập 4.2 để tính cả hai phân phối trên cùng một mô hình duy nhất!

Mở rộng

Hiện tượng suy giảm xác suất của cả ywy_w và yly_l trong DPO (Likelihood Displacement). Trong Bài tập 4.4, hãy vẽ đồ thị của log⁡πθ(yw∣x)\log \pi_\theta(y_w \mid x) và log⁡πθ(yl∣x)\log \pi_\theta(y_l \mid x) theo từng bước tối ưu hóa khi hai chuỗi ywy_w và yly_l có chung 4/6 token đầu tiên. Điều gì xảy ra với xác suất của các token dùng chung? Hãy thử cộng thêm một hạng tử nhỏ +λLSFT(yw)+ \lambda \mathcal{L}_{\text{SFT}}(y_w) (như trong công thức RPO / CPO) và so sánh kết quả.

Mở rộng

Trường hợp suy biến trong GRPO khi mọi mẫu trong nhóm đều đúng hoặc đều sai. Trong Bài tập 4.5, điều gì xảy ra với lợi thế AiA_i và đạo hàm của GRPO nếu cả G=6G = 6 câu trả lời được lấy mẫu từ πθold\pi_{\theta_{\text{old}}} đều giải sai (r1=⋯=r6=0r_1 = \dots = r_6 = 0) hoặc đều giải đúng tuyệt đối (r1=⋯=r6=1.25r_1 = \dots = r_6 = 1.25)? Tại sao việc lọc bỏ các câu hỏi có std(r)=0\text{std}(r) = 0 (dynamic sampling) lại giúp tăng tốc huấn luyện GRPO lên đáng kể?

Ý tưởng

Tinh chỉnh QLoRA thực tế trên SmolLM2-135M. Sử dụng thư viện transformers và peft, hãy tải mô hình HuggingFaceTB/SmolLM2-135M, gắn bộ thích nghi LoRA (r=8,α=16r = 8, \alpha = 16) vào các lớp q_proj, k_proj, v_proj, o_proj, và tinh chỉnh SFT trên 200 mẫu hội thoại tiếng Việt trong 3 phút trên CPU/GPU. So sánh câu trả lời của mô hình trước và sau khi tinh chỉnh!


Tài liệu tham khảo

  1. 2017 Christiano et al. Deep Reinforcement Learning from Human Preferences. NeurIPS. 3
  2. 2017 Schulman et al. Proximal Policy Optimization Algorithms. arXiv. 3
  3. 2022 Bai et al. Constitutional AI: Harmlessness from AI Feedback. arXiv. 3
  4. 2022 Hu et al. LoRA: Low-Rank Adaptation of Large Language Models. ICLR. 2
  5. 2022 Ouyang et al. Training Language Models to Follow Instructions with Human Feedback. NeurIPS. 3
  6. 2022 Wei et al. Finetuned Language Models Are Zero-Shot Learners. ICLR. 1
  7. 2023 Dettmers et al. QLoRA: Efficient Finetuning of Quantized LLMs. NeurIPS. 2
  8. 2023 Rafailov et al. Direct Preference Optimization: Your Language Model is Secretly a Reward Model. NeurIPS. 4
  9. 2023 Zhou et al. LIMA: Less Is More for Alignment. NeurIPS. 1
  10. 2024 Shao et al. DeepSeekMath: Pushing the Limits of Mathematical Reasoning in Open Language Models. arXiv. 5
  11. 2025 DeepSeek-AI. DeepSeek-R1: Incentivizing Reasoning Capability in LLMs via Reinforcement Learning. arXiv. 5