OCM-26400 / train_deep_encoder.py
akone's picture
Upload train_deep_encoder.py with huggingface_hub
c105d12 verified
Raw
History Blame Contribute Delete
8.36 kB
#!/usr/bin/env python3
"""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
# banc de filtres Mel (fixe)
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))
# 4 conv layers (PROFONDEUR) → features invariantes
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) # (B, n_mels, frames)
h = self.convs(mel) # (B, 32, frames) — 4 layers profond
pooled = h.mean(dim=-1) # (B, 32) — invariant global
return self.proj(pooled) # (B, out_dim)
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)
# données
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)
# splits
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]
# text + phonetic views
out_t = model.forward_view(text_all[wi_batch], model.text_proj)
out_p = model.forward_view(phon_all[wi_batch], model.phon_proj)
# audio view (deep encoder)
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)
# 1-cos loss (crown-jewel)
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
# train acc aussi
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()
# final eval
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()