Avijit070's picture
Upload folder using huggingface_hub
bc51147 verified
Raw
History Blame Contribute Delete
7.6 kB
"""Shared training infrastructure — metrics, leaderboard, artifact I/O, optional 5-fold CV evaluation.
Used by Track A (classical) and Track B (transformer) to ensure identical
data splits, identical metrics, and artifact compatibility.
Cross-validation: Stored in shared.py, used by train_classical.py for
classical model evaluation and OOF prediction generation.
"""
from __future__ import annotations
import hashlib
import json
import pickle
import time
import warnings
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
import numpy as np
from sklearn.exceptions import ConvergenceWarning
from sklearn.metrics import (
classification_report,
confusion_matrix,
roc_auc_score,
)
MODEL_DIR = Path(__file__).resolve().parent
def _ram_usage_mb() -> float:
try:
import psutil
return psutil.Process().memory_info().rss / (1024 * 1024)
except ImportError:
return -1.0
def ram_report(label: str) -> str:
mb = _ram_usage_mb()
return f"{label}: RAM {mb:.0f} MB" if mb >= 0 else f"{label}: RAM N/A"
@dataclass
class EvalMetrics:
model_name: str
accuracy: float
spam_precision: float
spam_recall: float
spam_f1: float
roc_auc: float | None
train_time_seconds: float
support: int
track: str
confusion_matrix: list[list[int]] | None = None
model_size_bytes: int = 0
converged: bool = True
effective_iterations: int | None = None
eval_method: str = "holdout"
def to_dict(self) -> dict[str, Any]:
return {
"track": self.track,
"model_name": self.model_name,
"accuracy": round(self.accuracy, 4),
"spam_precision": round(self.spam_precision, 4),
"spam_recall": round(self.spam_recall, 4),
"spam_f1": round(self.spam_f1, 4),
"roc_auc": round(self.roc_auc, 4) if self.roc_auc is not None else None,
"train_time_seconds": round(self.train_time_seconds, 1),
"support": self.support,
"converged": self.converged,
"effective_iterations": self.effective_iterations,
"model_size_bytes": self.model_size_bytes,
"eval_method": self.eval_method,
}
def score_model(
name: str,
track: str,
estimator: Any,
x_train: Any,
x_test: Any,
y_train: np.ndarray,
y_test: np.ndarray,
sample_weight_train: np.ndarray | None = None,
) -> EvalMetrics:
t0 = time.perf_counter()
with warnings.catch_warnings():
warnings.filterwarnings("ignore", category=ConvergenceWarning)
warnings.filterwarnings("ignore", category=UserWarning)
try:
estimator.fit(x_train, y_train, sample_weight=sample_weight_train)
except TypeError:
estimator.fit(x_train, y_train)
train_time = time.perf_counter() - t0
predictions = estimator.predict(x_test)
report = classification_report(
y_test, predictions, target_names=["Ham", "Spam"],
output_dict=True, zero_division=0,
)
try:
if hasattr(estimator, "predict_proba"):
probs = estimator.predict_proba(x_test)[:, 1]
else:
probs = estimator.decision_function(x_test)
roc_auc = float(roc_auc_score(y_test, probs))
except (AttributeError, ValueError):
roc_auc = None
n_iter = None
converged = True
if hasattr(estimator, "n_iter_"):
val = estimator.n_iter_
n_iter = int(val[0]) if hasattr(val, "__len__") else int(val)
spam_metrics = report["Spam"]
cm = confusion_matrix(y_test, predictions)
metrics = EvalMetrics(
model_name=name,
track=track,
accuracy=float(report["accuracy"]),
spam_precision=float(spam_metrics["precision"]),
spam_recall=float(spam_metrics["recall"]),
spam_f1=float(spam_metrics["f1-score"]),
roc_auc=roc_auc,
train_time_seconds=train_time,
support=int(spam_metrics["support"]),
confusion_matrix=cm.tolist(),
converged=n_iter is not None or track == "transformer",
effective_iterations=n_iter,
)
print(f"\n--- [{track}] {name} ---")
print(f"Accuracy : {metrics.accuracy:.4f}")
print(f"Spam F1 : {metrics.spam_f1:.4f}")
print(f"Spam Precision : {metrics.spam_precision:.4f}")
print(f"Spam Recall : {metrics.spam_recall:.4f}")
print(f"ROC-AUC : {metrics.roc_auc}")
print(f"Train time : {metrics.train_time_seconds:.1f}s")
print("Confusion matrix:")
print(cm)
return metrics
def print_leaderboard(all_metrics: list[EvalMetrics], title: str = "LEADERBOARD") -> None:
print(f"\n{'=' * 95}")
print(f" {title}")
print(f"{'=' * 95}")
header = f"{'Track':<14s} {'Model':<28s} {'F1':>7s} {'Prec':>7s} {'Recall':>7s} {'ROC-AUC':>8s} {'Time':>8s} {'CV':>6s}"
print(header)
print("-" * 95)
sorted_metrics = sorted(all_metrics, key=lambda e: e.spam_f1, reverse=True)
best_overall_f1 = sorted_metrics[0].spam_f1 if sorted_metrics else 0.0
for m in sorted_metrics:
f1_s = f"{m.spam_f1:.4f}"
prec_s = f"{m.spam_precision:.4f}"
rec_s = f"{m.spam_recall:.4f}"
roc_s = f"{m.roc_auc:.4f}" if m.roc_auc else "N/A"
t_s = f"{m.train_time_seconds:.0f}s"
cv_s = m.eval_method
mark = ">" if m.spam_f1 == best_overall_f1 else " "
track_icon = "A" if m.track == "classical" else "B"
print(f"{mark}Track {track_icon:<10s} {m.model_name:<28s} {f1_s:>7s} {prec_s:>7s} {rec_s:>7s} {roc_s:>8s} {t_s:>8s} {cv_s:>6s}")
print("=" * 95)
def print_cross_track_summary(track_a: EvalMetrics | None, track_b: EvalMetrics | None) -> None:
print(f"\n{'=' * 95}")
print(" CROSS-TRACK COMPARISON")
print(f"{'=' * 95}")
if track_a:
print(f" Track A Best (Classical): {track_a.model_name} → Spam F1 = {track_a.spam_f1:.4f} @ {track_a.train_time_seconds:.0f}s")
if track_b:
print(f" Track B Best (Transformer): {track_b.model_name} → Spam F1 = {track_b.spam_f1:.4f} @ {track_b.train_time_seconds:.0f}s")
if track_a and track_b:
delta = track_b.spam_f1 - track_a.spam_f1
print(f" F1 Delta (B - A): {delta:+.4f}")
if track_b.model_size_bytes and track_a.model_size_bytes:
size_ratio = track_b.model_size_bytes / max(track_a.model_size_bytes, 1)
print(f" Size ratio (B / A): {size_ratio:.1f}x")
print("=" * 95)
def save_artifacts(
model: Any,
vectorizer_bundle: dict[str, Any],
metadata: dict[str, Any],
model_path: Path,
vectorizer_path: Path,
metadata_path: Path,
) -> tuple[str, str]:
model_path.parent.mkdir(parents=True, exist_ok=True)
with open(model_path, "wb") as f:
pickle.dump(model, f)
with open(vectorizer_path, "wb") as f:
pickle.dump(vectorizer_bundle, f)
model_hash = hashlib.sha256(model_path.read_bytes()).hexdigest()
vec_hash = hashlib.sha256(vectorizer_path.read_bytes()).hexdigest()
(model_path.parent / (model_path.name + ".sha256")).write_text(model_hash)
(vectorizer_path.parent / (vectorizer_path.name + ".sha256")).write_text(vec_hash)
with open(metadata_path, "w", encoding="utf-8") as f:
json.dump(metadata, f, indent=2)
return model_hash, vec_hash