| |
| """Encodeur audio PROFOND + cross-modal simultané → vers SOTA. |
| |
| PRINCIPE : profondeur > params (ton principe). L'encodeur actuel (1 conv) est trop |
| léger → features bruitées → le FFT ne peut pas grok. Un encodeur PROFOND |
| (4 conv layers) apprend l'INVARIANT statistique (ce qui fait qu'un mot EST ce mot, |
| indépendamment du locuteur). Le SpectralCoreBlock grok sur ces features propres. |
| |
| Le cross-modal simultané (texte + phonétique + audio profond → même ID) ancre |
| l'apprentissage de l'invariant. La 1-cos loss (crown-jewel) guide le grokking. |
| """ |
| import torch, torch.nn as nn, torch.nn.functional as F |
| import glob, os, numpy as np, time |
| import soundfile as sf |
| from ocm26400.spectral_core import SpectralCoreBlock |
| from ocm26400.amv import D_MODEL, PART |
| from ocm26400.learned_vocab import LearnedVocab |
|
|
| device = "cuda" if torch.cuda.is_available() else "cpu" |
| torch.manual_seed(0) |
| SC = "/media/akone/SAVENVME2/Datasets/_speechcommands_cache/SpeechCommands/speech_commands_v0.02" |
| T = 8000 |
|
|
|
|
| class DeepAudioEncoder(nn.Module): |
| """Encodeur audio PROFOND : Mel-STFT → 4 conv layers → features invariantes. |
| Profondeur (pas largeur) pour capturer l'invariant statistique du mot.""" |
| def __init__(self, out_dim=D_MODEL, n_mels=64): |
| super().__init__() |
| self.n_fft = 256 |
| self.n_mels = n_mels |
| |
| fb = torch.zeros(n_mels, self.n_fft // 2 + 1) |
| for m in range(n_mels): |
| center = (m + 1) * (self.n_fft // 2 + 1 - 1) / (n_mels + 1) |
| for f in range(self.n_fft // 2 + 1): |
| d = abs(f - center) / max(1.0, (self.n_fft // 2) / (n_mels + 1)) |
| fb[m, f] = max(0.0, 1.0 - d) |
| self.register_buffer("mel_fb", fb / (fb.sum(dim=1, keepdim=True) + 1e-8)) |
| self.register_buffer("window", torch.hann_window(self.n_fft)) |
| |
| self.convs = nn.Sequential( |
| nn.Conv1d(n_mels, 128, 3, padding=1), nn.ReLU(), nn.BatchNorm1d(128), |
| nn.Conv1d(128, 128, 3, padding=1), nn.ReLU(), nn.BatchNorm1d(128), |
| nn.Conv1d(128, 64, 3, padding=1), nn.ReLU(), nn.BatchNorm1d(64), |
| nn.Conv1d(64, 32, 3, padding=1), nn.ReLU()) |
| self.proj = nn.Linear(32, out_dim) |
|
|
| def forward(self, wav): |
| spec = torch.stft(wav, n_fft=self.n_fft, hop_length=self.n_fft // 2, |
| win_length=self.n_fft, window=self.window, |
| return_complex=True, center=False) |
| mel = torch.matmul(self.mel_fb, spec.abs() ** 2) |
| mel = torch.log1p(mel) |
| h = self.convs(mel) |
| pooled = h.mean(dim=-1) |
| return self.proj(pooled) |
|
|
|
|
| class CrossModalDeep(nn.Module): |
| """Cross-modal avec encodeur audio PROFOND. UN SpectralCoreBlock partagé.""" |
| def __init__(self, n_concepts): |
| super().__init__() |
| self.audio_enc = DeepAudioEncoder(out_dim=D_MODEL, n_mels=64) |
| self.text_proj = nn.Linear(PART, D_MODEL) |
| self.phon_proj = nn.Linear(PART, D_MODEL) |
| self.core = SpectralCoreBlock(d_model=D_MODEL, seq_len=1) |
| self.head = nn.Linear(D_MODEL, PART) |
|
|
| def forward_view(self, feat, proj): |
| return self.head(self.core(proj(feat).unsqueeze(1)).squeeze(1)) |
|
|
| def forward_audio(self, wav): |
| return self.head(self.core(self.audio_enc(wav).unsqueeze(1)).squeeze(1)) |
|
|
|
|
| def text_feat(word): |
| v = np.zeros(PART, dtype=np.float32) |
| for c in word.lower(): v[(ord(c) * 167) % PART] += 1.0 |
| return v |
|
|
| def phon_feat(word): |
| w = word.lower(); vw = sum(1 for c in w if c in "aeiou"); cs = len(w) - vw |
| pat = "".join("v" if c in "aeiou" else "c" for c in w)[:8] |
| v = np.zeros(PART, dtype=np.float32) |
| for c in pat: v[(ord(c) * 167) % PART] += 1.0 |
| v[(vw * 7) % PART] += 1.0; v[(cs * 11 + PART // 2) % PART] += 1.0 |
| return v |
|
|
| def load_wav(p): |
| y, sr = sf.read(p); y = y.astype(np.float32) |
| if y.ndim > 1: y = y.mean(1) |
| if len(y) < T: y = np.pad(y, (0, T - len(y))) |
| else: y = y[:T] |
| return torch.tensor(y) |
|
|
|
|
| def train(): |
| words = sorted([w for w in os.listdir(SC) |
| if os.path.isdir(os.path.join(SC, w)) and not w.startswith("_")]) |
| NW = len(words) |
| print(f"[deep encoder + cross-modal] {NW} mots", flush=True) |
|
|
| |
| audio_by_word = {} |
| for wi, w in enumerate(words): |
| wavs = [load_wav(p) for p in glob.glob(os.path.join(SC, w, "*.wav"))[:100]] |
| audio_by_word[wi] = torch.stack(wavs).to(device) |
| text_all = torch.tensor([text_feat(w) for w in words]).to(device) |
| phon_all = torch.tensor([phon_feat(w) for w in words]).to(device) |
|
|
| cv = LearnedVocab(n=NW, dim=PART, init="ortho" if NW <= PART else "random", seed=0) |
| cv.freeze(); canon = cv._matrix().to(device) |
|
|
| model = CrossModalDeep(NW).to(device) |
| opt = torch.optim.Adam(model.parameters(), lr=3e-3) |
|
|
| |
| audio_tr, audio_te = {}, {} |
| for wi in range(NW): |
| n = len(audio_by_word[wi]); p = torch.randperm(n); n_te = max(1, n // 5) |
| audio_te[wi] = p[:n_te]; audio_tr[wi] = p[n_te:] |
|
|
| print(f" encodeur: 4 conv layers (profond) + SpectralCoreBlock\n", flush=True) |
| print(f"[GROK cross-modal + encodeur profond — 20000 steps]", flush=True) |
| t0 = time.time() |
| for step in range(20000): |
| wi_batch = torch.randint(0, NW, (32,)) |
| tgt = canon[wi_batch] |
| |
| out_t = model.forward_view(text_all[wi_batch], model.text_proj) |
| out_p = model.forward_view(phon_all[wi_batch], model.phon_proj) |
| |
| wavs = torch.stack([audio_by_word[wi.item()][audio_tr[wi.item()][torch.randint(0, len(audio_tr[wi.item()]), (1,)).item()]] |
| for wi in wi_batch]) |
| out_a = model.forward_audio(wavs) |
| |
| loss = ((1 - F.cosine_similarity(out_t, tgt).clamp(-1, 1)).mean() + |
| (1 - F.cosine_similarity(out_p, tgt).clamp(-1, 1)).mean() + |
| (1 - F.cosine_similarity(out_a, tgt).clamp(-1, 1)).mean()) |
| opt.zero_grad(); loss.backward(); opt.step() |
|
|
| if step % 2000 == 0: |
| model.eval() |
| with torch.no_grad(): |
| ok = 0; tot = 0 |
| for wi in range(NW): |
| for j in audio_te[wi][:2]: |
| wav = audio_by_word[wi][j:j+1] |
| pred = (model.forward_audio(wav) @ canon.t()).argmax(1).item() |
| ok += (pred == wi); tot += 1 |
| |
| ok_tr = 0; tot_tr = 0 |
| for wi in range(NW): |
| for j in audio_tr[wi][:1]: |
| wav = audio_by_word[wi][j:j+1] |
| pred = (model.forward_audio(wav) @ canon.t()).argmax(1).item() |
| ok_tr += (pred == wi); tot_tr += 1 |
| print(f" step {step:>5} loss={loss.item():.4f} | train={ok_tr}/{tot_tr} " |
| f"test={ok}/{tot} ({ok/max(tot,1)*100:.1f}%) " |
| f"t={time.time()-t0:.0f}s", flush=True) |
| model.train() |
|
|
| |
| model.eval() |
| with torch.no_grad(): |
| ok = sum(1 for wi in range(NW) for j in audio_te[wi] |
| if (model.forward_audio(audio_by_word[wi][j:j+1]) @ canon.t()).argmax(1).item() == wi) |
| tot = sum(len(audio_te[wi]) for wi in range(NW)) |
| print(f"\n{'='*60}") |
| print(f"AUDIO CROSS-MODAL + ENCODEUR PROFOND (4 conv)") |
| print(f"{'='*60}") |
| print(f" TEST (OOD): {ok}/{tot} = {ok/max(tot,1)*100:.1f}% (hasard {100/NW:.1f}%)") |
| print(f" SOTA SpeechCommands: ~96%") |
| print(f" Gap: {ok/max(tot,1)*100 - 96:+.1f}pt") |
| print(f" temps: {time.time()-t0:.0f}s, 20000 steps") |
| print(f" encodeur: Mel(64) → Conv1d(128) → Conv1d(128) → Conv1d(64) → Conv1d(32) → proj") |
| ckpt = "/media/akone/SAVENVME2/Datasets/ocm26400/deep_encoder_trained.pt" |
| torch.save({"model_state": model.state_dict(), "test_acc": ok/max(tot,1), |
| "words": words, "method": "deep 4-conv encoder + cross-modal + 1-cos"}, ckpt) |
| print(f" [SAUVÉ] {ckpt}") |
|
|
|
|
| if __name__ == "__main__": |
| train() |
|
|