voice-detection-api / src /train_dsp_v2.py
pp22's picture
Upload folder using huggingface_hub
ec06657 verified
Raw
History Blame Contribute Delete
19.3 kB
"""
DSP Classifier v2 — Training Pipeline
=======================================
Extracts v2 features from raw audio, trains multiple classifiers,
selects the best, calibrates probabilities, and saves the model.
Run: python src/train_dsp_v2.py
"""
import os
import sys
import time
import json
import numpy as np
import pandas as pd
import joblib
import warnings
from pathlib import Path
from tqdm import tqdm
from sklearn.model_selection import StratifiedShuffleSplit, cross_val_score
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.metrics import (
accuracy_score, classification_report, confusion_matrix,
f1_score, roc_auc_score, precision_score, recall_score
)
from sklearn.calibration import CalibratedClassifierCV
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
warnings.filterwarnings("ignore")
# Add project root to path
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
from src.config import DATA_DIR, SAMPLE_RATE
from src.features.extract_dsp_v2 import extract_all_features_v2
# Try importing optional dependencies
try:
import xgboost as xgb
HAS_XGB = True
except ImportError:
HAS_XGB = False
try:
import lightgbm as lgb
HAS_LGB = True
except ImportError:
HAS_LGB = False
try:
import optuna
HAS_OPTUNA = True
except ImportError:
HAS_OPTUNA = False
MODELS_DIR = os.path.join(os.path.dirname(__file__), '..', 'models')
os.makedirs(MODELS_DIR, exist_ok=True)
RESULTS_DIR = os.path.join(os.path.dirname(__file__), '..', 'results')
os.makedirs(RESULTS_DIR, exist_ok=True)
# ============================================================
# Step 1: Scan raw data and extract v2 features
# ============================================================
def scan_raw_data():
"""Scan raw data directories for audio files with proper labels."""
raw_human = os.path.join(DATA_DIR, 'raw', 'human')
raw_ai = os.path.join(DATA_DIR, 'raw', 'ai')
records = []
# Scan human files
if os.path.exists(raw_human):
for root, dirs, files in os.walk(raw_human):
for f in files:
if f.endswith(('.flac', '.wav', '.mp3', '.ogg')):
full_path = os.path.join(root, f)
# Extract language from filename or path
lang = 'unknown'
for code in ['en', 'ta', 'hi', 'ml', 'te']:
if f'_{code}_' in f or f'/{code}/' in root or f'\\{code}\\' in root:
lang = code
break
records.append({
'path': full_path,
'filename': f,
'label': 'human',
'language': lang,
})
# Scan AI files
if os.path.exists(raw_ai):
for root, dirs, files in os.walk(raw_ai):
for f in files:
if f.endswith(('.flac', '.wav', '.mp3', '.ogg')):
full_path = os.path.join(root, f)
lang = 'unknown'
for code in ['en', 'ta', 'hi', 'ml', 'te']:
if f'_{code}_' in f or f'/{code}/' in root or f'\\{code}\\' in root:
lang = code
break
# Detect TTS source from filename
source = 'unknown'
if 'edge' in f.lower():
source = 'edge_tts'
elif 'gtts' in f.lower():
source = 'gtts'
records.append({
'path': full_path,
'filename': f,
'label': 'ai',
'language': lang,
'source': source,
})
df = pd.DataFrame(records)
print(f"Found {len(df)} raw audio files:")
print(f" Human: {(df['label']=='human').sum()}")
print(f" AI: {(df['label']=='ai').sum()}")
print(f" Languages: {df['language'].value_counts().to_dict()}")
return df
def extract_features_batch(df, cache_path=None):
"""Extract v2 DSP features from all audio files."""
# Check cache
if cache_path and os.path.exists(cache_path):
print(f"Loading cached features from {cache_path}")
return pd.read_csv(cache_path)
feature_list = []
failed = []
print(f"\nExtracting v2 DSP features from {len(df)} files...")
for idx, row in tqdm(df.iterrows(), total=len(df)):
features = extract_all_features_v2(row['path'])
if features is not None:
features['filename'] = row['filename']
features['label'] = row['label']
features['language'] = row['language']
feature_list.append(features)
else:
failed.append(row['path'])
feature_df = pd.DataFrame(feature_list)
print(f" Extracted: {len(feature_list)} / {len(df)}")
print(f" Failed: {len(failed)}")
# Cache results
if cache_path:
os.makedirs(os.path.dirname(cache_path), exist_ok=True)
feature_df.to_csv(cache_path, index=False)
print(f" Cached to: {cache_path}")
return feature_df
# ============================================================
# Step 2: Prepare data for training
# ============================================================
def prepare_data(feature_df):
"""Split features into X, y and train/test sets."""
# Feature columns = everything except metadata
meta_cols = ['filename', 'label', 'language', 'source']
feature_cols = [c for c in feature_df.columns if c not in meta_cols]
X = feature_df[feature_cols].values
y = (feature_df['label'] == 'ai').astype(int).values
languages = feature_df['language'].values
# Replace any remaining NaN/Inf
X = np.nan_to_num(X, nan=0.0, posinf=0.0, neginf=0.0)
# Stratified split
splitter = StratifiedShuffleSplit(n_splits=1, test_size=0.2, random_state=42)
train_idx, test_idx = next(splitter.split(X, y))
X_train, X_test = X[train_idx], X[test_idx]
y_train, y_test = y[train_idx], y[test_idx]
langs_test = languages[test_idx]
print(f"\nData split:")
print(f" Train: {len(X_train)} (human={sum(y_train==0)}, ai={sum(y_train==1)})")
print(f" Test: {len(X_test)} (human={sum(y_test==0)}, ai={sum(y_test==1)})")
print(f" Features: {X_train.shape[1]}")
return X_train, X_test, y_train, y_test, feature_cols, langs_test
# ============================================================
# Step 3: Train and compare classifiers
# ============================================================
def train_classifiers(X_train, y_train, X_test, y_test):
"""Train multiple classifiers and return results."""
# Calculate class weights for imbalanced data
n_human = sum(y_train == 0)
n_ai = sum(y_train == 1)
scale_pos_weight = n_human / max(n_ai, 1)
class_weight = {0: 1.0, 1: scale_pos_weight}
print(f"\nClass weight for AI: {scale_pos_weight:.2f} (compensating for {n_human} human vs {n_ai} ai)")
candidates = {}
# 1. Random Forest (with class weight)
print("\n--- Random Forest ---")
rf = Pipeline([
('scaler', StandardScaler()),
('clf', RandomForestClassifier(
n_estimators=200,
max_depth=15,
min_samples_split=5,
min_samples_leaf=2,
class_weight=class_weight,
random_state=42,
n_jobs=-1
))
])
rf.fit(X_train, y_train)
candidates['RandomForest'] = rf
# 2. Gradient Boosting
print("--- Gradient Boosting ---")
gb = Pipeline([
('scaler', StandardScaler()),
('clf', GradientBoostingClassifier(
n_estimators=200,
max_depth=6,
learning_rate=0.1,
min_samples_split=5,
subsample=0.8,
random_state=42
))
])
gb.fit(X_train, y_train)
candidates['GradientBoosting'] = gb
# 3. XGBoost (if available)
if HAS_XGB:
print("--- XGBoost ---")
xgb_clf = Pipeline([
('scaler', StandardScaler()),
('clf', xgb.XGBClassifier(
n_estimators=200,
max_depth=6,
learning_rate=0.1,
scale_pos_weight=scale_pos_weight,
eval_metric='logloss',
random_state=42,
n_jobs=-1
))
])
xgb_clf.fit(X_train, y_train)
candidates['XGBoost'] = xgb_clf
# 4. LightGBM (if available)
if HAS_LGB:
print("--- LightGBM ---")
lgb_clf = Pipeline([
('scaler', StandardScaler()),
('clf', lgb.LGBMClassifier(
n_estimators=200,
max_depth=6,
learning_rate=0.1,
scale_pos_weight=scale_pos_weight,
random_state=42,
n_jobs=-1,
verbose=-1
))
])
lgb_clf.fit(X_train, y_train)
candidates['LightGBM'] = lgb_clf
# Evaluate all
results = []
for name, model in candidates.items():
y_pred = model.predict(X_test)
y_prob = model.predict_proba(X_test)[:, 1]
acc = accuracy_score(y_test, y_pred)
f1 = f1_score(y_test, y_pred)
auc = roc_auc_score(y_test, y_prob) if len(np.unique(y_test)) > 1 else 0
prec = precision_score(y_test, y_pred, zero_division=0)
rec = recall_score(y_test, y_pred, zero_division=0)
results.append({
'model': name,
'accuracy': acc,
'f1': f1,
'auc': auc,
'precision': prec,
'recall': rec,
})
print(f" {name:20s} | Acc: {acc:.4f} | F1: {f1:.4f} | AUC: {auc:.4f} | Prec: {prec:.4f} | Rec: {rec:.4f}")
results_df = pd.DataFrame(results)
# Pick best by F1
best_name = results_df.loc[results_df['f1'].idxmax(), 'model']
best_model = candidates[best_name]
print(f"\n* Best model: {best_name} (F1: {results_df.loc[results_df['f1'].idxmax(), 'f1']:.4f})")
return best_model, best_name, candidates, results_df
# ============================================================
# Step 4: Hyperparameter tuning (optional, if Optuna available)
# ============================================================
def tune_best_model(X_train, y_train, X_test, y_test, best_name):
"""Tune the best model with Optuna."""
if not HAS_OPTUNA:
print("Optuna not available, skipping hyperparameter tuning.")
return None
print(f"\nTuning {best_name} with Optuna (30 trials)...")
n_human = sum(y_train == 0)
n_ai = sum(y_train == 1)
scale_pos_weight = n_human / max(n_ai, 1)
def objective(trial):
if best_name in ('XGBoost', 'LightGBM', 'GradientBoosting'):
params = {
'n_estimators': trial.suggest_int('n_estimators', 100, 500),
'max_depth': trial.suggest_int('max_depth', 3, 12),
'learning_rate': trial.suggest_float('learning_rate', 0.01, 0.3, log=True),
'subsample': trial.suggest_float('subsample', 0.6, 1.0),
'min_child_weight': trial.suggest_int('min_child_weight', 1, 10),
}
if best_name == 'XGBoost' and HAS_XGB:
clf = xgb.XGBClassifier(
**params,
scale_pos_weight=scale_pos_weight,
eval_metric='logloss',
random_state=42,
n_jobs=-1
)
elif best_name == 'LightGBM' and HAS_LGB:
clf = lgb.LGBMClassifier(
**{k: v for k, v in params.items() if k != 'min_child_weight'},
scale_pos_weight=scale_pos_weight,
random_state=42,
n_jobs=-1,
verbose=-1
)
else:
clf = GradientBoostingClassifier(
n_estimators=params['n_estimators'],
max_depth=params['max_depth'],
learning_rate=params['learning_rate'],
subsample=params['subsample'],
random_state=42
)
else:
# Random Forest
params = {
'n_estimators': trial.suggest_int('n_estimators', 100, 500),
'max_depth': trial.suggest_int('max_depth', 5, 25),
'min_samples_split': trial.suggest_int('min_samples_split', 2, 15),
'min_samples_leaf': trial.suggest_int('min_samples_leaf', 1, 8),
}
clf = RandomForestClassifier(
**params,
class_weight={0: 1.0, 1: scale_pos_weight},
random_state=42,
n_jobs=-1
)
pipe = Pipeline([('scaler', StandardScaler()), ('clf', clf)])
# Cross-validation on training data
scores = cross_val_score(pipe, X_train, y_train, cv=5, scoring='f1', n_jobs=-1)
return scores.mean()
# Suppress Optuna logs
optuna.logging.set_verbosity(optuna.logging.WARNING)
study = optuna.create_study(direction='maximize')
study.optimize(objective, n_trials=30, show_progress_bar=True)
print(f" Best trial F1: {study.best_value:.4f}")
print(f" Best params: {study.best_params}")
return study.best_params
# ============================================================
# Step 5: Calibrate and save
# ============================================================
def calibrate_and_save(best_model, best_name, X_train, y_train, X_test, y_test,
feature_cols, langs_test, results_df, tuned_params=None):
"""Calibrate the best model's probabilities and save everything."""
# If we have tuned params, retrain with them
if tuned_params is not None:
print(f"\nRetraining {best_name} with tuned parameters...")
n_human = sum(y_train == 0)
n_ai = sum(y_train == 1)
scale_pos_weight = n_human / max(n_ai, 1)
if best_name == 'XGBoost' and HAS_XGB:
clf = xgb.XGBClassifier(
**tuned_params, scale_pos_weight=scale_pos_weight,
eval_metric='logloss', random_state=42, n_jobs=-1
)
elif best_name == 'LightGBM' and HAS_LGB:
clf = lgb.LGBMClassifier(
**tuned_params, scale_pos_weight=scale_pos_weight,
random_state=42, n_jobs=-1, verbose=-1
)
elif best_name == 'RandomForest':
clf = RandomForestClassifier(
**tuned_params, class_weight={0: 1.0, 1: scale_pos_weight},
random_state=42, n_jobs=-1
)
else:
clf = GradientBoostingClassifier(**tuned_params, random_state=42)
best_model = Pipeline([('scaler', StandardScaler()), ('clf', clf)])
best_model.fit(X_train, y_train)
# Calibrate probabilities
print("\nCalibrating probabilities (isotonic)...")
calibrated = CalibratedClassifierCV(best_model, method='isotonic', cv=5)
calibrated.fit(X_train, y_train)
# Final evaluation
y_pred = calibrated.predict(X_test)
y_prob = calibrated.predict_proba(X_test)[:, 1]
acc = accuracy_score(y_test, y_pred)
f1 = f1_score(y_test, y_pred)
auc = roc_auc_score(y_test, y_prob) if len(np.unique(y_test)) > 1 else 0
print(f"\n{'='*60}")
print(f"FINAL CALIBRATED MODEL RESULTS")
print(f"{'='*60}")
print(f" Model: {best_name} (calibrated)")
print(f" Accuracy: {acc:.4f} ({acc*100:.1f}%)")
print(f" F1: {f1:.4f}")
print(f" AUC: {auc:.4f}")
print(f"\n{classification_report(y_test, y_pred, target_names=['HUMAN', 'AI'])}")
# Confusion matrix
cm = confusion_matrix(y_test, y_pred)
print(f"Confusion Matrix:")
print(f" Predicted")
print(f" HUMAN AI")
print(f"Actual HUMAN {cm[0,0]:3d} {cm[0,1]:3d}")
print(f"Actual AI {cm[1,0]:3d} {cm[1,1]:3d}")
# Per-language accuracy
lang_names = {'en': 'English', 'ta': 'Tamil', 'hi': 'Hindi', 'ml': 'Malayalam', 'te': 'Telugu'}
print(f"\nPer-Language Accuracy:")
for lang_code in ['en', 'ta', 'hi', 'ml', 'te']:
mask = langs_test == lang_code
if mask.sum() > 0:
lang_acc = accuracy_score(y_test[mask], y_pred[mask])
print(f" {lang_names.get(lang_code, lang_code):12s}: {lang_acc*100:.1f}% ({mask.sum()} samples)")
# Save model
model_path = os.path.join(MODELS_DIR, 'dsp_model_v2.pkl')
cols_path = os.path.join(MODELS_DIR, 'dsp_cols_v2.pkl')
joblib.dump(calibrated, model_path)
joblib.dump(feature_cols, cols_path)
print(f"\n Model saved: {model_path}")
print(f" Columns saved: {cols_path}")
# Save results
results_path = os.path.join(RESULTS_DIR, 'dsp_v2_results.json')
results_data = {
'model': best_name,
'accuracy': float(acc),
'f1': float(f1),
'auc': float(auc),
'n_features': len(feature_cols),
'n_train': len(y_train),
'n_test': len(y_test),
'confusion_matrix': cm.tolist(),
'feature_columns': feature_cols,
}
with open(results_path, 'w') as f:
json.dump(results_data, f, indent=2)
# Save comparison results
results_df.to_csv(os.path.join(RESULTS_DIR, 'dsp_v2_model_comparison.csv'), index=False)
print(f" Results saved: {results_path}")
return calibrated
# ============================================================
# Main
# ============================================================
def main():
print("=" * 60)
print("DSP CLASSIFIER v2 — TRAINING PIPELINE")
print("=" * 60)
# Step 1: Scan raw data
print("\n[1/5] Scanning raw data...")
raw_df = scan_raw_data()
if len(raw_df) == 0:
print("No audio files found! Make sure data/raw/ has human/ and ai/ subdirs.")
return
# Step 2: Extract v2 features (with caching)
print("\n[2/5] Extracting v2 DSP features...")
cache_path = os.path.join(DATA_DIR, 'features', 'dsp_features_v2.csv')
feature_df = extract_features_batch(raw_df, cache_path=cache_path)
# Step 3: Prepare data
print("\n[3/5] Preparing train/test split...")
X_train, X_test, y_train, y_test, feature_cols, langs_test = prepare_data(feature_df)
# Step 4: Train and compare classifiers
print("\n[4/5] Training classifiers...")
best_model, best_name, all_models, results_df = train_classifiers(
X_train, y_train, X_test, y_test
)
# Step 4b: Hyperparameter tuning (optional)
tuned_params = tune_best_model(X_train, y_train, X_test, y_test, best_name)
# Step 5: Calibrate and save
print("\n[5/5] Calibrating and saving...")
final_model = calibrate_and_save(
best_model, best_name, X_train, y_train, X_test, y_test,
feature_cols, langs_test, results_df, tuned_params
)
print("\n" + "=" * 60)
print("TRAINING COMPLETE!")
print("=" * 60)
if __name__ == "__main__":
main()