Đế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).
⚠️ Cạm bẫy: Fine-tune mô hình chỉ để nạp kiến thức tĩnh
Lỗi phổ biến nhất của các kỹ sư mới vào nghề là cố gắng Fine-tune mô hình chỉ để bắt nó nhớ danh sách sản phẩm mới của doanh nghiệp hoặc tài liệu chính sách nội bộ. Việc này cực kỳ tốn kém GPU, mất nhiều thời gian, dễ gây ra hiện tượng quên lãng thảm họa (Catastrophic Forgetting) khiến mô hình mất đi khả năng suy luận cơ bản, và tri thức học được sẽ nhanh chóng bị lỗi thời. Hãy dùng RAG cho bài toán nạp tri thức, chỉ dùng Fine-tuning khi cần thay đổi định dạng phản hồi hoặc hành vi suy luận logic chuyên ngành.

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.

🔢 Vì sao demo NumPy ở mục 19.4 không giảm được 250 lần tham số?
Con số "giảm hơn 250 lần" ở trên chỉ đúng ở quy mô mô hình thật ($d = k = 4096$). Dự án thực hành bên dưới cố tình dùng kích thước rất nhỏ ($d_{in} = d_{out} = 8$, $r = 2$) để chạy tức thời trên máy cá nhân. Tính lại tỷ lệ ở quy mô này (đã kiểm chứng): $W_0$ có $8 \times 8 = 64$ tham số, còn LoRA $A$ + $B$ chỉ có $2 \times 8 + 8 \times 2 = 32$ tham số — tỷ lệ giảm chỉ còn 2 lần, không hề ấn tượng như con số 250 lần ở trên. Đây không phải mâu thuẫn: tỷ lệ tiết kiệm của LoRA tăng theo cấp số nhân khi kích thước ma trận gốc $d \times k$ tăng lên (vì tham số LoRA chỉ tăng tuyến tính theo $d + k$ trong khi tham số gốc tăng theo tích $d \times k$), nên lợi ích thực sự chỉ bộc lộ rõ rệt ở quy mô mô hình production hàng tỷ tham số, không phải ở ví dụ minh họa nhỏ này.

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ụ:

dataset.jsonl
{"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.

lora_simulation.py
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}")
🧠 Kết quả chạy thực tế (đã kiểm chứng bằng Python + NumPy)
Chạy chính xác đoạn code trên (cùng seed 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.
Quan trọng hơn cả con số: ma trận $W_0$ trước và sau huấn luyện hoàn toàn không đổi (không hề có dòng code nào cập nhật 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

🔑 Bài học đạt đượ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 về lora_simulation.py

📖 Tài liệu tham khảo

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

Bài 18: Đại lý có trạng thái với LangGraph Bài 20: MLOps: Deploy, Tracing & Đánh giá Quay lại Lộ trình Kỹ Sư AI Thực Chiến

Bình luận