| """Small deterministic before/after evaluator for causal language models.""" |
| from __future__ import annotations |
|
|
| import math |
| from dataclasses import dataclass |
| from typing import Iterable, Mapping, Optional |
|
|
| import torch |
|
|
|
|
| @dataclass(frozen=True) |
| class LossMetrics: |
| nll: float |
| perplexity: float |
| predicted_tokens: int |
|
|
|
|
| @torch.inference_mode() |
| def evaluate_causal_lm( |
| model, |
| batches: Iterable[Mapping[str, torch.Tensor]], |
| *, |
| device: Optional[torch.device | str] = None, |
| ) -> LossMetrics: |
| """Evaluate an HF-style causal LM on already-tokenized frozen batches.""" |
| total_nll = 0.0 |
| total_tokens = 0 |
| was_training = model.training |
| model.eval() |
| try: |
| for original in batches: |
| batch = { |
| key: value.to(device) if device is not None else value |
| for key, value in original.items() |
| } |
| labels = batch.get("labels") |
| if labels is None: |
| labels = batch["input_ids"].clone() |
| batch["labels"] = labels |
| predicted = labels[..., 1:] |
| token_count = int((predicted != -100).sum().item()) |
| if token_count == 0: |
| continue |
| output = model(**batch) |
| loss = output.loss if hasattr(output, "loss") else output[0] |
| total_nll += float(loss.detach().float().item()) * token_count |
| total_tokens += token_count |
| finally: |
| model.train(was_training) |
| if total_tokens == 0: |
| raise ValueError("evaluation batches contain no predicted tokens") |
| mean_nll = total_nll / total_tokens |
| return LossMetrics(mean_nll, math.exp(mean_nll), total_tokens) |
|
|