""" scripts/fine_tune.py Fix #8 — Fine-tunes BERT on the Amazon QA dataset for e-commerce domain accuracy. Usage: pip install datasets accelerate python scripts/fine_tune.py --output_dir models/bert-amazon-qa --epochs 3 The fine-tuned model is saved locally and can be used in QAModel by changing: EN_MODEL = "models/bert-amazon-qa" """ import argparse import logging from pathlib import Path logging.basicConfig(level=logging.INFO) logger = logging.getLogger("fine_tuner") BASE_MODEL = "deepset/bert-base-cased-squad2" # start from already-fine-tuned checkpoint DATASET_ID = "amazon_qa" # HuggingFace Hub dataset def parse_args(): p = argparse.ArgumentParser() p.add_argument("--output_dir", default="models/bert-amazon-qa") p.add_argument("--epochs", type=int, default=3) p.add_argument("--batch_size", type=int, default=16) p.add_argument("--lr", type=float, default=3e-5) p.add_argument("--max_samples",type=int, default=50_000, help="Cap training samples to keep training time manageable") p.add_argument("--max_length", type=int, default=384) p.add_argument("--doc_stride", type=int, default=128) return p.parse_args() def load_amazon_qa(max_samples: int): """ Loads Amazon QA dataset and converts it to SQuAD-format dicts. Amazon QA has: questionText, answerText, asin, category. We use the answer as the span and the product description as context. """ from datasets import load_dataset logger.info("Loading %s dataset (this may take a few minutes)…", DATASET_ID) ds = load_dataset(DATASET_ID, split="train", streaming=True) examples = [] for item in ds: if len(examples) >= max_samples: break q = item.get("questionText", "").strip() a = item.get("answerText", "").strip() if not q or not a or len(a) > 200: continue # Use the answer as both context and answer span (extractive style) context = a examples.append({ "id": str(len(examples)), "title": item.get("asin", "product"), "context": context, "question": q, "answers": { "text": [a], "answer_start": [0], } }) logger.info("Loaded %d training examples", len(examples)) return examples def tokenize_and_align(examples, tokenizer, max_length, doc_stride): """Standard SQuAD tokenisation for extractive QA training.""" tokenized = tokenizer( examples["question"], examples["context"], truncation="only_second", max_length=max_length, stride=doc_stride, return_overflowing_tokens=True, return_offsets_mapping=True, padding="max_length", ) sample_mapping = tokenized.pop("overflow_to_sample_mapping") offset_mapping = tokenized.pop("offset_mapping") answers = examples["answers"] start_positions, end_positions = [], [] for i, offsets in enumerate(offset_mapping): sample_idx = sample_mapping[i] answer = answers[sample_idx] start_char = answer["answer_start"][0] end_char = start_char + len(answer["text"][0]) sequence_ids = tokenized.sequence_ids(i) # Find the span of context tokens ctx_start = next((j for j, s in enumerate(sequence_ids) if s == 1), None) ctx_end = next((j for j in range(len(sequence_ids)-1, -1, -1) if sequence_ids[j] == 1), None) if ctx_start is None or ctx_end is None: start_positions.append(0) end_positions.append(0) continue token_start = token_end = 0 for j in range(ctx_start, ctx_end + 1): if offsets[j][0] <= start_char < offsets[j][1]: token_start = j if offsets[j][0] < end_char <= offsets[j][1]: token_end = j start_positions.append(token_start) end_positions.append(token_end) tokenized["start_positions"] = start_positions tokenized["end_positions"] = end_positions return tokenized def fine_tune(args): from transformers import ( AutoTokenizer, AutoModelForQuestionAnswering, TrainingArguments, Trainer, DefaultDataCollator ) from datasets import Dataset import torch output_dir = Path(args.output_dir) output_dir.mkdir(parents=True, exist_ok=True) # Load tokenizer + model logger.info("Loading base model: %s", BASE_MODEL) tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL) model = AutoModelForQuestionAnswering.from_pretrained(BASE_MODEL) # Load data raw_examples = load_amazon_qa(args.max_samples) dataset = Dataset.from_list(raw_examples) split = dataset.train_test_split(test_size=0.05, seed=42) # Tokenise logger.info("Tokenising dataset…") tok_fn = lambda ex: tokenize_and_align(ex, tokenizer, args.max_length, args.doc_stride) train_ds = split["train"].map(tok_fn, batched=True, remove_columns=dataset.column_names) eval_ds = split["test"].map(tok_fn, batched=True, remove_columns=dataset.column_names) # Training arguments training_args = TrainingArguments( output_dir=str(output_dir), num_train_epochs=args.epochs, per_device_train_batch_size=args.batch_size, per_device_eval_batch_size=args.batch_size, learning_rate=args.lr, weight_decay=0.01, warmup_ratio=0.1, evaluation_strategy="epoch", save_strategy="epoch", load_best_model_at_end=True, fp16=torch.cuda.is_available(), logging_steps=100, report_to="none", ) trainer = Trainer( model=model, args=training_args, train_dataset=train_ds, eval_dataset=eval_ds, tokenizer=tokenizer, data_collator=DefaultDataCollator(), ) logger.info("Starting fine-tuning for %d epochs…", args.epochs) trainer.train() trainer.save_model(str(output_dir)) tokenizer.save_pretrained(str(output_dir)) logger.info("Fine-tuned model saved to %s", output_dir) logger.info("Update src/models/qa_model.py: EN_MODEL = '%s'", output_dir) if __name__ == "__main__": args = parse_args() fine_tune(args)