Instructions to use echoforest/naver-psy-classifier-gemma4-12b with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- PEFT
How to use echoforest/naver-psy-classifier-gemma4-12b with PEFT:
Task type is invalid.
- Notebooks
- Google Colab
- Kaggle
Download code/metrics.py from echoforest/naver-psy-classifier-gemma4-12b: direct link, hf CLI and curl.
- Browser
- Download file 9.79 kB
-
https://huggingface.co/echoforest/naver-psy-classifier-gemma4-12b/resolve/main/code/metrics.py
- Command line
-
hf download hf://echoforest/naver-psy-classifier-gemma4-12b/code/metrics.py
-
curl -L -o metrics.py https://huggingface.co/echoforest/naver-psy-classifier-gemma4-12b/resolve/main/code/metrics.py
9.79 kB
| """Evaluation metrics (pure Python, standard library only). Spec: docs/classifier-plan.md §5.1. | |
| Labels are option strings; a distribution is a dict option -> probability in taxonomy option order. Conventions: | |
| - **Masked / null gold** is skipped by every per-field metric (``None`` in the gold list). | |
| - **F1 of a class** = 2TP / (2TP + FP + FN); precision is None without predictions, recall None without support. | |
| - **Macro-F1** = unweighted mean F1 over the field's options, leaving out options with no support and no | |
| prediction (F1 undefined) and the options in ``exclude`` (the taxonomy's ``macro_f1_exclude``; nsb-v2: | |
| emotion_fine ``mixed``, still in ``per_class``); a missed or only falsely predicted option counts as 0. | |
| - **Cohen's kappa** = (p_o - p_e) / (1 - p_e) over paired non-null labels; 1.0 when p_e == 1 and p_o == 1. | |
| - **Top-label ECE**: 15 equal-width bins on the top-label probability, bin = min(floor(p x 15), 14); | |
| sum over non-empty bins of (bin size / n) x |accuracy - mean confidence|. Class-wise ECE: the same on p(class). | |
| - **Log-loss**: mean -ln(max(p(gold), 1e-12)). | |
| - **Weighted shares** (§2.3): predicted share = sum(p(class) / inclusion_prob) / sum(1 / inclusion_prob) against | |
| the teacher share with the same weights; a missing inclusion_prob counts as 1. | |
| - **Bootstrap**: author-cluster percentile bootstrap: sorted clusters, ``random.Random(seed)``, each replicate draws | |
| as many clusters as there are with replacement; ``fn(ids)`` returning None is skipped; interval | |
| [v[floor(a/2 (m-1))], v[floor((1-a/2)(m-1))]] over the m sorted kept values. | |
| """ | |
| from __future__ import annotations | |
| import math | |
| import random | |
| from collections import Counter, defaultdict | |
| from typing import Callable, Hashable, Iterable, Mapping, Sequence | |
| VERSION = "nsb-metrics-v1" | |
| FLOOR = 1e-12 | |
| BINS = 15 | |
| REPS = 1000 | |
| LEVEL = 0.95 | |
| SEED = "nsb-eval-20261017" | |
| def top(dist: Mapping[str, float]) -> str: | |
| """Highest-probability option; ties go to the earlier option.""" | |
| best = None | |
| for k, v in dist.items(): | |
| if best is None or v > dist[best]: | |
| best = k | |
| return best | |
| def _pairs(gold: Sequence, other: Sequence) -> list[tuple]: | |
| gold, other = list(gold), list(other) | |
| if len(gold) != len(other): | |
| raise ValueError(f"lengths differ: {len(gold)}, {len(other)}") | |
| return [(g, o) for g, o in zip(gold, other) if g is not None and o is not None] | |
| def _ratio(a: float, b: float) -> float | None: | |
| return a / b if b else None | |
| def _bin(p: float, bins: int) -> int: | |
| return min(max(int(math.floor(p * bins)), 0), bins - 1) | |
| def _ece(points: list[tuple[float, float]], bins: int) -> float | None: | |
| if not points: | |
| return None | |
| total: dict[int, list[float]] = defaultdict(lambda: [0, 0.0, 0.0]) | |
| for conf, hit in points: | |
| b = total[_bin(conf, bins)] | |
| b[0] += 1 | |
| b[1] += conf | |
| b[2] += hit | |
| n = len(points) | |
| return sum(c / n * abs(acc / c - s / c) for c, s, acc in total.values()) | |
| # ---------------------------------------------------------------------- per field | |
| def accuracy(gold, pred) -> float | None: | |
| pairs = _pairs(gold, pred) | |
| return _ratio(sum(1 for g, p in pairs if g == p), len(pairs)) | |
| def per_class(gold, pred, classes) -> dict: | |
| """{class: {support, predicted, tp, precision, recall, f1}} for every given class.""" | |
| classes = list(classes) | |
| known = set(classes) | |
| hits, support, predicted = Counter(), Counter(), Counter() | |
| for g, p in _pairs(gold, pred): | |
| if g not in known or p not in known: | |
| raise ValueError(f"label outside the classes: {g!r} / {p!r}") | |
| support[g] += 1 | |
| predicted[p] += 1 | |
| if g == p: | |
| hits[g] += 1 | |
| out = {} | |
| for c in classes: | |
| tp, s, n = hits[c], support[c], predicted[c] | |
| out[c] = {"support": s, "predicted": n, "tp": tp, "precision": _ratio(tp, n), "recall": _ratio(tp, s), | |
| "f1": _ratio(2 * tp, n + s)} | |
| return out | |
| def _macro(pc: Mapping[str, dict], exclude: Iterable[str] = ()) -> float | None: | |
| skip = set(exclude) | |
| scores = [v["f1"] for c, v in pc.items() if v["f1"] is not None and c not in skip] | |
| return sum(scores) / len(scores) if scores else None | |
| def macro_f1(gold, pred, classes, exclude: Iterable[str] = ()) -> float | None: | |
| return _macro(per_class(gold, pred, classes), exclude) | |
| def kappa(a, b) -> float | None: | |
| """Cohen's kappa of two label lists (pairs with a None on either side are skipped).""" | |
| pairs = _pairs(a, b) | |
| n = len(pairs) | |
| if not n: | |
| return None | |
| po = sum(1 for x, y in pairs if x == y) / n | |
| ca, cb = Counter(x for x, _ in pairs), Counter(y for _, y in pairs) | |
| pe = sum(ca[k] * cb[k] for k in ca) / (n * n) | |
| if pe >= 1.0: | |
| return 1.0 if po >= 1.0 else 0.0 | |
| return (po - pe) / (1.0 - pe) | |
| def log_loss(gold, dists) -> float | None: | |
| pairs = _pairs(gold, dists) | |
| if not pairs: | |
| return None | |
| return sum(-math.log(max(d[g], FLOOR)) for g, d in pairs) / len(pairs) | |
| def ece(gold, dists, bins: int = BINS) -> float | None: | |
| """Top-label expected calibration error.""" | |
| points = [] | |
| for g, d in _pairs(gold, dists): | |
| best = top(d) | |
| points.append((d[best], 1.0 if best == g else 0.0)) | |
| return _ece(points, bins) | |
| def class_ece(gold, dists, cls: str, bins: int = BINS) -> float | None: | |
| return _ece([(d[cls], 1.0 if g == cls else 0.0) for g, d in _pairs(gold, dists)], bins) | |
| def weighted_shares(gold, dists, cls: str, weights=None) -> dict: | |
| """Predicted share (mean p(cls)) and teacher share, both weighted (weights default 1).""" | |
| gold, dists = list(gold), list(dists) | |
| weights = [1.0] * len(gold) if weights is None else [1.0 if w is None else float(w) for w in weights] | |
| num_p = num_g = den = 0.0 | |
| n = 0 | |
| for g, d, w in zip(gold, dists, weights): | |
| if g is None: | |
| continue | |
| num_p += w * d[cls] | |
| num_g += w * (1.0 if g == cls else 0.0) | |
| den += w | |
| n += 1 | |
| pred, obs = _ratio(num_p, den), _ratio(num_g, den) | |
| return {"n": n, "predicted_share": pred, "teacher_share": obs, | |
| "difference": None if pred is None else pred - obs} | |
| def inverse_weights(inclusion_probs: Iterable[float | None]) -> list[float]: | |
| """1 / inclusion_prob (1.0 when missing or not positive).""" | |
| return [1.0 / p if p else 1.0 for p in inclusion_probs] | |
| def key_class(gold, dists, cls: str, weights=None) -> dict: | |
| """Precision, recall, F1 (top label), class-wise ECE and weighted predicted vs teacher share of one class.""" | |
| gold, dists = list(gold), list(dists) | |
| pred = [top(d) for d in dists] | |
| known = {x for x in gold + pred if x is not None} | {cls} | |
| out = dict(per_class(gold, pred, sorted(known))[cls], class_ece=class_ece(gold, dists, cls)) | |
| out.update(weighted_shares(gold, dists, cls, weights)) | |
| return out | |
| def field_summary(gold, dists, classes, macro_exclude: Iterable[str] = ()) -> dict: | |
| """Accuracy, macro-F1 (without ``macro_exclude``), kappa vs teacher, log-loss, top-label ECE and per-class | |
| figures (every class) of one field.""" | |
| gold, dists = list(gold), list(dists) | |
| pred = [top(d) for d in dists] | |
| pc = per_class(gold, pred, classes) | |
| out = {"n": sum(1 for g in gold if g is not None), "masked": sum(1 for g in gold if g is None), | |
| "accuracy": accuracy(gold, pred), "macro_f1": _macro(pc, macro_exclude), | |
| "kappa": kappa(gold, pred), "log_loss": log_loss(gold, dists), "ece": ece(gold, dists), | |
| "per_class": pc} | |
| if macro_exclude: | |
| out["macro_f1_excludes"] = sorted(macro_exclude) | |
| out["macro_f1_all"] = _macro(pc) | |
| return out | |
| # ---------------------------------------------------------------------- series | |
| def mae(xs: Sequence[float], ys: Sequence[float]) -> float | None: | |
| if len(xs) != len(ys): | |
| raise ValueError("lengths differ") | |
| return sum(abs(x - y) for x, y in zip(xs, ys)) / len(xs) if xs else None | |
| def pearson(xs: Sequence[float], ys: Sequence[float]) -> float | None: | |
| """Pearson r; None with fewer than 2 points or a constant series.""" | |
| n = len(xs) | |
| if n != len(ys): | |
| raise ValueError("lengths differ") | |
| if n < 2: | |
| return None | |
| mx, my = sum(xs) / n, sum(ys) / n | |
| sxy = sum((x - mx) * (y - my) for x, y in zip(xs, ys)) | |
| sxx = sum((x - mx) ** 2 for x in xs) | |
| syy = sum((y - my) ** 2 for y in ys) | |
| if sxx <= 0 or syy <= 0: | |
| return None | |
| return sxy / math.sqrt(sxx * syy) | |
| # ---------------------------------------------------------------------- bootstrap | |
| def percentile_interval(values: Sequence[float], level: float = LEVEL) -> list[float] | None: | |
| if not values: | |
| return None | |
| values = sorted(values) | |
| low = round((1.0 - level) / 2, 12) | |
| high = round(1.0 - low, 12) | |
| m = len(values) - 1 | |
| return [values[int(low * m)], values[int(high * m)]] | |
| def bootstrap(ids: Iterable[Hashable], group_of: Mapping | Callable, fn: Callable[[list], float | None], | |
| reps: int = REPS, seed=SEED, level: float = LEVEL) -> dict: | |
| """Author-cluster percentile bootstrap of fn(ids). ``group_of``: dict or callable id -> cluster.""" | |
| get = group_of.__getitem__ if isinstance(group_of, Mapping) else group_of | |
| groups: dict = defaultdict(list) | |
| for i in ids: | |
| groups[get(i)].append(i) | |
| keys, rng, values = sorted(groups), random.Random(str(seed)), [] | |
| for _ in range(reps): | |
| draw = [i for k in (rng.choice(keys) for _ in keys) for i in groups[k]] if keys else [] | |
| v = fn(draw) | |
| if v is not None: | |
| values.append(v) | |
| return {"interval": percentile_interval(values, level), "level": level, "reps": reps, "reps_used": len(values), | |
| "groups": len(keys), "seed": str(seed)} | |