Rthur2003 commited on
Commit
5446f0d
·
1 Parent(s): 7f1ed48

feat: enhance training pipeline with multi-model evaluation and detailed metrics

Browse files
Files changed (1) hide show
  1. app/training/train_classifier.py +177 -52
app/training/train_classifier.py CHANGED
@@ -1,16 +1,26 @@
1
  """
2
- Train AURIS classifier on extracted audio features.
3
 
4
- Increment 1: RandomForest / GradientBoosting on librosa + vocal features.
5
- This replaces the heuristic scoring with a data-driven classifier.
 
 
 
 
 
 
 
 
 
6
 
7
  Usage:
8
- python -m app.training.train_classifier data/sonics/features.csv
9
 
10
  Outputs:
11
- models/auris_classifier_v1.pkl — trained model
12
  models/feature_scaler_v1.pkl — fitted StandardScaler
13
  models/feature_columns_v1.json — ordered feature column names
 
14
  """
15
 
16
  from __future__ import annotations
@@ -19,7 +29,9 @@ import csv
19
  import json
20
  import pickle
21
  import sys
 
22
  from pathlib import Path
 
23
 
24
  import numpy as np
25
 
@@ -27,6 +39,9 @@ from sklearn.ensemble import (
27
  GradientBoostingClassifier,
28
  RandomForestClassifier,
29
  )
 
 
 
30
  from sklearn.model_selection import (
31
  StratifiedKFold,
32
  cross_val_predict,
@@ -35,10 +50,19 @@ from sklearn.preprocessing import StandardScaler
35
  from sklearn.metrics import (
36
  accuracy_score,
37
  f1_score,
 
 
38
  roc_auc_score,
39
  )
40
 
41
- # Optional: LightGBM for better performance
 
 
 
 
 
 
 
42
  try:
43
  import lightgbm as lgb
44
  HAS_LGBM = True
@@ -56,15 +80,12 @@ def train(
56
  features_csv: str | Path,
57
  models_dir: str | Path = "models",
58
  n_folds: int = 5,
59
- ) -> dict:
60
  """
61
- Train and evaluate classifier on extracted features.
62
-
63
- Uses 5-fold cross-validation to estimate real accuracy,
64
- then trains final model on all data.
65
 
66
  Returns:
67
- Dict with metrics and model paths.
68
  """
69
  models_dir = Path(models_dir)
70
  models_dir.mkdir(parents=True, exist_ok=True)
@@ -72,7 +93,6 @@ def train(
72
  # ── Load data ──────────────────────────────────
73
  X, y = load_features_csv(features_csv)
74
 
75
- # Get feature column names
76
  with open(features_csv, "r", encoding="utf-8") as f:
77
  reader = csv.DictReader(f)
78
  feature_cols = [
@@ -87,39 +107,54 @@ def train(
87
  scaler = StandardScaler()
88
  X_scaled = scaler.fit_transform(X)
89
 
90
- # ── Train multiple models, pick best ───────────
91
  candidates = _build_candidates()
 
 
92
  best_model = None
93
  best_name = ""
94
  best_auc = 0.0
95
- results = {}
96
-
97
- cv = StratifiedKFold(n_splits=n_folds, shuffle=True, random_state=42)
98
 
99
  for name, model in candidates:
100
- print(f"\n{'─' * 40}")
101
- print(f"Training: {name}")
102
- print(f"{'─' * 40}")
103
 
104
- # Cross-validated predictions
 
 
105
  y_prob = cross_val_predict(
106
  model, X_scaled, y,
107
  cv=cv, method="predict_proba",
108
  )[:, 1]
109
  y_pred = (y_prob > 0.5).astype(int)
110
 
 
 
111
  acc = accuracy_score(y, y_pred)
112
- f1 = f1_score(y, y_pred)
 
 
113
  auc = roc_auc_score(y, y_prob)
114
 
115
- print(f" CV Accuracy: {acc:.4f}")
116
- print(f" CV F1: {f1:.4f}")
117
- print(f" CV ROC-AUC: {auc:.4f}")
 
 
 
118
 
119
- results[name] = {
120
  "accuracy": round(acc, 4),
 
 
121
  "f1": round(f1, 4),
122
  "roc_auc": round(auc, 4),
 
 
 
 
123
  }
124
 
125
  if auc > best_auc:
@@ -128,16 +163,12 @@ def train(
128
  best_model = model
129
 
130
  # ── Final evaluation of best model ─────────────
131
- print(f"\n{'=' * 50}")
132
- print(f" Best model: {best_name} (AUC={best_auc:.4f})")
133
- print(f"{'=' * 50}")
134
 
135
- # Cross-val predictions for detailed report
136
- y_prob_best = cross_val_predict(
137
- best_model, X_scaled, y,
138
- cv=cv, method="predict_proba",
139
- )[:, 1]
140
- y_pred_best = (y_prob_best > 0.5).astype(int)
141
 
142
  evaluate_predictions(
143
  y, y_pred_best, y_prob_best,
@@ -145,26 +176,22 @@ def train(
145
  )
146
 
147
  # ── Train final model on ALL data ──────────────
148
- print(f"\nTraining final {best_name} on all data...")
149
  best_model.fit(X_scaled, y)
150
 
151
  # ── Feature importance ─────────────────────────
152
- if hasattr(best_model, "feature_importances_"):
153
- importances = best_model.feature_importances_
154
- top_features = sorted(
155
- zip(feature_cols, importances),
156
- key=lambda x: x[1],
157
- reverse=True,
158
- )
159
- print("\nTop 10 features:")
160
- for fname, imp in top_features[:10]:
161
  bar = "█" * int(imp * 100)
162
- print(f" {fname:<30} {imp:.4f} {bar}")
163
 
164
  # ── Save artifacts ─────────────────────────────
165
  model_path = models_dir / "auris_classifier_v1.pkl"
166
  scaler_path = models_dir / "feature_scaler_v1.pkl"
167
  columns_path = models_dir / "feature_columns_v1.json"
 
168
 
169
  with open(model_path, "wb") as f:
170
  pickle.dump(best_model, f)
@@ -173,24 +200,54 @@ def train(
173
  with open(columns_path, "w") as f:
174
  json.dump(feature_cols, f, indent=2)
175
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
176
  print(f"\nSaved:")
177
  print(f" Model: {model_path}")
178
  print(f" Scaler: {scaler_path}")
179
  print(f" Columns: {columns_path}")
 
180
 
181
  return {
182
  "best_model": best_name,
183
  "best_auc": best_auc,
184
- "results": results,
 
185
  "model_path": str(model_path),
186
  }
187
 
188
 
189
- def _build_candidates() -> list[tuple[str, object]]:
190
  """Build list of classifier candidates to evaluate."""
191
- candidates = [
192
  (
193
- "RandomForest",
 
 
 
 
 
 
 
 
 
194
  RandomForestClassifier(
195
  n_estimators=300,
196
  max_depth=20,
@@ -201,7 +258,7 @@ def _build_candidates() -> list[tuple[str, object]]:
201
  ),
202
  ),
203
  (
204
- "GradientBoosting",
205
  GradientBoostingClassifier(
206
  n_estimators=200,
207
  max_depth=6,
@@ -210,8 +267,49 @@ def _build_candidates() -> list[tuple[str, object]]:
210
  random_state=42,
211
  ),
212
  ),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
213
  ]
214
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
  if HAS_LGBM:
216
  candidates.append((
217
  "LightGBM",
@@ -231,7 +329,34 @@ def _build_candidates() -> list[tuple[str, object]]:
231
  return candidates
232
 
233
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
234
  if __name__ == "__main__":
235
- csv_path = sys.argv[1] if len(sys.argv) > 1 else "data/sonics/features.csv"
236
  model_dir = sys.argv[2] if len(sys.argv) > 2 else "models"
237
  train(csv_path, model_dir)
 
1
  """
2
+ Comprehensive multi-model training pipeline for AURIS.
3
 
4
+ Trains and evaluates multiple classifier families on extracted
5
+ audio features using stratified k-fold cross-validation, then
6
+ selects the best model and exports it for production use.
7
+
8
+ Models compared:
9
+ - Random Forest
10
+ - Gradient Boosting
11
+ - XGBoost
12
+ - LightGBM
13
+ - Support Vector Machine (RBF)
14
+ - Multi-Layer Perceptron (Neural Network)
15
 
16
  Usage:
17
+ python -m app.training.train_classifier data/training/features.csv
18
 
19
  Outputs:
20
+ models/auris_classifier_v1.pkl — best trained model
21
  models/feature_scaler_v1.pkl — fitted StandardScaler
22
  models/feature_columns_v1.json — ordered feature column names
23
+ models/training_results.json — all model metrics + CV folds
24
  """
25
 
26
  from __future__ import annotations
 
29
  import json
30
  import pickle
31
  import sys
32
+ import time
33
  from pathlib import Path
34
+ from typing import Any
35
 
36
  import numpy as np
37
 
 
39
  GradientBoostingClassifier,
40
  RandomForestClassifier,
41
  )
42
+ from sklearn.linear_model import LogisticRegression
43
+ from sklearn.neural_network import MLPClassifier
44
+ from sklearn.svm import SVC
45
  from sklearn.model_selection import (
46
  StratifiedKFold,
47
  cross_val_predict,
 
50
  from sklearn.metrics import (
51
  accuracy_score,
52
  f1_score,
53
+ precision_score,
54
+ recall_score,
55
  roc_auc_score,
56
  )
57
 
58
+ # Optional: XGBoost
59
+ try:
60
+ import xgboost as xgb
61
+ HAS_XGB = True
62
+ except ImportError:
63
+ HAS_XGB = False
64
+
65
+ # Optional: LightGBM
66
  try:
67
  import lightgbm as lgb
68
  HAS_LGBM = True
 
80
  features_csv: str | Path,
81
  models_dir: str | Path = "models",
82
  n_folds: int = 5,
83
+ ) -> dict[str, Any]:
84
  """
85
+ Train and evaluate all classifier candidates.
 
 
 
86
 
87
  Returns:
88
+ Dict with per-model metrics, best model info, and saved paths.
89
  """
90
  models_dir = Path(models_dir)
91
  models_dir.mkdir(parents=True, exist_ok=True)
 
93
  # ── Load data ──────────────────────────────────
94
  X, y = load_features_csv(features_csv)
95
 
 
96
  with open(features_csv, "r", encoding="utf-8") as f:
97
  reader = csv.DictReader(f)
98
  feature_cols = [
 
107
  scaler = StandardScaler()
108
  X_scaled = scaler.fit_transform(X)
109
 
110
+ # ── Train multiple models ──────────────────────
111
  candidates = _build_candidates()
112
+ cv = StratifiedKFold(n_splits=n_folds, shuffle=True, random_state=42)
113
+
114
  best_model = None
115
  best_name = ""
116
  best_auc = 0.0
117
+ all_results: dict[str, dict[str, Any]] = {}
 
 
118
 
119
  for name, model in candidates:
120
+ print(f"\n{'─' * 50}")
121
+ print(f" Training: {name}")
122
+ print(f"{'─' * 50}")
123
 
124
+ t0 = time.time()
125
+
126
+ # Cross-validated probability predictions
127
  y_prob = cross_val_predict(
128
  model, X_scaled, y,
129
  cv=cv, method="predict_proba",
130
  )[:, 1]
131
  y_pred = (y_prob > 0.5).astype(int)
132
 
133
+ train_time = time.time() - t0
134
+
135
  acc = accuracy_score(y, y_pred)
136
+ prec = precision_score(y, y_pred, zero_division=0)
137
+ rec = recall_score(y, y_pred, zero_division=0)
138
+ f1 = f1_score(y, y_pred, zero_division=0)
139
  auc = roc_auc_score(y, y_prob)
140
 
141
+ print(f" Accuracy: {acc:.4f}")
142
+ print(f" Precision: {prec:.4f}")
143
+ print(f" Recall: {rec:.4f}")
144
+ print(f" F1 Score: {f1:.4f}")
145
+ print(f" ROC-AUC: {auc:.4f}")
146
+ print(f" Train time: {train_time:.1f}s")
147
 
148
+ all_results[name] = {
149
  "accuracy": round(acc, 4),
150
+ "precision": round(prec, 4),
151
+ "recall": round(rec, 4),
152
  "f1": round(f1, 4),
153
  "roc_auc": round(auc, 4),
154
+ "train_time_sec": round(train_time, 2),
155
+ "y_true": y.tolist(),
156
+ "y_pred": y_pred.tolist(),
157
+ "y_prob": y_prob.tolist(),
158
  }
159
 
160
  if auc > best_auc:
 
163
  best_model = model
164
 
165
  # ── Final evaluation of best model ─────────────
166
+ print(f"\n{'' * 60}")
167
+ print(f" BEST MODEL: {best_name} (ROC-AUC = {best_auc:.4f})")
168
+ print(f"{'' * 60}")
169
 
170
+ y_prob_best = np.array(all_results[best_name]["y_prob"])
171
+ y_pred_best = np.array(all_results[best_name]["y_pred"])
 
 
 
 
172
 
173
  evaluate_predictions(
174
  y, y_pred_best, y_prob_best,
 
176
  )
177
 
178
  # ── Train final model on ALL data ──────────────
179
+ print(f"\nTraining final {best_name} on all {len(y)} samples...")
180
  best_model.fit(X_scaled, y)
181
 
182
  # ── Feature importance ─────────────────────────
183
+ importance_data = _extract_importance(best_model, feature_cols)
184
+ if importance_data:
185
+ print("\nTop 15 features:")
186
+ for fname, imp in importance_data[:15]:
 
 
 
 
 
187
  bar = "█" * int(imp * 100)
188
+ print(f" {fname:<35} {imp:.4f} {bar}")
189
 
190
  # ── Save artifacts ─────────────────────────────
191
  model_path = models_dir / "auris_classifier_v1.pkl"
192
  scaler_path = models_dir / "feature_scaler_v1.pkl"
193
  columns_path = models_dir / "feature_columns_v1.json"
194
+ results_path = models_dir / "training_results.json"
195
 
196
  with open(model_path, "wb") as f:
197
  pickle.dump(best_model, f)
 
200
  with open(columns_path, "w") as f:
201
  json.dump(feature_cols, f, indent=2)
202
 
203
+ # Save full results (without numpy arrays for JSON)
204
+ json_results = {}
205
+ for name, data in all_results.items():
206
+ json_results[name] = {
207
+ k: v for k, v in data.items()
208
+ if k not in ("y_true", "y_pred", "y_prob")
209
+ }
210
+ json_results["_best_model"] = best_name
211
+ json_results["_n_samples"] = len(y)
212
+ json_results["_n_features"] = X.shape[1]
213
+ json_results["_n_folds"] = n_folds
214
+ if importance_data:
215
+ json_results["_feature_importance"] = {
216
+ name: round(imp, 6) for name, imp in importance_data
217
+ }
218
+
219
+ with open(results_path, "w") as f:
220
+ json.dump(json_results, f, indent=2)
221
+
222
  print(f"\nSaved:")
223
  print(f" Model: {model_path}")
224
  print(f" Scaler: {scaler_path}")
225
  print(f" Columns: {columns_path}")
226
+ print(f" Results: {results_path}")
227
 
228
  return {
229
  "best_model": best_name,
230
  "best_auc": best_auc,
231
+ "all_results": all_results,
232
+ "feature_cols": feature_cols,
233
  "model_path": str(model_path),
234
  }
235
 
236
 
237
+ def _build_candidates() -> list[tuple[str, Any]]:
238
  """Build list of classifier candidates to evaluate."""
239
+ candidates: list[tuple[str, Any]] = [
240
  (
241
+ "Logistic Regression",
242
+ LogisticRegression(
243
+ C=1.0,
244
+ max_iter=1000,
245
+ class_weight="balanced",
246
+ random_state=42,
247
+ ),
248
+ ),
249
+ (
250
+ "Random Forest",
251
  RandomForestClassifier(
252
  n_estimators=300,
253
  max_depth=20,
 
258
  ),
259
  ),
260
  (
261
+ "Gradient Boosting",
262
  GradientBoostingClassifier(
263
  n_estimators=200,
264
  max_depth=6,
 
267
  random_state=42,
268
  ),
269
  ),
270
+ (
271
+ "SVM (RBF)",
272
+ SVC(
273
+ kernel="rbf",
274
+ C=10.0,
275
+ gamma="scale",
276
+ class_weight="balanced",
277
+ probability=True,
278
+ random_state=42,
279
+ ),
280
+ ),
281
+ (
282
+ "MLP Neural Network",
283
+ MLPClassifier(
284
+ hidden_layer_sizes=(128, 64, 32),
285
+ activation="relu",
286
+ solver="adam",
287
+ alpha=0.001,
288
+ learning_rate="adaptive",
289
+ max_iter=500,
290
+ early_stopping=True,
291
+ validation_fraction=0.15,
292
+ random_state=42,
293
+ ),
294
+ ),
295
  ]
296
 
297
+ if HAS_XGB:
298
+ candidates.append((
299
+ "XGBoost",
300
+ xgb.XGBClassifier(
301
+ n_estimators=300,
302
+ max_depth=8,
303
+ learning_rate=0.05,
304
+ subsample=0.8,
305
+ colsample_bytree=0.8,
306
+ scale_pos_weight=1.0,
307
+ eval_metric="logloss",
308
+ random_state=42,
309
+ verbosity=0,
310
+ ),
311
+ ))
312
+
313
  if HAS_LGBM:
314
  candidates.append((
315
  "LightGBM",
 
329
  return candidates
330
 
331
 
332
+ def _extract_importance(
333
+ model: Any,
334
+ feature_cols: list[str],
335
+ ) -> list[tuple[str, float]]:
336
+ """Extract feature importance from the trained model."""
337
+ importances = None
338
+
339
+ if hasattr(model, "feature_importances_"):
340
+ importances = model.feature_importances_
341
+ elif hasattr(model, "coef_"):
342
+ importances = np.abs(model.coef_[0])
343
+
344
+ if importances is None:
345
+ return []
346
+
347
+ # Normalize to sum to 1
348
+ total = np.sum(importances)
349
+ if total > 0:
350
+ importances = importances / total
351
+
352
+ return sorted(
353
+ zip(feature_cols, importances.tolist()),
354
+ key=lambda x: x[1],
355
+ reverse=True,
356
+ )
357
+
358
+
359
  if __name__ == "__main__":
360
+ csv_path = sys.argv[1] if len(sys.argv) > 1 else "data/training/features.csv"
361
  model_dir = sys.argv[2] if len(sys.argv) > 2 else "models"
362
  train(csv_path, model_dir)