| """ |
| data/prepare_dataset.py |
| |
| Downloads a HuggingFace dataset, cleans it, formats it into |
| instruction-tuning format, and saves train/val splits. |
| |
| Run: python -m data.prepare_dataset |
| """ |
|
|
| import json |
| import logging |
| from pathlib import Path |
|
|
| from datasets import load_dataset, DatasetDict |
| from sklearn.model_selection import train_test_split |
|
|
| import sys |
| sys.path.append(str(Path(__file__).parent.parent)) |
| from config import cfg |
|
|
| logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") |
| log = logging.getLogger(__name__) |
|
|
|
|
| |
| PROMPT_TEMPLATE = """<s>[INST] {instruction} |
| |
| Context: {input} [/INST] {output} </s>""" |
|
|
| PROMPT_TEMPLATE_NO_INPUT = """<s>[INST] {instruction} [/INST] {output} </s>""" |
|
|
|
|
| def format_example(example: dict) -> dict: |
| """Convert a raw dataset example to instruction-tuning format.""" |
| instruction = example.get("instruction", "").strip() |
| context = example.get("input", "").strip() |
| output = example.get("output", "").strip() |
|
|
| if not instruction or not output: |
| return None |
|
|
| if context: |
| text = PROMPT_TEMPLATE.format( |
| instruction=instruction, |
| input=context, |
| output=output, |
| ) |
| else: |
| text = PROMPT_TEMPLATE_NO_INPUT.format( |
| instruction=instruction, |
| output=output, |
| ) |
|
|
| return {"text": text, "instruction": instruction, "context": context, "output": output} |
|
|
|
|
| def clean_text(text: str) -> str: |
| """Basic text cleaning.""" |
| text = text.strip() |
| text = " ".join(text.split()) |
| return text |
|
|
|
|
| def prepare_dataset(): |
| """Main pipeline: download β clean β format β split β save.""" |
| cfg.ensure_dirs() |
| log.info(f"Loading dataset: {cfg.data.dataset_name}") |
|
|
| |
| raw = load_dataset( |
| cfg.data.dataset_name, |
| split=cfg.data.dataset_split, |
| trust_remote_code=True, |
| ) |
| log.info(f"Raw dataset size: {len(raw):,} examples") |
|
|
| |
| formatted = [] |
| skipped = 0 |
| for example in raw: |
| |
| normalized = { |
| "instruction": clean_text(example.get("instruction", example.get("question", ""))), |
| "input": clean_text(example.get("input", example.get("context", ""))), |
| "output": clean_text(example.get("output", example.get("answer", ""))), |
| } |
| result = format_example(normalized) |
| if result: |
| formatted.append(result) |
| else: |
| skipped += 1 |
|
|
| log.info(f"Formatted: {len(formatted):,} | Skipped (empty): {skipped:,}") |
|
|
| |
| train_data, val_data = train_test_split( |
| formatted, |
| test_size=cfg.data.val_size, |
| random_state=cfg.data.seed, |
| ) |
| log.info(f"Train: {len(train_data):,} | Val: {len(val_data):,}") |
|
|
| |
| out_dir = Path(cfg.data.processed_data_dir) |
|
|
| for split_name, split_data in [("train", train_data), ("val", val_data)]: |
| path = out_dir / f"{split_name}.jsonl" |
| with open(path, "w") as f: |
| for item in split_data: |
| f.write(json.dumps(item) + "\n") |
| log.info(f"Saved {split_name} β {path}") |
|
|
| |
| meta = { |
| "dataset": cfg.data.dataset_name, |
| "total_examples": len(formatted), |
| "train_size": len(train_data), |
| "val_size": len(val_data), |
| "prompt_format": "mistral-instruct", |
| } |
| with open(out_dir / "metadata.json", "w") as f: |
| json.dump(meta, f, indent=2) |
|
|
| log.info("β
Dataset preparation complete!") |
| return train_data, val_data |
|
|
|
|
| def load_processed_dataset() -> DatasetDict: |
| """Load already-processed JSONL files as a HuggingFace DatasetDict.""" |
| from datasets import Dataset |
| out_dir = Path(cfg.data.processed_data_dir) |
|
|
| splits = {} |
| for split in ["train", "val"]: |
| path = out_dir / f"{split}.jsonl" |
| if not path.exists(): |
| raise FileNotFoundError(f"Run prepare_dataset() first. Missing: {path}") |
| data = [json.loads(line) for line in open(path)] |
| splits[split] = Dataset.from_list(data) |
|
|
| return DatasetDict(splits) |
|
|
|
|
| if __name__ == "__main__": |
| prepare_dataset() |
|
|