Download code/paya/classification.py from yeabwang/Paya: direct link, hf CLI and curl.
- Browser
- Download file 4.91 kB
-
https://huggingface.co/yeabwang/Paya/resolve/main/code/paya/classification.py
- Command line
-
hf download hf://yeabwang/Paya/code/paya/classification.py
-
curl -L -o classification.py https://huggingface.co/yeabwang/Paya/resolve/main/code/paya/classification.py
4.91 kB
| """Class-sensitive metrics aligned by answer text, with disk-backed exact AUROC.""" | |
| from collections import Counter | |
| import math | |
| import sqlite3 | |
| import tempfile | |
| import numpy as np | |
| def classification_metrics(records, temperature=1.0): | |
| """Score one task; report undefined class AUCs without inventing values. | |
| Candidate order may vary, but every row must contain the same answer set. | |
| Scores are sorted on disk, keeping memory bounded for large evaluations. | |
| """ | |
| if not math.isfinite(temperature) or temperature <= 0: | |
| raise ValueError("Temperature must be finite and positive") | |
| confusion = Counter() | |
| labels, count = None, 0 | |
| with tempfile.TemporaryDirectory(prefix="paya-auc-") as directory: | |
| db = sqlite3.connect(directory + "/scores.sqlite3") | |
| try: | |
| db.executescript("PRAGMA cache_size=-8192; PRAGMA temp_store=FILE; " | |
| "CREATE TABLE scores(label TEXT, score REAL, positive INTEGER)") | |
| pending = [] | |
| for row in records: | |
| criteria = row["criteria"] | |
| current = frozenset(criteria) | |
| if len(current) != len(criteria) or len(current) < 2: | |
| raise ValueError("Need distinct candidate answers") | |
| if labels is None: | |
| labels = current | |
| if current != labels: | |
| raise ValueError("Classification metrics require one consistent task vocabulary") | |
| target = row["target"] | |
| if isinstance(target, bool) or not isinstance(target, int) or not 0 <= target < len(criteria): | |
| raise ValueError("Invalid candidate target") | |
| logits = np.asarray(row["logits"], dtype=np.float64) / temperature | |
| if logits.shape != (len(criteria),) or not np.isfinite(logits).all(): | |
| raise ValueError("Need one finite logit per candidate") | |
| logits -= logits.max() | |
| probabilities = np.exp(logits) | |
| probabilities /= probabilities.sum() | |
| truth = criteria[target] | |
| winner = min(range(len(criteria)), key=lambda i: (-probabilities[i], criteria[i])) | |
| confusion[truth, criteria[winner]] += 1 | |
| pending.extend((answer, float(score), int(answer == truth)) | |
| for answer, score in zip(criteria, probabilities)) | |
| count += 1 | |
| if len(pending) >= 4096: | |
| db.executemany("INSERT INTO scores VALUES (?,?,?)", pending) | |
| pending.clear() | |
| if not count: | |
| raise ValueError("Need at least one prediction") | |
| db.executemany("INSERT INTO scores VALUES (?,?,?)", pending) | |
| db.execute("CREATE INDEX ranked ON scores(label,score)") | |
| per_class = {} | |
| for label in sorted(labels): | |
| support = sum(v for (truth, _), v in confusion.items() if truth == label) | |
| predicted = sum(v for (_, winner), v in confusion.items() if winner == label) | |
| tp = confusion[label, label] | |
| negative = count - support | |
| auc = None | |
| if support and negative: | |
| below, concordant = 0, 0.0 | |
| for _, positives, size in db.execute( | |
| "SELECT score,SUM(positive),COUNT(*) FROM scores WHERE label=? " | |
| "GROUP BY score ORDER BY score", (label,)): | |
| negatives = size - positives | |
| concordant += positives * (below + 0.5 * negatives) | |
| below += negatives | |
| auc = concordant / (support * negative) | |
| per_class[label] = {"support": support, "predicted": predicted, | |
| "correct": tp, "precision": tp / predicted if predicted else 0.0, | |
| "recall": tp / support if support else None, | |
| "f1": 2 * tp / (support + predicted) if support + predicted else 0.0, | |
| "auroc_ovr": auc} | |
| aucs = [v["auroc_ovr"] for v in per_class.values() if v["auroc_ovr"] is not None] | |
| observed = [v for v in per_class.values() if v["support"]] | |
| return {"count": count, "macro_f1": sum(v["f1"] for v in per_class.values()) / len(labels), | |
| "observed_class_macro_recall": sum(v["recall"] for v in observed) / len(observed), | |
| "macro_auroc_ovr": sum(aucs) / len(aucs) if aucs else None, | |
| "auroc_supported_classes": len(aucs), "candidate_classes": len(labels), | |
| "per_class": per_class, | |
| "confusion": {truth: {winner: confusion[truth, winner] for winner in sorted(labels)} | |
| for truth in sorted(labels)}} | |
| finally: | |
| db.close() | |