Spaces:
Running on Zero
Running on Zero
| """ | |
| Dataset-specific evaluation for Model V2. | |
| This script evaluates the trained Model V2 separately on: | |
| 1. CREMA-D test samples | |
| 2. RAVDESS test samples | |
| 3. Combined test samples | |
| Why: | |
| Combined accuracy can hide dataset-specific weakness. Since Model V2 was trained | |
| on CREMA-D + RAVDESS, we need to know whether it performs equally well on both | |
| datasets or whether one dataset is hurting the score. | |
| Run from ml-services: | |
| python -m src.evaluation.evaluate_model_by_dataset | |
| """ | |
| import json | |
| from pathlib import Path | |
| from typing import Dict, List, Optional | |
| import numpy as np | |
| import pandas as pd | |
| import torch | |
| from sklearn.metrics import accuracy_score, classification_report, confusion_matrix, f1_score | |
| from torch.utils.data import DataLoader | |
| from transformers import Wav2Vec2ForSequenceClassification, Wav2Vec2Processor | |
| from src.data.audio_dataset import ( | |
| DEFAULT_SAMPLE_RATE, | |
| build_label_encoding, | |
| load_audio_file, | |
| load_metadata, | |
| resolve_audio_path, | |
| ) | |
| from src.models.wav2vec2_emotion_model import Wav2Vec2DataCollator, Wav2Vec2EmotionDataset | |
| PROJECT_ROOT = Path(__file__).resolve().parents[3] | |
| ML_SERVICES_ROOT = PROJECT_ROOT / "ml-services" | |
| MODEL_DIR = ML_SERVICES_ROOT / "outputs" / "wav2vec2" / "model_v2_cremad_ravdess" / "best_model" | |
| METADATA_PATH = ML_SERVICES_ROOT / "data" / "processed" / "combined_emotion_metadata.csv" | |
| REPORTS_DIR = ML_SERVICES_ROOT / "outputs" / "reports" | |
| DATASET_SPECIFIC_REPORT_PATH = REPORTS_DIR / "model_v2_dataset_specific_evaluation.json" | |
| DATASET_SPECIFIC_MD_PATH = REPORTS_DIR / "model_v2_dataset_specific_evaluation.md" | |
| CREMAD_CONFUSION_MATRIX_PATH = REPORTS_DIR / "model_v2_cremad_only_confusion_matrix.csv" | |
| RAVDESS_CONFUSION_MATRIX_PATH = REPORTS_DIR / "model_v2_ravdess_only_confusion_matrix.csv" | |
| COMBINED_CONFUSION_MATRIX_PATH = REPORTS_DIR / "model_v2_combined_recomputed_confusion_matrix.csv" | |
| BATCH_SIZE = 4 | |
| MAX_DURATION_SECONDS = 6.0 | |
| def get_device() -> torch.device: | |
| """ | |
| Select available device. | |
| """ | |
| if torch.cuda.is_available(): | |
| return torch.device("cuda") | |
| if hasattr(torch.backends, "mps") and torch.backends.mps.is_available(): | |
| return torch.device("mps") | |
| return torch.device("cpu") | |
| def get_device_note(device: torch.device) -> str: | |
| if device.type == "cuda": | |
| return "CUDA GPU available" | |
| if device.type == "mps": | |
| return "Apple Silicon MPS available" | |
| return "CPU only" | |
| def create_subset_dataset( | |
| metadata: pd.DataFrame, | |
| dataset_name: Optional[str], | |
| label_to_id: Dict[str, int], | |
| ) -> Wav2Vec2EmotionDataset: | |
| """ | |
| Create a test dataset subset. | |
| If dataset_name is None, use all test samples. | |
| Otherwise, filter by dataset column. | |
| """ | |
| test_df = metadata[metadata["split"] == "test"].copy() | |
| if dataset_name is not None: | |
| test_df = test_df[test_df["dataset"] == dataset_name].copy() | |
| if test_df.empty: | |
| raise ValueError(f"No test samples found for dataset_name={dataset_name}") | |
| return Wav2Vec2EmotionDataset( | |
| metadata=test_df, | |
| label_to_id=label_to_id, | |
| sample_rate=DEFAULT_SAMPLE_RATE, | |
| max_duration_seconds=MAX_DURATION_SECONDS, | |
| ) | |
| def evaluate_dataset( | |
| model: Wav2Vec2ForSequenceClassification, | |
| processor: Wav2Vec2Processor, | |
| dataset: Wav2Vec2EmotionDataset, | |
| id_to_label: Dict[int, str], | |
| device: torch.device, | |
| confusion_matrix_path: Path, | |
| ) -> Dict: | |
| """ | |
| Evaluate model on one dataset subset. | |
| """ | |
| collator = Wav2Vec2DataCollator(processor=processor) | |
| dataloader = DataLoader( | |
| dataset, | |
| batch_size=BATCH_SIZE, | |
| shuffle=False, | |
| collate_fn=collator, | |
| ) | |
| model.eval() | |
| all_predictions: List[int] = [] | |
| all_labels: List[int] = [] | |
| with torch.no_grad(): | |
| for batch in dataloader: | |
| labels = batch.pop("labels") | |
| batch = {key: value.to(device) for key, value in batch.items()} | |
| labels = labels.to(device) | |
| outputs = model(**batch) | |
| predictions = torch.argmax(outputs.logits, dim=-1) | |
| all_predictions.extend(predictions.detach().cpu().numpy().tolist()) | |
| all_labels.extend(labels.detach().cpu().numpy().tolist()) | |
| label_ids = sorted(id_to_label.keys()) | |
| target_names = [id_to_label[index] for index in label_ids] | |
| accuracy = accuracy_score(all_labels, all_predictions) | |
| macro_f1 = f1_score( | |
| all_labels, | |
| all_predictions, | |
| labels=label_ids, | |
| average="macro", | |
| zero_division=0, | |
| ) | |
| weighted_f1 = f1_score( | |
| all_labels, | |
| all_predictions, | |
| labels=label_ids, | |
| average="weighted", | |
| zero_division=0, | |
| ) | |
| report = classification_report( | |
| all_labels, | |
| all_predictions, | |
| labels=label_ids, | |
| target_names=target_names, | |
| output_dict=True, | |
| zero_division=0, | |
| ) | |
| matrix = confusion_matrix( | |
| all_labels, | |
| all_predictions, | |
| labels=label_ids, | |
| ) | |
| matrix_df = pd.DataFrame( | |
| matrix, | |
| index=[f"actual_{label}" for label in target_names], | |
| columns=[f"predicted_{label}" for label in target_names], | |
| ) | |
| confusion_matrix_path.parent.mkdir(parents=True, exist_ok=True) | |
| matrix_df.to_csv(confusion_matrix_path) | |
| return { | |
| "samples": len(dataset), | |
| "accuracy": float(accuracy), | |
| "macro_f1": float(macro_f1), | |
| "weighted_f1": float(weighted_f1), | |
| "classification_report": report, | |
| "confusion_matrix_path": str(confusion_matrix_path), | |
| } | |
| def create_markdown_report(results: Dict) -> str: | |
| lines = [] | |
| lines.append("# Model V2 Dataset-Specific Evaluation") | |
| lines.append("") | |
| lines.append("## Summary") | |
| lines.append("") | |
| lines.append( | |
| "This report evaluates Model V2 separately on CREMA-D and RAVDESS test samples. " | |
| "The goal is to identify whether the combined test score hides dataset-specific weakness." | |
| ) | |
| lines.append("") | |
| lines.append("## Metrics") | |
| lines.append("") | |
| lines.append("| Dataset | Samples | Accuracy | Macro F1 | Weighted F1 |") | |
| lines.append("|---|---:|---:|---:|---:|") | |
| for dataset_key in ["CREMA-D", "RAVDESS", "Combined"]: | |
| item = results["dataset_results"][dataset_key] | |
| lines.append( | |
| f"| {dataset_key} | {item['samples']} | " | |
| f"{item['accuracy'] * 100:.2f}% | " | |
| f"{item['macro_f1'] * 100:.2f}% | " | |
| f"{item['weighted_f1'] * 100:.2f}% |" | |
| ) | |
| lines.append("") | |
| lines.append("## Interpretation") | |
| lines.append("") | |
| cremad_acc = results["dataset_results"]["CREMA-D"]["accuracy"] | |
| ravdess_acc = results["dataset_results"]["RAVDESS"]["accuracy"] | |
| if cremad_acc > ravdess_acc: | |
| lines.append( | |
| "Model V2 performs better on CREMA-D than on RAVDESS. This suggests that the model " | |
| "may still be more adapted to CREMA-D even after adding RAVDESS." | |
| ) | |
| elif ravdess_acc > cremad_acc: | |
| lines.append( | |
| "Model V2 performs better on RAVDESS than on CREMA-D. This suggests that adding " | |
| "RAVDESS helped the model learn patterns that transfer well to that dataset, but " | |
| "we need to check if CREMA-D performance dropped." | |
| ) | |
| else: | |
| lines.append( | |
| "Model V2 performs similarly on CREMA-D and RAVDESS, which suggests balanced generalization." | |
| ) | |
| lines.append("") | |
| lines.append("## Next Improvement Direction") | |
| lines.append("") | |
| lines.append( | |
| "Use this dataset-specific result to decide whether Model V3 should focus on " | |
| "hyperparameter tuning, augmentation, partial unfreezing, or dataset balancing." | |
| ) | |
| lines.append("") | |
| return "\n".join(lines) | |
| def main() -> None: | |
| if not MODEL_DIR.exists(): | |
| raise FileNotFoundError(f"Missing trained model directory: {MODEL_DIR}") | |
| if not METADATA_PATH.exists(): | |
| raise FileNotFoundError(f"Missing combined metadata: {METADATA_PATH}") | |
| device = get_device() | |
| print("\nLoading Model V2 for dataset-specific evaluation") | |
| print("-" * 80) | |
| print(f"Model directory: {MODEL_DIR}") | |
| print(f"Metadata path: {METADATA_PATH}") | |
| print(f"Device: {get_device_note(device)}") | |
| print("-" * 80) | |
| metadata = load_metadata(METADATA_PATH) | |
| label_encoding = build_label_encoding(task="emotion") | |
| processor = Wav2Vec2Processor.from_pretrained(str(MODEL_DIR)) | |
| model = Wav2Vec2ForSequenceClassification.from_pretrained(str(MODEL_DIR)) | |
| model.to(device) | |
| cremad_dataset = create_subset_dataset( | |
| metadata=metadata, | |
| dataset_name="CREMA-D", | |
| label_to_id=label_encoding.label_to_id, | |
| ) | |
| ravdess_dataset = create_subset_dataset( | |
| metadata=metadata, | |
| dataset_name="RAVDESS", | |
| label_to_id=label_encoding.label_to_id, | |
| ) | |
| combined_dataset = create_subset_dataset( | |
| metadata=metadata, | |
| dataset_name=None, | |
| label_to_id=label_encoding.label_to_id, | |
| ) | |
| results = { | |
| "model_version": "model_v2", | |
| "run_name": "model_v2_cremad_ravdess", | |
| "model_dir": str(MODEL_DIR), | |
| "metadata_path": str(METADATA_PATH), | |
| "device": get_device_note(device), | |
| "dataset_results": { | |
| "CREMA-D": evaluate_dataset( | |
| model=model, | |
| processor=processor, | |
| dataset=cremad_dataset, | |
| id_to_label=label_encoding.id_to_label, | |
| device=device, | |
| confusion_matrix_path=CREMAD_CONFUSION_MATRIX_PATH, | |
| ), | |
| "RAVDESS": evaluate_dataset( | |
| model=model, | |
| processor=processor, | |
| dataset=ravdess_dataset, | |
| id_to_label=label_encoding.id_to_label, | |
| device=device, | |
| confusion_matrix_path=RAVDESS_CONFUSION_MATRIX_PATH, | |
| ), | |
| "Combined": evaluate_dataset( | |
| model=model, | |
| processor=processor, | |
| dataset=combined_dataset, | |
| id_to_label=label_encoding.id_to_label, | |
| device=device, | |
| confusion_matrix_path=COMBINED_CONFUSION_MATRIX_PATH, | |
| ), | |
| }, | |
| } | |
| with DATASET_SPECIFIC_REPORT_PATH.open("w", encoding="utf-8") as file: | |
| json.dump(results, file, indent=2) | |
| markdown = create_markdown_report(results) | |
| with DATASET_SPECIFIC_MD_PATH.open("w", encoding="utf-8") as file: | |
| file.write(markdown) | |
| print("\nModel V2 Dataset-Specific Evaluation") | |
| print("-" * 80) | |
| for dataset_key, result in results["dataset_results"].items(): | |
| print( | |
| f"{dataset_key}: " | |
| f"samples={result['samples']}, " | |
| f"accuracy={result['accuracy']:.4f}, " | |
| f"macro_f1={result['macro_f1']:.4f}, " | |
| f"weighted_f1={result['weighted_f1']:.4f}" | |
| ) | |
| print("-" * 80) | |
| print(f"Saved JSON report to: {DATASET_SPECIFIC_REPORT_PATH}") | |
| print(f"Saved Markdown report to: {DATASET_SPECIFIC_MD_PATH}") | |
| if __name__ == "__main__": | |
| main() |