Upload 9 files
Browse filesuploaded training folder
- training/.DS_Store +0 -0
- training/__init__.py +0 -0
- training/__pycache__/config.cpython-311.pyc +0 -0
- training/__pycache__/losses.cpython-311.pyc +0 -0
- training/config.py +89 -0
- training/losses.py +54 -0
- training/train_autoencoder.py +169 -0
- training/train_diffusion.py +314 -0
- training/train_vocoder.py +352 -0
training/.DS_Store
ADDED
|
Binary file (6.15 kB). View file
|
|
|
training/__init__.py
ADDED
|
File without changes
|
training/__pycache__/config.cpython-311.pyc
ADDED
|
Binary file (3.63 kB). View file
|
|
|
training/__pycache__/losses.cpython-311.pyc
ADDED
|
Binary file (2.64 kB). View file
|
|
|
training/config.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Training hyperparameters."""
|
| 2 |
+
|
| 3 |
+
from dataclasses import dataclass
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
@dataclass
|
| 8 |
+
class AutoencoderConfig:
|
| 9 |
+
# Data
|
| 10 |
+
data_dir: Path = Path("data/processed")
|
| 11 |
+
val_split: float = 0.1
|
| 12 |
+
|
| 13 |
+
# Model
|
| 14 |
+
latent_dim: int = 4
|
| 15 |
+
|
| 16 |
+
# Training
|
| 17 |
+
batch_size: int = 32
|
| 18 |
+
learning_rate: float = 1e-4
|
| 19 |
+
epochs: int = 100
|
| 20 |
+
num_workers: int = 4
|
| 21 |
+
|
| 22 |
+
# KL annealing: weight ramps from kl_weight_start to kl_weight_end
|
| 23 |
+
# over the first kl_anneal_epochs epochs
|
| 24 |
+
kl_weight_start: float = 0.0001
|
| 25 |
+
kl_weight_end: float = 0.001
|
| 26 |
+
kl_anneal_epochs: int = 20
|
| 27 |
+
|
| 28 |
+
# Mixed precision
|
| 29 |
+
use_amp: bool = True
|
| 30 |
+
|
| 31 |
+
# Checkpointing
|
| 32 |
+
checkpoint_dir: Path = Path("checkpoints")
|
| 33 |
+
checkpoint_every: int = 10
|
| 34 |
+
|
| 35 |
+
# Logging
|
| 36 |
+
log_dir: Path = Path("runs/autoencoder")
|
| 37 |
+
|
| 38 |
+
def kl_weight_at_epoch(self, epoch: int) -> float:
|
| 39 |
+
"""Linearly anneal KL weight over first N epochs."""
|
| 40 |
+
if epoch >= self.kl_anneal_epochs:
|
| 41 |
+
return self.kl_weight_end
|
| 42 |
+
t = epoch / self.kl_anneal_epochs
|
| 43 |
+
return self.kl_weight_start + t * (
|
| 44 |
+
self.kl_weight_end - self.kl_weight_start
|
| 45 |
+
)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
@dataclass
|
| 49 |
+
class DiffusionConfig:
|
| 50 |
+
# Data
|
| 51 |
+
data_dir: Path = Path("data/processed")
|
| 52 |
+
latents_dir: Path = Path("data/latents")
|
| 53 |
+
metadata_csv: Path = Path("data/metadata.csv")
|
| 54 |
+
val_split: float = 0.1
|
| 55 |
+
|
| 56 |
+
# VAE
|
| 57 |
+
vae_checkpoint: Path = Path("checkpoints/vae_epoch_100.pt")
|
| 58 |
+
latent_dim: int = 4
|
| 59 |
+
|
| 60 |
+
# Noise schedule
|
| 61 |
+
timesteps: int = 1000
|
| 62 |
+
beta_start: float = 0.0001
|
| 63 |
+
beta_end: float = 0.02
|
| 64 |
+
|
| 65 |
+
# Model
|
| 66 |
+
base_channels: int = 64
|
| 67 |
+
cond_dim: int = 256
|
| 68 |
+
text_embed_dim: int = 64
|
| 69 |
+
|
| 70 |
+
# Training
|
| 71 |
+
batch_size: int = 16
|
| 72 |
+
learning_rate: float = 1e-4
|
| 73 |
+
iterations: int = 100_000
|
| 74 |
+
num_workers: int = 4
|
| 75 |
+
gradient_accumulation: int = 2
|
| 76 |
+
use_amp: bool = True
|
| 77 |
+
|
| 78 |
+
# EMA
|
| 79 |
+
ema_decay: float = 0.9999
|
| 80 |
+
|
| 81 |
+
# Classifier-free guidance
|
| 82 |
+
cfg_dropout: float = 0.15
|
| 83 |
+
|
| 84 |
+
# Checkpointing
|
| 85 |
+
checkpoint_dir: Path = Path("checkpoints")
|
| 86 |
+
checkpoint_every: int = 5000 # iterations
|
| 87 |
+
|
| 88 |
+
# Logging
|
| 89 |
+
log_dir: Path = Path("runs/diffusion")
|
training/losses.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Loss functions for autoencoder training."""
|
| 2 |
+
|
| 3 |
+
import torch
|
| 4 |
+
import torch.nn as nn
|
| 5 |
+
import torch.nn.functional as F
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def reconstruction_loss(
|
| 9 |
+
recon: torch.Tensor, target: torch.Tensor
|
| 10 |
+
) -> torch.Tensor:
|
| 11 |
+
"""MSE reconstruction loss."""
|
| 12 |
+
return F.mse_loss(recon, target)
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def spectral_convergence_loss(
|
| 16 |
+
recon: torch.Tensor, target: torch.Tensor
|
| 17 |
+
) -> torch.Tensor:
|
| 18 |
+
"""L1 loss on magnitude (encourages spectral fidelity)."""
|
| 19 |
+
return F.l1_loss(recon, target)
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def kl_divergence(
|
| 23 |
+
mu: torch.Tensor, logvar: torch.Tensor
|
| 24 |
+
) -> torch.Tensor:
|
| 25 |
+
"""KL divergence from N(mu, sigma) to N(0, 1)."""
|
| 26 |
+
return -0.5 * torch.mean(1 + logvar - mu.pow(2) - logvar.exp())
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def vae_loss(
|
| 30 |
+
recon: torch.Tensor,
|
| 31 |
+
target: torch.Tensor,
|
| 32 |
+
mu: torch.Tensor,
|
| 33 |
+
logvar: torch.Tensor,
|
| 34 |
+
kl_weight: float = 0.0001,
|
| 35 |
+
) -> tuple[torch.Tensor, dict[str, float]]:
|
| 36 |
+
"""Combined VAE loss.
|
| 37 |
+
|
| 38 |
+
Returns:
|
| 39 |
+
Total loss tensor and dict of individual loss values for logging.
|
| 40 |
+
"""
|
| 41 |
+
mse = reconstruction_loss(recon, target)
|
| 42 |
+
spectral = spectral_convergence_loss(recon, target)
|
| 43 |
+
kl = kl_divergence(mu, logvar)
|
| 44 |
+
|
| 45 |
+
total = mse + spectral + kl_weight * kl
|
| 46 |
+
|
| 47 |
+
metrics = {
|
| 48 |
+
"mse": mse.item(),
|
| 49 |
+
"spectral": spectral.item(),
|
| 50 |
+
"kl": kl.item(),
|
| 51 |
+
"kl_weighted": (kl_weight * kl).item(),
|
| 52 |
+
"total": total.item(),
|
| 53 |
+
}
|
| 54 |
+
return total, metrics
|
training/train_autoencoder.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Training script for the kick drum VAE."""
|
| 2 |
+
|
| 3 |
+
import sys
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
|
| 6 |
+
import torch
|
| 7 |
+
from torch.utils.data import DataLoader, Dataset, random_split
|
| 8 |
+
from torch.utils.tensorboard import SummaryWriter
|
| 9 |
+
from tqdm import tqdm
|
| 10 |
+
|
| 11 |
+
# Add project root to path
|
| 12 |
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
| 13 |
+
|
| 14 |
+
from models.autoencoder import KickVAE
|
| 15 |
+
from training.config import AutoencoderConfig
|
| 16 |
+
from training.losses import vae_loss
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class MelDataset(Dataset):
|
| 20 |
+
"""Dataset of preprocessed mel spectrogram tensors."""
|
| 21 |
+
|
| 22 |
+
def __init__(self, data_dir: Path) -> None:
|
| 23 |
+
self.files = sorted(
|
| 24 |
+
f for f in data_dir.glob("*.pt") if not f.name.startswith("._")
|
| 25 |
+
)
|
| 26 |
+
if not self.files:
|
| 27 |
+
raise FileNotFoundError(
|
| 28 |
+
f"No .pt files found in {data_dir}"
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
def __len__(self) -> int:
|
| 32 |
+
return len(self.files)
|
| 33 |
+
|
| 34 |
+
def __getitem__(self, idx: int) -> torch.Tensor:
|
| 35 |
+
return torch.load(self.files[idx], weights_only=False)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def train(cfg: AutoencoderConfig | None = None) -> None:
|
| 39 |
+
"""Run VAE training."""
|
| 40 |
+
if cfg is None:
|
| 41 |
+
cfg = AutoencoderConfig()
|
| 42 |
+
|
| 43 |
+
device = torch.device(
|
| 44 |
+
"cuda" if torch.cuda.is_available()
|
| 45 |
+
else "mps" if torch.backends.mps.is_available()
|
| 46 |
+
else "cpu"
|
| 47 |
+
)
|
| 48 |
+
print(f"Using device: {device}")
|
| 49 |
+
|
| 50 |
+
# Data
|
| 51 |
+
dataset = MelDataset(cfg.data_dir)
|
| 52 |
+
val_size = int(len(dataset) * cfg.val_split)
|
| 53 |
+
train_size = len(dataset) - val_size
|
| 54 |
+
train_set, val_set = random_split(
|
| 55 |
+
dataset, [train_size, val_size],
|
| 56 |
+
generator=torch.Generator().manual_seed(42),
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
train_loader = DataLoader(
|
| 60 |
+
train_set,
|
| 61 |
+
batch_size=cfg.batch_size,
|
| 62 |
+
shuffle=True,
|
| 63 |
+
num_workers=cfg.num_workers,
|
| 64 |
+
pin_memory=True,
|
| 65 |
+
)
|
| 66 |
+
val_loader = DataLoader(
|
| 67 |
+
val_set,
|
| 68 |
+
batch_size=cfg.batch_size,
|
| 69 |
+
shuffle=False,
|
| 70 |
+
num_workers=cfg.num_workers,
|
| 71 |
+
pin_memory=True,
|
| 72 |
+
)
|
| 73 |
+
|
| 74 |
+
print(f"Train: {train_size}, Val: {val_size}")
|
| 75 |
+
|
| 76 |
+
# Model
|
| 77 |
+
model = KickVAE(latent_dim=cfg.latent_dim).to(device)
|
| 78 |
+
optimizer = torch.optim.AdamW(model.parameters(), lr=cfg.learning_rate)
|
| 79 |
+
scaler = torch.amp.GradScaler(enabled=cfg.use_amp and device.type == "cuda")
|
| 80 |
+
|
| 81 |
+
# Logging
|
| 82 |
+
cfg.log_dir.mkdir(parents=True, exist_ok=True)
|
| 83 |
+
cfg.checkpoint_dir.mkdir(parents=True, exist_ok=True)
|
| 84 |
+
writer = SummaryWriter(cfg.log_dir)
|
| 85 |
+
|
| 86 |
+
global_step = 0
|
| 87 |
+
|
| 88 |
+
for epoch in range(cfg.epochs):
|
| 89 |
+
model.train()
|
| 90 |
+
kl_weight = cfg.kl_weight_at_epoch(epoch)
|
| 91 |
+
|
| 92 |
+
epoch_metrics: dict[str, float] = {}
|
| 93 |
+
epoch_count = 0
|
| 94 |
+
|
| 95 |
+
pbar = tqdm(train_loader, desc=f"Epoch {epoch+1}/{cfg.epochs}")
|
| 96 |
+
for batch in pbar:
|
| 97 |
+
batch = batch.to(device)
|
| 98 |
+
|
| 99 |
+
with torch.amp.autocast(
|
| 100 |
+
device_type=device.type,
|
| 101 |
+
enabled=cfg.use_amp and device.type == "cuda",
|
| 102 |
+
):
|
| 103 |
+
recon, mu, logvar = model(batch)
|
| 104 |
+
loss, metrics = vae_loss(
|
| 105 |
+
recon, batch, mu, logvar, kl_weight
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
+
optimizer.zero_grad()
|
| 109 |
+
scaler.scale(loss).backward()
|
| 110 |
+
scaler.step(optimizer)
|
| 111 |
+
scaler.update()
|
| 112 |
+
|
| 113 |
+
# Accumulate metrics
|
| 114 |
+
for k, v in metrics.items():
|
| 115 |
+
epoch_metrics[k] = epoch_metrics.get(k, 0.0) + v
|
| 116 |
+
epoch_count += 1
|
| 117 |
+
global_step += 1
|
| 118 |
+
|
| 119 |
+
pbar.set_postfix(loss=f"{metrics['total']:.4f}")
|
| 120 |
+
|
| 121 |
+
# Log epoch averages
|
| 122 |
+
for k, v in epoch_metrics.items():
|
| 123 |
+
writer.add_scalar(f"train/{k}", v / epoch_count, epoch)
|
| 124 |
+
writer.add_scalar("train/kl_weight", kl_weight, epoch)
|
| 125 |
+
|
| 126 |
+
# Validation
|
| 127 |
+
model.eval()
|
| 128 |
+
val_metrics: dict[str, float] = {}
|
| 129 |
+
val_count = 0
|
| 130 |
+
|
| 131 |
+
with torch.no_grad():
|
| 132 |
+
for batch in val_loader:
|
| 133 |
+
batch = batch.to(device)
|
| 134 |
+
recon, mu, logvar = model(batch)
|
| 135 |
+
_, metrics = vae_loss(
|
| 136 |
+
recon, batch, mu, logvar, kl_weight
|
| 137 |
+
)
|
| 138 |
+
for k, v in metrics.items():
|
| 139 |
+
val_metrics[k] = val_metrics.get(k, 0.0) + v
|
| 140 |
+
val_count += 1
|
| 141 |
+
|
| 142 |
+
avg_val_loss = val_metrics.get("total", 0.0) / max(val_count, 1)
|
| 143 |
+
for k, v in val_metrics.items():
|
| 144 |
+
writer.add_scalar(f"val/{k}", v / val_count, epoch)
|
| 145 |
+
|
| 146 |
+
print(
|
| 147 |
+
f"Epoch {epoch+1}: "
|
| 148 |
+
f"train={epoch_metrics['total']/epoch_count:.4f} "
|
| 149 |
+
f"val={avg_val_loss:.4f} "
|
| 150 |
+
f"kl_w={kl_weight:.6f}"
|
| 151 |
+
)
|
| 152 |
+
|
| 153 |
+
# Checkpoint
|
| 154 |
+
if (epoch + 1) % cfg.checkpoint_every == 0:
|
| 155 |
+
path = cfg.checkpoint_dir / f"vae_epoch_{epoch+1}.pt"
|
| 156 |
+
torch.save({
|
| 157 |
+
"epoch": epoch + 1,
|
| 158 |
+
"model_state_dict": model.state_dict(),
|
| 159 |
+
"optimizer_state_dict": optimizer.state_dict(),
|
| 160 |
+
"config": cfg,
|
| 161 |
+
}, path)
|
| 162 |
+
print(f"Saved checkpoint: {path}")
|
| 163 |
+
|
| 164 |
+
writer.close()
|
| 165 |
+
print("Training complete.")
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
if __name__ == "__main__":
|
| 169 |
+
train()
|
training/train_diffusion.py
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Training script for the latent diffusion model."""
|
| 2 |
+
|
| 3 |
+
import copy
|
| 4 |
+
import csv
|
| 5 |
+
import sys
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
|
| 8 |
+
import torch
|
| 9 |
+
from torch.utils.data import DataLoader, Dataset, random_split
|
| 10 |
+
from torch.utils.tensorboard import SummaryWriter
|
| 11 |
+
from tqdm import tqdm
|
| 12 |
+
|
| 13 |
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
| 14 |
+
|
| 15 |
+
from models.autoencoder import KickVAE
|
| 16 |
+
from models.diffusion import LatentUNet, NoiseScheduler
|
| 17 |
+
from models.text_encoder import KeywordEncoder, build_vocab
|
| 18 |
+
from training.config import DiffusionConfig
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
# ---------------------------------------------------------------------------
|
| 22 |
+
# Dataset
|
| 23 |
+
# ---------------------------------------------------------------------------
|
| 24 |
+
|
| 25 |
+
class LatentDataset(Dataset):
|
| 26 |
+
"""Dataset of pre-encoded VAE latents with keyword token IDs."""
|
| 27 |
+
|
| 28 |
+
def __init__(
|
| 29 |
+
self,
|
| 30 |
+
latents_dir: Path,
|
| 31 |
+
metadata_csv: Path,
|
| 32 |
+
vocab: list[str],
|
| 33 |
+
) -> None:
|
| 34 |
+
self.latent_files = sorted(
|
| 35 |
+
f for f in latents_dir.glob("*.pt") if not f.name.startswith("._")
|
| 36 |
+
)
|
| 37 |
+
if not self.latent_files:
|
| 38 |
+
raise FileNotFoundError(f"No .pt files in {latents_dir}")
|
| 39 |
+
|
| 40 |
+
# Build keyword lookup: filename_stem -> list of keyword strings
|
| 41 |
+
self.kw_to_idx = {kw: i for i, kw in enumerate(vocab)}
|
| 42 |
+
self.keywords: dict[str, list[int]] = {}
|
| 43 |
+
with open(metadata_csv) as f:
|
| 44 |
+
reader = csv.DictReader(f)
|
| 45 |
+
for row in reader:
|
| 46 |
+
stem = Path(row["filename"]).stem
|
| 47 |
+
ids = []
|
| 48 |
+
for kw in row["keywords"].split(","):
|
| 49 |
+
kw = kw.strip().lower()
|
| 50 |
+
if kw in self.kw_to_idx:
|
| 51 |
+
ids.append(self.kw_to_idx[kw])
|
| 52 |
+
self.keywords[stem] = ids
|
| 53 |
+
|
| 54 |
+
def __len__(self) -> int:
|
| 55 |
+
return len(self.latent_files)
|
| 56 |
+
|
| 57 |
+
def __getitem__(self, idx: int) -> tuple[torch.Tensor, list[int]]:
|
| 58 |
+
path = self.latent_files[idx]
|
| 59 |
+
latent = torch.load(path, weights_only=False)
|
| 60 |
+
# latent filename matches mel filename stem
|
| 61 |
+
stem = path.stem
|
| 62 |
+
token_ids = self.keywords.get(stem, [])
|
| 63 |
+
return latent, token_ids
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def collate_fn(
|
| 67 |
+
batch: list[tuple[torch.Tensor, list[int]]],
|
| 68 |
+
) -> tuple[torch.Tensor, list[list[int]]]:
|
| 69 |
+
"""Custom collate to handle variable-length keyword lists."""
|
| 70 |
+
latents = torch.stack([b[0] for b in batch])
|
| 71 |
+
token_ids = [b[1] for b in batch]
|
| 72 |
+
return latents, token_ids
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
# ---------------------------------------------------------------------------
|
| 76 |
+
# Pre-encode latents
|
| 77 |
+
# ---------------------------------------------------------------------------
|
| 78 |
+
|
| 79 |
+
def pre_encode_latents(cfg: DiffusionConfig) -> None:
|
| 80 |
+
"""Encode all mel spectrograms to latents using frozen VAE."""
|
| 81 |
+
cfg.latents_dir.mkdir(parents=True, exist_ok=True)
|
| 82 |
+
|
| 83 |
+
# Check if already done
|
| 84 |
+
existing = list(cfg.latents_dir.glob("*.pt"))
|
| 85 |
+
if len(existing) > 100:
|
| 86 |
+
print(f"Latents dir already has {len(existing)} files, skipping encoding.")
|
| 87 |
+
return
|
| 88 |
+
|
| 89 |
+
device = torch.device(
|
| 90 |
+
"cuda" if torch.cuda.is_available()
|
| 91 |
+
else "mps" if torch.backends.mps.is_available()
|
| 92 |
+
else "cpu"
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
# Load VAE
|
| 96 |
+
checkpoint = torch.load(cfg.vae_checkpoint, weights_only=False)
|
| 97 |
+
vae = KickVAE(latent_dim=cfg.latent_dim).to(device)
|
| 98 |
+
vae.load_state_dict(checkpoint["model_state_dict"])
|
| 99 |
+
vae.eval()
|
| 100 |
+
|
| 101 |
+
mel_files = sorted(
|
| 102 |
+
f for f in cfg.data_dir.glob("*.pt") if not f.name.startswith("._")
|
| 103 |
+
)
|
| 104 |
+
print(f"Encoding {len(mel_files)} mel spectrograms to latents...")
|
| 105 |
+
|
| 106 |
+
with torch.no_grad():
|
| 107 |
+
for f in tqdm(mel_files):
|
| 108 |
+
out_path = cfg.latents_dir / f.name
|
| 109 |
+
if out_path.exists():
|
| 110 |
+
continue
|
| 111 |
+
mel = torch.load(f, weights_only=False).unsqueeze(0).to(device)
|
| 112 |
+
latent = vae.encode(mel).squeeze(0).cpu()
|
| 113 |
+
torch.save(latent, out_path)
|
| 114 |
+
|
| 115 |
+
print("Latent encoding complete.")
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
# ---------------------------------------------------------------------------
|
| 119 |
+
# EMA
|
| 120 |
+
# ---------------------------------------------------------------------------
|
| 121 |
+
|
| 122 |
+
class EMA:
|
| 123 |
+
"""Exponential moving average of model parameters."""
|
| 124 |
+
|
| 125 |
+
def __init__(self, model: torch.nn.Module, decay: float = 0.9999) -> None:
|
| 126 |
+
self.decay = decay
|
| 127 |
+
self.shadow = copy.deepcopy(model)
|
| 128 |
+
self.shadow.eval()
|
| 129 |
+
for p in self.shadow.parameters():
|
| 130 |
+
p.requires_grad_(False)
|
| 131 |
+
|
| 132 |
+
@torch.no_grad()
|
| 133 |
+
def update(self, model: torch.nn.Module) -> None:
|
| 134 |
+
for s, p in zip(self.shadow.parameters(), model.parameters()):
|
| 135 |
+
s.data.mul_(self.decay).add_(p.data, alpha=1 - self.decay)
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
# ---------------------------------------------------------------------------
|
| 139 |
+
# Training
|
| 140 |
+
# ---------------------------------------------------------------------------
|
| 141 |
+
|
| 142 |
+
def train(cfg: DiffusionConfig | None = None) -> None:
|
| 143 |
+
if cfg is None:
|
| 144 |
+
cfg = DiffusionConfig()
|
| 145 |
+
|
| 146 |
+
# Pre-encode latents
|
| 147 |
+
pre_encode_latents(cfg)
|
| 148 |
+
|
| 149 |
+
device = torch.device(
|
| 150 |
+
"cuda" if torch.cuda.is_available()
|
| 151 |
+
else "mps" if torch.backends.mps.is_available()
|
| 152 |
+
else "cpu"
|
| 153 |
+
)
|
| 154 |
+
print(f"Using device: {device}")
|
| 155 |
+
|
| 156 |
+
# Build vocab and dataset
|
| 157 |
+
vocab = build_vocab(cfg.metadata_csv)
|
| 158 |
+
print(f"Vocabulary size: {len(vocab)}")
|
| 159 |
+
|
| 160 |
+
dataset = LatentDataset(cfg.latents_dir, cfg.metadata_csv, vocab)
|
| 161 |
+
val_size = int(len(dataset) * cfg.val_split)
|
| 162 |
+
train_size = len(dataset) - val_size
|
| 163 |
+
train_set, val_set = random_split(
|
| 164 |
+
dataset, [train_size, val_size],
|
| 165 |
+
generator=torch.Generator().manual_seed(42),
|
| 166 |
+
)
|
| 167 |
+
|
| 168 |
+
train_loader = DataLoader(
|
| 169 |
+
train_set,
|
| 170 |
+
batch_size=cfg.batch_size,
|
| 171 |
+
shuffle=True,
|
| 172 |
+
num_workers=cfg.num_workers,
|
| 173 |
+
pin_memory=True,
|
| 174 |
+
collate_fn=collate_fn,
|
| 175 |
+
)
|
| 176 |
+
val_loader = DataLoader(
|
| 177 |
+
val_set,
|
| 178 |
+
batch_size=cfg.batch_size,
|
| 179 |
+
shuffle=False,
|
| 180 |
+
num_workers=cfg.num_workers,
|
| 181 |
+
pin_memory=True,
|
| 182 |
+
collate_fn=collate_fn,
|
| 183 |
+
)
|
| 184 |
+
print(f"Train: {train_size}, Val: {val_size}")
|
| 185 |
+
|
| 186 |
+
# Model
|
| 187 |
+
model = LatentUNet(
|
| 188 |
+
latent_dim=cfg.latent_dim,
|
| 189 |
+
base_channels=cfg.base_channels,
|
| 190 |
+
cond_dim=cfg.cond_dim,
|
| 191 |
+
).to(device)
|
| 192 |
+
text_enc = KeywordEncoder(
|
| 193 |
+
vocab_size=len(vocab),
|
| 194 |
+
embed_dim=cfg.text_embed_dim,
|
| 195 |
+
cond_dim=cfg.cond_dim,
|
| 196 |
+
).to(device)
|
| 197 |
+
|
| 198 |
+
scheduler = NoiseScheduler(cfg.timesteps, cfg.beta_start, cfg.beta_end).to(device)
|
| 199 |
+
ema = EMA(model, cfg.ema_decay)
|
| 200 |
+
|
| 201 |
+
optimizer = torch.optim.AdamW(
|
| 202 |
+
list(model.parameters()) + list(text_enc.parameters()),
|
| 203 |
+
lr=cfg.learning_rate,
|
| 204 |
+
)
|
| 205 |
+
scaler = torch.amp.GradScaler(enabled=cfg.use_amp and device.type == "cuda")
|
| 206 |
+
|
| 207 |
+
# Logging
|
| 208 |
+
cfg.log_dir.mkdir(parents=True, exist_ok=True)
|
| 209 |
+
cfg.checkpoint_dir.mkdir(parents=True, exist_ok=True)
|
| 210 |
+
writer = SummaryWriter(cfg.log_dir)
|
| 211 |
+
|
| 212 |
+
# Training loop (iteration-based)
|
| 213 |
+
global_step = 0
|
| 214 |
+
model.train()
|
| 215 |
+
text_enc.train()
|
| 216 |
+
|
| 217 |
+
print(f"Training for {cfg.iterations} iterations...")
|
| 218 |
+
|
| 219 |
+
while global_step < cfg.iterations:
|
| 220 |
+
for latents, token_ids in train_loader:
|
| 221 |
+
if global_step >= cfg.iterations:
|
| 222 |
+
break
|
| 223 |
+
|
| 224 |
+
latents = latents.to(device)
|
| 225 |
+
batch_size = latents.shape[0]
|
| 226 |
+
|
| 227 |
+
# Classifier-free guidance dropout: replace keywords with empty list
|
| 228 |
+
dropped_ids = []
|
| 229 |
+
for ids in token_ids:
|
| 230 |
+
if torch.rand(1).item() < cfg.cfg_dropout:
|
| 231 |
+
dropped_ids.append([])
|
| 232 |
+
else:
|
| 233 |
+
dropped_ids.append(ids)
|
| 234 |
+
|
| 235 |
+
# Sample timesteps and noise
|
| 236 |
+
t = torch.randint(0, cfg.timesteps, (batch_size,), device=device)
|
| 237 |
+
noise = torch.randn_like(latents)
|
| 238 |
+
noisy = scheduler.add_noise(latents, noise, t)
|
| 239 |
+
|
| 240 |
+
with torch.amp.autocast(
|
| 241 |
+
device_type=device.type,
|
| 242 |
+
enabled=cfg.use_amp and device.type == "cuda",
|
| 243 |
+
):
|
| 244 |
+
cond = text_enc(dropped_ids, device)
|
| 245 |
+
pred_noise = model(noisy, t, cond)
|
| 246 |
+
loss = torch.nn.functional.mse_loss(pred_noise, noise)
|
| 247 |
+
loss = loss / cfg.gradient_accumulation
|
| 248 |
+
|
| 249 |
+
scaler.scale(loss).backward()
|
| 250 |
+
|
| 251 |
+
if (global_step + 1) % cfg.gradient_accumulation == 0:
|
| 252 |
+
scaler.step(optimizer)
|
| 253 |
+
scaler.update()
|
| 254 |
+
optimizer.zero_grad()
|
| 255 |
+
ema.update(model)
|
| 256 |
+
|
| 257 |
+
# Logging
|
| 258 |
+
if global_step % 50 == 0:
|
| 259 |
+
writer.add_scalar(
|
| 260 |
+
"train/loss", loss.item() * cfg.gradient_accumulation, global_step
|
| 261 |
+
)
|
| 262 |
+
|
| 263 |
+
if global_step % 500 == 0:
|
| 264 |
+
print(
|
| 265 |
+
f"Step {global_step}/{cfg.iterations} "
|
| 266 |
+
f"loss={loss.item() * cfg.gradient_accumulation:.6f}"
|
| 267 |
+
)
|
| 268 |
+
|
| 269 |
+
# Validation
|
| 270 |
+
if global_step % 1000 == 0 and global_step > 0:
|
| 271 |
+
model.eval()
|
| 272 |
+
text_enc.eval()
|
| 273 |
+
val_loss_sum = 0.0
|
| 274 |
+
val_count = 0
|
| 275 |
+
with torch.no_grad():
|
| 276 |
+
for vl, vt in val_loader:
|
| 277 |
+
vl = vl.to(device)
|
| 278 |
+
vt_step = torch.randint(
|
| 279 |
+
0, cfg.timesteps, (vl.shape[0],), device=device
|
| 280 |
+
)
|
| 281 |
+
vn = torch.randn_like(vl)
|
| 282 |
+
vnoisy = scheduler.add_noise(vl, vn, vt_step)
|
| 283 |
+
vcond = text_enc(vt, device)
|
| 284 |
+
vpred = model(vnoisy, vt_step, vcond)
|
| 285 |
+
val_loss_sum += torch.nn.functional.mse_loss(vpred, vn).item()
|
| 286 |
+
val_count += 1
|
| 287 |
+
avg_val = val_loss_sum / max(val_count, 1)
|
| 288 |
+
writer.add_scalar("val/loss", avg_val, global_step)
|
| 289 |
+
print(f" val_loss={avg_val:.6f}")
|
| 290 |
+
model.train()
|
| 291 |
+
text_enc.train()
|
| 292 |
+
|
| 293 |
+
# Checkpoint
|
| 294 |
+
if (global_step + 1) % cfg.checkpoint_every == 0:
|
| 295 |
+
path = cfg.checkpoint_dir / f"diffusion_step_{global_step+1}.pt"
|
| 296 |
+
torch.save({
|
| 297 |
+
"step": global_step + 1,
|
| 298 |
+
"model_state_dict": model.state_dict(),
|
| 299 |
+
"ema_state_dict": ema.shadow.state_dict(),
|
| 300 |
+
"text_enc_state_dict": text_enc.state_dict(),
|
| 301 |
+
"optimizer_state_dict": optimizer.state_dict(),
|
| 302 |
+
"vocab": vocab,
|
| 303 |
+
"config": cfg,
|
| 304 |
+
}, path)
|
| 305 |
+
print(f"Saved checkpoint: {path}")
|
| 306 |
+
|
| 307 |
+
global_step += 1
|
| 308 |
+
|
| 309 |
+
writer.close()
|
| 310 |
+
print("Diffusion training complete.")
|
| 311 |
+
|
| 312 |
+
|
| 313 |
+
if __name__ == "__main__":
|
| 314 |
+
train()
|
training/train_vocoder.py
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
HiFi-GAN vocoder training.
|
| 3 |
+
|
| 4 |
+
Trains a mel-to-waveform generator with multi-period and multi-scale discriminators.
|
| 5 |
+
Designed for 6GB VRAM: lazy-loads audio, uses random 8192-sample segments, small batch size.
|
| 6 |
+
|
| 7 |
+
Usage:
|
| 8 |
+
uv run training/train_vocoder.py
|
| 9 |
+
uv run training/train_vocoder.py --batch-size 4 --segment-size 8192
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
import argparse
|
| 13 |
+
import sys
|
| 14 |
+
from pathlib import Path
|
| 15 |
+
|
| 16 |
+
import numpy as np
|
| 17 |
+
import soundfile as sf
|
| 18 |
+
import torch
|
| 19 |
+
import torch.nn.functional as F
|
| 20 |
+
from torch.utils.data import Dataset, DataLoader
|
| 21 |
+
from torch.utils.tensorboard import SummaryWriter
|
| 22 |
+
from tqdm import tqdm
|
| 23 |
+
|
| 24 |
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
| 25 |
+
|
| 26 |
+
from models.vocoder import HiFiGANGenerator, MultiPeriodDiscriminator, MultiScaleDiscriminator
|
| 27 |
+
|
| 28 |
+
# Audio params (must match preprocess.py)
|
| 29 |
+
SAMPLE_RATE = 44100
|
| 30 |
+
N_FFT = 2048
|
| 31 |
+
HOP_LENGTH = 512
|
| 32 |
+
N_MELS = 128
|
| 33 |
+
DURATION_SECONDS = 2.0
|
| 34 |
+
TARGET_SAMPLES = int(SAMPLE_RATE * DURATION_SECONDS)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
# ---------------------------------------------------------------------------
|
| 38 |
+
# Dataset - lazy loads audio, pairs with pre-computed mel
|
| 39 |
+
# ---------------------------------------------------------------------------
|
| 40 |
+
|
| 41 |
+
class VocoderDataset(Dataset):
|
| 42 |
+
"""Lazy-loading dataset that pairs raw audio with processed mel spectrograms.
|
| 43 |
+
|
| 44 |
+
Only stores file paths in memory. Loads audio on-the-fly per __getitem__.
|
| 45 |
+
Returns random segments of `segment_size` samples for memory efficiency.
|
| 46 |
+
"""
|
| 47 |
+
|
| 48 |
+
def __init__(self, raw_dir: Path, processed_dir: Path, segment_size: int = 8192) -> None:
|
| 49 |
+
self.segment_size = segment_size
|
| 50 |
+
self.raw_dir = raw_dir
|
| 51 |
+
self.processed_dir = processed_dir
|
| 52 |
+
|
| 53 |
+
# Build list of (raw_path, mel_path) pairs
|
| 54 |
+
self.pairs: list[tuple[Path, Path]] = []
|
| 55 |
+
mel_stems = {p.stem for p in processed_dir.glob("*.pt")}
|
| 56 |
+
for raw_path in sorted(raw_dir.iterdir()):
|
| 57 |
+
if raw_path.suffix.lower() not in (".wav", ".aif", ".aiff", ".mp3", ".flac"):
|
| 58 |
+
continue
|
| 59 |
+
stem = raw_path.stem
|
| 60 |
+
if stem in mel_stems:
|
| 61 |
+
self.pairs.append((raw_path, processed_dir / f"{stem}.pt"))
|
| 62 |
+
|
| 63 |
+
print(f"VocoderDataset: {len(self.pairs)} paired samples found")
|
| 64 |
+
|
| 65 |
+
def __len__(self) -> int:
|
| 66 |
+
return len(self.pairs)
|
| 67 |
+
|
| 68 |
+
def __getitem__(self, idx: int) -> tuple[torch.Tensor, torch.Tensor]:
|
| 69 |
+
raw_path, mel_path = self.pairs[idx]
|
| 70 |
+
|
| 71 |
+
# Load raw audio
|
| 72 |
+
audio, sr = sf.read(raw_path, dtype="float32", always_2d=True)
|
| 73 |
+
audio = audio[:, 0] # mono
|
| 74 |
+
if sr != SAMPLE_RATE:
|
| 75 |
+
# Simple resample via linear interpolation
|
| 76 |
+
audio = np.interp(
|
| 77 |
+
np.linspace(0, len(audio) - 1, int(len(audio) * SAMPLE_RATE / sr)),
|
| 78 |
+
np.arange(len(audio)),
|
| 79 |
+
audio,
|
| 80 |
+
).astype(np.float32)
|
| 81 |
+
|
| 82 |
+
# Pad/trim to target length
|
| 83 |
+
if len(audio) < TARGET_SAMPLES:
|
| 84 |
+
audio = np.pad(audio, (0, TARGET_SAMPLES - len(audio)))
|
| 85 |
+
else:
|
| 86 |
+
audio = audio[:TARGET_SAMPLES]
|
| 87 |
+
|
| 88 |
+
# Load pre-computed mel
|
| 89 |
+
mel = torch.load(mel_path, weights_only=True) # (1, 128, 173)
|
| 90 |
+
|
| 91 |
+
# Pick random segment
|
| 92 |
+
# segment_size audio samples = segment_size // HOP_LENGTH mel frames
|
| 93 |
+
mel_frames = self.segment_size // HOP_LENGTH
|
| 94 |
+
max_mel_start = mel.shape[-1] - mel_frames
|
| 95 |
+
if max_mel_start > 0:
|
| 96 |
+
mel_start = torch.randint(0, max_mel_start, (1,)).item()
|
| 97 |
+
else:
|
| 98 |
+
mel_start = 0
|
| 99 |
+
|
| 100 |
+
audio_start = mel_start * HOP_LENGTH
|
| 101 |
+
audio_end = audio_start + self.segment_size
|
| 102 |
+
|
| 103 |
+
mel_seg = mel[:, :, mel_start:mel_start + mel_frames] # (1, 128, mel_frames)
|
| 104 |
+
audio_seg = torch.from_numpy(audio[audio_start:audio_end]) # (segment_size,)
|
| 105 |
+
|
| 106 |
+
# Pad if needed (edge cases)
|
| 107 |
+
if mel_seg.shape[-1] < mel_frames:
|
| 108 |
+
mel_seg = F.pad(mel_seg, (0, mel_frames - mel_seg.shape[-1]))
|
| 109 |
+
if audio_seg.shape[-1] < self.segment_size:
|
| 110 |
+
audio_seg = F.pad(audio_seg, (0, self.segment_size - audio_seg.shape[-1]))
|
| 111 |
+
|
| 112 |
+
return mel_seg.squeeze(0), audio_seg.unsqueeze(0) # (128, mel_frames), (1, segment_size)
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
# ---------------------------------------------------------------------------
|
| 116 |
+
# Loss functions
|
| 117 |
+
# ---------------------------------------------------------------------------
|
| 118 |
+
|
| 119 |
+
def generator_adversarial_loss(disc_outputs: list[torch.Tensor]) -> torch.Tensor:
|
| 120 |
+
loss = 0.0
|
| 121 |
+
for dg in disc_outputs:
|
| 122 |
+
loss = loss + torch.mean((1 - dg) ** 2)
|
| 123 |
+
return loss
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def discriminator_loss(real_outputs: list[torch.Tensor], fake_outputs: list[torch.Tensor]) -> torch.Tensor:
|
| 127 |
+
loss = 0.0
|
| 128 |
+
for dr, dg in zip(real_outputs, fake_outputs):
|
| 129 |
+
loss = loss + torch.mean((1 - dr) ** 2) + torch.mean(dg ** 2)
|
| 130 |
+
return loss
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def feature_matching_loss(real_fmaps: list[list[torch.Tensor]], fake_fmaps: list[list[torch.Tensor]]) -> torch.Tensor:
|
| 134 |
+
loss = 0.0
|
| 135 |
+
for rf, ff in zip(real_fmaps, fake_fmaps):
|
| 136 |
+
for r, f in zip(rf, ff):
|
| 137 |
+
loss = loss + F.l1_loss(f, r.detach())
|
| 138 |
+
return loss
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def mel_spectrogram_loss(y: torch.Tensor, y_hat: torch.Tensor) -> torch.Tensor:
|
| 142 |
+
"""L1 loss on mel spectrograms of real vs generated audio."""
|
| 143 |
+
mel_transform = torch.nn.Sequential(
|
| 144 |
+
torch.nn.Identity(), # placeholder
|
| 145 |
+
)
|
| 146 |
+
# Compute mel on-the-fly for loss
|
| 147 |
+
# Use torchaudio for consistency
|
| 148 |
+
import torchaudio
|
| 149 |
+
mel_spec = torchaudio.transforms.MelSpectrogram(
|
| 150 |
+
sample_rate=SAMPLE_RATE, n_fft=N_FFT, hop_length=HOP_LENGTH,
|
| 151 |
+
n_mels=N_MELS, power=1.0,
|
| 152 |
+
).to(y.device)
|
| 153 |
+
|
| 154 |
+
mel_real = torch.log(mel_spec(y.squeeze(1)).clamp(min=1e-5))
|
| 155 |
+
mel_fake = torch.log(mel_spec(y_hat.squeeze(1)).clamp(min=1e-5))
|
| 156 |
+
return F.l1_loss(mel_real, mel_fake)
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
# ---------------------------------------------------------------------------
|
| 160 |
+
# Training loop
|
| 161 |
+
# ---------------------------------------------------------------------------
|
| 162 |
+
|
| 163 |
+
def train(args: argparse.Namespace) -> None:
|
| 164 |
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 165 |
+
print(f"Device: {device}")
|
| 166 |
+
|
| 167 |
+
if device.type == "cuda":
|
| 168 |
+
vram_gb = torch.cuda.get_device_properties(0).total_memory / 1e9
|
| 169 |
+
print(f"GPU VRAM: {vram_gb:.1f} GB")
|
| 170 |
+
|
| 171 |
+
# Dataset
|
| 172 |
+
dataset = VocoderDataset(
|
| 173 |
+
raw_dir=Path(args.raw_dir),
|
| 174 |
+
processed_dir=Path(args.processed_dir),
|
| 175 |
+
segment_size=args.segment_size,
|
| 176 |
+
)
|
| 177 |
+
loader = DataLoader(
|
| 178 |
+
dataset,
|
| 179 |
+
batch_size=args.batch_size,
|
| 180 |
+
shuffle=True,
|
| 181 |
+
num_workers=args.num_workers,
|
| 182 |
+
pin_memory=True,
|
| 183 |
+
drop_last=True,
|
| 184 |
+
persistent_workers=args.num_workers > 0,
|
| 185 |
+
)
|
| 186 |
+
|
| 187 |
+
# Models
|
| 188 |
+
generator = HiFiGANGenerator(in_channels=N_MELS).to(device)
|
| 189 |
+
mpd = MultiPeriodDiscriminator().to(device)
|
| 190 |
+
msd = MultiScaleDiscriminator().to(device)
|
| 191 |
+
|
| 192 |
+
# Print param counts
|
| 193 |
+
g_params = sum(p.numel() for p in generator.parameters()) / 1e6
|
| 194 |
+
d_params = (sum(p.numel() for p in mpd.parameters()) + sum(p.numel() for p in msd.parameters())) / 1e6
|
| 195 |
+
print(f"Generator: {g_params:.1f}M params | Discriminators: {d_params:.1f}M params")
|
| 196 |
+
|
| 197 |
+
# Optimizers
|
| 198 |
+
optim_g = torch.optim.AdamW(generator.parameters(), lr=args.lr, betas=(0.8, 0.99))
|
| 199 |
+
optim_d = torch.optim.AdamW(
|
| 200 |
+
list(mpd.parameters()) + list(msd.parameters()),
|
| 201 |
+
lr=args.lr, betas=(0.8, 0.99),
|
| 202 |
+
)
|
| 203 |
+
|
| 204 |
+
# Schedulers
|
| 205 |
+
sched_g = torch.optim.lr_scheduler.ExponentialLR(optim_g, gamma=0.999)
|
| 206 |
+
sched_d = torch.optim.lr_scheduler.ExponentialLR(optim_d, gamma=0.999)
|
| 207 |
+
|
| 208 |
+
# Checkpointing - resume if exists
|
| 209 |
+
checkpoint_dir = Path(args.checkpoint_dir)
|
| 210 |
+
checkpoint_dir.mkdir(parents=True, exist_ok=True)
|
| 211 |
+
start_epoch = 0
|
| 212 |
+
|
| 213 |
+
resume_path = checkpoint_dir / "vocoder_latest.pt"
|
| 214 |
+
if resume_path.exists():
|
| 215 |
+
print(f"Resuming from {resume_path}")
|
| 216 |
+
ckpt = torch.load(resume_path, weights_only=False, map_location=device)
|
| 217 |
+
generator.load_state_dict(ckpt["generator"])
|
| 218 |
+
mpd.load_state_dict(ckpt["mpd"])
|
| 219 |
+
msd.load_state_dict(ckpt["msd"])
|
| 220 |
+
optim_g.load_state_dict(ckpt["optim_g"])
|
| 221 |
+
optim_d.load_state_dict(ckpt["optim_d"])
|
| 222 |
+
sched_g.load_state_dict(ckpt["sched_g"])
|
| 223 |
+
sched_d.load_state_dict(ckpt["sched_d"])
|
| 224 |
+
start_epoch = ckpt["epoch"] + 1
|
| 225 |
+
print(f"Resumed at epoch {start_epoch}")
|
| 226 |
+
|
| 227 |
+
# Logging
|
| 228 |
+
log_dir = Path(args.log_dir)
|
| 229 |
+
log_dir.mkdir(parents=True, exist_ok=True)
|
| 230 |
+
writer = SummaryWriter(log_dir)
|
| 231 |
+
|
| 232 |
+
# Training
|
| 233 |
+
global_step = start_epoch * len(loader)
|
| 234 |
+
for epoch in range(start_epoch, args.epochs):
|
| 235 |
+
generator.train()
|
| 236 |
+
mpd.train()
|
| 237 |
+
msd.train()
|
| 238 |
+
|
| 239 |
+
pbar = tqdm(loader, desc=f"Epoch {epoch+1}/{args.epochs}")
|
| 240 |
+
for mel, audio in pbar:
|
| 241 |
+
mel = mel.to(device) # (B, 128, mel_frames)
|
| 242 |
+
audio = audio.to(device) # (B, 1, segment_size)
|
| 243 |
+
|
| 244 |
+
# ---- Discriminator step ----
|
| 245 |
+
optim_d.zero_grad()
|
| 246 |
+
with torch.no_grad():
|
| 247 |
+
audio_fake = generator(mel)
|
| 248 |
+
# Trim to match lengths
|
| 249 |
+
min_len = min(audio.shape[-1], audio_fake.shape[-1])
|
| 250 |
+
audio_t = audio[..., :min_len]
|
| 251 |
+
audio_f = audio_fake[..., :min_len]
|
| 252 |
+
|
| 253 |
+
mpd_real, _ = mpd(audio_t)
|
| 254 |
+
mpd_fake, _ = mpd(audio_f)
|
| 255 |
+
msd_real, _ = msd(audio_t)
|
| 256 |
+
msd_fake, _ = msd(audio_f)
|
| 257 |
+
|
| 258 |
+
loss_d = discriminator_loss(mpd_real, mpd_fake) + discriminator_loss(msd_real, msd_fake)
|
| 259 |
+
|
| 260 |
+
loss_d.backward()
|
| 261 |
+
optim_d.step()
|
| 262 |
+
|
| 263 |
+
# ---- Generator step ----
|
| 264 |
+
optim_g.zero_grad()
|
| 265 |
+
audio_fake = generator(mel)
|
| 266 |
+
min_len = min(audio.shape[-1], audio_fake.shape[-1])
|
| 267 |
+
audio_t = audio[..., :min_len]
|
| 268 |
+
audio_f = audio_fake[..., :min_len]
|
| 269 |
+
|
| 270 |
+
mpd_real, mpd_real_fmap = mpd(audio_t)
|
| 271 |
+
mpd_fake, mpd_fake_fmap = mpd(audio_f)
|
| 272 |
+
msd_real, msd_real_fmap = msd(audio_t)
|
| 273 |
+
msd_fake, msd_fake_fmap = msd(audio_f)
|
| 274 |
+
|
| 275 |
+
loss_gen = generator_adversarial_loss(mpd_fake) + generator_adversarial_loss(msd_fake)
|
| 276 |
+
loss_fm = feature_matching_loss(mpd_real_fmap, mpd_fake_fmap) + feature_matching_loss(msd_real_fmap, msd_fake_fmap)
|
| 277 |
+
loss_mel = mel_spectrogram_loss(audio_t, audio_f)
|
| 278 |
+
|
| 279 |
+
loss_g = loss_gen + 2.0 * loss_fm + 45.0 * loss_mel
|
| 280 |
+
|
| 281 |
+
loss_g.backward()
|
| 282 |
+
optim_g.step()
|
| 283 |
+
|
| 284 |
+
global_step += 1
|
| 285 |
+
pbar.set_postfix(loss_g=f"{loss_g.item():.3f}", loss_d=f"{loss_d.item():.3f}")
|
| 286 |
+
|
| 287 |
+
if global_step % 100 == 0:
|
| 288 |
+
writer.add_scalar("loss/generator", loss_g.item(), global_step)
|
| 289 |
+
writer.add_scalar("loss/discriminator", loss_d.item(), global_step)
|
| 290 |
+
writer.add_scalar("loss/mel", loss_mel.item(), global_step)
|
| 291 |
+
writer.add_scalar("loss/feature_matching", loss_fm.item(), global_step)
|
| 292 |
+
|
| 293 |
+
# End of epoch
|
| 294 |
+
sched_g.step()
|
| 295 |
+
sched_d.step()
|
| 296 |
+
|
| 297 |
+
# Save checkpoint
|
| 298 |
+
if (epoch + 1) % args.checkpoint_every == 0 or epoch == args.epochs - 1:
|
| 299 |
+
ckpt_path = checkpoint_dir / f"vocoder_epoch_{epoch+1}.pt"
|
| 300 |
+
torch.save({
|
| 301 |
+
"generator": generator.state_dict(),
|
| 302 |
+
"mpd": mpd.state_dict(),
|
| 303 |
+
"msd": msd.state_dict(),
|
| 304 |
+
"optim_g": optim_g.state_dict(),
|
| 305 |
+
"optim_d": optim_d.state_dict(),
|
| 306 |
+
"sched_g": sched_g.state_dict(),
|
| 307 |
+
"sched_d": sched_d.state_dict(),
|
| 308 |
+
"epoch": epoch,
|
| 309 |
+
}, ckpt_path)
|
| 310 |
+
print(f"Saved {ckpt_path}")
|
| 311 |
+
|
| 312 |
+
# Always save latest for resume
|
| 313 |
+
torch.save({
|
| 314 |
+
"generator": generator.state_dict(),
|
| 315 |
+
"mpd": mpd.state_dict(),
|
| 316 |
+
"msd": msd.state_dict(),
|
| 317 |
+
"optim_g": optim_g.state_dict(),
|
| 318 |
+
"optim_d": optim_d.state_dict(),
|
| 319 |
+
"sched_g": sched_g.state_dict(),
|
| 320 |
+
"sched_d": sched_d.state_dict(),
|
| 321 |
+
"epoch": epoch,
|
| 322 |
+
}, checkpoint_dir / "vocoder_latest.pt")
|
| 323 |
+
|
| 324 |
+
writer.close()
|
| 325 |
+
print("Training complete.")
|
| 326 |
+
|
| 327 |
+
# Save inference-ready checkpoint
|
| 328 |
+
torch.save({
|
| 329 |
+
"generator_state_dict": generator.state_dict(),
|
| 330 |
+
}, checkpoint_dir / "vocoder.pt")
|
| 331 |
+
print(f"Saved inference checkpoint: {checkpoint_dir / 'vocoder.pt'}")
|
| 332 |
+
|
| 333 |
+
|
| 334 |
+
def main() -> None:
|
| 335 |
+
parser = argparse.ArgumentParser(description="Train HiFi-GAN vocoder")
|
| 336 |
+
parser.add_argument("--raw-dir", type=str, default="data/raw")
|
| 337 |
+
parser.add_argument("--processed-dir", type=str, default="data/processed")
|
| 338 |
+
parser.add_argument("--batch-size", type=int, default=8)
|
| 339 |
+
parser.add_argument("--segment-size", type=int, default=8192)
|
| 340 |
+
parser.add_argument("--lr", type=float, default=2e-4)
|
| 341 |
+
parser.add_argument("--epochs", type=int, default=50)
|
| 342 |
+
parser.add_argument("--num-workers", type=int, default=2)
|
| 343 |
+
parser.add_argument("--use-amp", action="store_true", default=True)
|
| 344 |
+
parser.add_argument("--checkpoint-dir", type=str, default="checkpoints")
|
| 345 |
+
parser.add_argument("--checkpoint-every", type=int, default=10)
|
| 346 |
+
parser.add_argument("--log-dir", type=str, default="runs/vocoder")
|
| 347 |
+
args = parser.parse_args()
|
| 348 |
+
train(args)
|
| 349 |
+
|
| 350 |
+
|
| 351 |
+
if __name__ == "__main__":
|
| 352 |
+
main()
|