Spaces:
Running on Zero
Running on Zero
Download src/data/split.py from SAJIDIUBAT/bangla-ser-api: direct link, hf CLI and curl.
- Browser
- Download file 7.34 kB
-
https://huggingface.co/spaces/SAJIDIUBAT/bangla-ser-api/resolve/main/src/data/split.py
- Command line
-
hf download hf://spaces/SAJIDIUBAT/bangla-ser-api/src/data/split.py
-
curl -L -o split.py https://huggingface.co/spaces/SAJIDIUBAT/bangla-ser-api/resolve/main/src/data/split.py
7.34 kB
| """Data Splitting and Metadata Management with Data Leakage Protection. | |
| Enforces: | |
| 1. Exact Duplicate Quarantine: Identical audio clips (all 22 duplicate clusters) | |
| are strictly confined to the TRAIN set to prevent any data leakage into Validation or Test. | |
| 2. Stratified 70/15/15 Splitting: Ensures balanced class distribution across Train, Validation, and Test. | |
| 3. Cryptographic Hash Lock: Generates splits_v1.json with a SHA-256 hash for strict reproducibility. | |
| 4. Generates dataset/metadata/metadata.csv with all established fields. | |
| """ | |
| import os | |
| import json | |
| import hashlib | |
| from pathlib import Path | |
| import pandas as pd | |
| import numpy as np | |
| from src.utils.config import ( | |
| RAW_DATA_DIR, | |
| PROCESSED_DATA_DIR, | |
| METADATA_DIR, | |
| RANDOM_SEED, | |
| EMOTION_CLASSES, | |
| PROJECT_ROOT | |
| ) | |
| def create_splits_and_metadata(): | |
| print("=" * 70) | |
| print("STARTING DATA SPLITTING & LEAKAGE PROTECTION") | |
| print(f"Random Seed: {RANDOM_SEED}") | |
| print("=" * 70) | |
| # 1. Load the comprehensive dataset audit CSV | |
| audit_csv_path = METADATA_DIR / "dataset_audit.csv" | |
| if not audit_csv_path.exists(): | |
| audit_csv_path = PROJECT_ROOT / "dataset_audit.csv" | |
| df = pd.read_csv(audit_csv_path) | |
| print(f"Loaded audit records for {len(df)} files.") | |
| # 2. Add processed file path and verify existence | |
| df["processed_file_path"] = df.apply( | |
| lambda row: str(PROCESSED_DATA_DIR / row["emotion"] / row["filename"]), | |
| axis=1 | |
| ) | |
| missing_processed = [p for p in df["processed_file_path"] if not os.path.exists(p)] | |
| if missing_processed: | |
| raise FileNotFoundError(f"Missing {len(missing_processed)} processed files! Run standardization first.") | |
| print("Verified all 5,000 processed files exist in dataset/processed/.") | |
| # 3. Identify duplicate audio files that MUST be quarantined to TRAIN | |
| # Any clip that belongs to a duplicate cluster goes to Train to prevent test leakage | |
| duplicate_mask = df["is_duplicate"] == True | |
| duplicate_df = df[duplicate_mask] | |
| non_duplicate_df = df[~duplicate_mask] | |
| print(f"Exact duplicate files to quarantine to TRAIN: {len(duplicate_df)} files ({df['duplicate_cluster_id'].nunique() - 1} clusters)") | |
| print(f"Unique files available for stratified partitioning: {len(non_duplicate_df)} files") | |
| # 4. Stratified Split on non-duplicate pool: | |
| # Target overall: Train ~70% (3500), Val ~15% (750), Test ~15% (750) | |
| # Since 44 duplicate files go into Train, remaining quota for Train is 3500 - 44 = 3456. | |
| # Total unique files = 4956. | |
| # Val = 750 (approx 150 per class) | |
| # Test = 750 (approx 150 per class) | |
| # Train unique = 4956 - 1500 = 3456 | |
| np.random.seed(RANDOM_SEED) | |
| train_indices = list(duplicate_df.index) | |
| val_indices = [] | |
| test_indices = [] | |
| for emotion in EMOTION_CLASSES: | |
| emo_indices = non_duplicate_df[non_duplicate_df["emotion"] == emotion].index.tolist() | |
| np.random.shuffle(emo_indices) | |
| n_val = 150 | |
| n_test = 150 | |
| emo_val = emo_indices[:n_val] | |
| emo_test = emo_indices[n_val:n_val + n_test] | |
| emo_train = emo_indices[n_val + n_test:] | |
| val_indices.extend(emo_val) | |
| test_indices.extend(emo_test) | |
| train_indices.extend(emo_train) | |
| train_indices.sort() | |
| val_indices.sort() | |
| test_indices.sort() | |
| df.loc[train_indices, "split"] = "train" | |
| df.loc[val_indices, "split"] = "val" | |
| df.loc[test_indices, "split"] = "test" | |
| # Add source, group_id, speaker_id explicitly documenting limitation | |
| # As mandated by prompt: "Only include fields that can actually be established. Do not invent speaker IDs or source IDs." | |
| df["source"] = "BASE_multimedia_corpus" | |
| df["group_id"] = df["duplicate_cluster_id"].apply(lambda x: x if pd.notna(x) and x != "" else "independent") | |
| df["speaker_id"] = "not_annotated" | |
| # Verify duplicate leakage | |
| val_hashes = set(df.loc[val_indices, "md5_hash"]) | |
| test_hashes = set(df.loc[test_indices, "md5_hash"]) | |
| train_hashes = set(df.loc[train_indices, "md5_hash"]) | |
| train_val_overlap = train_hashes.intersection(val_hashes) | |
| train_test_overlap = train_hashes.intersection(test_hashes) | |
| val_test_overlap = val_hashes.intersection(test_hashes) | |
| print("\n--- DATA LEAKAGE VERIFICATION ---") | |
| print(f"Identical Hash Overlap Train-Test: {len(train_test_overlap)} (Must be 0)") | |
| print(f"Identical Hash Overlap Train-Val: {len(train_val_overlap)} (Must be 0)") | |
| print(f"Identical Hash Overlap Val-Test: {len(val_test_overlap)} (Must be 0)") | |
| assert len(train_test_overlap) == 0, "CRITICAL ERROR: Data leakage detected between Train and Test!" | |
| assert len(train_val_overlap) == 0, "CRITICAL ERROR: Data leakage detected between Train and Val!" | |
| assert len(val_test_overlap) == 0, "CRITICAL ERROR: Data leakage detected between Val and Test!" | |
| print("PASSED: Zero audio duplicate leakage across splits!") | |
| # Print split summary | |
| print("\n--- SPLIT DISTRIBUTION ---") | |
| split_summary = df.groupby(["split", "emotion"]).size().unstack(fill_value=0) | |
| split_summary["Total"] = split_summary.sum(axis=1) | |
| print(split_summary) | |
| # 5. Save metadata.csv | |
| out_cols = [ | |
| "audio_id", | |
| "filename", | |
| "file_path", | |
| "processed_file_path", | |
| "emotion", | |
| "duration_sec", | |
| "sample_rate", | |
| "channels", | |
| "bit_depth", | |
| "silence_percentage", | |
| "source", | |
| "group_id", | |
| "speaker_id", | |
| "quality_status", | |
| "is_duplicate", | |
| "split" | |
| ] | |
| metadata_csv_path = METADATA_DIR / "metadata.csv" | |
| metadata_csv_root = PROJECT_ROOT / "metadata.csv" | |
| df[out_cols].to_csv(metadata_csv_path, index=False) | |
| df[out_cols].to_csv(metadata_csv_root, index=False) | |
| print(f"\nSaved metadata.csv to {metadata_csv_path}") | |
| # 6. Save splits_v1.json | |
| splits_dict = { | |
| "split_version": "v1.0", | |
| "random_seed": RANDOM_SEED, | |
| "strategy": "Stratified 70/15/15 with Exact Duplicate Quarantine to Train", | |
| "split_counts": { | |
| "train": len(train_indices), | |
| "val": len(val_indices), | |
| "test": len(test_indices), | |
| "total": len(df) | |
| }, | |
| "class_breakdown": { | |
| split: df[df["split"] == split]["emotion"].value_counts().to_dict() | |
| for split in ["train", "val", "test"] | |
| }, | |
| "train_audio_ids": df.loc[train_indices, "audio_id"].tolist(), | |
| "val_audio_ids": df.loc[val_indices, "audio_id"].tolist(), | |
| "test_audio_ids": df.loc[test_indices, "audio_id"].tolist() | |
| } | |
| splits_json_str = json.dumps(splits_dict, indent=2) | |
| splits_sha256 = hashlib.sha256(splits_json_str.encode("utf-8")).hexdigest() | |
| splits_dict["sha256_checksum"] = splits_sha256 | |
| splits_json_path = METADATA_DIR / "splits_v1.json" | |
| splits_json_root = PROJECT_ROOT / "splits_v1.json" | |
| with open(splits_json_path, "w", encoding="utf-8") as f: | |
| json.dump(splits_dict, f, indent=2) | |
| with open(splits_json_root, "w", encoding="utf-8") as f: | |
| json.dump(splits_dict, f, indent=2) | |
| print(f"Saved splits_v1.json to {splits_json_path} (SHA-256: {splits_sha256[:16]}...)") | |
| print("=" * 70) | |
| if __name__ == "__main__": | |
| create_splits_and_metadata() | |