Download src/train/train_audio.py from Frankenstein-Labs/Cortex-0: direct link, hf CLI and curl.
- Browser
- Download file 9.47 kB
-
https://huggingface.co/Frankenstein-Labs/Cortex-0/resolve/main/src/train/train_audio.py
- Command line
-
hf download hf://Frankenstein-Labs/Cortex-0/src/train/train_audio.py
-
curl -L -o train_audio.py https://huggingface.co/Frankenstein-Labs/Cortex-0/resolve/main/src/train/train_audio.py
9.47 kB
| """Phase 7 — entraînement de l'encodeur audio + transcription de voyelles. | |
| Le modèle : encodeur audio (Phase 7) -> projecteur -> transformer texte, qui | |
| transcrit le mel-spectrogramme en texte (« voyelle a grave »). Même mécanisme de | |
| fusion que le VLM (Phase 6), avec les tokens `<|audio_*|>`. | |
| Usage : | |
| python -m src.train.train_audio --data-dir data/processed/toy_audio \\ | |
| --out-dir checkpoints/audio_toy --epochs 400 | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import time | |
| from dataclasses import asdict, dataclass | |
| from pathlib import Path | |
| import numpy as np | |
| import torch | |
| import torch.nn as nn | |
| from src.data.prepare_audio import load_mels | |
| from src.model.audio_encoder import AudioConfig, AudioEncoder | |
| from src.model.projectors import MLPProjector | |
| from src.model.text_model import CortexTextLM, TextConfig | |
| from src.model.vlm import build_modality_example | |
| from src.tokenizer import load_tokenizer | |
| from src.tokenizer.special_tokens import BOS_ID, EOS_ID, PAD_ID, SPECIAL_TOKEN_IDS | |
| from src.train.train_text import pick_device | |
| AUDIO_START_ID = SPECIAL_TOKEN_IDS["<|audio_start|>"] | |
| AUDIO_END_ID = SPECIAL_TOKEN_IDS["<|audio_end|>"] | |
| AUDIO_PAD_ID = SPECIAL_TOKEN_IDS["<|audio_pad|>"] | |
| PROMPT = "Transcris :" | |
| class AudioTrainConfig: | |
| data_dir: str = "data/processed/toy_audio" | |
| out_dir: str = "checkpoints/audio_toy" | |
| epochs: int = 400 | |
| batch_size: int = 32 | |
| lr: float = 3e-3 | |
| weight_decay: float = 0.1 | |
| grad_clip: float = 1.0 | |
| text_layers: int = 2 | |
| text_embd: int = 96 | |
| text_heads: int = 4 | |
| audio_layers: int = 2 | |
| audio_embd: int = 96 | |
| patch_mels: int = 16 | |
| patch_frames: int = 16 | |
| seed: int = 1337 | |
| device: str = "auto" | |
| class CortexAudioModel(nn.Module): | |
| """Encodeur audio + projecteur + décodeur texte (transcription).""" | |
| def __init__(self, text_cfg: TextConfig, audio_cfg: AudioConfig): | |
| super().__init__() | |
| self.text = CortexTextLM(text_cfg) | |
| self.audio = AudioEncoder(audio_cfg) | |
| self.projector = MLPProjector(audio_cfg.n_embd, text_cfg.n_embd) | |
| def n_audio_tokens(self) -> int: | |
| return self.audio.n_audio_tokens | |
| def audio_features(self, mel: torch.Tensor) -> torch.Tensor: | |
| return self.projector(self.audio(mel)) | |
| def forward(self, mel, input_ids, audio_mask, targets): | |
| emb = self.text.token_embedding(input_ids) | |
| feat = self.audio_features(mel) | |
| emb = emb.clone() | |
| emb[audio_mask] = feat.reshape(-1, feat.size(-1)) | |
| return self.text(inputs_embeds=emb, targets=targets) | |
| def generate(self, mel, prompt_ids, audio_mask, max_new_tokens=12, greedy=True, | |
| eos_id=EOS_ID): | |
| self.eval() | |
| ids, mask = prompt_ids, audio_mask | |
| for _ in range(max_new_tokens): | |
| emb = self.text.token_embedding(ids) | |
| if mask.any(): | |
| feat = self.audio_features(mel) | |
| emb = emb.clone() | |
| emb[mask] = feat.reshape(-1, feat.size(-1)) | |
| logits, _ = self.text(inputs_embeds=emb) | |
| logits = logits[:, -1, :] | |
| nxt = (torch.argmax(logits, dim=-1, keepdim=True) if greedy | |
| else torch.multinomial(torch.softmax(logits, dim=-1), 1)) | |
| ids = torch.cat([ids, nxt], dim=1) | |
| mask = torch.cat([mask, torch.zeros_like(nxt, dtype=torch.bool)], dim=1) | |
| if eos_id is not None and (nxt == eos_id).all(): | |
| break | |
| return ids | |
| def build_model(cfg: AudioTrainConfig, tokenizer, n_mels, n_frames, n_audio_tokens): | |
| ctx = 16 + n_audio_tokens + 24 | |
| return CortexAudioModel( | |
| TextConfig(vocab_size=tokenizer.vocab_size, n_layer=cfg.text_layers, | |
| n_head=cfg.text_heads, n_embd=cfg.text_embd, ctx_len=ctx, | |
| tie_embeddings=True), | |
| AudioConfig(n_mels=n_mels, n_frames=n_frames, patch_mels=cfg.patch_mels, | |
| patch_frames=cfg.patch_frames, n_layer=cfg.audio_layers, | |
| n_head=4, n_embd=cfg.audio_embd), | |
| ) | |
| def make_batch(tokenizer, mels_np, captions, n_audio_tokens, device): | |
| rows, masks, labels = [], [], [] | |
| for cap in captions: | |
| prompt = [BOS_ID, *tokenizer.encode(PROMPT)] | |
| ans = [*tokenizer.encode(cap), EOS_ID] | |
| ids, mask, lab = build_modality_example( | |
| AUDIO_START_ID, AUDIO_PAD_ID, AUDIO_END_ID, n_audio_tokens, prompt, ans) | |
| rows.append(ids) | |
| masks.append(mask) | |
| labels.append(lab) | |
| maxlen = max(len(r) for r in rows) | |
| for r, m, lab in zip(rows, masks, labels, strict=True): | |
| pad = maxlen - len(r) | |
| r += [PAD_ID] * pad | |
| m += [False] * pad | |
| lab += [-100] * pad | |
| return (torch.from_numpy(mels_np).unsqueeze(1).to(device), | |
| torch.tensor(rows, dtype=torch.long, device=device), | |
| torch.tensor(masks, dtype=torch.bool, device=device), | |
| torch.tensor(labels, dtype=torch.long, device=device)) | |
| def train(cfg: AudioTrainConfig) -> dict: | |
| torch.manual_seed(cfg.seed) | |
| device = pick_device(cfg.device) | |
| tokenizer = load_tokenizer() | |
| out_dir = Path(cfg.out_dir) | |
| out_dir.mkdir(parents=True, exist_ok=True) | |
| tr_mel, tr_caps = load_mels(cfg.data_dir, "train") | |
| va_mel, va_caps = load_mels(cfg.data_dir, "val") | |
| n_mels, n_frames = tr_mel.shape[1], tr_mel.shape[2] | |
| probe = build_model(cfg, tokenizer, n_mels, n_frames, 4) | |
| n_audio_tokens = probe.n_audio_tokens | |
| model = build_model(cfg, tokenizer, n_mels, n_frames, n_audio_tokens).to(device) | |
| decay, no_decay = [], [] | |
| for _n, p in model.named_parameters(): | |
| if p.requires_grad: | |
| (decay if p.dim() >= 2 else no_decay).append(p) | |
| opt = torch.optim.AdamW( | |
| [{"params": decay, "weight_decay": cfg.weight_decay}, | |
| {"params": no_decay, "weight_decay": 0.0}], lr=cfg.lr) | |
| rng = np.random.default_rng(cfg.seed) | |
| t0 = time.time() | |
| for epoch in range(cfg.epochs): | |
| idx = rng.integers(0, len(tr_mel), cfg.batch_size) | |
| mels, ids, mask, labels = make_batch(tokenizer, tr_mel[idx], | |
| [tr_caps[i] for i in idx], n_audio_tokens, device) | |
| _, loss = model(mels, ids, mask, labels) | |
| opt.zero_grad() | |
| loss.backward() | |
| if cfg.grad_clip > 0: | |
| torch.nn.utils.clip_grad_norm_(model.parameters(), cfg.grad_clip) | |
| opt.step() | |
| if (epoch + 1) % 50 == 0 or epoch == 0: | |
| print(f"epoch {epoch:4d} | loss {loss.item():.4f} | {time.time() - t0:.0f}s", flush=True) | |
| torch.save({"model": model.state_dict(), "train_config": asdict(cfg), | |
| "n_audio_tokens": n_audio_tokens, "n_mels": n_mels, | |
| "n_frames": n_frames}, out_dir / "best.pt") | |
| acc_tf = teacher_forced_accuracy(model, tokenizer, va_mel, va_caps, n_audio_tokens, device) | |
| acc_gen, examples = greedy_accuracy(model, tokenizer, va_mel, va_caps, | |
| n_audio_tokens, device) | |
| summary = {"final_loss": round(loss.item(), 4), "epochs": cfg.epochs, | |
| "elapsed_s": round(time.time() - t0, 1), "teacher_forced_acc": acc_tf, | |
| "greedy_acc": acc_gen, "examples": examples} | |
| (out_dir / "summary.json").write_text(json.dumps(summary, ensure_ascii=False, indent=2), | |
| encoding="utf-8") | |
| print("Résumé :", json.dumps({k: v for k, v in summary.items() if k != "examples"}, | |
| ensure_ascii=False), flush=True) | |
| return summary | |
| def teacher_forced_accuracy(model, tokenizer, mel_np, caps, n_audio_tokens, device, | |
| n_eval: int = 200) -> float: | |
| model.eval() | |
| n = min(n_eval, len(caps)) | |
| _, ids, mask, labels = make_batch(tokenizer, mel_np[:n], caps[:n], n_audio_tokens, device) | |
| logits, _ = model(torch.from_numpy(mel_np[:n]).unsqueeze(1).to(device), ids, mask, labels) | |
| sup = labels != -100 | |
| return round((logits.argmax(-1)[sup] == labels[sup]).float().mean().item(), 4) | |
| def greedy_accuracy(model, tokenizer, mel_np, caps, n_audio_tokens, device, | |
| n_eval: int = 100): | |
| model.eval() | |
| n = min(n_eval, len(caps)) | |
| prompt = [BOS_ID, *tokenizer.encode(PROMPT)] | |
| ids0, mask0, _ = build_modality_example( | |
| AUDIO_START_ID, AUDIO_PAD_ID, AUDIO_END_ID, n_audio_tokens, prompt, []) | |
| ids0_t = torch.tensor([ids0], device=device) | |
| mask0_t = torch.tensor([mask0], dtype=torch.bool, device=device) | |
| correct, examples = 0, [] | |
| for i in range(n): | |
| mel = torch.from_numpy(mel_np[i:i + 1]).unsqueeze(1).to(device) | |
| out = model.generate(mel, ids0_t, mask0_t, max_new_tokens=10, eos_id=EOS_ID) | |
| text = tokenizer.decode(out[0].tolist()[len(ids0):], skip_special_tokens=True).strip() | |
| correct += int(text == caps[i]) | |
| if i < 6: | |
| examples.append({"target": caps[i], "pred": text}) | |
| return round(correct / n, 4), examples | |
| def main() -> None: | |
| parser = argparse.ArgumentParser(description="Entraîner l'encodeur audio (Phase 7).") | |
| for field, value in asdict(AudioTrainConfig()).items(): | |
| parser.add_argument(f"--{field.replace('_', '-')}", type=type(value), default=value) | |
| ns = parser.parse_args() | |
| train(AudioTrainConfig(**{k.replace("-", "_"): v for k, v in vars(ns).items()})) | |
| if __name__ == "__main__": | |
| main() | |