|
import logging |
|
import traceback |
|
from collections import defaultdict |
|
|
|
from datasets import load_dataset |
|
from datasets.load import load_from_disk |
|
from torch import nn |
|
import torch |
|
|
|
from sentence_transformers import SentenceTransformer |
|
from sentence_transformers.cross_encoder import CrossEncoder |
|
from sentence_transformers.cross_encoder.evaluation.CENanoBEIREvaluator import CENanoBEIREvaluator |
|
from sentence_transformers.cross_encoder.evaluation.CERerankingEvaluator import CERerankingEvaluator |
|
from sentence_transformers.cross_encoder.losses.BinaryCrossEntropyLoss import BinaryCrossEntropyLoss |
|
from sentence_transformers.cross_encoder.losses.CachedMultipleNegativesRankingLoss import ( |
|
CachedMultipleNegativesRankingLoss, |
|
) |
|
from sentence_transformers.cross_encoder.trainer import CrossEncoderTrainer |
|
from sentence_transformers.cross_encoder.training_args import CrossEncoderTrainingArguments |
|
from sentence_transformers.evaluation.SequentialEvaluator import SequentialEvaluator |
|
from sentence_transformers.training_args import BatchSamplers |
|
from sentence_transformers.util import mine_hard_negatives |
|
|
|
|
|
def main(): |
|
model_name = "answerdotai/ModernBERT-base" |
|
|
|
|
|
logging.basicConfig(format="%(asctime)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S", level=logging.INFO) |
|
|
|
train_batch_size = 64 |
|
num_epochs = 1 |
|
|
|
|
|
model = CrossEncoder(model_name) |
|
print("Model max length:", model.max_length) |
|
print("Model num labels:", model.num_labels) |
|
|
|
|
|
logging.info("Read train dataset") |
|
|
|
embedding_model = SentenceTransformer("sentence-transformers/static-retrieval-mrl-en-v1") |
|
|
|
full_dataset = load_dataset("sentence-transformers/natural-questions", split=f"train") |
|
dataset_dict = full_dataset.train_test_split(test_size=1_000, seed=12) |
|
train_dataset = dataset_dict["train"] |
|
eval_dataset = dataset_dict["test"] |
|
|
|
|
|
hard_eval_dataset = mine_hard_negatives( |
|
eval_dataset, |
|
embedding_model, |
|
corpus=full_dataset["answer"], |
|
num_negatives=30, |
|
batch_size=256, |
|
positive_among_negatives=True, |
|
as_triplets=False, |
|
|
|
use_faiss=True, |
|
) |
|
print(hard_eval_dataset) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hard_train_dataset = mine_hard_negatives( |
|
train_dataset, |
|
embedding_model, |
|
num_negatives=5, |
|
margin=0, |
|
range_min=0, |
|
range_max=100, |
|
sampling_strategy="top", |
|
batch_size=256, |
|
as_triplets=False, |
|
use_faiss=True, |
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def mapper(batch): |
|
batch_size = len(batch["query"]) |
|
num_negatives = len(batch) - 2 |
|
num_candidates = len(batch) - 1 |
|
return { |
|
"query": batch["query"] * num_candidates, |
|
"response": sum(list(batch.values())[1:], []), |
|
"label": [1] * batch_size + [0] * num_negatives * batch_size, |
|
} |
|
|
|
hard_train_dataset = hard_train_dataset.map(mapper, batched=True, remove_columns=hard_train_dataset.column_names) |
|
eval_dataset = eval_dataset.map(mapper, batched=True, remove_columns=eval_dataset.column_names) |
|
|
|
|
|
loss = BinaryCrossEntropyLoss(model=model, pos_weight=torch.tensor(5)) |
|
|
|
|
|
reranking_evaluator = CERerankingEvaluator( |
|
samples=[ |
|
{ |
|
"query": sample["query"], |
|
"positive": [sample["answer"]], |
|
"negative": [sample[column_name] for column_name in hard_eval_dataset.column_names[2:]], |
|
} |
|
for sample in hard_eval_dataset |
|
], |
|
batch_size=train_batch_size, |
|
negatives_are_ranked=True, |
|
name="nq-dev", |
|
) |
|
nano_beir_evaluator = CENanoBEIREvaluator( |
|
dataset_names=["msmarco", "nfcorpus", "nq"], |
|
batch_size=train_batch_size, |
|
) |
|
evaluator = SequentialEvaluator([reranking_evaluator, nano_beir_evaluator]) |
|
evaluator(model) |
|
|
|
|
|
short_model_name = model_name if "/" not in model_name else model_name.split("/")[-1] |
|
run_name = f"reranker-{short_model_name}-nq-bce-static-retriever-hardest" |
|
args = CrossEncoderTrainingArguments( |
|
|
|
output_dir=f"models/{run_name}", |
|
|
|
num_train_epochs=num_epochs, |
|
per_device_train_batch_size=train_batch_size, |
|
per_device_eval_batch_size=train_batch_size, |
|
learning_rate=2e-5, |
|
warmup_ratio=0.1, |
|
fp16=False, |
|
bf16=True, |
|
dataloader_num_workers=4, |
|
|
|
load_best_model_at_end=True, |
|
metric_for_best_model="eval_nq-dev_ndcg@10", |
|
|
|
eval_strategy="steps", |
|
eval_steps=1000, |
|
save_strategy="steps", |
|
save_steps=1000, |
|
save_total_limit=2, |
|
logging_steps=200, |
|
logging_first_step=True, |
|
run_name=run_name, |
|
seed=12, |
|
) |
|
|
|
|
|
trainer = CrossEncoderTrainer( |
|
model=model, |
|
args=args, |
|
train_dataset=hard_train_dataset, |
|
eval_dataset=eval_dataset, |
|
loss=loss, |
|
evaluator=evaluator, |
|
) |
|
trainer.train() |
|
|
|
|
|
evaluator(model) |
|
|
|
|
|
final_output_dir = f"models/{run_name}/final" |
|
model.save_pretrained(final_output_dir) |
|
|
|
|
|
|
|
try: |
|
model.push_to_hub(run_name) |
|
except Exception: |
|
logging.error( |
|
f"Error uploading model to the Hugging Face Hub:\n{traceback.format_exc()}To upload it manually, you can run " |
|
f"`huggingface-cli login`, followed by loading the model using `model = CrossEncoder({final_output_dir!r})` " |
|
f"and saving it using `model.push_to_hub('{run_name}')`." |
|
) |
|
|
|
|
|
if __name__ == "__main__": |
|
main() |
|
|