Download train.py from datadb/Telco-Troubleshooting-Agentic-Challenge-Track-A: direct link, hf CLI and curl.
- Browser
- Download file 18 kB
-
https://huggingface.co/datadb/Telco-Troubleshooting-Agentic-Challenge-Track-A/resolve/main/train.py
- Command line
-
hf download hf://datadb/Telco-Troubleshooting-Agentic-Challenge-Track-A/train.py
-
curl -L -o train.py https://huggingface.co/datadb/Telco-Troubleshooting-Agentic-Challenge-Track-A/resolve/main/train.py
18 kB
| #!/usr/bin/env python | |
| # -*- coding: utf-8 -*- | |
| """ | |
| Training and experiment entry point for Track A. | |
| This script owns model fitting and evaluation: | |
| 1. Load the labelled Phase 1 training scenarios. | |
| 2. Cross-validate the LightGBM template classifier and option selector. | |
| 3. Save fold metrics, overall metrics, OOF predictions, train/val split | |
| manifests, full train/val split JSON files, and training logs under a | |
| timestamped experiment directory. | |
| 4. Train final models on all labelled data. | |
| 5. Save the model bundle in the experiment folder and copy it to --out. | |
| Shared feature extraction, prediction-time selection, and bundle IO live in | |
| src/model_core.py. Runtime inference orchestration lives in main.py. | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import os | |
| import shutil | |
| import sys | |
| import time | |
| from collections import Counter | |
| from contextlib import redirect_stderr, redirect_stdout | |
| from datetime import datetime | |
| from pathlib import Path | |
| from typing import Any, Dict, List, Tuple | |
| import numpy as np | |
| import pandas as pd | |
| from lightgbm import LGBMClassifier | |
| from sklearn.feature_extraction import DictVectorizer | |
| from sklearn.model_selection import KFold, StratifiedKFold | |
| from sklearn.pipeline import Pipeline | |
| from src.model_core import ( | |
| MODEL_BUNDLE_VERSION, | |
| answer_to_template, | |
| build_prediction_context, | |
| extract_scenario_features, | |
| iou_score, | |
| get_options, | |
| load_json, | |
| option_feature_dict, | |
| parse_answer, | |
| predict_labels, | |
| predict_labels_batch, | |
| save_model_bundle, | |
| template_to_actions, | |
| ) | |
| class Tee: | |
| def __init__(self, *streams): | |
| self.streams = streams | |
| def write(self, data: str) -> None: | |
| for stream in self.streams: | |
| stream.write(data) | |
| stream.flush() | |
| def flush(self) -> None: | |
| for stream in self.streams: | |
| stream.flush() | |
| def json_safe(value: Any) -> Any: | |
| if isinstance(value, dict): | |
| return {str(k): json_safe(v) for k, v in value.items()} | |
| if isinstance(value, (list, tuple)): | |
| return [json_safe(v) for v in value] | |
| if isinstance(value, np.generic): | |
| return value.item() | |
| return value | |
| def get_last_version(results_dir: Path) -> int: | |
| versions = [] | |
| if not results_dir.exists(): | |
| return 0 | |
| for child in results_dir.iterdir(): | |
| if not child.is_dir(): | |
| continue | |
| prefix = child.name.split("_", 1)[0] | |
| if prefix.isdigit(): | |
| versions.append(int(prefix)) | |
| return sorted(versions)[-1] if versions else 0 | |
| def make_experiment_dir(root: Path, name: str = "") -> Path: | |
| root.mkdir(parents=True, exist_ok=True) | |
| clean_name = name.strip()[:10] | |
| last_version = get_last_version(root) | |
| base = ( | |
| f"{last_version + 1:02d}_{clean_name}" | |
| if clean_name | |
| else f"{last_version + 1:02d}" | |
| ) | |
| exp_dir = root / base | |
| suffix = 2 | |
| while exp_dir.exists(): | |
| exp_dir = root / f"{base}_{suffix}" | |
| suffix += 1 | |
| exp_dir.mkdir(parents=True) | |
| return exp_dir | |
| def scenario_id(s: Dict[str, Any], index: int) -> str: | |
| return str(s.get("scenario_id") or s.get("ID") or f"train_{index}") | |
| def write_json(path: Path, obj: Any) -> None: | |
| path.parent.mkdir(parents=True, exist_ok=True) | |
| path.write_text( | |
| json.dumps(json_safe(obj), ensure_ascii=False, indent=2), encoding="utf-8" | |
| ) | |
| def fit_template_model( | |
| train: List[Dict[str, Any]], | |
| seed: int = 42, | |
| n_jobs: int = 1, | |
| boost_rounds: int = 2000, | |
| ) -> Pipeline: | |
| started = time.time() | |
| print(f"Building template features for {len(train)} scenarios...") | |
| X = [extract_scenario_features(s) for s in train] | |
| y = [answer_to_template(s) for s in train] | |
| print( | |
| f"Template features built in {time.time() - started:.1f}s; fitting LightGBM for {boost_rounds} rounds..." | |
| ) | |
| clf = LGBMClassifier( | |
| objective="multiclass", | |
| n_estimators=boost_rounds, | |
| learning_rate=0.05, | |
| num_leaves=20, | |
| path_smooth=10, | |
| feature_fraction=0.8, | |
| bagging_fraction=0.8, | |
| bagging_freq=5, | |
| min_child_samples=20, | |
| class_weight="balanced", | |
| random_state=seed, | |
| n_jobs=n_jobs, | |
| verbosity=-1, | |
| force_col_wise=True, | |
| ) | |
| model = Pipeline([("vec", DictVectorizer(sparse=False)), ("clf", clf)]) | |
| model.fit(X, y) | |
| print(f"Template model fitted in {time.time() - started:.1f}s total.") | |
| return model | |
| def build_selector_rows( | |
| train: List[Dict[str, Any]], | |
| ) -> Tuple[List[Dict[str, float]], List[int], List[Dict[str, Any]]]: | |
| X, y, meta = [], [], [] | |
| for s in train: | |
| context = build_prediction_context(s) | |
| opts = context["options"] | |
| ans_ids = set(parse_answer(s.get("answer", ""))) | |
| template = answer_to_template(s) | |
| for cid, label in opts.items(): | |
| action = context["option_actions"].get(cid, "other") | |
| feats = option_feature_dict(s, cid, label, template, action, context) | |
| X.append(feats) | |
| y.append(1 if cid in ans_ids else 0) | |
| meta.append( | |
| { | |
| "scenario_id": s.get("scenario_id"), | |
| "cid": cid, | |
| "action": action, | |
| "template": template, | |
| "in_template": action in set(template_to_actions(template)), | |
| } | |
| ) | |
| return X, y, meta | |
| def fit_selector_model( | |
| train: List[Dict[str, Any]], | |
| seed: int = 42, | |
| n_jobs: int = 1, | |
| boost_rounds: int = 2000, | |
| ) -> Pipeline: | |
| started = time.time() | |
| print(f"Building selector rows for {len(train)} scenarios...") | |
| X, y, _ = build_selector_rows(train) | |
| print( | |
| f"Selector rows built: {len(X)} rows in {time.time() - started:.1f}s; fitting LightGBM for {boost_rounds} rounds..." | |
| ) | |
| clf = LGBMClassifier( | |
| objective="binary", | |
| n_estimators=boost_rounds, | |
| learning_rate=0.05, | |
| num_leaves=20, | |
| path_smooth=10, | |
| feature_fraction=0.8, | |
| bagging_fraction=0.8, | |
| bagging_freq=5, | |
| min_child_samples=20, | |
| class_weight="balanced", | |
| random_state=seed, | |
| n_jobs=n_jobs, | |
| verbosity=-1, | |
| force_col_wise=True, | |
| ) | |
| model = Pipeline([("vec", DictVectorizer(sparse=False)), ("clf", clf)]) | |
| model.fit(X, y) | |
| print(f"Selector model fitted in {time.time() - started:.1f}s total.") | |
| return model | |
| def save_split_artifacts( | |
| exp_dir: Path, | |
| fold: int, | |
| train: List[Dict[str, Any]], | |
| tr_idx: np.ndarray, | |
| va_idx: np.ndarray, | |
| ) -> Tuple[Path, Path]: | |
| split_dir = exp_dir / "splits" / f"fold_{fold}" | |
| split_dir.mkdir(parents=True, exist_ok=True) | |
| def rows(indices: np.ndarray, split: str) -> List[Dict[str, Any]]: | |
| out = [] | |
| for idx in indices: | |
| s = train[int(idx)] | |
| out.append( | |
| { | |
| "fold": fold, | |
| "split": split, | |
| "row_index": int(idx), | |
| "scenario_id": scenario_id(s, int(idx)), | |
| "answer": s.get("answer", ""), | |
| "template": answer_to_template(s), | |
| } | |
| ) | |
| return out | |
| train_rows = rows(tr_idx, "train") | |
| val_rows = rows(va_idx, "val") | |
| pd.DataFrame(train_rows).to_csv(split_dir / "train_manifest.csv", index=False) | |
| pd.DataFrame(val_rows).to_csv(split_dir / "val_manifest.csv", index=False) | |
| train_json = split_dir / "train.json" | |
| val_json = split_dir / "val.json" | |
| write_json(train_json, [train[int(i)] for i in tr_idx]) | |
| write_json(val_json, [train[int(i)] for i in va_idx]) | |
| return train_json, val_json | |
| def cross_validate_and_save( | |
| train: List[Dict[str, Any]], | |
| exp_dir: Path, | |
| folds: int, | |
| seed: int, | |
| n_jobs: int, | |
| template_boost_rounds: int, | |
| selector_boost_rounds: int, | |
| ) -> Dict[str, Any]: | |
| for child in ("metrics", "predictions", "splits"): | |
| (exp_dir / child).mkdir(parents=True, exist_ok=True) | |
| templates = [answer_to_template(s) for s in train] | |
| counts = Counter(templates) | |
| split_y = [t if counts[t] >= folds else "__rare__" for t in templates] | |
| split_counts = Counter(split_y) | |
| usable_folds = min(folds, len(train)) | |
| min_split_count = min(split_counts.values()) if split_counts else 0 | |
| if min_split_count >= 2: | |
| usable_folds = min(usable_folds, min_split_count) | |
| splitter = StratifiedKFold( | |
| n_splits=usable_folds, shuffle=True, random_state=seed | |
| ) | |
| splits = splitter.split(np.zeros(len(train)), split_y) | |
| split_strategy = "stratified" | |
| else: | |
| usable_folds = min(usable_folds, max(2, len(train))) | |
| splitter = KFold(n_splits=usable_folds, shuffle=True, random_state=seed) | |
| splits = splitter.split(np.zeros(len(train))) | |
| split_strategy = "kfold" | |
| metrics: List[Dict[str, Any]] = [] | |
| oof_rows: List[Dict[str, Any]] = [] | |
| split_manifest: List[Dict[str, Any]] = [] | |
| print(f"Template classes: {len(counts)}") | |
| print("Top templates:") | |
| for k, v in counts.most_common(20): | |
| print(f" {k}: {v}") | |
| if usable_folds != folds: | |
| print( | |
| f"Adjusted CV folds from {folds} to {usable_folds} because of rare classes." | |
| ) | |
| print(f"CV split strategy: {split_strategy}") | |
| for fold, (tr_idx, va_idx) in enumerate(splits, start=1): | |
| fold_start = time.time() | |
| print(f"\nTraining fold {fold}/{folds}...") | |
| train_json, val_json = save_split_artifacts( | |
| exp_dir, fold, train, tr_idx, va_idx | |
| ) | |
| split_manifest.append( | |
| { | |
| "fold": fold, | |
| "train_rows": int(len(tr_idx)), | |
| "val_rows": int(len(va_idx)), | |
| "train_json": str(train_json), | |
| "val_json": str(val_json), | |
| } | |
| ) | |
| tr = [train[int(i)] for i in tr_idx] | |
| va = [train[int(i)] for i in va_idx] | |
| template_model = fit_template_model( | |
| tr, | |
| seed + fold, | |
| n_jobs=n_jobs, | |
| boost_rounds=template_boost_rounds, | |
| ) | |
| selector_model = fit_selector_model( | |
| tr, | |
| seed + fold, | |
| n_jobs=n_jobs, | |
| boost_rounds=selector_boost_rounds, | |
| ) | |
| fold_ious: List[float] = [] | |
| fold_tpl: List[float] = [] | |
| pred_start = time.time() | |
| print(f"Scoring {len(va)} validation scenarios...") | |
| predictions = predict_labels_batch(template_model, selector_model, va) | |
| for local_i, s, (pred, dbg) in zip(va_idx, va, predictions): | |
| truth = parse_answer(s.get("answer", "")) | |
| iou = iou_score(pred, truth) | |
| template_ok = 1.0 if dbg["template"] == answer_to_template(s) else 0.0 | |
| fold_ious.append(iou) | |
| fold_tpl.append(template_ok) | |
| oof_rows.append( | |
| { | |
| "fold": fold, | |
| "row_index": int(local_i), | |
| "scenario_id": scenario_id(s, int(local_i)), | |
| "truth": "|".join(truth), | |
| "prediction": "|".join(pred), | |
| "iou": float(iou), | |
| "true_template": answer_to_template(s), | |
| "pred_template": dbg["template"], | |
| "template_prob": float(dbg["template_prob"]), | |
| "template_correct": bool(template_ok), | |
| } | |
| ) | |
| print(f"Validation scoring completed in {time.time() - pred_start:.1f}s.") | |
| row = { | |
| "fold": fold, | |
| "train_rows": int(len(tr_idx)), | |
| "val_rows": int(len(va_idx)), | |
| "iou_mean": float(np.mean(fold_ious)), | |
| "iou_std": float(np.std(fold_ious)), | |
| "template_acc": float(np.mean(fold_tpl)), | |
| "duration_seconds": round(time.time() - fold_start, 3), | |
| } | |
| metrics.append(row) | |
| print( | |
| f"Fold {fold}: IoU={row['iou_mean']:.4f} " | |
| f"template_acc={row['template_acc']:.4f} " | |
| f"duration={row['duration_seconds']:.1f}s" | |
| ) | |
| pd.DataFrame(metrics).to_csv( | |
| exp_dir / "metrics" / "fold_metrics.csv", index=False | |
| ) | |
| pd.DataFrame(oof_rows).to_csv( | |
| exp_dir / "predictions" / "oof_predictions.csv", index=False | |
| ) | |
| write_json(exp_dir / "splits" / "split_manifest.json", split_manifest) | |
| summary = { | |
| "requested_folds": folds, | |
| "folds": usable_folds, | |
| "split_strategy": split_strategy, | |
| "seed": seed, | |
| "n_jobs": n_jobs, | |
| "template_boost_rounds": template_boost_rounds, | |
| "selector_boost_rounds": selector_boost_rounds, | |
| "iou_mean": float(np.mean([m["iou_mean"] for m in metrics])), | |
| "iou_std": float(np.std([m["iou_mean"] for m in metrics])), | |
| "template_acc_mean": float(np.mean([m["template_acc"] for m in metrics])), | |
| "template_acc_std": float(np.std([m["template_acc"] for m in metrics])), | |
| "fold_metrics": metrics, | |
| } | |
| write_json(exp_dir / "metrics" / "overall_metrics.json", summary) | |
| pd.DataFrame([summary]).drop(columns=["fold_metrics"]).to_csv( | |
| exp_dir / "metrics" / "overall_metrics.csv", index=False | |
| ) | |
| return summary | |
| def run_training(args: argparse.Namespace) -> None: | |
| started = time.time() | |
| exp_dir = make_experiment_dir(args.experiments_root, args.experiment_name) | |
| log_path = exp_dir / "training.log" | |
| config = { | |
| "train_path": args.train_path, | |
| "out": args.out, | |
| "experiments_root": str(args.experiments_root), | |
| "experiment_dir": str(exp_dir), | |
| "experiment_name": args.experiment_name, | |
| "folds": args.folds, | |
| "seed": args.seed, | |
| "n_jobs": args.n_jobs, | |
| "template_boost_rounds": args.template_boost_rounds, | |
| "selector_boost_rounds": args.selector_boost_rounds, | |
| "selector_training_scope": "all_options", | |
| "selector_prediction_mode": "candidate_ranker", | |
| "model_bundle_version": MODEL_BUNDLE_VERSION, | |
| } | |
| write_json(exp_dir / "config.json", config) | |
| (exp_dir / "experiment.txt").write_text( | |
| f"{exp_dir.name}\n{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n", | |
| encoding="utf-8", | |
| ) | |
| with log_path.open("w", encoding="utf-8") as log_f, redirect_stdout( | |
| Tee(sys.stdout, log_f) | |
| ), redirect_stderr(Tee(sys.stderr, log_f)): | |
| print(f"Experiment directory: {exp_dir}") | |
| print(f"Training log: {log_path}") | |
| print(f"Config: {json.dumps(config, indent=2)}") | |
| train = load_json(args.train_path) | |
| print(f"Loaded train={len(train)} from {args.train_path}") | |
| cv_summary = cross_validate_and_save( | |
| train, | |
| exp_dir, | |
| args.folds, | |
| args.seed, | |
| args.n_jobs, | |
| args.template_boost_rounds, | |
| args.selector_boost_rounds, | |
| ) | |
| print("\nTraining full template model...") | |
| template_model = fit_template_model( | |
| train, | |
| args.seed, | |
| n_jobs=args.n_jobs, | |
| boost_rounds=args.template_boost_rounds, | |
| ) | |
| print("Training full selector model...") | |
| selector_model = fit_selector_model( | |
| train, | |
| args.seed, | |
| n_jobs=args.n_jobs, | |
| boost_rounds=args.selector_boost_rounds, | |
| ) | |
| templates = Counter(answer_to_template(s) for s in train) | |
| metadata = { | |
| "version": MODEL_BUNDLE_VERSION, | |
| "train_path": args.train_path, | |
| "train_rows": len(train), | |
| "seed": args.seed, | |
| "n_jobs": args.n_jobs, | |
| "template_boost_rounds": args.template_boost_rounds, | |
| "selector_boost_rounds": args.selector_boost_rounds, | |
| "selector_training_scope": "all_options", | |
| "selector_prediction_mode": "candidate_ranker", | |
| "template_classes": len(templates), | |
| "top_templates": templates.most_common(20), | |
| "cv": cv_summary, | |
| "experiment_dir": str(exp_dir), | |
| "trained_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), | |
| "duration_seconds": round(time.time() - started, 3), | |
| } | |
| bundle_path = exp_dir / "models" / "model_v4_bundle.pkl" | |
| save_model_bundle(str(bundle_path), template_model, selector_model, metadata) | |
| write_json(exp_dir / "metadata.json", metadata) | |
| Path(args.out).parent.mkdir(parents=True, exist_ok=True) | |
| shutil.copy2(bundle_path, args.out) | |
| print(f"\nSaved experiment model bundle: {bundle_path}") | |
| print(f"Copied model bundle to: {args.out}") | |
| print(f"Saved metadata: {exp_dir / 'metadata.json'}") | |
| print(f"Saved metrics: {exp_dir / 'metrics'}") | |
| print(f"Saved splits: {exp_dir / 'splits'}") | |
| print(f"Total duration: {metadata['duration_seconds']:.1f}s") | |
| def main() -> None: | |
| parser = argparse.ArgumentParser( | |
| description="Cross-validate, train, and save the Track A v4 ML model bundle." | |
| ) | |
| parser.add_argument("--train_path", default="data/Phase_1/train.json") | |
| parser.add_argument("--out", default="models/model_v4_bundle.pkl") | |
| parser.add_argument( | |
| "--experiments_root", type=Path, default=Path("results/experiments") | |
| ) | |
| parser.add_argument("--experiment_name", default="lgbm_v4") | |
| parser.add_argument("--folds", type=int, default=5) | |
| parser.add_argument("--seed", type=int, default=42) | |
| parser.add_argument( | |
| "--n_jobs", | |
| type=int, | |
| default=-1, | |
| help="Parallel jobs for LightGBM.", | |
| ) | |
| parser.add_argument("--template_boost_rounds", type=int, default=2000) | |
| parser.add_argument("--selector_boost_rounds", type=int, default=2000) | |
| args = parser.parse_args() | |
| run_training(args) | |
| if __name__ == "__main__": | |
| main() | |