|
Download docs/architecture.md from Frankenstein-Labs/Cortex-0: direct link, hf CLI and curl.
- Browser
- Download file 26.1 kB
-
https://huggingface.co/Frankenstein-Labs/Cortex-0/resolve/main/docs/architecture.md
- Command line
-
hf download hf://Frankenstein-Labs/Cortex-0/docs/architecture.md
-
curl -L -o architecture.md https://huggingface.co/Frankenstein-Labs/Cortex-0/resolve/main/docs/architecture.md
26.1 kB
| # Architecture — Justifications techniques | |
| > Ce document est **structuré dès la Phase 0** et rempli **au fur et à mesure**. | |
| > Règle 7 du cahier des charges : **chaque choix technique est justifié ici**. | |
| > Tant qu'une section est vide, elle porte la mention `⏳ À documenter (Phase N)`. | |
| ## 0. Vue d'ensemble | |
| Un **modèle unifié** traite toutes les modalités comme des **tokens** dans une | |
| **séquence unique** : | |
| ``` | |
| texte ──► embeddings texte ─┐ | |
| image ──► ViT maison ──► MLP ─┤ | |
| audio ──► transformer mel ──► MLP ─┼──► séquence concaténée ──► transformer texte ──► logits | |
| vidéo ──► (frames = images) + audio ┘ (avec tokens de délimitation) | |
| ``` | |
| **Pourquoi une séquence unique plutôt que des têtes séparées ?** | |
| Parce qu'un seul décodeur autoregressif peut alors *conditionner* sa génération sur | |
| n'importe quelle combinaison d'entrées, sans architecture ad hoc par tâche. Les tokens | |
| de délimitation (`<|image_start|>`, `<|audio_start|>`, …) indiquent au modèle où une | |
| modalité commence et finit. C'est le principe des VLM modernes, mais **réimplémenté | |
| intégralement à la main**. | |
| ## 1. Tokenizer (Phase 1) — ✅ implémenté, entraîné, testé | |
| Choix retenus (voir aussi ADR-010) : | |
| - **BPE** (`tokenizers.models.BPE`) plutôt qu'Unigram/SentencePiece : algorithme | |
| déterministe, simple à auditer, excellent sur les langues à morphologie riche | |
| comme le français. Entraînement **sur notre corpus**, aucun tokenizer pré-entraîné. | |
| - **`pre_tokenizer = ByteLevel(add_prefix_space=False)`** : découpe au niveau des | |
| octets. Conséquence : aucun caractère n'est jamais hors-vocabulaire, les accents | |
| (é è ê à ç ù î ô œ æ), les emojis et tout script (japonais, arabe…) sont gérés | |
| nativement. | |
| - **`initial_alphabet = ByteLevel.alphabet()` (256 octets)** : première idée | |
| fausse — sans cet alphabet initial, seuls les octets *vus dans le corpus* ont un | |
| token, donc un emoji jamais vu était **perdu** au round-trip (vérifié | |
| empiriquement). En forçant les 256 octets en tokens `0x00..0xFF`, `encode` → | |
| `decode` devient un **round-trip exact** pour toute entrée. | |
| - **`byte_fallback=True`** sur le modèle BPE : repli octet pour les caractères | |
| inconnus. *(En `tokenizers` 0.23, `byte_fallback` est un paramètre du **modèle | |
| BPE**, pas du trainer — un argument `byte_fallback` passé au trainer est ignoré | |
| avec un warning. Corrigé dans le code.)* | |
| - **`min_frequency=2`** : évite de figer des paires vues une seule fois. | |
| - **Tokens spéciaux réservés en premier**, dans l'ordre canonique de | |
| `src/tokenizer/special_tokens.py` → ids **0..18**, stables et connus à l'avance : | |
| - base (4) : `<|pad|> <|bos|> <|eos|> <|unk|>` | |
| - raisonnement/agent (6) : `<|think|> <|plan|> <|tool_call|> <|tool_result|> <|critic|> <|moe_route|>` | |
| - modalités (9) : `<|image_start|> <|image_end|> <|image_pad|>`, idem audio et vidéo. | |
| - la forme `<|...|>` garantit qu'aucun token spécial ne peut émerger d'une | |
| fusion BPE sur du texte naturel nettoyé. | |
| - **`vocab_size` est une borne supérieure, pas une garantie** : `BpeTrainer` ne | |
| produit jamais plus que le nombre de fusions possibles. Sur ~150 Mo de français | |
| on atteint bien **32000 exactement** (mesuré), mais sur un corpus pauvre le | |
| vocab final serait plus petit → le script **échoue explicitement** plutôt que | |
| de produire un tokenizer trompeur. | |
| **Résultat mesuré (local, sur cette machine CPU)** : corpus Wikipedia FR | |
| (`wikimedia/wikipedia` `20231101.fr`, streaming, plafond 150 Mo), **32 000 | |
| tokens**, **21,8 s**, round-trip exact sur accents/emojis/CJK/arabe. | |
| **Corpus** : plafonné à ~150–200 Mo (au lieu des ~6 Go de Wikipedia FR complet), | |
| pour tenir sur Colab et rester raisonnable en local. Documenté en ADR-010. | |
| **Pas** d'en-tête de licence ni de boilerplate injecté dans le corpus (consigne | |
| explicite). | |
| ## 2. Transformer texte (Phase 2) — ✅ implémenté, testé | |
| Modules : `src/model/{rope,attention,ffn,block,text_model}.py`. Aucun module | |
| `transformers`, aucun `nn.MultiheadAttention` : projections, découpage en têtes, | |
| masque causal, softmax et rotations RoPE sont **écrits à la main**. | |
| ### 2.1 RoPE (Rotary Position Embedding) | |
| **Choix** : RoPE plutôt qu'un embedding positionnel absolu appris. | |
| **Justification** : | |
| - En faisant tourner les paires de dimensions de Q et K d'un angle proportionnel | |
| à la position, le produit scalaire `Q·K` **ne dépend que de l'écart relatif** | |
| de position (test `test_rope_makes_attention_depend_on_relative_position`). | |
| - **Zéro paramètre**, aucune limite intrinsèque de longueur. | |
| - Meilleure **extrapolation** qu'un embedding absolu, et réutilisable tel quel | |
| pour image/audio (une position = un indice dans la séquence). | |
| - Chaque paire (2i, 2i+1) est une **rotation** → la norme est conservée | |
| (test `test_rope_preserves_norm`). | |
| Config : `rope_theta = 10000`, `head_dim` pair requis. | |
| ### 2.2 Pre-norm + residual | |
| x = x + Attention(LN(x)) | |
| x = x + FFN(LN(x)) | |
| **Justification** : le gradient traverse la connexion résiduelle **sans être | |
| multiplié** par la norme → stabilise les réseaux profonds, rend le warmup moins | |
| critique. Forme des transformers modernes. Test : `test_block_is_prenorm`. | |
| ### 2.3 Attention causale | |
| Le masque est un triangulaire supérieur mis à `-inf` **avant** le softmax, donc | |
| les poids vers le futur valent **exactement 0**. Preuves par test : | |
| - `test_attention_is_strictly_causal` : modifier le futur ne change pas les sorties passées ; | |
| - `test_model_is_causal` : idem au niveau des logits du modèle complet ; | |
| - `test_attention_matches_manual_inference_for_second_token` : comparaison à un | |
| calcul d'attention refait entièrement à la main. | |
| ### 2.4 FFN GELU | |
| MLP à deux couches, dimension cachée `ffn_mult * n_embd` (défaut 4). `lm_head` | |
| lié aux embeddings (`tie_embeddings`) dans un profil, séparé dans l'autre. | |
| ### 2.5 Paramètres : l'écart 50 M vs réalité | |
| Point d'honnêteté déjà noté (valeurs **vérifiées** en Phase 0) : | |
| Formule utilisée (vocab `V`, dim `E`, couches `L`, FFN `4E`, biais désactivés) : | |
| ``` | |
| params = V·E (embeddings) + L·(4E² + 8E² + 2E) + E (norm finale) [+ V·E si lm_head non lié] | |
| ``` | |
| | Profil | V | E | L | lm_head | Total | | |
| |---|---|---|---|---|---| | |
| | `text_model` (défaut, demandé) | 32000 | 384 | 6 | lié | **22.9 M** | | |
| | `text_model` (idem) | 32000 | 384 | 6 | non lié | **35.2 M** | | |
| | `text_model_50m` | 32000 | 512 | 6 | non lié | **51.6 M** | | |
| Le profil demandé (`n_embd=384, n_layer=6`) ne fait donc **pas** 50 M. Le profil | |
| `text_model_50m` (`n_embd=512, n_head=8, n_layer=6`, embeddings non liés) atteint | |
| **51.6 M**. Les deux sont fournis ; l'écart est documenté, jamais masqué. | |
| Ces tailles sont **recalculées à partir du vrai modèle** dans | |
| `tests/test_phase0_params.py` (aucun chiffre codé en dur non vérifié). | |
| ### 2.6 Résultats mesurés (local, CPU) | |
| - Test unitaire `test_overfit_single_batch` : loss → **< 0.05** sur un lot unique. | |
| - Démo réelle : mini-corpus FR (4 phrases × 40, `n_layer=4, n_embd=128`, 300 pas, | |
| **68 s**) → loss **10.45 → 0.011**. Génération (T=0.7, top-k=40) : | |
| > « Le chat noir dort sur le canapé du salon. Marie mange une pomme rouge dans le jardin. Les enfants jouent ensemble après… » | |
| ### 2.7 Génération (`src/generate.py`) | |
| Échantillonnage entièrement écrit à la main : **température**, **top-k**, | |
| **top-p (nucleus)**, glouton, arrêt sur `<|eos|>`. Aucune fonction `model.generate`. | |
| ### 2.8 Entraînement texte (Phase 3) | |
| `src/data/prepare_text.py` + `src/train/train_text.py` + `src/evaluate.py`. | |
| - **Données** : tokenisation du corpus en un tableau `uint16` (le vocab 32 000 | |
| tient sur 16 bits → 2 octets/token), un `<|eos|>` après chaque document. | |
| Découpe train/val **séquentielle** (la val est la fin du flux) pour éviter | |
| toute fuite. `np.memmap` : aucune copie en RAM. Encodage par lots | |
| (`encode_batch`, Rust) pour la vitesse. | |
| - **Optimiseur** : AdamW, **weight decay 0.1 uniquement sur les matrices** | |
| (`dim >= 2`), jamais sur les normes ni les biais — convention standard. | |
| - **LR** : warmup linéaire puis cosinus jusqu'à `lr * min_lr_ratio`, planificateur | |
| écrit à la main (testé : croissant puis décroissant, monotone après warmup). | |
| - **Stabilité** : `clip_grad_norm_` (1.0), `autocast` (bf16/fp16 sur GPU, fp32 sur CPU). | |
| - **Checkpoints** : `last.pt` et `best.pt` (sur val), logs `train_log.jsonl`. | |
| **Résultat mesuré (local, CPU, 8 Mo de Wikipedia FR = 1,91 M tokens)** : | |
| `n_layer=4, n_embd=128` (~9 M params), 300 pas en 258 s → loss **10.40 → 6.06**, | |
| perplexité val **475**. Échantillon réel (T=0.7) : | |
| > « La France est alors que les trois villes, les États-Unis, dans des plus de la plus de la ville. Le gouvernement et la fin de l'est que une première place de l'origine… » | |
| À 300 pas sur 8 Mo, la cible **ppl < 30** n'est **pas** atteinte : elle demande le | |
| run complet (~200 Mo, profil 50 M, plusieurs heures GPU — voir | |
| `notebooks/02_train_text.ipynb`). Aucun résultat n'est annoncé sans mesure. | |
| ## 3. Encodeur image (Phase 4) — ✅ implémenté, testé | |
| Modules : `src/model/vit.py`, `src/model/projectors.py`, `src/data/toy_images.py`. | |
| ### 3.1 Architecture | |
| - **Patch embedding** : convolution noyau/pas `patch_size` → découpe l'image en | |
| patchs non chevauchants et projette chacun en `n_embd`. Équivaut à une | |
| projection linéaire par patch, mais plus simple à écrire. | |
| - **Token CLS** appris (pool `cls`) ou moyenne des patchs (pool `mean`). | |
| - **Positional embedding appris** : sur une grille 2D de taille **fixe et connue**, | |
| un vecteur par position est simple et suffisant. **RoPE 2D n'apporte rien** ici, | |
| donc on ne l'utilise pas pour l'image (il reste utilisé pour le texte). | |
| - N blocs transformer + LayerNorm finale. | |
| ### 3.2 Attention bidirectionnelle (bug réel corrigé) | |
| Un encodeur d'image doit utiliser une attention **non causale** : chaque patch | |
| (et le CLS) doit voir toute l'image. La première version réutilisait l'attention | |
| **causale** du texte → le CLS, en position 0, ne voyait **aucun** patch et le | |
| modèle restait bloqué à la perte de hasard (logits identiques pour toutes les | |
| images, vérifié au débogage). Correction : drapeau `causal` sur l'attention | |
| (`causal=True` pour le texte, `causal=False` pour image/audio). Un test de | |
| régression (`test_encoder_is_bidirectional_not_causal`) empêche le retour du bug. | |
| ### 3.3 Générateur d'images jouets (maison) | |
| `src/data/toy_images.py` génère des images RGB 32×32 avec une forme colorée | |
| (cercle / carré / triangle × rouge / vert / bleu = **9 classes**), une légende | |
| française (« un cercle rouge ») et du bruit léger. Déterministe (seed), aucune | |
| dépendance à des données externes. Sert aux Phases 4, 5 et 6. | |
| ### 3.4 Projecteurs | |
| `MLPProjector` : `LayerNorm → Linear → GELU → Linear` projette image/audio vers | |
| `text_dim`. Un MLP (non linéaire) plutôt qu'une projection linéaire, pour un coût | |
| modeste — c'est le composant qui aligne les modalités. | |
| ### 3.5 Résultats mesurés (local, CPU) | |
| - Classification 9 classes, 18 images, `n_embd=32`, 2 blocs : **acc > 0.9** | |
| (overfit volontaire, jeu minuscule). | |
| - Reconstruction (auto-encodeur) : **MSE < 0.05** après 120 pas. | |
| ## 4. Alignement image-texte (Phase 5) — ✅ implémenté, testé | |
| Modules : `src/model/clip.py`, `src/data/prepare_image_text.py`, | |
| `src/train/train_clip.py`. | |
| ### 4.1 Architecture | |
| `CortexClip` = `VisionEncoder` (Phase 4) + transformer texte (Phase 2), chacun suivi | |
| d'un `MLPProjector` vers un espace commun `embed_dim`, **normalisés L2**. La | |
| similarité est le produit scalaire ; la température est une `logit_scale` apprise | |
| (init `1/0.07`, plafonnée à 100). | |
| ### 4.2 Loss contrastive symétrique | |
| `contrastive_loss` (écrite à la main) : cross-entropy sur les lignes **et** les | |
| colonnes, bonne paire sur la diagonale, moyenne des deux. Vérifié par tests : | |
| diagonale forte → perte ≈ 0 ; similarités uniformes → perte = `log(batch)` ; | |
| symétrie en échangeant image/texte. | |
| ### 4.3 Deux pièges réels rencontrés (et corrigés) | |
| 1. **Le pooling du texte incluait les tokens de padding.** Toutes les légendes se | |
| ressemblaient. Correction : un **masque d'attention** (1 sur les vrais tokens, | |
| 0 sur le PAD) est maintenant threadé jusqu'au pooling. | |
| 2. **Les légendes se répètent dans le lot.** Avec 9 classes et 1800 images, un lot | |
| aléatoire contient des légendes dupliquées : l'appariement image↔texte n'est plus | |
| 1-1 et la perte symétrique devient **contradictoire → collapse** (perte figée à | |
| `log(batch)`, embeddings alignés). Correction : `_build_batches` construit des | |
| lots où **chaque légende apparaît une seule fois** (au plus une image par classe | |
| et par lot). Sans ce correctif, même une architecture correcte ne converge pas. | |
| Un lr trop élevé (3e-3 → 8e-3) provoque aussi un collapse ; **gradient clipping 1.0** | |
| + lr modéré stabilisent l'entraînement. | |
| ### 4.4 Métrique | |
| `class_retrieval_accuracy` : retrieval image→texte **au niveau des classes** (banque | |
| de légendes uniques), pas l'argmax diagonal (biaisé par les doublons). | |
| ### 4.5 Résultats mesurés (local, CPU) | |
| - Paires jouets 32×32 (9 classes), `embed_dim=64`, 2+2 blocs, **1200 pas en 126 s** : | |
| loss **2.20 → 0.15**, retrieval val **0.81** (hasard 0.11). | |
| - Sur 9 images/légendes uniques, retrieval diagonal **1.00** (loss → 0.00). | |
| ## 5. Modèle multimodal génératif image+texte (Phase 6) — ✅ implémenté, testé | |
| Modules : `src/model/vlm.py`, `src/train/train_vlm.py`, `src/model/text_model.py` | |
| (ajout de `inputs_embeds`). | |
| ### 5.1 Fusion par embeddings | |
| Les patchs d'image (ViT, Phase 4) sont projetés dans l'espace texte puis **injectés | |
| comme des « tokens d'image »** dans la séquence, encadrés par `<|image_start|>` … | |
| `<|image_end|>`. On passe des `inputs_embeds` au transformer : features projetées | |
| sur les positions d'image, embeddings de tokens ailleurs. Le décodeur causal voit | |
| l'image avant de générer la réponse. | |
| ### 5.2 Bug critique corrigé : décalage des labels | |
| `CortexTextLM.forward` ne décale **pas** les targets (convention « décalage géré par | |
| l'appelant », comme en Phase 3). Or `build_example` mettait d'abord `label[t] = ids[t]` | |
| (non décalé) : le modèle prédisait le token qu'il voyait déjà → **loss triviale | |
| (0.0006) et génération cassée**. Correction : `labels[t] = ids[t+1]` sur la réponse | |
| (convention GPT). Un test vérifie l'invariant `labels[t] == ids[t+1]`. | |
| ### 5.3 Masquage de la perte | |
| `ignore_index=-100` ajouté à la cross-entropy : seules les positions de réponse | |
| comptent. Vérifié par un test qui recalcule la CE à la main. | |
| ### 5.4 Grounding visuel mesuré (local, CPU, 115 s, 400 pas) | |
| Config `grid=32, patch=8` (16 tokens d'image), `n_layer=2, n_embd=64`. | |
| - **Exactitude teacher-forced** (métrique honnête) : légende **0.85**, forme **0.80**, | |
| couleur **1.00**, comptage **1.00** (hasard : forme 0.33, couleur 0.33). | |
| - **Génération libre (greedy)** : 0.35 — bien plus faible à cause du **biais | |
| d'exposition** sur un modèle aussi petit. On ne prétend donc pas que la génération | |
| libre est bonne : le grounding visuel est réel, la génération reste fragile. | |
| ### 5.5 Leçons de réglage | |
| Un lr de 5e-3 collapsait la génération ; **2e-3 + gradient clipping 1.0** est stable. | |
| Avec 16×16 et patch 8 (4 tokens seulement) le modèle ne voit pas la forme ; 32×32 | |
| (16 tokens) le permet. | |
| ## 6. Encodeur audio (Phase 7) — ✅ implémenté, testé | |
| Modules : `src/data/audio_features.py`, `src/data/toy_audio.py`, | |
| `src/model/audio_encoder.py`, `src/data/prepare_audio.py`, `src/train/train_audio.py`. | |
| ### 6.1 Mel-spectrogramme écrit à la main | |
| `torchaudio` n'étant pas installé dans l'environnement, l'extraction mel est | |
| **entièrement maison** (encore plus « from scratch ») : STFT via `torch.fft`, fenêtre | |
| de Hann périodique, banc de filtres mel triangulaires (`hz_to_mel`/`mel_to_hz`), | |
| puis log. Vérifié par un test : un sinus pur de 1 kHz produit bien un pic de | |
| magnitude à ~1 kHz. | |
| ### 6.2 Architecture | |
| Patch embedding (conv 2D sur le mel, patchs fréquence × temps) + **positional | |
| embedding appris** + blocs transformer **bidirectionnels** (l'audio n'est pas causal : | |
| le clip entier est visible). Un test vérifie la bidirectionnalité. Pooling moyen → | |
| projecteur MLP → espace texte. | |
| ### 6.3 Données — réduction de scope assumée | |
| L'environnement de dev n'a pas d'accès audio (pas de Common Voice FR). On | |
| **synthétise** des voyelles par un modèle de formants (F0 + F1/F2, deux hauteurs) : | |
| **10 classes** (5 voyelles × grave/aigu). C'est un jeu jouet, pas de la parole réelle. | |
| La transcription de **parole réelle** (Common Voice fr) reste à faire dans un | |
| environnement avec audio ; le pipeline (mel → encodeur → décodeur) est identique. | |
| ### 6.4 Résultats mesurés (local, CPU, 233 s, 600 pas) | |
| - **Transcription** : teacher-forced **1.00**, génération libre **1.00** sur 10 classes | |
| (hasard 0.10), loss 10.4 → 0.0005. Les voyelles synthétiques sont parfaitement | |
| transcrites (« voyelle a grave », …). | |
| ### 6.5 Fusion réutilisée | |
| `build_modality_example` (généralisé en Phase 7) construit la séquence | |
| `prompt <|audio_start|> [audio_pad × N] <|audio_end|> réponse`, avec labels décalés | |
| comme pour l'image. | |
| ## 7. Modèle multimodal complet texte + image + audio (Phase 8) — ✅ implémenté, testé | |
| Modules : `src/model/multimodal.py`, `src/train/train_full_multimodal.py`. | |
| ### 8.1 Un seul décodeur pour trois modalités | |
| `CortexMultimodal` réunit le décodeur texte (Phase 2), l'encodeur ViT (Phase 4) et | |
| l'encodeur audio (Phase 7). Chaque modalité est projetée (`MLPProjector`) puis | |
| **injectée dans la même séquence** aux positions marquées par `<|image_pad|>` / | |
| `<|audio_pad|>`. `assemble_embeds` remplace l'embedding du token par la feature | |
| projetée ; le décodeur causal génère la réponse. | |
| ### 8.2 Détail important : lignes du lot sans modalité | |
| Dans un lot multi-tâches, certaines lignes n'ont ni image ni audio. On n'encode que | |
| les **lignes concernées** (`mask.any(dim=1)`) avant d'affecter les embeddings, ce qui | |
| évite un décalage de forme (bug rencontré puis corrigé). Un test dédié vérifie ce cas. | |
| ### 8.3 Tâches et résultats mesurés (local, CPU, 328 s, 600 pas) | |
| Tâches : `image` (« Décris l'image : »), `audio` (« Transcris : »), `mixte` | |
| (« Image et audio : » -> « <légende> | <transcription> »). | |
| - **Exactitude teacher-forced** : image **0.92**, audio **1.00**, mixte **0.93** | |
| (hasard : image ~0.11, audio 0.10). | |
| - **Génération libre mixte** : 0.17 — le modèle lit correctement les deux modalités | |
| isolément, mais la génération libre conjointe reste fragile (biais d'exposition), | |
| ce qui est attendu pour un modèle de cette taille. On ne le cache pas. | |
| ### 8.4 Fusion réutilisable | |
| `build_modality_example` (Phase 7) et `build_sequence` (Phase 8) partagent la même | |
| convention : blocs de tokens spéciaux + labels décalés + `ignore_index=-100`. | |
| ## 8. Vidéo — images + audio synchronisés (Phase 9) — ✅ implémenté, testé | |
| Modules : `src/data/toy_video.py`, `src/model/multimodal.py::VideoEncoder`, | |
| `src/train/train_video.py`. | |
| ### 7.1 Données jouets — mouvement (tâche réellement temporelle) | |
| Une vidéo = ``n_frames`` images d'une forme colorée qui **se déplace** horizontalement | |
| (vers la droite ou la gauche), + une piste audio synthétique (Phase 7). 18 classes | |
| (3 formes × 3 couleurs × 2 directions). La direction ne peut être lue qu'en comparant | |
| les frames : un test vérifie que le centroïde horizontal croît (« droite ») ou décroît | |
| (« gauche »). | |
| ### 7.2 Architecture | |
| `VideoEncoder` = un ViT (Phase 4) **partagé** sur chaque frame + un **embedding | |
| temporel appris** par frame + un petit transformer **spatio-temporel** (bidirectionnel) | |
| sur l'ensemble des `n_frames × n_patches` tokens. Un test vérifie que réordonner les | |
| frames change les features (le modèle regarde bien le temps). Coût quadratique de | |
| l'attention sur le nombre de tokens (limite documentée : on garde `n_frames` petit). | |
| ### 7.3 Tâches et résultats mesurés (local, CPU, 98 s, 300 pas, 4 frames) | |
| - « Décris la vidéo : » -> exactitude teacher-forced **0.78** ; | |
| - « Que se passe-t-il ? » (frames + audio) -> **0.85** (hasard ~0.055). | |
| - Génération libre 0.0 : non caché — décrire une vidéo en génération libre reste | |
| hors de portée d'un modèle de cette taille ; le grounding est réel. | |
| ## 9. Mixture-of-Experts — 20 experts, top-2 (Phase 10) — ✅ implémenté, entraîné, testé | |
| Module : `src/model/moe.py`, entraînement `src/train/train_moe.py`. | |
| ### 9.1 Pourquoi MoE | |
| Un MoE **augmente la capacité** (nombre total de paramètres) **sans** augmenter le coût | |
| de calcul par token : seuls les `top_k` experts activés sont traversés. On remplace les | |
| FFN denses du transformer texte par `MoEFFN` (`n_experts=20`, `top_k=2`). | |
| ### 9.2 Routeur (écrit à la main) | |
| `router = Linear(n_embd, n_experts)` ; on garde les `top_k` plus grands logits, on | |
| renormalise les portes, et on n'applique chaque expert qu'aux tokens qui le choisissent | |
| (calcul épars). Pendant l'entraînement, un **bruit gaussien** (`jitter`) est ajouté aux | |
| logits pour l'exploration. | |
| ### 9.3 Loss auxiliaire d'équilibrage | |
| Loss Switch-Transformer : `E * Σ_i f_i · P_i` (`f_i` = fraction de tokens routés vers | |
| l'expert `i`, `P_i` = probabilité moyenne). Pénalise la concentration. Poids | |
| `aux_loss_coef=0.01`. La perte totale = perte de langage + `aux_loss_coef * aux_loss`. | |
| ### 9.4 Résultats mesurés (local, CPU, 400 pas, 373 s, corpus FR jouet) | |
| - Perte val **0.0052**, **perplexité val 1.005** (le modèle apprend le corpus jouet). | |
| - **Entropie de charge normalisée 0.9999** sur 20 experts → routage quasi parfaitement | |
| équilibré, **0 expert mort**. | |
| - **14 855 296** paramètres totaux vs **5 418 112** actifs par token (ratio ~2.7×) : | |
| c'est le gain MoE — plus de capacité, coût par token borné par `top_k`. | |
| ### 9.5 Coûts / limites | |
| Tous les experts restent **résidents en mémoire** ; le routeur peut devenir instable | |
| sans loss auxiliaire ; le débit d'entraînement dépend de l'implémentation éparse | |
| (boucle par expert ici — suffisant à l'échelle jouet, à vectoriser pour du grand). | |
| ## 10. Mémoire / « apprendre des cours » (Phase 11) — ✅ implémenté, testé | |
| Modules : `src/memory/rag.py` (mode A), `src/memory/permanent.py` (mode B). | |
| ### 10.1 Mode A — RAG (réaliste, recommandé) | |
| Pipeline écrit à la main, sans service externe : | |
| 1. **Découpage** d'un cours en passages (`chunk_text`, par phrases jusqu'à `max_words`). | |
| 2. **Embeddings maison** par *feature hashing* déterministe (sac de mots signé, pondéré | |
| par IDF), L2-normalisés. Aucun paramètre appris — honnête à petite échelle : deux | |
| textes lexicalement proches le sont aussi vectoriellement (testé). | |
| 3. **Index vectoriel numpy** (`VectorIndex`) : recherche par similarité cosinus. | |
| 4. Un cours **vidéo** = transcription horodatée **+ légendes de frames clés** | |
| (`add_video`) ; les sources (fichier, timestamps) sont conservées. | |
| 5. Réponse **extractive** : le meilleur passage réel du cours est renvoyé verbatim, | |
| avec sources et confiance (cosinus) — **pas d'hallucination** pour le mode A. | |
| Une variante générative (`answer_with_model`) met le contexte dans le prompt. | |
| ### 10.2 Mode B — apprentissage permanent : EXPÉRIMENTAL ET INSTABLE ⚠️ | |
| Mise à jour des **poids** pendant l'ingestion (`PermanentLearner.ingest`) : quelques pas | |
| de gradient AdamW (maison) sur le texte du cours, avec un **tampon de rejeu** pour | |
| limiter l'oubli catastrophique. Avertissements tenus par des **tests** : | |
| - après ingestion, la perte sur la leçon **chute** (mémorisation réelle) ; | |
| - **sans rejeu**, la perte sur une phrase déjà apprise **augmente** — l'oubli | |
| catastrophique est démontré, pas caché. | |
| Ce mode **mémorise un texte court** et le restitue ; il **ne remplace pas** un | |
| fine-tuning, peut **dégrader** d'autres capacités, et n'offre **aucune garantie** de | |
| stabilité. Il est implémenté **en dernier** et présenté comme recherche. | |
| ## 12. API · Démo · Publication (Phase 12) — ✅ implémenté, testé | |
| ### 12.1 Moteur d'inférence unifié (`src/inference.py`) | |
| `CortexEngine` rassemble le décodeur texte et les encodeurs image/audio/vidéo derrière | |
| une API unique (`chat`, `describe_image`, `transcribe_audio`, `describe_video`). Les | |
| entrées sont décodées sans dépendance lourde : images via numpy/Pillow, audio WAV via | |
| `wave`, vidéo via tableau `(T,H,W,3)`. La fusion se fait par masques : les embeddings | |
| du décodeur sont remplacés aux positions `image_pad`/`audio_pad`/`video_pad` par les | |
| features projetées. `trained=False` tant qu'aucun checkpoint n'est chargé — **honnête** : | |
| les poids sont alors l'initialisation aléatoire, seule la tuyauterie est réelle. | |
| ### 12.2 API FastAPI (`src/api.py`) | |
| Endpoints `/health`, `/capabilities`, `/chat`, `/vision`, `/audio`, `/video`, | |
| `/memory/ingest`, `/memory/query`. Le checkpoint est choisi par `CORTEX_CHECKPOINT` | |
| (jamais de secret en dur). Tests via `TestClient` (14 tests) — tournent sur CPU. | |
| ### 12.3 Démo Gradio (`app/gradio_app.py`) | |
| Onglets Texte/Image/Audio/Vidéo/Mémoire. Les handlers sont testés sans lancer de serveur. | |
| ### 12.4 Publication (`src/push_to_hub.py`, `Dockerfile`) | |
| `push_to_hub` publie le dépôt, un `checkpoints/*.pt` optionnel et la carte de modèle | |
| (front-matter avec `license: apache-2.0`). Le Dockerfile expose l'API (8000) et la démo | |
| (7860) ; `HF_TOKEN`/`CORTEX_CHECKPOINT` sont fournis au runtime, jamais cuits dans l'image. | |
| ## 11. Limites assumées | |
| - Un modèle de quelques dizaines de millions de paramètres **ne raisonne pas** comme un | |
| modèle de plusieurs centaines de milliards. Aucune équivalence ne sera prétendue. | |
| - Entraînement réel limité par le matériel (CPU ici → Colab pour l'entraînement). | |
| - Les résultats seront **mesurés**, jamais inventés. | |