Retrieval fine-tuning starts with pairs of text that should land close together in embedding space. In Sentence Transformers, the multiple negatives ranking loss class turns the other positives in the same batch into implicit negatives, so anchor-positive rows can train a bi-encoder without a separately labeled negative column.
The Python workflow uses SentenceTransformerTrainer, a small Dataset with anchor and positive columns, and InformationRetrievalEvaluator to compare the base model against the trained model on a tiny retrieval check. The saved directory is then loaded again to confirm that the output can be used like any other Sentence Transformers model.
Batch contents matter more for this loss than for ordinary pairwise objectives. Keep duplicate or near-duplicate positives out of the same batch, use the no-duplicates batch sampler when possible, and switch to hard-negative triplets or the cached loss variant when larger data or larger effective batches require it.
Steps to train with multiple negatives ranking loss in Sentence Transformers:
- Install Sentence Transformers with training dependencies in the active Python environment.
$ python -m pip install --upgrade "sentence-transformers[train]"
The training extra installs the datasets and accelerate packages used by the current trainer workflow.
- Create the training script with anchor-positive rows and a retrieval evaluator.
- train_mnrl.py
from pathlib import Path import shutil from datasets import Dataset from sentence_transformers import ( SentenceTransformer, SentenceTransformerTrainer, SentenceTransformerTrainingArguments, ) from sentence_transformers.sentence_transformer.evaluation import InformationRetrievalEvaluator from sentence_transformers.sentence_transformer.losses import MultipleNegativesRankingLoss from sentence_transformers.sentence_transformer.training_args import BatchSamplers output_dir = Path("support-mnrl-model") training_dir = Path("mnrl-training-output") shutil.rmtree(output_dir, ignore_errors=True) shutil.rmtree(training_dir, ignore_errors=True) train_dataset = Dataset.from_dict( { "anchor": [ "reset a locked account", "reset a user password", "enable two-factor authentication", "rotate an API token", "restore a deleted project", "invite a new team member", "export audit logs", "change the billing contact", ], "positive": [ "Unlock the account from the admin users page.", "Open the user profile and send a password reset email.", "Open account security and enroll an authenticator app.", "Revoke the old API token and create a replacement token.", "Open deleted projects and restore the selected project.", "Open team settings and send an invitation email.", "Open compliance reports and export the audit log CSV.", "Open billing settings and update the primary contact.", ], } ) queries = { "q1": "How do I send a password reset?", "q2": "How do I replace an API token?", "q3": "How do I recover a deleted project?", } corpus = { "d1": "Open the user profile and send a password reset email.", "d2": "Revoke the old API token and create a replacement token.", "d3": "Open deleted projects and restore the selected project.", "d4": "Open billing settings and update the primary contact.", } relevant_docs = { "q1": {"d1"}, "q2": {"d2"}, "q3": {"d3"}, } model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2") evaluator = InformationRetrievalEvaluator( queries=queries, corpus=corpus, relevant_docs=relevant_docs, name="support-faq", accuracy_at_k=[1], precision_recall_at_k=[1, 3], map_at_k=[3], ndcg_at_k=[3], show_progress_bar=False, ) before = evaluator(model) args = SentenceTransformerTrainingArguments( output_dir=str(training_dir), num_train_epochs=1, per_device_train_batch_size=4, learning_rate=2e-5, warmup_steps=0.0, batch_sampler=BatchSamplers.NO_DUPLICATES, save_strategy="no", eval_strategy="no", logging_steps=1, disable_tqdm=True, report_to=[], ) loss = MultipleNegativesRankingLoss(model) trainer = SentenceTransformerTrainer( model=model, args=args, train_dataset=train_dataset, loss=loss, ) trainer.train() after = evaluator(model) model.save_pretrained(output_dir) reloaded = SentenceTransformer(str(output_dir)) embedding = reloaded.encode(["How do I replace an API token?"], convert_to_numpy=True) metric_key = "support-faq_cosine_ndcg@3" print(f"train rows: {len(train_dataset)}") print(f"loss: {loss.__class__.__name__}") print(f"retrieval metric: {metric_key}") print(f"before score: {before[metric_key]:.4f}") print(f"after score: {after[metric_key]:.4f}") print(f"saved model path: {output_dir}") print(f"reloaded embedding shape: {embedding.shape}")
MultipleNegativesRankingLoss treats the other positives in the current batch as negatives for each anchor. BatchSamplers.NO_DUPLICATES reduces false negatives caused by repeated or equivalent pairs in the same batch.
- Run the training script.
$ python train_mnrl.py {'loss': '0.0005047', 'grad_norm': '0.04047', 'learning_rate': '2e-05', 'epoch': '0.5'} {'loss': '0.005828', 'grad_norm': '0.699', 'learning_rate': '1e-05', 'epoch': '1'} ##### snipped ##### train rows: 8 loss: MultipleNegativesRankingLoss retrieval metric: support-faq_cosine_ndcg@3 before score: 1.0000 after score: 1.0000 saved model path: support-mnrl-model reloaded embedding shape: (1, 384)A tiny retrieval set can already score perfectly before training. For this smoke run, the important signals are completed loss logging, the retrieval metric, the saved model path, and the reload shape.
- Confirm that the saved model reloads and returns the expected embedding width.
reloaded embedding shape: (1, 384)
Mohd Shakir Zakaria is a cloud architect with deep roots in software development and open-source advocacy. Certified in AWS, Red Hat, VMware, ITIL, and Linux, he specializes in designing and managing robust cloud and on-premises infrastructures.