Trong bài viết về VN-MTEB, mình có nhắc khá nhiều tới việc xây dựng Dataset và Benchmark cho Embedding Model tiếng Việt. Nhưng sau khi có Dataset thì câu hỏi tiếp theo sẽ là: Làm thế nào để Fine-tune một Embedding Model từ những dữ liệu đó?
Khi tìm hiểu phần này, mình thấy Sentence Transformers là một thư viện khá dễ tiếp cận. Thay vì phải tự viết toàn bộ Training Loop bằng PyTorch, thư viện đã cung cấp gần như đầy đủ những thứ cần thiết: Dataset, Loss Function, Evaluator, Trainer.
Trong bài viết này mình sẽ tóm tắt những phần quan trọng nhất để có thể bắt đầu Fine-tune một Embedding Model, chủ yếu dựa trên bài viết Training and Finetuning Embedding Models with Sentence Transformers của Hugging Face.
I. Vì sao cần Fine-tune Embedding Model?
Một Embedding Model sau Pre-training đã có khả năng biểu diễn Semantic khá tốt. Tuy nhiên, khái niệm "giống nhau" lại phụ thuộc rất nhiều vào bài toán.
Ví dụ với Query:
"Học phí ngành Trí tuệ nhân tạo HCMUS bao nhiêu?"
Ta có hai Document:
A: Thông tin học phí chương trình Trí tuệ nhân tạo năm 2026.
B: Điểm chuẩn ngành Trí tuệ nhân tạo HCMUS năm 2026.
Cả hai đều chứa những thông tin khá giống nhau về mặt Semantic như HCMUS, Trí tuệ nhân tạo, 2026. Nhưng đối với bài toán Retrieval thì rõ ràng Document A phải được xếp cao hơn.
Fine-tuning lúc này giúp điều chỉnh Embedding Space để phù hợp hơn với bài toán cụ thể của chúng ta.
II. Dataset cho Embedding Model
Một điểm mình từng khá dễ nhầm là Dataset để Train Embedding Model không bắt buộc luôn phải có 3 cột Anchor, Positive, Negative.
Sentence Transformers hỗ trợ nhiều dạng Dataset, phổ biến nhất gồm: Positive Pair, Triplet, Pair + Similarity Score, Text + Class.
Positive Pair
Dạng đơn giản nhất:
{
"anchor": "Học phí HCMUS năm 2026 là bao nhiêu?",
"positive": "Thông tin học phí HCMUS năm học 2026."
}Ở đây chúng ta chỉ nói với Model rằng hai nội dung này nên nằm gần nhau trong Embedding Space.
Dạng này khá phù hợp với Retrieval Dataset, ví dụ: Query + Relevant Document, Question + Answer, Title + Content.
Triplet
Nếu đã có Negative:
{
"anchor": "Học phí HCMUS năm 2026 là bao nhiêu?",
"positive": "Thông tin học phí HCMUS năm học 2026.",
"negative": "Điểm chuẩn HCMUS năm 2026."
}
Model sẽ học sao cho:
similarity(anchor, positive) > similarity(anchor, negative)
Điểm quan trọng ở đây là Negative không nên quá dễ.
Ví dụ:
Query: Học phí HCMUS năm 2026?
Negative: Cách nấu phở bò.
Negative này gần như không cung cấp nhiều Training Signal.
Trong khi:
Negative: Điểm chuẩn HCMUS năm 2026.
khó hơn đáng kể vì Model phải thực sự hiểu người dùng đang tìm học phí, chứ không chỉ dựa vào những Keyword như HCMUS hay 2026.
III. Loss Function
Sau Dataset, phần quan trọng tiếp theo là Loss Function.
Không có một Loss tốt nhất cho tất cả trường hợp. Loss cần được chọn dựa trên Format Dataset và mục tiêu của Model.
Với Retrieval, một Loss được sử dụng khá phổ biến là:
from sentence_transformers import losses
loss = losses.MultipleNegativesRankingLoss(model)
MultipleNegativesRankingLoss, hay MNRL, hoạt động khá thú vị.
Giả sử một Batch có:
Q1 + P1
Q2 + P2
Q3 + P3
Q4 + P4
Khi xử lý Q1, P1 là Positive, còn P2, P3, P4 có thể được sử dụng làm Negative.
Đây được gọi là In-batch Negative.
Nhờ vậy chúng ta không nhất thiết phải tự tạo Negative cho tất cả Sample. Batch càng lớn thì mỗi Query càng nhìn thấy nhiều Negative hơn.
Tuy nhiên có một vấn đề cần chú ý là False Negative. Một Positive của Sample khác hoàn toàn có thể cũng là Relevant Document của Query hiện tại, nhưng lại vô tình bị xem là Negative.
Sentence Transformers có BatchSamplers.NO_DUPLICATES để hạn chế một số trường hợp Duplicate trong cùng Batch.
Ngoài MNRL, Sentence Transformers còn khá nhiều Loss khác. Nếu Dataset của bạn không phải Retrieval dạng Pair/Triplet thì nên xem thêm phần Loss Overview trong Documentation thay vì chọn MNRL một cách mặc định.
IV. Hard Negative
Đối với Retrieval, Hard Negative là một phần khá quan trọng vì không phải Negative nào cũng mang lại giá trị như nhau. Đối với truy vấn "Cách Fine-tune Embedding Model", nếu Negative là "Cách nấu phở bò" thì model gần như không cần học thêm nhiều để phân biệt hai nội dung này. Một Negative tốt hơn sẽ là "Cách Fine-tune Large Language Model bằng LoRA" vì nội dung khá gần về mặt Semantic, cùng nói về Fine-tuning và Model, nhưng vẫn không phải kết quả phù hợp với truy vấn ban đầu. Những trường hợp như vậy được gọi là Hard Negative.
Sentence Transformers cung cấp sẵn hàm:from sentence_transformers.util import mine_hard_negatives
Hàm này có thể sử dụng Embedding Model hiện tại để Retrieval những Candidate có độ tương đồng cao với Query, sau đó chọn một số Candidate phù hợp làm Hard Negative. Cách này giúp giảm đáng kể công sức nếu Dataset có số lượng lớn.
Tuy nhiên, Hard Negative Mining cũng là bước cần kiểm tra kỹ. Chẳng hạn với truy vấn "Học phí HCMUS năm 2026?", nếu Positive hiện tại là "Thông tin học phí HCMUS năm học 2026" thì một Candidate như "Mức học phí Trường Đại học Khoa học Tự nhiên năm 2026" rất có thể cũng là một Positive hợp lệ. Nếu tự động gán Candidate này thành Negative, chúng ta sẽ trực tiếp dạy model rằng hai nội dung đúng phải nằm xa nhau trong Embedding Space.
Vì vậy, theo mình một pipeline hợp lý hơn sẽ là Positive Pair → Hard Negative Mining → Filtering/Validation → Training. Với Dataset lớn, có thể kết hợp thêm ngưỡng Similarity hoặc Cross Encoder để lọc Candidate trước khi đưa vào Training. Phần này Sentence Transformers có Documentation riêng về Hard Negative Mining, nếu muốn xây Dataset ở quy mô lớn thì mình nghĩ nên đọc kỹ thêm.
V. Training với SentenceTransformerTrainer
Sau khi đã có Model, Dataset và Loss thì phần Training thực tế lại khá ngắn.
Load Model:
from sentence_transformers import SentenceTransformer
model = SentenceTransformer(
"intfloat/multilingual-e5-base"
)Chuẩn bị Dataset:
from datasets import Dataset
train_dataset = Dataset.from_dict({
"anchor": anchors,
"positive": positives,
"negative": negatives,
})
Chọn Loss:
from sentence_transformers import losses
loss = losses.MultipleNegativesRankingLoss(model)Training Arguments:
from sentence_transformers import SentenceTransformerTrainingArguments
from sentence_transformers.training_args import BatchSamplers
args = SentenceTransformerTrainingArguments(
output_dir="models/vietnamese-embedding",
num_train_epochs=2,
per_device_train_batch_size=32,
learning_rate=2e-5,
warmup_ratio=0.1,
fp16=True,
batch_sampler=BatchSamplers.NO_DUPLICATES,
)Training:
from sentence_transformers import SentenceTransformerTrainer
trainer = SentenceTransformerTrainer(
model=model,
args=args,
train_dataset=train_dataset,
loss=loss,
)
trainer.train()Nhìn chung, Code Training không phải phần quá phức tạp. Phần khó hơn vẫn nằm ở Dataset, Negative và cách Evaluation.
Một lưu ý nhỏ: một số Model như E5 yêu cầu Prefix riêng cho Query và Document, ví dụ query: và passage:. Vì vậy trước khi Train nên đọc Model Card của Backbone đang sử dụng.
VI. Evaluation
Một lỗi khá dễ gặp là chỉ nhìn vào Training Loss.
Nếu mục tiêu cuối cùng là Retrieval thì Metric cuối cùng cũng nên đánh giá khả năng Retrieval.
Sentence Transformers có InformationRetrievalEvaluator, hỗ trợ các Metric như: MRR, Recall@K, Precision@K, nDCG@K. Sau đó có thể Benchmark Model trước và sau Fine-tuning.
Theo mình đây là phần không nên bỏ qua. Một Model có Training Loss giảm không đồng nghĩa với việc Recall@10 hay nDCG@10 của hệ thống Retrieval sẽ tốt hơn.
VII. Một số lưu ý
Sau khi đọc Documentation và bài viết của Hugging Face, mình thấy có một số điểm quan trọng cần lưu ý: Loss phải phù hợp với Dataset, Negative không nên quá dễ, Hard Negative cần được Validate, tránh Data Leakage giữa Train/Test, Benchmark Base Model trước khi Fine-tune và Evaluation Metric phải phù hợp với bài toán cuối cùng.
Đặc biệt với Embedding Model, mình nghĩ phần Dataset vẫn quan trọng hơn rất nhiều so với việc cố tối ưu Training Script.
Model và Hyperparameter có thể thay đổi tương đối nhanh. Nhưng nếu hàng triệu Sample đã bị sai Positive/Negative thì việc sửa lại Dataset sẽ khó hơn rất nhiều.
VIII. Kết luận
Sau khi tìm hiểu Sentence Transformers, điều mình thấy khá thú vị là Fine-tune một Embedding Model không quá phức tạp về mặt Code.
Một Training Script hoàn chỉnh có thể chỉ vài chục dòng.
Phần thực sự cần đầu tư thời gian lại nằm ở những câu hỏi trước đó: Positive được định nghĩa như thế nào? Negative lấy từ đâu? Hard Negative có thực sự là Negative? Loss nào phù hợp? Evaluation Dataset có phản ánh đúng bài toán không?
Điều này cũng khá giống với những gì mình rút ra trong bài viết về VN-MTEB: Dataset và cách Evaluation có ảnh hưởng rất lớn tới việc chúng ta đánh giá một Embedding Model có thực sự tốt hay không.
Nếu bắt đầu Fine-tune một Model cho Retrieval, mình sẽ ưu tiên pipeline:
Xác định Task → Xây Dataset → Hard Negative Mining → Validation → Fine-tune → Evaluate → Error Analysis
Trong đó mình nghĩ Error Analysis là bước khá quan trọng. Thay vì chỉ nhìn Recall tăng hay giảm, hãy lấy những Query mà Model Retrieval sai ra xem nó đang nhầm ở đâu. Đây thường là nguồn thông tin tốt nhất để quyết định vòng Dataset tiếp theo cần bổ sung những gì.
Nguồn tham khảo
- Hugging Face, Training and Finetuning Embedding Models with Sentence Transformers
- Sentence Transformers Documentation, Training Overview
- Sentence Transformers Documentation, Dataset Overview
- Sentence Transformers Documentation, Loss Overview
- Sentence Transformers Documentation, Hard Negative Mining
- Sentence Transformers Documentation, Evaluation

Thảo luận bài viết
Câu hỏi, ghi chú và góc nhìn của bạn về nội dung này.