zhinit commited on
Commit
a3b520a
·
verified ·
1 Parent(s): e765aad

Upload 9 files

Browse files

uploaded training folder

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()