Spaces:
Sleeping
Sleeping
| """ | |
| train_audio.py β Training script for AudioDeepfakeDetector (Wav2Vec2) | |
| Compatible dataset formats: | |
| 1. ASVspoof 2019 / 2021 (LA / PA protocols) | |
| 2. Custom folders: | |
| data/audio/ | |
| train/real/ β bonafide .wav / .flac / .mp3 files | |
| train/fake/ β spoofed .wav / .flac / .mp3 files | |
| val/real/ | |
| val/fake/ | |
| Usage: | |
| python train_audio.py \ | |
| --data_dir ../data/audio \ | |
| --epochs 20 \ | |
| --batch_size 16 \ | |
| --lr 1e-4 \ | |
| --save_dir ../models | |
| """ | |
| import argparse, os, time, random | |
| from pathlib import Path | |
| import numpy as np | |
| import torch | |
| import torch.nn as nn | |
| from torch.utils.data import Dataset, DataLoader | |
| from torch.optim import AdamW | |
| from torch.optim.lr_scheduler import CosineAnnealingLR | |
| from sklearn.metrics import roc_auc_score, accuracy_score | |
| try: | |
| import librosa | |
| LIBROSA_AVAILABLE = True | |
| except ImportError: | |
| print("[ERROR] librosa not installed. Run: pip install librosa>=0.10.0") | |
| LIBROSA_AVAILABLE = False | |
| try: | |
| from transformers import Wav2Vec2FeatureExtractor | |
| TRANSFORMERS_AVAILABLE = True | |
| except ImportError: | |
| print("[ERROR] transformers not installed. Run: pip install transformers>=4.30.0") | |
| TRANSFORMERS_AVAILABLE = False | |
| from audio_detector import AudioDeepfakeDetector | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| SAMPLE_RATE = 16_000 | |
| MAX_DURATION_SEC = 6.0 | |
| MAX_SAMPLES = int(MAX_DURATION_SEC * SAMPLE_RATE) | |
| SEED = 42 | |
| def seed_everything(seed: int = SEED): | |
| random.seed(seed) | |
| np.random.seed(seed) | |
| torch.manual_seed(seed) | |
| if torch.cuda.is_available(): | |
| torch.cuda.manual_seed_all(seed) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Dataset | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| AUDIO_EXTS = {".wav", ".flac", ".mp3", ".ogg", ".m4a"} | |
| class AudioFakeDataset(Dataset): | |
| """ | |
| Folder-based audio dataset. | |
| Expects: <root>/<split>/real/ and <root>/<split>/fake/ | |
| """ | |
| def __init__(self, root: str, split: str, feature_extractor): | |
| self.feature_extractor = feature_extractor | |
| self.samples = [] | |
| for label, name in [(0, "real"), (1, "fake")]: | |
| folder = Path(root) / split / name | |
| if not folder.exists(): | |
| print(f"[WARN] Folder not found: {folder}") | |
| continue | |
| for f in folder.rglob("*"): | |
| if f.suffix.lower() in AUDIO_EXTS: | |
| self.samples.append((str(f), label)) | |
| random.shuffle(self.samples) | |
| real_n = sum(1 for _, l in self.samples if l == 0) | |
| fake_n = sum(1 for _, l in self.samples if l == 1) | |
| print(f"[Dataset/{split}] real={real_n} fake={fake_n} total={len(self.samples)}") | |
| def __len__(self): | |
| return len(self.samples) | |
| def __getitem__(self, idx): | |
| path, label = self.samples[idx] | |
| try: | |
| waveform, _ = librosa.load(path, sr=SAMPLE_RATE, mono=True) | |
| except Exception: | |
| waveform = np.zeros(SAMPLE_RATE, dtype=np.float32) | |
| # Pad / truncate | |
| if len(waveform) > MAX_SAMPLES: | |
| start = random.randint(0, len(waveform) - MAX_SAMPLES) | |
| waveform = waveform[start: start + MAX_SAMPLES] | |
| else: | |
| waveform = np.pad(waveform, (0, MAX_SAMPLES - len(waveform))) | |
| inputs = self.feature_extractor( | |
| waveform.astype(np.float32), | |
| sampling_rate=SAMPLE_RATE, | |
| return_tensors="pt", | |
| padding=True, | |
| ) | |
| return inputs.input_values.squeeze(0), torch.tensor(label, dtype=torch.float32) | |
| def collate_fn(batch): | |
| input_values, labels = zip(*batch) | |
| # Pad to the longest sample in the batch | |
| max_len = max(x.shape[0] for x in input_values) | |
| padded = torch.stack([ | |
| torch.nn.functional.pad(x, (0, max_len - x.shape[0])) for x in input_values | |
| ]) | |
| return padded, torch.stack(labels) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Training helpers | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def run_epoch(model, loader, criterion, optimizer, device, training: bool): | |
| model.train(training) | |
| total_loss, all_probs, all_labels = 0.0, [], [] | |
| for input_values, labels in loader: | |
| input_values = input_values.to(device) | |
| labels = labels.to(device) | |
| with torch.set_grad_enabled(training): | |
| logits = model(input_values).squeeze(1) | |
| loss = criterion(logits, labels) | |
| if training: | |
| optimizer.zero_grad() | |
| loss.backward() | |
| nn.utils.clip_grad_norm_(model.parameters(), 1.0) | |
| optimizer.step() | |
| total_loss += loss.item() * len(labels) | |
| probs = torch.sigmoid(logits).detach().cpu().numpy() | |
| all_probs.extend(probs.tolist()) | |
| all_labels.extend(labels.cpu().numpy().tolist()) | |
| avg_loss = total_loss / len(loader.dataset) | |
| preds = [1 if p >= 0.5 else 0 for p in all_probs] | |
| acc = accuracy_score(all_labels, preds) | |
| try: | |
| auc = roc_auc_score(all_labels, all_probs) | |
| except Exception: | |
| auc = 0.5 | |
| return avg_loss, acc, auc | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Main | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def main(args): | |
| if not LIBROSA_AVAILABLE or not TRANSFORMERS_AVAILABLE: | |
| raise SystemExit("Missing dependencies. See error messages above.") | |
| seed_everything() | |
| device = "cuda" if torch.cuda.is_available() else "cpu" | |
| print(f"[Train] Device: {device} | Data: {args.data_dir}") | |
| # ββ Model βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| model = AudioDeepfakeDetector(pretrained=True, freeze_base=True) | |
| model.to(device) | |
| # ββ Feature extractor for dataset βββββββββββββββββββββββββ | |
| feat_ext = model.feature_extractor | |
| # ββ Datasets & loaders ββββββββββββββββββββββββββββββββββββ | |
| train_ds = AudioFakeDataset(args.data_dir, "train", feat_ext) | |
| val_ds = AudioFakeDataset(args.data_dir, "val", feat_ext) | |
| if len(train_ds) == 0: | |
| print("\n[WARN] No training files found!") | |
| print("Expected structure:") | |
| print(" data/audio/train/real/*.wav") | |
| print(" data/audio/train/fake/*.wav") | |
| print(" data/audio/val/real/*.wav") | |
| print(" data/audio/val/fake/*.wav") | |
| print("\nRunning in demo mode (no actual training).") | |
| return | |
| train_loader = DataLoader( | |
| train_ds, batch_size=args.batch_size, shuffle=True, | |
| num_workers=2, pin_memory=(device == "cuda"), | |
| collate_fn=collate_fn, drop_last=True, | |
| ) | |
| val_loader = DataLoader( | |
| val_ds, batch_size=args.batch_size, shuffle=False, | |
| num_workers=2, collate_fn=collate_fn, | |
| ) | |
| # ββ Loss, optimiser, scheduler ββββββββββββββββββββββββββββ | |
| # Compute class weights to handle imbalance | |
| real_n = sum(1 for _, l in train_ds.samples if l == 0) | |
| fake_n = sum(1 for _, l in train_ds.samples if l == 1) | |
| pos_weight = torch.tensor([real_n / max(fake_n, 1)], device=device) | |
| criterion = nn.BCEWithLogitsLoss(pos_weight=pos_weight) | |
| # Only fine-tune the classifier head (Wav2Vec2 frozen) | |
| optimizer = AdamW( | |
| filter(lambda p: p.requires_grad, model.parameters()), | |
| lr=args.lr, weight_decay=1e-4, | |
| ) | |
| scheduler = CosineAnnealingLR(optimizer, T_max=args.epochs, eta_min=1e-6) | |
| # ββ Training loop βββββββββββββββββββββββββββββββββββββββββ | |
| save_dir = Path(args.save_dir) | |
| save_dir.mkdir(parents=True, exist_ok=True) | |
| best_auc = 0.0 | |
| print("\n" + "=" * 60) | |
| print(" DeepShield β Audio Detector Training (Wav2Vec2)") | |
| print("=" * 60) | |
| for epoch in range(1, args.epochs + 1): | |
| t0 = time.time() | |
| tr_loss, tr_acc, tr_auc = run_epoch(model, train_loader, criterion, optimizer, device, training=True) | |
| va_loss, va_acc, va_auc = run_epoch(model, val_loader, criterion, None, device, training=False) | |
| scheduler.step() | |
| elapsed = time.time() - t0 | |
| print( | |
| f"Epoch {epoch:03d}/{args.epochs} " | |
| f"| train loss={tr_loss:.4f} acc={tr_acc:.3f} AUC={tr_auc:.3f}" | |
| f" | val loss={va_loss:.4f} acc={va_acc:.3f} AUC={va_auc:.3f}" | |
| f" | {elapsed:.1f}s" | |
| ) | |
| if va_auc > best_auc: | |
| best_auc = va_auc | |
| ckpt = save_dir / "audio_model_best.pth" | |
| torch.save(model.state_dict(), ckpt) | |
| print(f" β Best model saved β {ckpt} (AUC={best_auc:.4f})") | |
| # Save final checkpoint | |
| final = save_dir / "audio_model_final.pth" | |
| torch.save(model.state_dict(), final) | |
| print(f"\n[Done] Final model saved β {final}") | |
| print(f"[Done] Best val AUC: {best_auc:.4f}") | |
| # ββ Phase 2: Unfreeze Wav2Vec2 and fine-tune further ββββββ | |
| if args.unfreeze_epochs > 0: | |
| print(f"\n[Phase 2] Unfreezing Wav2Vec2 for {args.unfreeze_epochs} more epochs...") | |
| for param in model.wav2vec2.parameters(): | |
| param.requires_grad = True | |
| optimizer2 = AdamW(model.parameters(), lr=args.lr * 0.1, weight_decay=1e-4) | |
| scheduler2 = CosineAnnealingLR(optimizer2, T_max=args.unfreeze_epochs, eta_min=1e-7) | |
| for epoch in range(1, args.unfreeze_epochs + 1): | |
| t0 = time.time() | |
| tr_loss, tr_acc, tr_auc = run_epoch(model, train_loader, criterion, optimizer2, device, True) | |
| va_loss, va_acc, va_auc = run_epoch(model, val_loader, criterion, None, device, False) | |
| scheduler2.step() | |
| elapsed = time.time() - t0 | |
| print( | |
| f"[P2] Epoch {epoch:03d}/{args.unfreeze_epochs} " | |
| f"| val AUC={va_auc:.3f} acc={va_acc:.3f} | {elapsed:.1f}s" | |
| ) | |
| if va_auc > best_auc: | |
| best_auc = va_auc | |
| ckpt = save_dir / "audio_model_best.pth" | |
| torch.save(model.state_dict(), ckpt) | |
| print(f" β Best model updated β {ckpt} (AUC={best_auc:.4f})") | |
| torch.save(model.state_dict(), save_dir / "audio_model_phase2.pth") | |
| print(f"\n[Done] Phase-2 model saved. Best AUC = {best_auc:.4f}") | |
| if __name__ == "__main__": | |
| parser = argparse.ArgumentParser(description="DeepShield Audio Deepfake Detector Training") | |
| parser.add_argument("--data_dir", type=str, default="../datasets/audio", | |
| help="Root folder with train/val subfolders") | |
| parser.add_argument("--epochs", type=int, default=20) | |
| parser.add_argument("--unfreeze_epochs", type=int, default=5, | |
| help="Extra epochs after unfreezing Wav2Vec2 backbone") | |
| parser.add_argument("--batch_size", type=int, default=16) | |
| parser.add_argument("--lr", type=float, default=1e-4) | |
| parser.add_argument("--save_dir", type=str, default="../models") | |
| args = parser.parse_args() | |
| main(args) | |