Spaces:
Running
Running
| """Phase 4a: train and evaluate the three lightweight models. | |
| Trains majority baseline, keyword heuristic, and TF-IDF + logistic regression, | |
| then evaluates on both in-domain and OOD test sets. | |
| Usage: | |
| python scripts/train_classical.py | |
| """ | |
| import json | |
| import os | |
| import sys | |
| sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) | |
| import config # noqa: E402 | |
| from scripts import metrics # noqa: E402 | |
| from scripts.classical_models import ( # noqa: E402 | |
| KeywordHeuristicModel, | |
| MajorityClassModel, | |
| TfidfLogRegModel, | |
| ) | |
| def load_split(processed_dir, name): | |
| """Load one split file into parallel question and label lists.""" | |
| path = os.path.join(processed_dir, f"{name}.jsonl") | |
| questions, labels = [], [] | |
| with open(path, "r", encoding="utf-8") as handle: | |
| for line in handle: | |
| line = line.strip() | |
| if not line: | |
| continue | |
| record = json.loads(line) | |
| questions.append(record["question"]) | |
| labels.append(record["bloom_class"]) | |
| return questions, labels | |
| class ClassicalExperiment: | |
| """Train and evaluate the three lightweight models.""" | |
| def __init__(self, training_config): | |
| self.cfg = training_config | |
| self.results = {} | |
| def _evaluate_on(self, model, questions, labels): | |
| """Predict and compute metrics for one eval set.""" | |
| predictions = model.predict(questions) | |
| return metrics.compute_metrics(labels, predictions) | |
| def run(self): | |
| """Load data, train each model, and evaluate on test and OOD.""" | |
| train_q, train_y = load_split(self.cfg.processed_dir, "train") | |
| test_q, test_y = load_split(self.cfg.processed_dir, "test") | |
| ood_q, ood_y = load_split(self.cfg.processed_dir, "ood_test") | |
| print(f"train={len(train_q)} test={len(test_q)} ood_test={len(ood_q)}") | |
| model_specs = [ | |
| ("majority_baseline", MajorityClassModel()), | |
| ("keyword_heuristic", KeywordHeuristicModel()), | |
| ("tfidf_logreg", TfidfLogRegModel(self.cfg)), | |
| ] | |
| for name, model in model_specs: | |
| print(f"\n=== {name} ===") | |
| model.fit(train_q, train_y) | |
| if name == "tfidf_logreg": | |
| print(f" best params : {model.best_params}") | |
| print(f" best CV F1 : {model.best_cv_score:.3f}") | |
| in_domain = self._evaluate_on(model, test_q, test_y) | |
| ood = self._evaluate_on(model, ood_q, ood_y) | |
| metrics.print_metrics(f"[{name}] in-domain test", in_domain) | |
| metrics.print_metrics(f"[{name}] OOD test", ood) | |
| metrics.plot_confusion_matrix( | |
| in_domain, | |
| f"{name} (in-domain)", | |
| os.path.join(self.cfg.output_dir, f"cm_{name}_indomain.png"), | |
| ) | |
| metrics.plot_confusion_matrix( | |
| ood, | |
| f"{name} (OOD)", | |
| os.path.join(self.cfg.output_dir, f"cm_{name}_ood.png"), | |
| ) | |
| entry = {"in_domain": in_domain, "ood": ood} | |
| if name == "tfidf_logreg": | |
| entry["best_params"] = model.best_params | |
| entry["best_cv_macro_f1"] = float(model.best_cv_score) | |
| self.results[name] = entry | |
| self._save_results() | |
| self._print_comparison() | |
| def _save_results(self): | |
| """Save all results to a JSON file.""" | |
| os.makedirs(self.cfg.output_dir, exist_ok=True) | |
| path = os.path.join(self.cfg.output_dir, "classical_results.json") | |
| with open(path, "w", encoding="utf-8") as handle: | |
| json.dump(self.results, handle, indent=2) | |
| print(f"\nSaved results -> {path}") | |
| def _print_comparison(self): | |
| """Print in-domain and OOD comparison tables.""" | |
| print("\n" + "=" * 60) | |
| print("In-domain test") | |
| print(f" {'model':20s} {'acc':>6s} {'macroF1':>8s} {'QWK':>6s}") | |
| for name, entry in self.results.items(): | |
| m = entry["in_domain"] | |
| print(f" {name:20s} {m['accuracy']:6.3f} " | |
| f"{m['macro_f1']:8.3f} {m['qwk']:6.3f}") | |
| print("\nCross-domain (OOD) test") | |
| print(f" {'model':20s} {'inF1':>6s} {'oodF1':>6s} {'degrade':>8s}") | |
| for name, entry in self.results.items(): | |
| in_f1 = entry["in_domain"]["macro_f1"] | |
| ood_f1 = entry["ood"]["macro_f1"] | |
| print(f" {name:20s} {in_f1:6.3f} {ood_f1:6.3f} " | |
| f"{in_f1 - ood_f1:8.3f}") | |
| print("=" * 60) | |
| def main(): | |
| """Run Phase 4a.""" | |
| experiment = ClassicalExperiment(config.TrainingConfig()) | |
| experiment.run() | |
| if __name__ == "__main__": | |
| main() | |