File size: 5,433 Bytes
d4d0bc7 c2dd476 d4d0bc7 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 | #!/usr/bin/env python3
"""
train_classifier.py — Train sklearn RandomForest classifier on labelled data.
Usage:
python3 scripts/train_classifier.py
Reads all JSON files from training_data/ directory.
Saves model to training_data/models/instrument_classifier.pkl
"""
import json
import os
import sys
from pathlib import Path
# Allow running from project root
PROJECT_ROOT = Path(__file__).parent.parent
sys.path.insert(0, str(PROJECT_ROOT))
# Data dir is overridable (MAESTRO_TRAINING_DIR) so tests can point at an
# isolated fixture dir instead of the repo's accumulated training_data/.
TRAINING_DIR = Path(os.environ.get("MAESTRO_TRAINING_DIR", PROJECT_ROOT / "training_data"))
MODEL_DIR = TRAINING_DIR / "models"
MODEL_PATH = MODEL_DIR / "instrument_classifier.pkl"
LABEL_ENCODER_PATH = MODEL_DIR / "label_encoder.pkl"
# Feature keys (must match ml_classifier.py)
FEATURE_KEYS = [
"spectral_centroid_hz",
"spectral_rolloff_hz",
"spectral_flatness",
"harmonic_ratio",
"zero_crossing_rate",
"percussive_ratio",
"dynamic_range_db",
"inharmonicity_score",
"tempo_bpm",
"pitch_std_hz",
"attack_sharpness",
]
def extract_feature_vector(features: dict) -> list:
"""Extract a flat feature vector from a features dict."""
vec = []
for key in FEATURE_KEYS:
vec.append(float(features.get(key, 0.0) or 0.0))
# mel_band_stats sub-keys
mel = features.get("mel_band_stats", {}) or {}
for sub in ["mel_sub", "mel_bass", "mel_low_mid", "mel_high_mid", "mel_high"]:
vec.append(float(mel.get(sub, 0.0) or 0.0))
# frequency_bands_db sub-keys
bands = features.get("frequency_bands_db", {}) or {}
for sub in ["sub_bass_db", "bass_db", "low_mid_db", "high_mid_db", "presence_db", "air_db"]:
vec.append(float(bands.get(sub, 0.0) or 0.0))
# mfcc_means — first 13 coefficients
mfcc = features.get("mfcc_means", []) or []
for i in range(13):
vec.append(float(mfcc[i]) if i < len(mfcc) else 0.0)
# spectral_contrast — first 7 bands
sc = features.get("spectral_contrast", []) or []
for i in range(7):
vec.append(float(sc[i]) if i < len(sc) else 0.0)
return vec
def load_training_data():
"""Load all JSON training records from training_data/ directory."""
X = []
y = []
skipped = 0
for json_file in TRAINING_DIR.glob("*.json"):
try:
with open(json_file) as f:
records = json.load(f)
# Support both list of records and single record
if isinstance(records, dict):
records = [records]
for record in records:
try:
features = record.get("features", {})
analysis = record.get("analysis", {})
instruments = analysis.get("instruments", [])
if not instruments:
skipped += 1
continue
label = instruments[0].get("model")
if not label:
skipped += 1
continue
vec = extract_feature_vector(features)
X.append(vec)
y.append(label)
except Exception as e:
skipped += 1
continue
except Exception as e:
print(f"Warning: Could not read {json_file}: {e}")
continue
if skipped > 0:
print(f"Skipped {skipped} records (missing features or labels)")
return X, y
def main():
print("Loading training data...")
X, y = load_training_data()
n_samples = len(X)
print(f"Found {n_samples} labelled samples")
if n_samples < 5:
print(f"Insufficient data ({n_samples} samples). Need at least 5 to train.")
sys.exit(1)
import numpy as np
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import cross_val_score
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import LabelEncoder, StandardScaler
import joblib
X_arr = np.array(X)
le = LabelEncoder()
y_enc = le.fit_transform(y)
n_classes = len(le.classes_)
print(f"Labels: {n_classes} unique instruments")
pipeline = Pipeline([
("scaler", StandardScaler()),
("clf", RandomForestClassifier(n_estimators=100, random_state=42)),
])
# Cross-validation (cv=3 if enough samples)
cv = min(3, n_samples // n_classes) if n_samples >= 3 else 2
cv = max(cv, 2) # at least 2
if n_samples >= cv * n_classes:
try:
scores = cross_val_score(pipeline, X_arr, y_enc, cv=cv, scoring="accuracy")
print(f"Cross-validation accuracy (cv={cv}): {scores.mean():.3f} ± {scores.std():.3f}")
except Exception as e:
print(f"Cross-validation skipped: {e}")
else:
print("Not enough samples for cross-validation, skipping.")
# Train on full data
print("Training final model on all data...")
pipeline.fit(X_arr, y_enc)
# Save model
MODEL_DIR.mkdir(parents=True, exist_ok=True)
joblib.dump(pipeline, MODEL_PATH)
joblib.dump(le, LABEL_ENCODER_PATH)
print(f"Model saved to: {MODEL_PATH}")
print(f"Label encoder saved to: {LABEL_ENCODER_PATH}")
print("Training complete.")
if __name__ == "__main__":
main()
|