dialectica / scripts /metrics.py
Kattine
Phase 4b: DistilBERT in-domain 0.952, OOD 0.916, degradation halved vs LogReg
25cfe3f
Raw
History Blame Contribute Delete
3.54 kB
"""Shared evaluation metrics for cognitive-level classification.
Used by both classical and deep models. Reports accuracy, macro-F1, and
Quadratic Weighted Kappa (QWK), which accounts for the ordinal label structure.
"""
import os
import matplotlib
matplotlib.use("Agg") # headless backend, safe for servers
import matplotlib.pyplot as plt
import numpy as np
from sklearn.metrics import (
accuracy_score,
cohen_kappa_score,
confusion_matrix,
f1_score,
precision_recall_fscore_support,
)
# Ordered class labels, lowest to highest cognitive level.
ORDERED_LABELS = ["Surface", "Mechanistic", "Critical"]
def _to_ordinal(labels):
"""Map class names to ordinal ranks."""
index = {label: rank for rank, label in enumerate(ORDERED_LABELS)}
return [index[label] for label in labels]
def compute_metrics(y_true, y_pred):
"""Compute accuracy, macro-F1, QWK, and per-class scores."""
accuracy = accuracy_score(y_true, y_pred)
macro_f1 = f1_score(y_true, y_pred, labels=ORDERED_LABELS, average="macro")
qwk = cohen_kappa_score(
_to_ordinal(y_true), _to_ordinal(y_pred), weights="quadratic"
)
precision, recall, f1, support = precision_recall_fscore_support(
y_true, y_pred, labels=ORDERED_LABELS, zero_division=0
)
per_class = {}
for i, label in enumerate(ORDERED_LABELS):
per_class[label] = {
"precision": float(precision[i]),
"recall": float(recall[i]),
"f1": float(f1[i]),
"support": int(support[i]),
}
matrix = confusion_matrix(y_true, y_pred, labels=ORDERED_LABELS)
return {
"accuracy": float(accuracy),
"macro_f1": float(macro_f1),
"qwk": float(qwk),
"per_class": per_class,
"confusion_matrix": matrix.tolist(),
}
def print_metrics(name, metrics):
"""Print headline metrics and per-class scores."""
print(f"\n{name}")
print(f" accuracy : {metrics['accuracy']:.3f}")
print(f" macro-F1 : {metrics['macro_f1']:.3f}")
print(f" QWK : {metrics['qwk']:.3f}")
print(f" {'class':12s} {'prec':>6s} {'rec':>6s} {'f1':>6s} {'n':>6s}")
for label in ORDERED_LABELS:
scores = metrics["per_class"][label]
print(f" {label:12s} {scores['precision']:6.3f} "
f"{scores['recall']:6.3f} {scores['f1']:6.3f} {scores['support']:6d}")
def plot_confusion_matrix(metrics, title, save_path):
"""Save a confusion matrix plot to disk."""
matrix = np.array(metrics["confusion_matrix"])
os.makedirs(os.path.dirname(save_path), exist_ok=True)
figure, axis = plt.subplots(figsize=(4.5, 4))
image = axis.imshow(matrix, cmap="Blues")
axis.set_xticks(range(len(ORDERED_LABELS)))
axis.set_yticks(range(len(ORDERED_LABELS)))
axis.set_xticklabels(ORDERED_LABELS, rotation=30, ha="right")
axis.set_yticklabels(ORDERED_LABELS)
axis.set_xlabel("Predicted")
axis.set_ylabel("True")
axis.set_title(title)
threshold = matrix.max() / 2.0 if matrix.max() > 0 else 0.5
for row in range(matrix.shape[0]):
for col in range(matrix.shape[1]):
axis.text(
col, row, int(matrix[row, col]),
ha="center", va="center",
color="white" if matrix[row, col] > threshold else "black",
)
figure.colorbar(image, ax=axis, fraction=0.046, pad=0.04)
figure.tight_layout()
figure.savefig(save_path, dpi=150)
plt.close(figure)
print(f" saved confusion matrix -> {save_path}")