Spaces:
Sleeping
Sleeping
| """ | |
| OpenMind Evaluation Suite. | |
| Evaluates language models on standard benchmarks: | |
| - Perplexity on held-out data | |
| - HellaSwag (commonsense reasoning) | |
| - ARC-Easy / ARC-Challenge (science QA) | |
| - TruthfulQA (truthfulness) | |
| - MMLU subset (multitask knowledge) | |
| Results are exported as JSON for comparison. | |
| """ | |
| import os | |
| import sys | |
| import json | |
| import time | |
| import argparse | |
| from pathlib import Path | |
| from datetime import datetime | |
| import torch | |
| import torch.nn.functional as F | |
| import numpy as np | |
| from tqdm import tqdm | |
| sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent)) | |
| from src.models.modeling_openmind import OpenMindModel | |
| from src.data.tokenizer import BPETokenizer | |
| # βββ Perplexity Evaluation ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def compute_perplexity( | |
| model: OpenMindModel, | |
| tokenizer: BPETokenizer, | |
| eval_data_path: str = None, | |
| eval_text: str = None, | |
| max_samples: int = 1000, | |
| max_seq_len: int = 2048, | |
| device: str = "cpu", | |
| ) -> float: | |
| """ | |
| Compute perplexity on evaluation data. | |
| Args: | |
| model: The language model | |
| tokenizer: BPE tokenizer | |
| eval_data_path: Path to .bin file or text file | |
| eval_text: Raw text to evaluate on (alternative to file) | |
| max_samples: Maximum number of sequences to evaluate | |
| max_seq_len: Sequence length | |
| device: Device to run on | |
| Returns: | |
| Perplexity score (lower is better) | |
| """ | |
| model.eval() | |
| total_loss = 0.0 | |
| total_tokens = 0 | |
| if eval_data_path and eval_data_path.endswith(".bin"): | |
| # Memory-mapped binary | |
| data = np.memmap(eval_data_path, dtype=np.uint16, mode="r") | |
| num_sequences = min(len(data) // max_seq_len, max_samples) | |
| with torch.no_grad(): | |
| for i in tqdm(range(num_sequences), desc="Computing perplexity"): | |
| start = i * max_seq_len | |
| end = start + max_seq_len | |
| tokens = torch.tensor(data[start:end].astype(np.int64), dtype=torch.long) | |
| tokens = tokens.unsqueeze(0).to(device) | |
| outputs = model(tokens, labels=tokens) | |
| total_loss += outputs["loss"].item() * (max_seq_len - 1) | |
| total_tokens += max_seq_len - 1 | |
| elif eval_text: | |
| # Tokenize raw text | |
| token_ids = tokenizer.encode(eval_text) | |
| num_sequences = min(len(token_ids) // max_seq_len, max_samples) | |
| with torch.no_grad(): | |
| for i in tqdm(range(num_sequences), desc="Computing perplexity"): | |
| start = i * max_seq_len | |
| end = start + max_seq_len | |
| tokens = torch.tensor(token_ids[start:end], dtype=torch.long) | |
| tokens = tokens.unsqueeze(0).to(device) | |
| outputs = model(tokens, labels=tokens) | |
| total_loss += outputs["loss"].item() * (max_seq_len - 1) | |
| total_tokens += max_seq_len - 1 | |
| if total_tokens == 0: | |
| return float("inf") | |
| avg_loss = total_loss / total_tokens | |
| perplexity = np.exp(avg_loss) | |
| return perplexity | |
| # βββ Multiple Choice Evaluation ββββββββββββββββββββββββββββββββββββββββββββββ | |
| def evaluate_multiple_choice( | |
| model: OpenMindModel, | |
| tokenizer: BPETokenizer, | |
| examples: list[dict], | |
| num_fewshot: int = 0, | |
| device: str = "cpu", | |
| ) -> dict: | |
| """ | |
| Evaluate model on multiple-choice questions. | |
| Each example should have: | |
| - "context": The question/context text | |
| - "choices": List of possible completions | |
| - "answer": Index of correct answer (0-based) | |
| Args: | |
| model: Language model | |
| tokenizer: Tokenizer | |
| examples: List of MC examples | |
| num_fewshot: Number of few-shot examples to prepend | |
| device: Device | |
| Returns: | |
| Dictionary with accuracy and per-example results | |
| """ | |
| model.eval() | |
| correct = 0 | |
| total = 0 | |
| results = [] | |
| for example in tqdm(examples, desc="Evaluating"): | |
| context = example["context"] | |
| choices = example["choices"] | |
| answer = example["answer"] | |
| # Score each choice by computing log-likelihood | |
| scores = [] | |
| for choice in choices: | |
| full_text = context + " " + choice | |
| token_ids = tokenizer.encode(full_text) | |
| context_ids = tokenizer.encode(context) | |
| input_tensor = torch.tensor([token_ids], dtype=torch.long).to(device) | |
| with torch.no_grad(): | |
| outputs = model(input_tensor) | |
| logits = outputs["logits"] | |
| # Compute log probability of the choice tokens only | |
| choice_start = len(context_ids) | |
| log_probs = F.log_softmax(logits[0, choice_start - 1: -1], dim=-1) | |
| choice_token_ids = token_ids[choice_start:] | |
| score = sum( | |
| log_probs[i, tid].item() | |
| for i, tid in enumerate(choice_token_ids) | |
| if i < len(log_probs) | |
| ) | |
| # Length-normalize | |
| score /= max(len(choice_token_ids), 1) | |
| scores.append(score) | |
| predicted = int(np.argmax(scores)) | |
| is_correct = predicted == answer | |
| correct += int(is_correct) | |
| total += 1 | |
| results.append({ | |
| "context": context[:100] + "...", | |
| "predicted": predicted, | |
| "answer": answer, | |
| "correct": is_correct, | |
| "scores": scores, | |
| }) | |
| accuracy = correct / max(total, 1) | |
| return { | |
| "accuracy": accuracy, | |
| "correct": correct, | |
| "total": total, | |
| "results": results, | |
| } | |
| # βββ Benchmark Loaders βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def load_hellaswag(max_examples: int = 1000) -> list[dict]: | |
| """Load HellaSwag benchmark from Hugging Face.""" | |
| try: | |
| from datasets import load_dataset | |
| ds = load_dataset("Rowan/hellaswag", split="validation") | |
| examples = [] | |
| for i, item in enumerate(ds): | |
| if i >= max_examples: | |
| break | |
| examples.append({ | |
| "context": item["ctx"], | |
| "choices": item["endings"], | |
| "answer": int(item["label"]), | |
| }) | |
| return examples | |
| except Exception as e: | |
| print(f"Could not load HellaSwag: {e}") | |
| return [] | |
| def load_arc(difficulty: str = "easy", max_examples: int = 1000) -> list[dict]: | |
| """Load ARC benchmark from Hugging Face.""" | |
| try: | |
| from datasets import load_dataset | |
| subset = "ARC-Easy" if difficulty == "easy" else "ARC-Challenge" | |
| ds = load_dataset("allenai/ai2_arc", subset, split="test") | |
| examples = [] | |
| for i, item in enumerate(ds): | |
| if i >= max_examples: | |
| break | |
| choices = item["choices"] | |
| choice_texts = choices["text"] | |
| answer_key = item["answerKey"] | |
| # Convert answer key to index | |
| labels = choices["label"] | |
| answer_idx = labels.index(answer_key) if answer_key in labels else 0 | |
| examples.append({ | |
| "context": item["question"], | |
| "choices": choice_texts, | |
| "answer": answer_idx, | |
| }) | |
| return examples | |
| except Exception as e: | |
| print(f"Could not load ARC: {e}") | |
| return [] | |
| def load_truthfulqa(max_examples: int = 500) -> list[dict]: | |
| """Load TruthfulQA benchmark.""" | |
| try: | |
| from datasets import load_dataset | |
| ds = load_dataset("truthful_qa", "multiple_choice", split="validation") | |
| examples = [] | |
| for i, item in enumerate(ds): | |
| if i >= max_examples: | |
| break | |
| mc = item["mc1_targets"] | |
| choices = mc["choices"] | |
| labels = mc["labels"] | |
| answer_idx = labels.index(1) if 1 in labels else 0 | |
| examples.append({ | |
| "context": item["question"], | |
| "choices": choices, | |
| "answer": answer_idx, | |
| }) | |
| return examples | |
| except Exception as e: | |
| print(f"Could not load TruthfulQA: {e}") | |
| return [] | |
| # βββ Full Benchmark Suite βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| BENCHMARK_LOADERS = { | |
| "hellaswag": lambda n: load_hellaswag(n), | |
| "arc_easy": lambda n: load_arc("easy", n), | |
| "arc_challenge": lambda n: load_arc("challenge", n), | |
| "truthfulqa": lambda n: load_truthfulqa(n), | |
| } | |
| def run_benchmark_suite( | |
| model_path: str, | |
| tasks: list[str] = None, | |
| num_fewshot: int = 0, | |
| max_examples: int = 500, | |
| output_dir: str = "results", | |
| device: str = None, | |
| ) -> dict: | |
| """ | |
| Run the full evaluation benchmark suite. | |
| Args: | |
| model_path: Path to model directory | |
| tasks: List of benchmark names to run | |
| num_fewshot: Number of few-shot examples | |
| max_examples: Max examples per benchmark | |
| output_dir: Directory to save results | |
| device: Device to use | |
| Returns: | |
| Dictionary with all results | |
| """ | |
| if device is None: | |
| device = "cuda" if torch.cuda.is_available() else "cpu" | |
| if tasks is None: | |
| tasks = list(BENCHMARK_LOADERS.keys()) | |
| print(f"Loading model from {model_path}...") | |
| model = OpenMindModel.from_pretrained(model_path, device=device) | |
| model.eval() | |
| # Try to load tokenizer | |
| tokenizer_path = os.path.join(model_path, "tokenizer") | |
| if os.path.exists(tokenizer_path): | |
| tokenizer = BPETokenizer.load(tokenizer_path) | |
| else: | |
| tokenizer = BPETokenizer(vocab_size=32000) | |
| print("Warning: Using untrained tokenizer!") | |
| all_results = { | |
| "model": model_path, | |
| "timestamp": datetime.now().isoformat(), | |
| "num_fewshot": num_fewshot, | |
| "device": device, | |
| "tasks": {}, | |
| } | |
| for task_name in tasks: | |
| if task_name not in BENCHMARK_LOADERS: | |
| print(f"Unknown task: {task_name}, skipping") | |
| continue | |
| print(f"\n{'='*60}") | |
| print(f"Running: {task_name}") | |
| print(f"{'='*60}") | |
| examples = BENCHMARK_LOADERS[task_name](max_examples) | |
| if not examples: | |
| print(f"No examples loaded for {task_name}") | |
| continue | |
| result = evaluate_multiple_choice( | |
| model, tokenizer, examples, num_fewshot, device | |
| ) | |
| all_results["tasks"][task_name] = { | |
| "accuracy": result["accuracy"], | |
| "correct": result["correct"], | |
| "total": result["total"], | |
| } | |
| print(f" Accuracy: {result['accuracy']:.2%} ({result['correct']}/{result['total']})") | |
| # Save results | |
| os.makedirs(output_dir, exist_ok=True) | |
| model_name = Path(model_path).name | |
| timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") | |
| result_path = os.path.join(output_dir, f"eval_{model_name}_{timestamp}.json") | |
| with open(result_path, "w") as f: | |
| json.dump(all_results, f, indent=2, default=str) | |
| print(f"\n{'='*60}") | |
| print("EVALUATION SUMMARY") | |
| print(f"{'='*60}") | |
| for task, res in all_results["tasks"].items(): | |
| print(f" {task:20s}: {res['accuracy']:.2%}") | |
| print(f"\nResults saved to: {result_path}") | |
| return all_results | |
| if __name__ == "__main__": | |
| parser = argparse.ArgumentParser(description="OpenMind Evaluation") | |
| parser.add_argument("--model", type=str, required=True, help="Path to model directory") | |
| parser.add_argument("--tasks", type=str, nargs="+", default=None) | |
| parser.add_argument("--fewshot", type=int, default=0) | |
| parser.add_argument("--max-examples", type=int, default=500) | |
| parser.add_argument("--output", type=str, default="results") | |
| parser.add_argument("--device", type=str, default=None) | |
| args = parser.parse_args() | |
| run_benchmark_suite( | |
| args.model, args.tasks, args.fewshot, args.max_examples, args.output, args.device | |
| ) | |