Đến thời điểm này, chúng ta đã tối ưu hóa các ứng dụng LLM bằng cách thiết lập prompt, lập trình đường ống RAG để bổ sung tri thức ngoài, và dựng các AI Agent dùng công cụ. Nhưng chuyện gì sẽ xảy ra nếu bạn cần mô hình tuân thủ tuyệt đối một cấu trúc đầu ra đặc thù, nói chuyện theo đúng giọng điệu thương hiệu của doanh nghiệp, hoặc dạy cho một mô hình nhỏ (7B) học kỹ năng suy luận chuyên ngành sâu của mô hình lớn?
Đó là lúc chúng ta phải can thiệp trực tiếp vào trọng số của mạng neural thông qua kỹ thuật Fine-tuning (Tinh chỉnh mô hình). Bài học này sẽ mổ xẻ sự khác biệt thực chiến giữa RAG và Fine-tuning, giải mã bản chất toán học đằng sau kỹ thuật tối ưu hóa tham số cực kỳ phổ biến LoRA (Low-Rank Adaptation), định dạng dữ liệu Instruction Tuning chuẩn, và tự lập trình một chu trình huấn luyện LoRA từ số 0 bằng NumPy.
19.1 Khi nào nên Fine-tune? Phân biệt với RAG
Có một câu nói kinh điển trong ngành kỹ nghệ AI: "RAG giống như cho mô hình đọc sách mở trước kỳ thi (Open-book Exam), còn Fine-tuning giống như cho mô hình đi học đại học ngành chuyên sâu trong nhiều năm để thay đổi tư duy."
| Đặc trưng | RAG (Retrieval-Augmented Generation) | Fine-tuning (Tinh chỉnh trọng số) |
|---|---|---|
| Mục đích chính | Cung cấp kiến thức ngoài, dữ liệu động cập nhật liên tục. | Dạy kỹ năng mới, định hình văn phong, cấu trúc đầu ra. |
| Cơ chế hoạt động | Truy xuất đoạn văn bản liên quan nạp vào context prompt. | Huấn luyện lan truyền ngược để cập nhật trọng số mô hình. |
| Giảm ảo tưởng (Hallucination) | Cực tốt (nhờ có tài liệu dẫn chứng trực tiếp trong prompt). | Trung bình (chỉ nhớ theo dạng phân phối xác suất trọng số). |
| Chi phí token prompt | Cao (vì phải đính kèm lượng lớn ngữ cảnh thô). | Thấp (mô hình đã học kỹ năng trực tiếp, prompt rất ngắn). |
| Độ động của dữ liệu | Cực tốt (chỉ cần cập nhật Vector Database). | Kém (mỗi lần cập nhật tri thức mới lại phải training lại). |
19.2 Kỹ thuật PEFT & LoRA (Low-Rank Adaptation)
Việc tinh chỉnh toàn bộ các tham số của một mô hình ngôn ngữ lớn (Full Fine-Tuning) đòi hỏi tài nguyên tính toán vô cùng khổng lồ. Ví dụ, để huấn luyện một mô hình Llama-3 8B, chúng ta cần lưu trữ trọng số, gradients, trạng thái của bộ tối ưu hóa (Optimizer States), yêu cầu hệ thống GPU doanh nghiệp cực lớn (hàng trăm GB VRAM).
Để giải quyết bài toán này, kỹ thuật LoRA (Low-Rank Adaptation) đề xuất giải pháp đóng băng (freeze) hoàn toàn ma trận trọng số gốc $W_0 \in \mathbb{R}^{d \times k}$ của mô hình, không cho phép cập nhật. Thay vào đó, ta đưa vào một nhánh phụ song song chứa ma trận hiệu chỉnh trọng số $\Delta W$, được phân rã thành tích của hai ma trận hạng thấp (low-rank matrices) $A \in \mathbb{R}^{r \times k}$ và $B \in \mathbb{R}^{d \times r}$ với hạng $r \ll \min(d, k)$:
\[\Delta W = B \cdot A\]Ví dụ, nếu chiều ẩn của mô hình $d = 4096$, $k = 4096$, ma trận gốc chứa $4096 \times 4096 \approx 16.7$ triệu tham số. Nếu ta cấu hình hạng $r = 8$, ma trận $A$ chỉ có $8 \times 4096 \approx 32,768$ tham số, ma trận $B$ cũng có $32,768$ tham số. Tổng số lượng tham số cần huấn luyện của LoRA chỉ là $65,536$ — giảm hơn 250 lần so với ban đầu!
Trong quá trình Forward Pass, với vector đầu vào $x$, đầu ra $h$ của lớp Linear được tính bằng:
\[h = x \cdot W_0 + \frac{\alpha}{r} (x \cdot A \cdot B)\]Trong đó $\alpha$ là một hằng số scaling (LoRA Alpha) giúp điều chỉnh cường độ tác động của phần trọng số LoRA huấn luyện mới lên đầu ra.
19.3 Định dạng dữ liệu huấn luyện: Instruction Tuning
Để huấn luyện LLM học cách làm theo chỉ dẫn, chúng ta phải chuẩn bị dữ liệu dưới dạng các mẫu hội thoại có cấu trúc chặt chẽ. Định dạng phổ biến nhất là JSON Lines (JSONL), ví dụ:
{"instruction": "Hãy viết email xin nghỉ phép bằng giọng điệu lịch sự.", "input": "Lý do: đi khám bệnh ngày 15/7", "output": "Kính gửi Ban Giám đốc, tôi viết email này để xin phép được nghỉ làm vào ngày 15/7 vì lý do sức khỏe cần đi khám định kỳ..."}
{"instruction": "Hãy viết email xin nghỉ phép bằng giọng điệu lịch sự.", "input": "Lý do: giải quyết việc gia đình ngày 20/7", "output": "Kính gửi anh/chị quản lý, tôi xin phép được nghỉ phép ngày 20/7 để giải quyết một số công việc gia đình đột xuất..."}
19.4 Dự án thực hành bài 19: Lập trình giả lập LoRA Matrix Multiplication & Training bằng NumPy
Dự án thực hành của bài học này là tự viết mã nguồn Python sử dụng thư viện NumPy để mô phỏng cơ chế lan truyền xuôi (Forward Pass) và lan truyền ngược (Backpropagation) cập nhật trọng số của ma trận LoRA phân rã hạng thấp.
Chúng ta sẽ đóng băng ma trận trọng số gốc $W_0$, thực hiện phép nhân ma trận LoRA song song trên vector input $x$, tính toán giá trị Loss, tính đạo hàm riêng của Loss đối với ma trận $A$ và $B$, rồi cập nhật trọng số của $A$ và $B$ bằng thuật toán Gradient Descent. Chương trình sẽ hiển thị rõ ràng giá trị Loss giảm dần qua từng epoch huấn luyện giả lập, minh họa trực quan bản chất toán học của LoRA ở tầng sâu nhất.
import numpy as np
# Cấu hình hạt giống ngẫu nhiên để kết quả chạy nhất quán
np.random.seed(42)
# 1. Khởi tạo kích thước mô hình giả lập
# d_in: chiều của input, d_out: chiều của output, r: hạng (rank) của LoRA
d_in = 8
d_out = 8
r = 2
alpha = 4.0
learning_rate = 0.01
# 2. Khởi tạo trọng số gốc W0 (Đóng băng) và giả lập input/target
W0 = np.random.randn(d_in, d_out) * 0.1
x = np.random.randn(1, d_in) # Vector đầu vào giả lập (1 dòng, 8 cột)
target = np.random.randn(1, d_out) # Kết quả đầu ra mong muốn (Target)
# 3. Khởi tạo các ma trận LoRA A và B
# Ma trận A khởi tạo bằng phân phối Gaussian ngẫu nhiên
lora_A = np.random.randn(r, d_out) * 0.1
# Ma trận B khởi tạo hoàn toàn bằng 0, đảm bảo ban đầu Delta W = B * A = 0
lora_B = np.zeros((d_in, r))
print("=== TRẠNG THÁI KHỞI TẠO ===")
print("Ma trận trọng số gốc W0:\n", W0)
print("\nMa trận LoRA A:\n", lora_A)
print("\nMa trận LoRA B:\n", lora_B)
# 4. Hàm Forward Pass của LoRA
def forward(x, W0, lora_A, lora_B, r, alpha):
# Đường đi gốc
h_base = np.dot(x, W0)
# Đường đi LoRA phân rã hạng thấp song song
scaling = alpha / r
h_lora = np.dot(np.dot(x, lora_B), lora_A) * scaling
# Kết hợp đầu ra cuối cùng
h_final = h_base + h_lora
return h_final, h_base, h_lora
# 5. Vòng lặp huấn luyện giả lập cập nhật trọng số LoRA
epochs = 100
print("\n=== BẮT ĐẦU QUÁ TRÌNH HUẤN LUYỆN LORA ===")
for epoch in range(epochs):
# Bước 5.1: Forward Pass
h_final, h_base, h_lora = forward(x, W0, lora_A, lora_B, r, alpha)
# Bước 5.2: Tính Loss (Mean Squared Error giữa dự đoán h_final và target)
loss = np.mean((h_final - target) ** 2)
# Bước 5.3: Backpropagation (Tính đạo hàm riêng)
# Đạo hàm của Loss đối với h_final
d_loss_d_h = 2 * (h_final - target) / d_out
# Tính gradient cho ma trận LoRA A và LoRA B theo quy tắc chuỗi (Chain Rule)
scaling = alpha / r
# d_loss/d_lora_A = scaling * (lora_B.T * x.T) * d_loss_d_h
grad_A = scaling * np.dot(np.dot(x, lora_B).T, d_loss_d_h)
# d_loss/d_lora_B = scaling * x.T * (d_loss_d_h * lora_A.T)
grad_B = scaling * np.dot(x.T, np.dot(d_loss_d_h, lora_A.T))
# Bước 5.4: Cập nhật trọng số LoRA bằng Gradient Descent (Giữ nguyên W0 đóng băng)
lora_A -= learning_rate * grad_A
lora_B -= learning_rate * grad_B
if (epoch + 1) % 10 == 0 or epoch == 0:
print(f"Epoch {epoch+1:3d} | Loss: {loss:.6f}")
print("\n=== KẾT QUẢ SAU HUÂN LUYỆN ===")
print("Ma trận LoRA B (không còn bằng 0):\n", lora_B)
print("\nMa trận LoRA A cập nhật:\n", lora_A)
# Kiểm tra lại output cuối cùng sau khi train
h_new, _, _ = forward(x, W0, lora_A, lora_B, r, alpha)
final_loss = np.mean((h_new - target) ** 2)
print(f"\nLoss cuối cùng sau 100 Epochs: {final_loss:.6f}")
42 để tái lập được) cho ra đường cong Loss giảm
đơn điệu qua 100 epochs:
- Epoch 1: Loss $\approx 1.740512$ (điểm khởi đầu, khi $\Delta W = B \cdot A = 0$).
- Epoch 30: Loss $\approx 1.424078$.
- Epoch 60: Loss $\approx 0.235969$ — bắt đầu giảm nhanh.
- Epoch 100: Loss $\approx 0.001004$ — giảm khoảng 1.735 lần so với epoch đầu.
W0) — toàn bộ quá trình "học" chỉ diễn ra trên 2 ma trận nhỏ
lora_A/lora_B, đúng minh chứng cho nguyên lý cốt lõi của LoRA: đóng băng trọng
số gốc, chỉ huấn luyện nhánh phụ hạng thấp song song.
Tóm tắt bài học & Cầu nối kiến thức
- Đạt được: Hiểu bản chất toán học đằng sau kỹ thuật LoRA đóng băng ma trận gốc và tối ưu ma trận hạng thấp song song.
- Đạt được: Tự viết luồng huấn luyện giả lập LoRA bằng NumPy từ số 0 để cập nhật trọng số.
Cầu nối bài tiếp theo: Sau khi mô hình đã được tinh chỉnh, bước cuối cùng là serving mô hình hiệu năng cao trên production, giám sát tracing luồng chạy và đánh giá tự động hệ thống qua Bài số 20 (MLOps).
Tải file code thực hành minh họa bài học
File Python lora_simulation.py — mã nguồn NumPy tự xây dựng phép nhân ma trận song song và
thuật toán lan truyền ngược cập nhật tham số LoRA (chạy python lora_simulation.py):
📖 Tài liệu tham khảo
- LoRA: Low-Rank Adaptation of Large Language Models — Bài báo khoa học gốc lịch sử phát minh ra kỹ thuật LoRA của Microsoft (Hu et al., 2021)
- Hugging Face PEFT (Parameter-Efficient Fine-Tuning) — Tài liệu thư viện chuẩn công nghiệp triển khai LoRA, QLoRA, Prefix Tuning
- Microsoft LoRA Repository — Mã nguồn chính thức của tác giả công bố cấu trúc cài đặt LoRA trên PyTorch
Bình luận