Fine-Tuning Mô Hình Embedding Chuyên Ngành với Sentence-Transformers cho RAG Tiếng Việt

AI tutorial - IT technology blog
AI tutorial - IT technology blog

Bối cảnh & Lý do

Cách đây sáu tháng, đội ngũ của chúng tôi đã triển khai một trợ lý RAG nội bộ để tra cứu hơn 45.000 trang văn bản luật thuế và thông tư tuân thủ doanh nghiệp của Việt Nam. Chúng tôi bắt đầu với các baseline đa ngôn ngữ phổ biến như intfloat/multilingual-e5-base và BAAI/bge-m3. Cả hai đều đạt kết quả tốt trên các benchmark tổng quát tiêu chuẩn. Tuy nhiên, ngay khi các chuyên viên pháp lý nhập những câu truy vấn dày đặc thuật ngữ hành chính địa phương, mã sắc thuế và từ ghép chuyên ngành, khả năng truy xuất đã sụp đổ hoàn toàn.

Các bi-encoder đa mục đích xây dựng dense vector từ dữ liệu crawl diện rộng trên web. Khi người dùng thực hiện các tìm kiếm như “quyết toán thuế thu nhập cá nhân đối với chuyên gia nước ngoài”, các mô hình có sẵn chỉ bắt khớp từ khóa bề nổi thay vì hiểu đúng ý định pháp lý. Ngữ cảnh rác tràn ngập cửa sổ prompt, dẫn đến hiện tượng hallucination ngầm trong các bản tóm tắt downstream của GPT-4 và Claude.

Chúng tôi đứng trước một bài toán đánh đổi nan giải. Chúng tôi có thể gắn thêm một cross-encoder reranker vào mỗi request, hoặc fine-tune trực tiếp bi-encoder trên các cặp dữ liệu in-domain. Reranker đã phá vỡ hoàn toàn ngân sách latency, đẩy thời gian phản hồi p99 vượt quá 410ms trên các instance CPU 8-core của chúng tôi. Ngược lại, fine-tuning base bi-encoder với sentence-transformers giúp giữ latency ở mức chỉ 22ms, đồng thời nâng Top-5 hit rate từ 61,4% lên 89,2%.

Fine-tuning là bài toán kỹ thuật thực chiến chứ không đơn thuần là lý thuyết học thuật. Nó mang lại cho bạn độ chính xác chuẩn doanh nghiệp đối với các ngôn ngữ chuyên biệt mà không làm đội chi phí hạ tầng cloud.

Cài đặt

Bắt đầu từ phiên bản 3, sentence-transformers tích hợp nguyên bản với Trainer của Hugging Face. Lần đại tu này mang lại hỗ trợ tích hợp sẵn cho việc huấn luyện phân tán (distributed runs), FP16/BF16 mixed-precision training và các early-stopping callback mà không cần viết boilerplate code.

Hãy thiết lập một môi trường Python độc lập và cài đặt các dependency cốt lõi với hỗ trợ CUDA 12.1:

python3 -m venv venv
source venv/bin/activate

# Cài đặt PyTorch với CUDA 12.1
pip install torch --index-url https://download.pytorch.org/whl/cu121

# Cài đặt Sentence-Transformers, Datasets và Accelerate
pip install -U sentence-transformers datasets accelerate

# Tùy chọn: Bộ tokenizer tiếng Việt phục vụ tiền xử lý văn bản
pip install pyvi underthesea

Kiểm tra xem PyTorch đã nhận diện GPU của bạn chưa trước khi tiếp tục:

python -c "import torch; print(f'CUDA khả dụng: {torch.cuda.is_available()} | Thiết bị: {torch.cuda.get_device_name(0)}')"

Cấu hình

Contrastive learning mang lại hiệu quả vượt trội khi tinh chỉnh các mô hình dense vector cho các domain ngách. Bạn cung cấp cho mô hình các cặp positive (query, passage) hoặc bộ ba (query, positive, hard negative). Trong quá trình backpropagation, MultipleNegativesRankingLoss (MNRL) sẽ tận dụng tất cả các đoạn trích positive khác trong mini-batch để làm negative ngầm định. Kích thước batch bằng 32 sẽ tự động tạo ra 31 negative cho mỗi sample mà không cần chọn lọc negative thủ công.

1. Chuẩn bị tập dữ liệu huấn luyện

Chúng tôi đã sinh 14.000 câu hỏi tổng hợp từ các đoạn text chunk nội bộ bằng LLM, ghép nối chúng với các đoạn văn bản nguồn và định dạng thành JSON Lines (train_dataset.jsonl):

[{"anchor": "Hồ sơ giảm trừ gia cảnh gồm những giấy tờ gì?", "positive": "Căn cứ theo Thông tư 111/2013/TT-BTC, hồ sơ chứng minh người phụ thuộc đối với con bao gồm bản sao giấy khai sinh..."},
{"anchor": "Quy định thời hạn nộp báo cáo tài chính năm", "positive": "Thời hạn nộp báo cáo tài chính năm chậm nhất là ngày thứ 90 kể từ ngày kết thúc năm dương lịch hoặc năm tài chính..."}]

2. Quy trình huấn luyện (Training Pipeline)

Dưới đây là training script đã được kiểm nghiệm thực tế trên môi trường production, sử dụng SentenceTransformerTrainer, FP16 mixed precision và loss hàm contrastive với in-batch negatives:

import os
from datasets import load_dataset
from sentence_transformers import (
    SentenceTransformer,
    SentenceTransformerTrainer,
    SentenceTransformerTrainingArguments,
    losses,
)
from sentence_transformers.training_args import BatchSamplers

# 1. Cấu hình base model
BASE_MODEL_NAME = "intfloat/multilingual-e5-base"
OUTPUT_DIR = "./vietnamese-domain-embedding-v1"

# 2. Khởi tạo base bi-encoder
model = SentenceTransformer(BASE_MODEL_NAME)

# 3. Load dataset (các cặp anchor-positive)
dataset = load_dataset("json", data_files={"train": "train_dataset.jsonl"})
split_dataset = dataset["train"].train_test_split(test_size=0.1, seed=42)
train_ds = split_dataset["train"]
eval_ds = split_dataset["test"]

# 4. Hàm loss contrastive
train_loss = losses.MultipleNegativesRankingLoss(model)

# 5. Đối số huấn luyện (tối ưu hóa cho GPU RTX 4090 / A10G 24GB)
training_args = SentenceTransformerTrainingArguments(
    output_dir=OUTPUT_DIR,
    num_train_epochs=3,
    per_device_train_batch_size=32,  # Batch lớn hơn cung cấp lượng in-batch negatives phong phú hơn
    per_device_eval_batch_size=32,
    learning_rate=2e-5,
    warmup_ratio=0.1,
    fp16=True,                       # Giảm mức tiêu thụ bộ nhớ và tăng tốc độ xử lý throughput
    eval_strategy="steps",
    eval_steps=100,
    save_steps=100,
    save_total_limit=2,
    logging_steps=20,
    batch_sampler=BatchSamplers.NO_DUPLICATES,  # Ngăn chặn anchor bị trùng lặp trong cùng một batch
    load_best_model_at_end=True,
)

# 6. Khởi tạo trainer
trainer = SentenceTransformerTrainer(
    model=model,
    args=training_args,
    train_dataset=train_ds,
    eval_dataset=eval_ds,
    loss=train_loss,
)

# 7. Thực thi huấn luyện
trainer.train()

# 8. Xuất checkpoint đã fine-tune
model.save_pretrained(os.path.join(OUTPUT_DIR, "best_model"))
print("Hoàn tất fine-tuning. Đã lưu checkpoint thành công.")

Kiểm thử & Giám sát

Đừng vội đưa checkpoint embedding thẳng lên vector index thực tế trên production chỉ dựa vào việc training loss giảm. Các dense model có thể gặp phải hiện tượng sụp đổ biểu diễn (representation collapse). Khi điều này xảy ra, các vector embedding bị co cụm lại trong một hình nón siêu cầu hẹp và mất đi hoàn toàn khả năng phân biệt.

1. Đánh giá định lượng với InformationRetrievalEvaluator

Đánh giá checkpoint của bạn trên tập test split độc lập gồm 800 câu truy vấn chưa từng thấy bằng các chỉ số xếp hạng: MRR@10, NDCG@10 và Hit@5.

from sentence_transformers.evaluation import InformationRetrievalEvaluator

queries = {
    "q1": "quy định khấu trừ thuế vãng lai 1%",
    "q2": "điều kiện hưởng chế độ thai sản cho lao động nam"
}
corpus = {
    "d1": "Thông tư hướng dẫn về thuế giá trị gia tăng đối với hoạt động xây dựng vãng lai...",
    "d2": "Luật Bảo hiểm xã hội quy định lao động nam có vợ sinh con được nghỉ từ 5 đến 14 ngày làm việc...",
    "d3": "Quy trình bảo dưỡng máy móc định kỳ trong nhà máy thép..."
}
relevant_docs = {
    "q1": {"d1"},
    "q2": {"d2"}
}

evaluator = InformationRetrievalEvaluator(
    queries=queries,
    corpus=corpus,
    relevant_docs=relevant_docs,
    name="vietnamese-ir-benchmark",
    mrr_at_k=[5, 10],
    ndcg_at_k=[10],
    accuracy_at_k=[1, 5]
)

results = evaluator(model)
print(f"MRR@10: {results['vietnamese-ir-benchmark_cosine_mrr@10']:.4f}")
print(f"NDCG@10: {results['vietnamese-ir-benchmark_cosine_ndcg@10']:.4f}")

2. Quan sát thực tế sau 6 tháng trên Production

Sau khi re-index lại vector database Qdrant với mô hình đã tinh chỉnh, chúng tôi đã phân tích 120.000 truy vấn thực tế trên production:

  • Độ bao phủ truy xuất (Retrieval Recall): Tỷ lệ Top-5 hit rate tăng từ 61,4% lên 89,2% đối với cách diễn đạt pháp lý tiếng Việt.
  • Tỷ lệ Hallucination: Các kết quả đầu ra được kiểm toán bị gắn cờ do sai lệch thông tin thực tế đã giảm 44%. Việc cung cấp trích đoạn chính xác giúp giảm thiểu trực tiếp lỗi sinh nội dung.
  • Độ phân tách vector (Vector Discrimination): Các mô hình đa ngôn ngữ có sẵn đã nén điểm cosine vào một dải hẹp chật chội từ 0,78 đến 0,88. Bi-encoder sau khi fine-tune đã khôi phục khả năng phân tách: các đoạn trích không liên quan dao động quanh mức 0,32, trong khi các câu trả lời thực sự đạt từ 0,85 trở lên. Độ biến thiên này mang lại cho chúng tôi ngưỡng cutoff rõ ràng ở mức 0,72 để xử lý gọn gàng các truy vấn ngoài phạm vi (out-of-scope).

Hãy duy trì chất lượng của các embedding theo thời gian. Theo dõi sự trôi dạt cosine (cosine drift) hàng tuần và ghi log các truy vấn bị người dùng downvote. Việc lên lịch fine-tuning định kỳ mỗi quý sẽ giúp cập nhật các quy định mới ban hành và duy trì độ tin cậy cho search engine truy xuất của bạn.

Share: