Sentence Transformers: Embedding, Kemiripan Semantik, dan Reranker
Sentence Transformers (sering disebut SBERT) adalah pustaka Python untuk mengubah teks menjadi embedding vektor padat yang menangkap makna, bukan sekadar kata di permukaan. Dalam tutorial ini kita membangun sistem retrieval kecil di atas korpus contoh, mengukur kemiripan semantik, menambahkan reranker cross-encoder, lalu melatih model embedding kita sendiri dengan API pelatihan modern. Tujuannya adalah gambaran praktis menyeluruh tentang bagaimana pustaka ini cocok ke dalam pipeline pencarian atau RAG nyata.
Apa Itu Sentence Transformers
Pustaka sentence-transformers membungkus model transformer (BERT, RoBERTa, MPNet, dan banyak lainnya) dan menambahkan pooling sehingga satu kalimat atau paragraf utuh dipetakan ke satu vektor berukuran tetap. Dua teks dengan makna serupa menghasilkan vektor yang berdekatan, sehingga Anda dapat membandingkannya dengan cosine similarity alih-alih pencocokan kata kunci.
Pustaka ini dikelola bersama ekosistem Hugging Face, sehingga model dimuat dari Hub, dataset memakai format datasets, dan model hasil pelatihan dapat diunggah kembali ke Hub dengan satu pemanggilan. Ini adalah alat standar untuk membangun pencarian berbasis embedding, clustering, deduplikasi, dan tahap retrieval pada sistem RAG.
Bi-Encoder vs Cross-Encoder
Ada dua keluarga model dalam pustaka ini, dan memilih dengan tepat adalah keputusan desain yang paling penting.
Sebuah bi-encoder mengkodekan setiap teks secara independen menjadi sebuah vektor. Anda meng-embed seluruh korpus sekali, menyimpan vektornya, dan saat kueri datang Anda cukup meng-embed kuerinya saja lalu membandingkannya dengan vektor tersimpan. Ini cepat dan dapat menskala hingga jutaan dokumen karena perbandingannya hanya berupa dot product. Komprominya adalah akurasi: model tidak pernah melihat kueri dan dokumen secara bersamaan, sehingga dapat melewatkan interaksi halus.
Sebuah cross-encoder menerima pasangan teks sekaligus (kueri dan kandidat) dan menghasilkan satu skor relevansi. Karena model memperhatikan kedua teks secara bersamaan, akurasinya jauh lebih tinggi. Biayanya adalah Anda tidak dapat menghitung apa pun di awal: setiap pasangan kueri-dokumen harus dijalankan melalui model. Menilai satu kueri terhadap satu juta dokumen menjadi tidak praktis.
Pola standar menggabungkan keduanya. Bi-encoder mengambil beberapa lusin kandidat dengan cepat, lalu cross-encoder me-rerank hanya kandidat tersebut untuk presisi. Inilah pola retrieve-then-rerank yang kita bangun nanti.
Kueri --> [Bi-encoder] --> 50 kandidat teratas --> [Cross-encoder] --> 5 teratas hasil rerank
(cepat, perkiraan) (lambat, presisi)
Instalasi
Pasang pustaka dengan pip. Ia akan menarik PyTorch, transformers, dan datasets sebagai dependensi.
pip install -U sentence-transformers
Untuk pelatihan dan evaluasi Anda mungkin juga membutuhkan beberapa tambahan. Memasang accelerate memungkinkan pelatihan lebih cepat dan multi-GPU, dan datasets diperlukan oleh API pelatihan (biasanya sudah ikut terpasang).
pip install -U accelerate datasets
Verifikasi pemasangan dan periksa perangkat yang tersedia.
import torch
from sentencetransformers import SentenceTransformer
print("sentence-transformers siap")
print("CUDA tersedia:", torch.cuda.isavailable())
Memuat Model dan Mengkodekan Teks
Kelas inti adalah SentenceTransformer. Berikan nama model dari Hub dan ia akan mengunduh serta menyimpan bobotnya di cache. Model awal serbaguna yang baik adalah all-MiniLM-L6-v2: kecil, cepat, berdimensi 384, dan kuat untuk kemiripan semantik bahasa Inggris.
from sentencetransformers import SentenceTransformer
model = SentenceTransformer("all-MiniLM-L6-v2")
sentences = [
"Bagaimana cara mengatur ulang kata sandi akun saya?",
"Langkah memulihkan login yang terlupa",
"Cuaca di Jakarta panas hari ini.",
]
embeddings = model.encode(sentences)
print(embeddings.shape) # (3, 384)
encode() mengembalikan array NumPy secara default. Argumen yang berguna:
batchsizemengatur berapa banyak teks diproses sekaligus. Batch lebih besar lebih cepat di GPU tetapi memakai lebih banyak memori.converttotensor=Truemengembalikan tensor PyTorch, praktis ketika semua tetap berada di GPU untuk perhitungan kemiripan.normalizeembeddings=Truemenskalakan setiap vektor ke panjang satuan, sehingga dot product setara dengan cosine similarity. Lakukan ini jika indeks hilir Anda menggunakan penilaian dot-product.showprogressbar=Truemembantu untuk korpus besar.
embeddings = model.encode(
sentences,
batchsize=32,
converttotensor=True,
normalizeembeddings=True,
showprogressbar=True,
)
Model Multibahasa
all-MiniLM-L6-v2 berorientasi bahasa Inggris. Untuk pencarian bahasa Indonesia, campuran, atau lintas bahasa, muat model multibahasa. paraphrase-multilingual-MiniLM-L12-v2 mencakup 50+ bahasa dan memetakan terjemahan dari kalimat yang sama ke vektor yang berdekatan.
multi = SentenceTransformer("paraphrase-multilingual-MiniLM-L12-v2")
pairs = [
"How do I reset my password?",
"Bagaimana cara mengatur ulang kata sandi saya?",
]
emb = multi.encode(pairs, converttotensor=True, normalizeembeddings=True)
print(multi.similarity(emb[0], emb[1])) # skor tinggi lintas bahasa
Prompt dan promptname
Beberapa model yang lebih baru (misalnya keluarga e5 dan bge, serta model Nomic) dilatih dengan awalan instruksi. Mereka bekerja paling baik ketika kueri dan dokumen mendapat awalan berbeda seperti "query: " dan "passage: ". Pustaka mendukung ini melalui konfigurasi prompts dan argumen promptname sehingga Anda tidak perlu menuliskan string itu di mana-mana.
model = SentenceTransformer(
"intfloat/multilingual-e5-small",
prompts={
"query": "query: ",
"passage": "passage: ",
},
)
queryemb = model.encode("atur ulang kata sandi", promptname="query")
docemb = model.encode("Langkah memulihkan login yang terlupa", promptname="passage")
Selalu periksa kartu model untuk mengetahui apakah ia memerlukan prompt. Memakai konvensi prompt yang salah diam-diam menurunkan kualitas.
Menghitung Kemiripan
Setelah memiliki embedding, model.similarity menghitung skor berpasangan antara dua kumpulan embedding. Secara default ia memakai cosine similarity, sesuai dengan cara sebagian besar model dilatih.
emb = model.encode(sentences, converttotensor=True)
Matriks berpasangan: setiap baris vs setiap kolom
scores = model.similarity(emb, emb)
print(scores)
Hasilnya adalah matriks di mana elemen [i][j] adalah kemiripan antara kalimat i dan kalimat j. Anda juga dapat mengubah metrik pada model dengan model.similarityfnname = "dot" jika model Anda dilatih untuk penilaian dot-product.
Untuk perbandingan sekali pakai tanpa objek model, modul util menyediakan pembantu.
from sentencetransformers import util
cos = util.cos
sim(emb[0], emb[1])
print(float(cos))
util.cossim praktis ketika Anda punya embedding mentah dari mana saja dan hanya butuh skor cosine.
Membangun Sistem Pencarian Semantik
Mari bangun inti retrieval di atas korpus kecil. Bi-encoder meng-embed korpus sekali; kueri di-embed sesuai permintaan dan dibandingkan dengan util.semanticsearch, yang menangani pemilihan top-k secara efisien.
from sentencetransformers import SentenceTransformer, util
model = SentenceTransformer("all-MiniLM-L6-v2")
corpus = [
"Atur ulang kata sandi Anda dari halaman pengaturan akun.",
"Aktifkan autentikasi dua faktor untuk keamanan ekstra.",
"Pengembalian dana diproses dalam 5 hari kerja.",
"Perbarui alamat penagihan sebelum faktur berikutnya.",
"Hubungi dukungan jika login Anda terkunci setelah gagal berulang.",
"Ekspor data Anda sebagai CSV dari dasbor.",
]
corpus
emb = model.encode(corpus, converttotensor=True, normalizeembeddings=True)
query = "Saya lupa kredensial login saya"
query
emb = model.encode(query, converttotensor=True, normalizeembeddings=True)
hits = util.semantic
search(queryemb, corpusemb, topk=3)
for hit in hits[0]:
print(f"{hit['score']:.3f} {corpus[hit['corpus
id']]}")
util.semanticsearch menerima sekumpulan embedding kueri dan mengembalikan, untuk setiap kueri, daftar hit yang diurutkan berdasarkan skor. Setiap hit memiliki corpusid dan score. Untuk skala produksi Anda akan memasukkan corpusemb ke basis data vektor, tetapi logika penilaiannya sama.
Paraphrase Mining dan Clustering
Selain pencarian, embedding mendukung dua tugas batch yang umum.
Paraphrase mining menemukan pasangan paling mirip di seluruh daftar tanpa menghitung matriks N-kali-N penuh di memori.util.paraphrasemining dioptimalkan untuk ini.
from sentencetransformers import util
candidates = [
"Bagaimana cara mengatur ulang kata sandi saya?",
"Apa langkah untuk memulihkan login saya?",
"Di mana saya bisa mengekspor data saya?",
"Bagaimana cara mengunduh data saya sebagai berkas?",
"Kapan pengembalian dana saya tiba?",
]
pairs = util.paraphrase
mining(model, candidates, topk=2)
for score, i, j in pairs[:5]:
print(f"{score:.3f} | {candidates[i]} <> {candidates[j]}")
Clustering mengelompokkan teks yang berkaitan secara semantik. Anda dapat memberikan embedding ternormalisasi ke KMeans dari scikit-learn, atau memakai util.communitydetection untuk clustering berbasis ambang yang cepat tanpa menentukan jumlah klaster di awal.
emb = model.encode(candidates, converttotensor=True, normalizeembeddings=True)
clusters = util.community
detection(emb, threshold=0.6, mincommunitysize=2)
for cid, cluster in enumerate(clusters):
print(f"Klaster {cid}:", [candidates[idx] for idx in cluster])
Reranking dengan Cross-Encoder
Bi-encoder cepat tetapi bisa menempatkan dokumen yang hanya berkaitan longgar di atas yang terbaik. Cross-encoder memperbaiki urutan kandidat teratas. Kelas CrossEncoder memuat model yang dilatih untuk menilai pasangan (kueri, dokumen); ms-marco-MiniLM-L-6-v2 adalah pilihan default yang solid untuk peringkat paragraf bahasa Inggris.
from sentencetransformers import CrossEncoder
reranker = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2")
query = "Saya lupa kredensial login saya"
Ambil kandidat yang diperoleh bi-encoder
candidate
texts = [corpus[hit["corpusid"]] for hit in hits[0]]
pairs = [(query, text) for text in candidate
texts]
rerankscores = reranker.predict(pairs)
reranked = sorted(zip(rerankscores, candidatetexts), reverse=True)
for score, text in reranked:
print(f"{score:.3f} {text}")
Pipeline Retrieve-Then-Rerank
Menggabungkan semuanya: ambil jaring lebar secara murah, lalu rerank dengan presisi. Metode praktis reranker.rank melakukan pemasangan dan pengurutan untuk Anda.
def search(query, topkretrieve=20, topkfinal=3):
q
emb = model.encode(query, converttotensor=True, normalizeembeddings=True)
hits = util.semantic
search(qemb, corpusemb, topk=topkretrieve)[0]
documents = [corpus[h["corpus
id"]] for h in hits]
ranked = reranker.rank(query, documents, topk=topkfinal)
return [(r["score"], documents[r["corpusid"]]) for r in ranked]
for score, text in search("cara mendapatkan uang kembali"):
print(f"{score:.3f} {text}")
Ambil lebih banyak kandidat daripada yang akhirnya Anda tampilkan (20 hingga 100 adalah lazim) agar reranker punya cukup bahan untuk menemukan jawaban terbaik.
Melatih Model Embedding Kustom
Model pralatih adalah baseline yang kuat, tetapi model yang disetel pada domain Anda sendiri biasanya melakukan retrieval jauh lebih baik. API modern mencerminkan Trainer Hugging Face: Anda menyediakan dataset, sebuah loss, argumen pelatihan, dan evaluator opsional.
Memilih Loss dan Format Dataset
Loss menentukan bentuk data Anda.
MultipleNegativesRankingLossadalah tulang punggung untuk retrieval. Ia berlatih pada pasangan positif(anchor, positive)dan memperlakukan semua contoh lain dalam batch sebagai negatif. Dataset Anda perlu dua (atau tiga, dengan negatif sulit) kolom teks dan tanpa label eksplisit. Batch lebih besar memberi lebih banyak negatif dalam-batch dan biasanya membantu.CosineSimilarityLossberlatih pada pasangan dengan label float antara 0 dan 1, berguna ketika Anda punya skor kemiripan bertingkat alih-alih pasangan positif yang bersih.
Dataset adalah objek Dataset Hugging Face; urutan kolom penting dan kolom label harus diberi nama label atau score.
from datasets import Dataset
Untuk MultipleNegativesRankingLoss: pasangan (anchor, positive)
traindataset = Dataset.fromdict({
"anchor": [
"Bagaimana cara mengatur ulang kata sandi saya?",
"Di mana saya bisa mengekspor data saya?",
"Kapan pengembalian dana saya tiba?",
],
"positive": [
"Atur ulang kata sandi Anda dari halaman pengaturan akun.",
"Ekspor data Anda sebagai CSV dari dasbor.",
"Pengembalian dana diproses dalam 5 hari kerja.",
],
})
Menyiapkan Trainer
SentenceTransformerTrainer mengikat semuanya. Argumen pelatihan berasal dari SentenceTransformerTrainingArguments, yang memperluas TrainingArguments yang sudah dikenal.
from sentencetransformers import (
SentenceTransformer,
SentenceTransformerTrainer,
SentenceTransformerTrainingArguments,
)
from sentence
transformers.losses import MultipleNegativesRankingLoss
model = SentenceTransformer("all-MiniLM-L6-v2")
loss = MultipleNegativesRankingLoss(model)
args = SentenceTransformerTrainingArguments(
outputdir="models/support-embeddings",
numtrainepochs=3,
perdevicetrainbatchsize=32,
learningrate=2e-5,
warmupratio=0.1,
fp16=True, # gunakan bf16=True pada GPU yang lebih baru
evalstrategy="steps",
evalsteps=100,
savestrategy="steps",
savesteps=100,
loggingsteps=20,
)
trainer = SentenceTransformerTrainer(
model=model,
args=args,
traindataset=traindataset,
loss=loss,
)
trainer.train()
Mengevaluasi Kualitas Retrieval
Untuk mengetahui apakah fine-tuning membantu, ukur metrik retrieval pada data yang ditahan. InformationRetrievalEvaluator melaporkan metrik seperti Recall@k, MRR, dan NDCG. Ia memerlukan tiga pemetaan: kueri, korpus, dan id korpus mana yang relevan untuk setiap kueri.
from sentencetransformers.evaluation import InformationRetrievalEvaluator
queries = {"q1": "cara mengatur ulang kata sandi", "q2": "unduh data saya"}
corpus
map = {str(i): text for i, text in enumerate(corpus)}
relevant = {"q1": {"0"}, "q2": {"5"}} # id korpus yang menjawab tiap kueri
irevaluator = InformationRetrievalEvaluator(
queries=queries,
corpus=corpusmap,
relevantdocs=relevant,
name="support-eval",
)
Jalankan mandiri, atau berikan evaluator=irevaluator ke Trainer
results = irevaluator(model)
print(results)
Berikan evaluator ke trainer dengan evaluator=irevaluator untuk melacak metrik ini selama pelatihan dan memilih checkpoint terbaik.
Melatih Cross-Encoder
Anda dapat menyetel reranker dengan cara yang sama memakai CrossEncoderTrainer. Data pelatihan cross-encoder berupa (kueri, dokumen, label) di mana label adalah skor relevansi, sering kali biner (1 untuk relevan, 0 untuk tidak).
from datasets import Dataset
from sentencetransformers.crossencoder import (
CrossEncoder,
CrossEncoderTrainer,
CrossEncoderTrainingArguments,
)
from sentencetransformers.crossencoder.losses import BinaryCrossEntropyLoss
cedata = Dataset.fromdict({
"query": ["atur ulang kata sandi", "atur ulang kata sandi", "ekspor data"],
"document": [
"Atur ulang kata sandi Anda dari halaman pengaturan akun.",
"Pengembalian dana diproses dalam 5 hari kerja.",
"Ekspor data Anda sebagai CSV dari dasbor.",
],
"label": [1.0, 0.0, 1.0],
})
cemodel = CrossEncoder("microsoft/MiniLM-L12-H384-uncased", numlabels=1)
celoss = BinaryCrossEntropyLoss(cemodel)
ceargs = CrossEncoderTrainingArguments(
outputdir="models/support-reranker",
numtrainepochs=2,
perdevicetrainbatchsize=16,
)
cetrainer = CrossEncoderTrainer(
model=cemodel, args=ceargs, traindataset=cedata, loss=celoss,
)
cetrainer.train()
Dalam praktiknya, menambang negatif sulit (dokumen salah yang tampak masuk akal) menghasilkan reranker yang jauh lebih kuat daripada negatif acak.
Menyimpan, Memuat, dan Membagikan
Simpan model terlatih ke disk dan muat kembali seperti model lainnya.
model.savepretrained("models/support-embeddings")
reloaded = SentenceTransformer("models/support-embeddings")
Untuk membagikan di Hugging Face Hub, masuk dulu (huggingface-cli login) lalu unggah. Ini mengunggah bobot, konfigurasi, dan kartu model yang dibuat otomatis.
model.pushtohub("nama-pengguna-anda/support-embeddings")
Mempercepat Inferensi: Kuantisasi dan ONNX
Untuk penyajian produksi, dua backend mengurangi latensi tanpa melatih ulang. Pustaka dapat memuat model dengan runtime ONNX atau OpenVINO melalui argumen backend, dan mendukung bobot terkuantisasi (int8) untuk percepatan lebih lanjut di CPU.
# Backend ONNX untuk inferensi CPU yang lebih cepat
model = SentenceTransformer(
"all-MiniLM-L6-v2",
backend="onnx",
modelkwargs={"filename": "onnx/modelqint8avx512.onnx"},
)
Model int8 terkuantisasi lebih kecil dan lebih cepat di CPU dengan sedikit biaya akurasi; ukur pada data Anda sendiri sebelum berkomitmen. Di GPU, memakai fp16/bf16 dan batch lebih besar biasanya adalah kemenangan paling sederhana.
Praktik Terbaik
- Cocokkan model dengan bahasa dan domain Anda. Gunakan model multibahasa untuk korpus bahasa Indonesia atau campuran, dan periksa kartu model untuk prompt yang diharapkan.
- Normalisasi embedding ketika indeks Anda menilai dengan dot product, dan jaga normalisasi yang sama saat kueri maupun pengindeksan.
- Selalu ambil lebih banyak kandidat daripada yang Anda tampilkan, lalu rerank dengan cross-encoder untuk urutan akhir.
- Bangun himpunan evaluasi sejak awal. Tanpa metrik
InformationRetrievalEvaluatorAnda tidak dapat tahu apakah fine-tuning membantu atau merugikan. - Utamakan
MultipleNegativesRankingLossdengan batch sebesar yang memori izinkan; negatif dalam-batch itulah yang membuatnya efektif. - Cache embedding korpus. Meng-encode ulang korpus besar setiap kali memulai ulang itu boros; simpan vektornya.
- Untuk pelatihan reranker, investasikan pada negatif sulit alih-alih mengumpulkan lebih banyak pasangan acak.
Kesimpulan dan Poin Utama
Sentence Transformers memberi Anda perangkat lengkap: bi-encoder cepat untuk retrieval, cross-encoder akurat untuk reranking, dan API pelatihan modern untuk menyesuaikan keduanya ke domain Anda.
- Bi-encoder meng-embed teks secara independen dan menskala ke korpus besar; cross-encoder menilai pasangan secara bersamaan untuk presisi lebih tinggi tetapi tidak dapat menghitung di awal.
- Pola retrieve-then-rerank menggabungkan kekuatan keduanya dan menjadi arsitektur default untuk pencarian dan RAG yang serius.
encode(),model.similarity, dan pembantuutil(semanticsearch,cossim,paraphrasemining,communitydetection) mencakup sebagian besar tugas sehari-hari.- Fine-tuning dengan
SentenceTransformerTrainer, loss sepertiMultipleNegativesRankingLoss, danInformationRetrievalEvaluatorbiasanya memberi peningkatan kualitas terbesar pada data domain. - Untuk produksi, pertimbangkan backend ONNX atau terkuantisasi untuk memangkas latensi, dan selalu cache embedding korpus Anda.
Mulai dari model pralatih yang kuat, ukur pada himpunan evaluasi Anda sendiri, dan lakukan fine-tuning hanya di tempat yang angkanya membenarkan.