# Parametres Papermill - JAMAIS modifier ce commentaire
# Configuration notebook
notebook_mode = "interactive" # "interactive" ou "batch"
skip_widgets = False # True pour mode batch MCP
debug_level = "INFO"
# Parametres MERT2
mert2_model_id = "m-a-p/MERT-v2-30s" # encodeur 30 s
mert2_fullsong_id = "m-a-p/MERT-v2-FullSong" # variante morceaux complets (300 s)
device_override = "auto" # "auto", "cuda" ou "cpu"
corpus_dir = "output/mert2-corpus" # repertoire de sortie des clips synthetisesComprehension Musicale avec MERT2 : Embeddings et Sondes Lineaires
Module : 04-Audio-Applications Niveau : Applications Technologies : MERT-v2-30s, MERT-v2-FullSong (m-a-p), PyTorch, scikit-learn, soundfile VRAM estimee : ~4 GB (632 M parametres en fp32, deux modeles charges sequentiellement) Duree estimee : 30 minutes
Licence des poids : MERT-v2-30s et MERT-v2-FullSong sont distribues sous CC BY-NC 4.0 (usage commercial interdit). Le code de la famille YuE2 est Apache 2.0, mais les poids telecharges depuis Hugging Face restent non commerciaux. Ce notebook l’utilise a des fins pedagogiques, conformement a la licence.
Objectifs d’Apprentissage
Les parametres Papermill identifient les deux checkpoints MERT2 publies par l’equipe MAP (YuE2 family). corpus_dir recoit les clips synthetises : ce sont des artefacts d’execution, ils ne sont pas commités dans le depot.
# Setup environnement et verification
import os
import sys
import time
import gc
import json
import platform
import numpy as np
import soundfile as sf
import sklearn
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import LeaveOneOut
# Avertissements huggingface_hub : sans ces variables, le hub imprime des chemins
# machine absolus (cache, site-packages) dans les sorties de cellules.
os.environ.setdefault("HF_HUB_DISABLE_SYMLINKS_WARNING", "1")
print("VERIFICATION DE L'ENVIRONNEMENT")
print("=" * 50)
# Pins recommandes par la carte modele MERT2 (quick start officiel)
try:
import torch
import torchaudio
import transformers
print(f"torch {torch.__version__}")
print(f"torchaudio {torchaudio.__version__}")
print(f"transformers {transformers.__version__}")
print(f"soundfile {sf.__version__}")
print(f"scikit-learn {sklearn.__version__}")
print(f"numpy {np.__version__}")
except ImportError as exc:
print(f"DEFAUT ENVIRONNEMENT : {exc}")
print("Pins attendus (carte MERT2) : torch==2.6.0 torchaudio==2.6.0 transformers==4.53.2")
print("Voir la cellule d'installation dans le README de la serie Audio (kernel mert2-gpu).")
device = device_override
if device == "auto":
device = "cuda" if torch.cuda.is_available() else "cpu"
print(f"device {device}")
if device == "cuda":
print(f"GPU {torch.cuda.get_device_name(0)}")
props = torch.cuda.get_device_properties(0)
print(f"VRAM totale {props.total_memory / 1024**3:.1f} GB")
os.makedirs(corpus_dir, exist_ok=True)
t_start = time.time()
print("\nEnvironnement pret. Poids MERT2 sous licence CC BY-NC 4.0 (usage commercial interdit).")VERIFICATION DE L'ENVIRONNEMENT
==================================================
torch 2.6.0+cu126
torchaudio 2.6.0+cu126
transformers 4.53.2
soundfile 0.14.0
scikit-learn 1.9.1
numpy 2.5.3
device cuda
GPU NVIDIA GeForce RTX 3070 Laptop GPU
VRAM totale 8.0 GB
Environnement pret. Poids MERT2 sous licence CC BY-NC 4.0 (usage commercial interdit).
Interpretation : environnement
La carte modele MERT2 epingle torch==2.6.0, torchaudio==2.6.0 et transformers==4.53.2 : le code de modelisation (configuration_mert2.py, modeling_mert2.py) est telecharge depuis le repo Hugging Face (trust_remote_code=True) et cible ces versions. Le kernel mert2-gpu du depot porte exactement ces pins ; un environment avec un torch 2.13+ ne dispose d’aucun torchaudio compatible (la serie torchaudio s’arrete a 2.11), d’ou le venv dedie.
Section 1 : MERT2, un encodeur musical self-supervised
MERT2 est l’encodeur bidirectionnel de la famille YuE2 (MAP / HKUST / Tokenwave.AI / NYU / Stanford et al.). Ou YuE2-3B genere des chansons (voir le notebook 02-7), MERT2 comprend : pre-entraine de maniere self-supervised sur 30 s extraits (MERT-v2-30s) puis sur morceaux complets (MERT-v2-FullSong), il produit des representations a 25 Hz utilisables pour la classification, le retrieval, l’analyse musicologique - ou comme tokenizer de la branche causale de YuE2.
Caracteristiques (carte officielle) :
| MERT-v2-30s | MERT-v2-FullSong | |
|---|---|---|
| Parametres | 632 M | 632 M |
| Entree | 24 kHz mono, extraits <= 30 s | 24 kHz mono, morceaux complets |
| Sortie | frames 25 Hz x 1024 dims, 24 blocs | idem, contexte long |
| Pre-entrainement | extraits de 30 s | continuation sur morceaux entiers |
| Licence poids | CC BY-NC 4.0 | CC BY-NC 4.0 |
Citations academiques : MERT (Li et al., ICLR 2024) et MARBLE (Yuan et al., NeurIPS 2023) - le rapport technique MERT2 est annonce “coming soon” a la date d’ecriture de ce notebook.
Le diagramme situe MERT2 dans la famille YuE2 - les deux branches du pre-entrainement bidirectionnel (30 s) et causal (tokenization pour la generation) :
flowchart LR
subgraph pre["Pre-entrainement MERT2 (self-supervised)"]
audio["Audio 24 kHz mono"] --> enc["Encodeur bidirectionnel<br/>632 M - 24 blocs - 1024 dims"]
enc -->|"branche bidirectionnelle"| m30["MERT-v2-30s<br/>extraits 30 s"]
enc -->|"continuation full-song"| mfs["MERT-v2-FullSong<br/>morceaux complets"]
end
subgraph yue2["Famille YuE2 (generation)"]
m30 -->|"tokenizer causal"| yue["YuE2-3B<br/>AR-NAR MoT 3.59 B"]
yue --> vae["YuE2-Vae 48 kHz stereo"]
ss["SheetSage2<br/>MERT2-FS + LoRA + AR"] -->|"audio vers partition ABC"| yue
end
m30 --> emb["Embeddings<br/>similarite / retrieval / sondes"]
mfs --> emb
emb --> taches["Classification - retrieval<br/>conditionnement"]
Ce notebook exerce la voie embeddings ; la voie generation est couverte par 02-7 (YuE2).
# Chargement de MERT-v2-30s
from transformers import AutoFeatureExtractor, AutoModel
print("CHARGEMENT MERT-v2-30s")
print("=" * 50)
t0 = time.time()
processor = AutoFeatureExtractor.from_pretrained(mert2_model_id, trust_remote_code=True)
model_30s = AutoModel.from_pretrained(mert2_model_id, trust_remote_code=True).eval().to(device)
n_params = sum(p.numel() for p in model_30s.parameters())
print(f"Modele charge en {time.time() - t0:.1f} s")
print(f"Parametres {n_params / 1e6:.0f} M")
print(f"Sampling rate {processor.sampling_rate} Hz (carte : 24 kHz)")
print(f"Couches {getattr(model_30s.config, 'num_hidden_layers', '?')} blocs")
print(f"Dimension {getattr(model_30s.config, 'hidden_size', '?')}")
if device == "cuda":
print(f"VRAM allouee {torch.cuda.memory_allocated() / 1024**3:.2f} GB (poids fp32)")CHARGEMENT MERT-v2-30s
==================================================
Modele charge en 6.5 s
Parametres 632 M
Sampling rate 24000 Hz (carte : 24 kHz)
Couches 24 blocs
Dimension 1024
VRAM allouee 2.36 GB (poids fp32)
Interpretation : chargement
Le checkpoint telecharge depuis Hugging Face fait ~2.5 GB (632 M parametres en fp32). Sur un GPU 8 GB le modele tient largement ; il sera remplace par MERT-v2-FullSong (meme taille) en Section 5, les deux ne cohabitent jamais en VRAM.
Section 2 : Corpus synthetique a facteurs controles
Pour mesurer ce que l’embedding encode, il faut un corpus ou les facteurs de variation sont maitrises. Un corpus reel (GTZAN, MTG-Jamendo) melange instrumentation, mixage, masterisation - autant de facteurs impossibles a isoler. Ici, chaque clip est synthetise par le notebook selon un plan factoriel 3 motifs x 4 timbres x 2 transpositions = 24 clips :
- Motif (contenu musical) : l’organisation intervallique - sauts majeurs, descente chromatique, arpege mineur. C’est l’identite “musicale” du clip, invariante par transposition.
- Timbre (rendu) : la composition harmonique de chaque note - sinus pur, harmoniques impaires (clarinette), harmoniques completes (richarme), attaque pincee avec decroissance.
- Transposition : +0 ou +2 demi-tons - la meme structure intervallique, hauteur decalee.
La question discriminante : un encodeur musical capte-t-il le motif (structure relationnelle, donc invariante par transposition et timbre) ou le timbre (signature spectrale immediate) ? Une sonde lineaire par facteur repond en Section 6.
# Synthese du corpus 3 x 4 x 2 = 24 clips de 10 s
SR = processor.sampling_rate
DUR = 10.0
MOTIFS = {
"A": {"nom": "sauts majeurs", "pitchs": [0, 2, 4, 7, 9, 12, 9, 7, 4, 2], "note_dur": 0.5},
"B": {"nom": "descente chromatique", "pitchs": list(range(12, 2, -1)), "note_dur": 0.5},
"C": {"nom": "arpege mineur", "pitchs": [0, 3, 7, 12, 3, 7, 12, 15], "note_dur": 0.25},
}
TIMBRES = {
"t1": {"nom": "sinus pur", "harmoniques": [1], "decay": None},
"t2": {"nom": "clarinette", "harmoniques": [1, 3, 5, 7], "decay": None},
"t3": {"nom": "richarme", "harmoniques": [1, 2, 3, 4, 5, 6, 7, 8], "decay": None},
"t4": {"nom": "pince", "harmoniques": [1, 2, 3, 4, 5, 6], "decay": 0.15},
}
TRANSPOS = {"tp0": 0, "tp2": 2}
def synthese_note(freq, dur, harmoniques, decay, sr):
n = int(dur * sr)
t = np.arange(n) / sr
signal = np.zeros(n)
for h in harmoniques:
signal += (1.0 / h) * np.sin(2 * np.pi * freq * h * t)
if decay is not None:
signal = signal * np.exp(-t / decay)
# enveloppe douce anti-clic (10 ms)
env = np.ones(n)
ramp = int(0.010 * sr)
if decay is None:
env[:ramp] = np.linspace(0, 1, ramp)
env[-ramp:] = np.linspace(1, 0, ramp)
else:
env[:ramp] = np.linspace(0, 1, ramp)
return signal * env
def synthese_clip(motif, timbre, transpo, sr):
cfg_m, cfg_t = MOTIFS[motif], TIMBRES[timbre]
total = np.zeros(int(DUR * sr))
pos = 0.0
while pos < DUR:
for p in cfg_m["pitchs"]:
if pos >= DUR:
break
freq = 220.0 * 2 ** ((p + TRANSPOS[transpo]) / 12)
note = synthese_note(freq, cfg_m["note_dur"], cfg_t["harmoniques"], cfg_t["decay"], sr)
i0 = int(pos * sr)
i1 = min(i0 + len(note), len(total))
total[i0:i1] += note[: i1 - i0]
pos += cfg_m["note_dur"]
total = total / (np.abs(total).max() + 1e-9) * 0.6
return total.astype(np.float32)
clips = [] # liste de dicts : fichier, motif, timbre, transposition
for m in MOTIFS:
for t in TIMBRES:
for tp in TRANSPOS:
wav = synthese_clip(m, t, tp, SR)
fname = f"{m}_{t}_{tp}.wav"
sf.write(os.path.join(corpus_dir, fname), wav, SR)
clips.append({"fichier": fname, "motif": m, "timbre": t, "tp": tp, "samples": len(wav)})
# Medley 120 s pour MERT-v2-FullSong : les 12 clips tp0 dans un ordre fixe
ordre_medley = [c for c in clips if c["tp"] == "tp0"]
medley = np.concatenate([
synthese_clip(c["motif"], c["timbre"], c["tp"], SR) for c in ordre_medley
])
sf.write(os.path.join(corpus_dir, "medley_120s.wav"), medley, SR)
print(f"CORPUS SYNTHETIQUE : {len(clips)} clips de {DUR:.0f} s a {SR} Hz + 1 medley de {len(medley) / SR:.0f} s")
print("=" * 60)
for c in clips[:8]:
print(f" {c['fichier']:14s} motif={c['motif']} ({MOTIFS[c['motif']]['nom']:22s}) timbre={c['timbre']} tp={c['tp']}")
print(f" ... ({len(clips) - 8} autres clips)")
for i, c in enumerate(ordre_medley):
print(f" medley segment {i:2d} : {c['motif']}_{c['timbre']}")CORPUS SYNTHETIQUE : 24 clips de 10 s a 24000 Hz + 1 medley de 120 s
============================================================
A_t1_tp0.wav motif=A (sauts majeurs ) timbre=t1 tp=tp0
A_t1_tp2.wav motif=A (sauts majeurs ) timbre=t1 tp=tp2
A_t2_tp0.wav motif=A (sauts majeurs ) timbre=t2 tp=tp0
A_t2_tp2.wav motif=A (sauts majeurs ) timbre=t2 tp=tp2
A_t3_tp0.wav motif=A (sauts majeurs ) timbre=t3 tp=tp0
A_t3_tp2.wav motif=A (sauts majeurs ) timbre=t3 tp=tp2
A_t4_tp0.wav motif=A (sauts majeurs ) timbre=t4 tp=tp0
A_t4_tp2.wav motif=A (sauts majeurs ) timbre=t4 tp=tp2
... (16 autres clips)
medley segment 0 : A_t1
medley segment 1 : A_t2
medley segment 2 : A_t3
medley segment 3 : A_t4
medley segment 4 : B_t1
medley segment 5 : B_t2
medley segment 6 : B_t3
medley segment 7 : B_t4
medley segment 8 : C_t1
medley segment 9 : C_t2
medley segment 10 : C_t3
medley segment 11 : C_t4
Interpretation : corpus controle
Chaque clip est deterministe (aucun aleatoire) : deux executions produisent des WAV byte-identiques. Le plan factoriel donne 8 clips par motif, 6 par timbre, 12 par transposition - la base des sondes de la Section 6. Le medley enchaine les 12 clips non transposes : il servira a mesurer la capacite de contexte long de MERT-v2-FullSong (Section 5).
Section 3 : Extraction des embeddings
Le modele produit, pour 10 s d’audio, une sequence de 250 frames a 25 Hz (10 s x 25 Hz), chacune de dimension 1024, plus les sorties intermediaires des 24 blocs (output_hidden_states=True). Pour obtenir un embedding par clip, on moyenne les frames valides (pooling masque) : le masque d’attention du processeur marque les frames remplies par le padding, il faut les exclure du calcul - sinon la longueur du clip fuit dans l’embedding.
On extrait ici le pooling pour chacune des 24 couches : la Section 6 compare la couche 1 (hidden_states[0], features acoustiques basses) a la couche 23 (hidden_states[22], recommandee par le guide officiel pour la classification de genre).
# Extraction des embeddings par couche (MERT-v2-30s)
N_COUCHES = getattr(model_30s.config, "num_hidden_layers", 24)
def extraire_embeddings(wav, modele):
inputs = processor(wav, sampling_rate=SR, return_tensors="pt").to(device)
with torch.inference_mode():
out = modele(**inputs, output_hidden_states=True)
mask = out.feature_attention_mask[..., None].float() # [1, frames, 1]
pool_par_couche = []
for hs in out.hidden_states: # 24 tenseurs [1, frames, 1024]
pooled = (hs * mask).sum(1) / mask.sum(1).clamp_min(1) # [1, 1024]
pool_par_couche.append(pooled[0].cpu().numpy())
n_frames = int(out.feature_attention_mask.sum().item())
return np.stack(pool_par_couche), n_frames # [24, 1024], frames valides
t0 = time.time()
embeddings_par_couche = {} # fichier -> [24 couches, 1024]
frames_counts = {}
for c in clips:
wav, _ = sf.read(os.path.join(corpus_dir, c["fichier"]), dtype="float32")
pool, nfr = extraire_embeddings(wav, model_30s)
embeddings_par_couche[c["fichier"]] = pool
frames_counts[c["fichier"]] = nfr
print(f"EXTRACTION MERT-v2-30s : {len(clips)} clips en {time.time() - t0:.1f} s")
print("=" * 50)
ex = clips[0]["fichier"]
print(f"Exemple {ex} : {frames_counts[ex]} frames valides (attendu {int(DUR * 25)})")
print(f"Forme par clip : {embeddings_par_couche[ex].shape} = [couches, dims]")
print(f"Norme L2 couche 1 : {np.linalg.norm(embeddings_par_couche[ex][0]):.2f} | couche 24 : {np.linalg.norm(embeddings_par_couche[ex][23]):.2f}")EXTRACTION MERT-v2-30s : 24 clips en 3.6 s
==================================================
Exemple A_t1_tp0.wav : 250 frames valides (attendu 250)
Forme par clip : (24, 1024) = [couches, dims]
Norme L2 couche 1 : 28.69 | couche 24 : 14.90
Interpretation : extraction
250 frames valides pour 10 s confirme la cadence 25 Hz de la carte modele (le processeur peut ajouter des frames de padding selon la fenetre interne - le pooling masque les ignore). Les normes L2 par couche montrent que l’echelle des representations varie avec la profondeur : la similarite cosinus de la Section 4 normalise cet effet.
Section 4 : Similarite cosinus - que structure l’espace ?
La matrice de similarite 24 x 24 entre embeddings pooled de la derniere couche (bloc 24) revele la structure de l’espace appris. Trois hypotheses :
- l’espace est groupe par motif (l’encodeur a capture la structure musicale relationnelle) ;
- l’espace est groupe par timbre (l’encodeur reste domine par la signature spectrale) ;
- l’espace est groupe par transposition (l’encodeur code la hauteur absolue).
La metrique tranche : moyenne des similarites intra-groupe pour chaque facteur, comparee a la similarite moyenne entre clips sans lien.
# Matrice de similarite cosinus (couche 24) et metriques par facteur
fichiers = [c["fichier"] for c in clips]
labels = {c["fichier"]: c for c in clips}
X = np.stack([embeddings_par_couche[f][23] for f in fichiers]) # [24 clips, 1024]
Xn = X / np.linalg.norm(X, axis=1, keepdims=True)
S = Xn @ Xn.T # cosinus 24 x 24
def similarite_groupe(facteur):
vals_intra, vals_cross = [], []
for i in range(len(fichiers)):
for j in range(len(fichiers)):
if i == j:
continue
meme = labels[fichiers[i]][facteur] == labels[fichiers[j]][facteur]
(vals_intra if meme else vals_cross).append(S[i, j])
return np.mean(vals_intra), np.mean(vals_cross)
print("SIMILARITE COSINUS - COUCHE 24 (derniere)")
print("=" * 50)
for facteur, attendu in [("motif", "structure musicale"), ("timbre", "rendu spectral"), ("tp", "hauteur absolue")]:
intra, cross = similarite_groupe(facteur)
print(f" intra-{facteur:7s} {intra:+.4f} vs hors-groupe {cross:+.4f} (delta {intra - cross:+.4f})")
# Heatmap textuelle compacte : initiale du motif, tri motif -> timbre -> tp
symbole = {"A": "a", "B": "b", "C": "c"}
print("\nHeatmap (caractere = motif du clip colonne, plus la valeur est haute plus les clips se ressemblent) :")
entete = " " + "".join(f"{symbole[labels[f]['motif']]}" for f in fichiers)
print(entete)
for i, fi in enumerate(fichiers):
ligne = "".join("#" if i == j else ("+" if S[i, j] > np.median(S) else ".") for j in range(len(fichiers)))
print(f" {symbole[labels[fi]['motif']]} {ligne}")SIMILARITE COSINUS - COUCHE 24 (derniere)
==================================================
intra-motif +0.8768 vs hors-groupe +0.8332 (delta +0.0437)
intra-timbre +0.9249 vs hors-groupe +0.8247 (delta +0.1002)
intra-tp +0.8435 vs hors-groupe +0.8492 (delta -0.0057)
Heatmap (caractere = motif du clip colonne, plus la valeur est haute plus les clips se ressemblent) :
aaaaaaaabbbbbbbbcccccccc
a #+.+++++++......++......
a +#.++++++++++...++......
a ..#+++....++++....++....
a +++#++....++++....++....
a ++++#+++..++++++....++++
a +++++#++..++++++....++..
a ++..++#+....++++......++
a ++..+++#....++++......++
b ++......#+++....++......
b ++......+#++....++......
b .+++++..++#+++....++....
b .+++++..+++#++....++....
b .+++++++..++#+++....++..
b ..++++++..+++#++....++..
b ....++++....++#+......++
b ....++++....+++#......++
c ++......++......#+++++++
c ++......++......+#++++++
c ..++......++....++#++++.
c ..++......++....+++#++..
c ....++......++..++++#+++
c ....++......++..+++++#++
c ....+.++......+++++.++#+
c ....+.++......++++..+++#
Interpretation : structure de l’espace
Le facteur dont l’ecart intra/hors-groupe est le plus eleve domine la geometrie de la derniere couche. La heatmap textuelle rend la structure bloc-diagonale visible a l’oeil si ce facteur structure l’espace. Ce resultat est le point de depart des sondes de la Section 6 : une sonde lineaire sait-elle separer le facteur dominant ET le facteur secondaire ? La mesure rend trois ecarts intra/hors-groupe : timbre +0,1002, motif +0,0437, transposition -0,0057. Le timbre domine la dernière couche — plus du double du motif (0,1002 contre 2 × 0,0437 = 0,0874). La transposition est le seul delta négatif : deux clips transposés l’un de l’autre (0,8435 en intra-tp) se ressemblent MOINS que deux clips pris hors groupe (0,8492). L’espace de la couche 24 n’a pas d’axe de hauteur ; il a un axe de timbre, un axe faible de motif, et rien pour la transposition.
Exercice 1 : Pooling masque
Objectif : implementer le pooling masque sans utiliser le code de la Section 3.
Etape 1 : ecrire une fonction qui prend frames (tenseur [1, F, D]), mask ([1, F], 1 = frame valide, 0 = padding) et rend l embedding moyenne [D] en ignorant les frames invalides.
Indice : mask[..., None] broadcast le masque sur la dimension D ; penser a clamp_min pour eviter une division par zero sur un clip entieremenent padding.
# Exercice 1 : pooling masque - a completer
def pooling_masque(frames, mask):
# TODO etudiant : moyenne des frames valides (mask == 1), forme de sortie [D]
# Etape 1 : broadcast du masque mask[..., None]
# Etape 2 : somme ponderee puis division par le nombre de frames valides
# Etape 3 : ne jamais diviser par zero (clamp_min(1))
return None
# Validation attendue : pooling_masque(frames_test, mask_test) doit redonner
# les memes vecteurs que extraire_embeddings() sur le meme clip (test enonce,
# la correction est la cellule 13 - comparer au triplet (frames, mask, pool)).
print("Exercice a completer : implementer pooling_masque(frames, mask).")Exercice a completer : implementer pooling_masque(frames, mask).
Section 5 : MERT-v2-30s vs MERT-v2-FullSong
Les deux checkpoints partagent la meme architecture ; FullSong poursuit le pre-entrainement sur des morceaux entiers. Deux questions mesurees :
- Geometrie : la similarite 24 x 24 de FullSong agree-t-elle avec celle du modele 30 s (correlation de Pearson sur le triangle superieur) ?
- Contexte long : sur le medley de 120 s, la frame FullSong au centre de chaque segment de 10 s retrouve-t-elle le clip correspondant (retrieval par plus proche voisins parmi les 24 embeddings FullSong) ? Le modele 30 s ne peut pas englutir les 120 s d’un seul tenant - c’est precisement la capacite que FullSong ajoute.
# MERT-v2-FullSong : geometrie comparee + retrieval sur medley 120 s
del model_30s
gc.collect()
if device == "cuda":
torch.cuda.empty_cache()
t0 = time.time()
processor = AutoFeatureExtractor.from_pretrained(mert2_fullsong_id, trust_remote_code=True)
model_fs = AutoModel.from_pretrained(mert2_fullsong_id, trust_remote_code=True).eval().to(device)
print(f"FullSong charge en {time.time() - t0:.1f} s "
f"({sum(p.numel() for p in model_fs.parameters()) / 1e6:.0f} M parametres)")
# 1. Embeddings des 24 clips avec FullSong
emb_fs = {}
for c in clips:
wav, _ = sf.read(os.path.join(corpus_dir, c["fichier"]), dtype="float32")
pool, _ = extraire_embeddings(wav, model_fs)
emb_fs[c["fichier"]] = pool[23] # derniere couche
Xfs = np.stack([emb_fs[f] for f in fichiers])
Xfs_n = Xfs / np.linalg.norm(Xfs, axis=1, keepdims=True)
Sfs = Xfs_n @ Xfs_n.T
from scipy.stats import pearsonr
tri = np.triu_indices(len(fichiers), k=1)
r, pval = pearsonr(S[tri], Sfs[tri])
print(f"\nGeometrie : correlation Pearson des similarites 30s vs FullSong = {r:.3f} (p={pval:.1e})")
# 2. Retrieval sur le medley 120 s (contexte long, seule FullSong peut l'englutir)
with torch.inference_mode():
inputs = processor(medley, sampling_rate=SR, return_tensors="pt").to(device)
out = model_fs(**inputs, output_hidden_states=True)
frames_medley = out.hidden_states[23][0].cpu().numpy() # [3000+, 1024]
print(f"Medley {len(medley) / SR:.0f} s -> {frames_medley.shape[0]} frames FullSong (25 Hz)")
hits = 0
print("\nRetrieval par segment (frame centrale du segment -> plus proche clip) :")
for i in range(len(ordre_medley)):
centre = int((i + 0.5) * DUR * 25)
v = frames_medley[centre] / np.linalg.norm(frames_medley[centre])
sims = Xfs_n @ v
best = int(np.argmax(sims))
ok = fichiers[best] == ordre_medley[i]["fichier"]
hits += ok
print(f" segment {i:2d} ({ordre_medley[i]['fichier']:10s}) -> predit {fichiers[best]:10s} "
f"{'OK' if ok else 'exact non, motif=' + labels[fichiers[best]]['motif']}")
print(f"\nRetrieval exact : {hits}/{len(ordre_medley)} segments (hasard : ~{len(ordre_medley) / 24:.1f})")FullSong charge en 3.9 s (632 M parametres)
Geometrie : correlation Pearson des similarites 30s vs FullSong = 0.970 (p=0.0e+00)
Medley 120 s -> 3000 frames FullSong (25 Hz)
Retrieval par segment (frame centrale du segment -> plus proche clip) :
segment 0 (A_t1_tp0.wav) -> predit A_t1_tp2.wav exact non, motif=A
segment 1 (A_t2_tp0.wav) -> predit A_t3_tp0.wav exact non, motif=A
segment 2 (A_t3_tp0.wav) -> predit A_t3_tp0.wav OK
segment 3 (A_t4_tp0.wav) -> predit C_t4_tp0.wav exact non, motif=C
segment 4 (B_t1_tp0.wav) -> predit C_t1_tp0.wav exact non, motif=C
segment 5 (B_t2_tp0.wav) -> predit C_t2_tp0.wav exact non, motif=C
segment 6 (B_t3_tp0.wav) -> predit C_t3_tp0.wav exact non, motif=C
segment 7 (B_t4_tp0.wav) -> predit C_t4_tp0.wav exact non, motif=C
segment 8 (C_t1_tp0.wav) -> predit C_t1_tp0.wav OK
segment 9 (C_t2_tp0.wav) -> predit C_t2_tp0.wav OK
segment 10 (C_t3_tp0.wav) -> predit C_t3_tp0.wav OK
segment 11 (C_t4_tp0.wav) -> predit C_t4_tp0.wav OK
Retrieval exact : 5/12 segments (hasard : ~0.5)
Interpretation : FullSong
La correlation elevee entre les deux matrices de similarite indique que la continuation de pre-entrainement preserve la geometrie sur extraits courts. Le retrieval par segment mesure la capacite de contexte long : une frame au centre d’un segment de 10 s, extraite d’un passage de 120 s vu d’un seul tenant, doit rester proche du clip correspondant. Les segments faux sont instructifs : regarder si le clip predit partage au moins le motif (structure conservee, timbre confondu) ou aucun facteur. Le run concret sur les 12 segments du medley donne : exact 5/12 (rendu par la sortie, hasard ~0,5), motif conservé 7/12 — les trois A des segments 0 à 2 restent A et les quatre C restent C, seul le A du segment 3 déraille — et timbre conservé 11/12 : seul le segment 1 perd son t2 pour un t3. Et les quatre segments B sont TOUS prédits C, jamais A : une frame centrale porte le timbre (signal instantané) mais pas la descente chromatique du motif B (structure temporelle). La corrélation 0,970 des géométries globales ne descend pas intacte à l’échelle de la frame isolée.
Section 6 : Sondes lineaires - contenu vs rendu
Une sonde lineaire (logistic regression sur embeddings geles) mesure la linearite de l’information dans la representation : si un plan separe les classes, l’information y est lineairement accessible. Le protocole :
- validation croisee leave-one-out (24 entrainements par configuration, chaque clip sert une fois de test) - le seul protocole honnete a cette taille de corpus ;
- deux taches : motif (3 classes - contenu musical, invariante timbre/transposition) et timbre (4 classes - rendu spectral) ;
- trois couches : L1 (
hidden_states[0], bas niveau), L23 (hidden_states[22], couche recommandee par le guide officiel MERT2 pour le genre), L24 (derniere).
Avertissement methodologique : ce corpus synthetique de 24 clips est un proxy pedagogique, pas une reproduction du benchmark MARBLE (GTZAN genre accuracy 91.72 pour MERT-v2-30s, carte officielle). Le claim MARBLE n’est ni reproduit ni infirme ici - il est rappele comme reference de l’echelle published, la sonde locale mesure la separation de facteurs sur un corpus controle.
# Sondes lineaires LOO : motif (3 classes) et timbre (4 classes) x couches L1/L23/L24
couches_test = {"L1": 0, "L23": 22, "L24": 23}
y_motif = np.array([labels[f]["motif"] for f in fichiers])
y_timbre = np.array([labels[f]["timbre"] for f in fichiers])
def sonde_loo(idx_couche, y):
Xc = np.stack([embeddings_par_couche[f][idx_couche] for f in fichiers])
y_pred = np.empty(len(y), dtype=object)
for tr, te in LeaveOneOut().split(Xc):
clf = LogisticRegression(max_iter=2000, C=1.0)
clf.fit(Xc[tr], y[tr])
y_pred[te] = clf.predict(Xc[te])[0]
return float(np.mean(y_pred == y))
print("SONDES LINEAIRES (leave-one-out, embeddings geles MERT-v2-30s)")
print("=" * 60)
print(f"{'couche':8s} {'motif (3 cl., hasard 0.33)':>28s} {'timbre (4 cl., hasard 0.25)':>30s}")
for nom, idx in couches_test.items():
acc_m = sonde_loo(idx, y_motif)
acc_t = sonde_loo(idx, y_timbre)
print(f"{nom:8s} {acc_m:28.3f} {acc_t:30.3f}")
print("\nGuide officiel MERT2 (carte HF) : tache genre -> couche L23, learning rate 5e-3.")SONDES LINEAIRES (leave-one-out, embeddings geles MERT-v2-30s)
============================================================
couche motif (3 cl., hasard 0.33) timbre (4 cl., hasard 0.25)
L1 0.917 1.000
L23 1.000 1.000
L24 1.000 1.000
Guide officiel MERT2 (carte HF) : tache genre -> couche L23, learning rate 5e-3.
Interpretation : sondes
Lecture attendue : la tache timbre (signature spectrale) est souvent lineairement accessible des les premieres couches ; la tache motif (structure relationnelle invariante par timbre et transposition) exige des couches profondes - c’est le motif du guide officiel (L23 pour le genre). Si la sonde motif reste proche du hasard a toutes les couches, la conclusion honnete est que la structure intervallique fine n’est pas lineairement codable dans cet espace - pas que le modele est “mauvais” : le benchmark MARBLE mesure des taches plus larges (genre, tonalite, emotions) sur des corpus reels ordres de grandeur plus grands.
Exercice 2 : Retrieval top-k
Objectif : implementer la recherche des k clips les plus similaires a une requete.
Etape 1 : ecrire top_k_similaires(embeddings, query_idx, k) qui rend les indices des k clips les plus proches (cosinus) de embeddings[query_idx], en excluant la requete elle-meme.
Indice : normaliser les lignes puis un produit matriciel donne toutes les similarites d’un coup ; np.argsort trie - attention a exclure l’indice de la requete avant de prendre les k premiers.
# Exercice 2 : retrieval top-k - a completer
def top_k_similaires(embeddings, query_idx, k=3):
# TODO etudiant : indices des k plus proches voisins cosinus de embeddings[query_idx]
# Etape 1 : normaliser chaque ligne (norme L2)
# Etape 2 : similarites = produit matriciel avec la requete normalisee
# Etape 3 : trier, exclure query_idx, garder les k premiers
return None
# Validation attendue : top_k_similaires(X, 0, 3) doit rendre 3 indices dont les
# clips partagent le facteur dominant releve en Section 4 (verifier avec labels).
print("Exercice a completer : implementer top_k_similaires(embeddings, query_idx, k).")Exercice a completer : implementer top_k_similaires(embeddings, query_idx, k).
Exercice 3 : Sonde sur d’autres couches
Objectif : etendre le balayage de la Section 6 aux couches intermediaires.
Etape 1 : ecrire accuracy_sonde(idx_couche) qui rend l’accuracy LOO de la tache motif a la couche demandee (reutiliser sonde_loo).
Etape 2 : balayer L8 (indice 7) et L16 (indice 15), comparer a L1/L23/L24 et situer le palier ou l’information de motif devient lineairement accessible.
# Exercice 3 : sonde multicouche - a completer
def accuracy_sonde(idx_couche):
# TODO etudiant : accuracy LOO de la tache motif a la couche idx_couche
# Etape 1 : appeler sonde_loo(idx_couche, y_motif)
return None
# Validation attendue : accuracy_sonde(22) doit redonner la valeur L23 de la Section 6.
for nom, idx in [("L8", 7), ("L16", 15)]:
print(f"Exercice a completer : accuracy_sonde({idx}) pour la couche {nom}.")Exercice a completer : accuracy_sonde(7) pour la couche L8.
Exercice a completer : accuracy_sonde(15) pour la couche L16.
# Statistiques de session
print("STATISTIQUES DE SESSION")
print("=" * 50)
print(f"Date {time.strftime('%Y-%m-%d %H:%M:%S')}")
print(f"Kernel mert2-gpu (torch 2.6.0 / torchaudio 2.6.0 / transformers 4.53.2)")
print(f"Device {device}" + (f" ({torch.cuda.get_device_name(0)})" if device == "cuda" else ""))
print(f"Modeles {mert2_model_id} + {mert2_fullsong_id} (632 M chacun, charges sequentiellement)")
print(f"Corpus {len(clips)} clips synthetiques {DUR:.0f} s + medley {len(medley) / SR:.0f} s")
print(f"Embeddings {len(fichiers)} clips x 24 couches x {embeddings_par_couche[fichiers[0]].shape[1]} dims")
print(f"Duree totale {time.time() - t_start:.0f} s")
print(f"Licence poids CC BY-NC 4.0 (usage commercial interdit)")STATISTIQUES DE SESSION
==================================================
Date 2026-09-13 03:42:50
Kernel mert2-gpu (torch 2.6.0 / torchaudio 2.6.0 / transformers 4.53.2)
Device cuda (NVIDIA GeForce RTX 3070 Laptop GPU)
Modeles m-a-p/MERT-v2-30s + m-a-p/MERT-v2-FullSong (632 M chacun, charges sequentiellement)
Corpus 24 clips synthetiques 10 s + medley 120 s
Embeddings 24 clips x 24 couches x 1024 dims
Duree totale 33 s
Licence poids CC BY-NC 4.0 (usage commercial interdit)
Verdict SOTA
| Axe | Verdict | Preuve |
|---|---|---|
| Chargement des vrais poids | SOTA-OK | m-a-p/MERT-v2-30s et m-a-p/MERT-v2-FullSong charges firsthand depuis Hugging Face (trust_remote_code), 632 M parametres chacun, execution GPU locale |
| Execution | SOTA-OK | outputs reels commités (execution complete, 0 erreur) sur GPU local 8 GB, venv dedie aux pins de la carte (torch 2.6.0 / torchaudio 2.6.0 / transformers 4.53.2) |
| Claims MARBLE (91.72 genre GTZAN etc.) | rappeles, non reproduits | proxy pedagogique n=24 synthetique - le benchmark MARBLE exige corpus reels et protocole complet ; ce notebook mesure la separation de facteurs controles, pas le score published |
| Licence | CC BY-NC 4.0 sur les poids | rappele en tete de notebook et en statistiques de session - usage commercial interdit |
Aucun verdict INTRINSIC n’est invoque : la checklist 6 axes ne s’applique pas (l’outil reel est installe et execute).
Synthese du module
Ce notebook a mesure ce qu’un encodeur musical self-supervised de 632 M parametres encode vraiment, sur un corpus a facteurs controles :
- Extraction : frames 25 Hz x 1024 dims, pooling masque par clip, pour les 24 couches
- Geometrie : la matrice de similarite cosinus identifie le facteur dominant de l’espace
- Contexte long : MERT-v2-FullSong englue 120 s d’un tenant et retrouve les segments par frame
- Sondes lineaires : timbre vs motif en LOO sur couches L1/L23/L24, confrontees au guide officiel des couches MERT2
Pour aller plus loin : le notebook 02-7 (YuE2) couvre la branche generation de la famille ; SheetSage2 (MERT2-FS + LoRA + AR) transcrit l’audio en partition ABC - l’espace d’embeddings mesure ici est le socle des deux.
Retour : README de la serie Audio