~/wiki

Cheatsheets — vue longue

retour à la liste

Toutes les pages concaténées sur un seul document, pour un Ctrl-F direct.

Bash & shell

page dédiée →

Les commandes qu'on tape sans réfléchir, et celles qu'on cherche à chaque fois.

Fichiers et dossiers

mkdir -p projet/src/utils    # -p : crée les parents, ne râle pas si ça existe
touch projet/src/main.py
ls -la projet
total 0
drwxr-xr-x  3 edouard  staff   96 Aug  7 14:02 .
drwxr-xr-x  8 edouard  staff  256 Aug  7 14:02 ..
drwxr-xr-x  4 edouard  staff  128 Aug  7 14:02 src
cp -r src/ backup/           # -r : récursif, indispensable pour un dossier
mv ancien.py nouveau.py
rm -rf build/                # -f : pas de confirmation. Vérifier le chemin AVANT

ls utile : -l long, -a cachés, -h tailles lisibles, -t tri par date, -S par taille.

Trouver

find . -name "*.py" -not -path "*/node_modules/*"
find . -type f -size +100M              # les gros fichiers
find . -name "*.log" -mtime +7 -delete  # modifiés il y a plus de 7 jours
rg "requests.post" --type py     # ripgrep : rapide, respecte .gitignore
rg -i "todo" -A 2                # -i insensible à la casse, -A 2 lignes après
rg -l "FastAPI"                  # -l : juste les noms de fichiers
rg -c "import"                   # -c : compte par fichier

rg bat grep sur tous les tableaux. Si grep est imposé : grep -rn "motif" .

Inspecter un fichier

head -20 gros.csv        # 20 premières lignes
tail -f app.log          # -f : suit le fichier en direct
wc -l data.csv           # compte les lignes
du -sh *                 # taille de chaque entrée du dossier
df -h                    # espace disque restant
du -sh * | sort -rh | head -5
1.2G	node_modules
340M	.venv
 12M	data
2.1M	src
 48K	README.md

Pipes et redirections

commande > fichier      # écrase
commande >> fichier     # ajoute
commande 2> erreurs.log # stderr seulement
commande &> tout.log    # stdout + stderr
commande 2>/dev/null    # jette les erreurs
commande | tee log.txt  # affiche ET écrit

curl -s ... | bash télécharge un script et l'exécute directement. Pratique pour les installeurs officiels, dangereux pour tout le reste : lire d'abord avec curl -s <url> | less.

Processus et ports

lsof -i :3000            # qui occupe le port 3000
kill <PID>               # demande poliment
kill -9 <PID>            # force, en dernier recours
kill -9 $(lsof -t -i :3000)
ps aux | rg python
lsof -i :3000
COMMAND   PID     USER   FD   TYPE DEVICE  NODE NAME
node    88008 edouard   24u  IPv6 0x9f2a   TCP *:hbci (LISTEN)

Variables d'environnement

export API_KEY="sk-..."     # pour cette session
echo $API_KEY
env | rg API                # lister
unset API_KEY

set -a; source .env; set +a  # charger un .env dans le shell

$VAR non défini vaut chaîne vide sans erreur. ${VAR:?message} échoue explicitement, ${VAR:-defaut} fournit une valeur de repli.

Enchaîner

a && b     # b seulement si a réussit  (exit code 0)
a || b     # b seulement si a échoue
a ; b      # b dans tous les cas
a &        # a en tâche de fond
echo $?    # code de sortie de la dernière commande

Boucles et substitution

for f in *.md; do echo "-- $f"; wc -l "$f"; done

for i in {1..5}; do curl -s "https://api.example.com/p/$i" > "p$i.json"; done

NOW=$(date +%Y-%m-%d)        # substitution de commande
echo "backup-$NOW.tar.gz"

Toujours guillemeter "$f" : sans guillemets, un nom de fichier avec un espace se découpe en deux arguments.

Archives et transferts

tar -czf archive.tar.gz dossier/    # c créer, z gzip, f fichier
tar -xzf archive.tar.gz             # x extraire
zip -r archive.zip dossier/
scp fichier user@host:/chemin/
rsync -avz --progress src/ user@host:/dest/

Raccourcis qui font gagner du temps

Touches Effet
Ctrl-R recherche dans l'historique
Ctrl-A / Ctrl-E début / fin de ligne
Ctrl-W supprime le mot précédent
Ctrl-U supprime jusqu'au début
Ctrl-C interrompt
Ctrl-D fin d'entrée / quitte le shell
!! dernière commande (sudo !!)
cd - dossier précédent

See also

Les flags qui couvrent 90 % des cas

Flag Effet
-X POST méthode (inutile avec -d, qui implique POST)
-H "K: V" un header, répétable
-d '{"a":1}' corps de la requête
-G envoie les -d en query string au lieu du corps
--data-urlencode "q=a b" encode la valeur
-s silencieux, pas de barre de progression
-S affiche quand même les erreurs (à coupler avec -s)
-i inclut les headers de réponse
-I headers seulement, requête HEAD
-L suit les redirections
-o fichier écrit dans un fichier
-O garde le nom distant
-u user:pass authentification basique
--max-time 30 timeout global
-w '%{http_code}' formate une sortie après coup

POST JSON, le cas de référence

curl -sS -X POST "https://api.example.com/v1/sessions" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"agent_id": "a_123", "task": "trouver le produit"}'
{"id":"s_88fe21","status":"queued","created_at":"2026-08-07T14:12:03Z"}

Sans Content-Type: application/json, beaucoup d'API répondent 415 Unsupported Media Type. C'est l'oubli le plus fréquent.

Corps trop long pour la ligne de commande : -d @payload.json lit un fichier.

GET avec des paramètres

curl -sS -G "https://api.example.com/v1/items" \
  --data-urlencode "q=chaussure noire" \
  --data-urlencode "page=2"

-G évite d'encoder soi-même les espaces et les accents.

Lire la réponse

curl -s https://api.github.com/repos/astral-sh/uv | jq '.stargazers_count, .license.name'
62841
"Apache License 2.0"
curl -s https://api.example.com/items | jq '.items[] | {id, name}'
curl -s https://api.example.com/items | jq -r '.items[].name'   # -r : sans guillemets

Debug

curl -i https://example.com          # headers + corps
curl -I https://example.com          # headers seuls
curl -v https://example.com          # tout le dialogue, TLS compris
curl -s -o /dev/null -w "%{http_code} %{time_total}s\n" https://example.com
200 0.184s

-w sert à mesurer sans polluer la sortie. Autres variables utiles : %{size_download}, %{redirect_url}, %{content_type}.

Fichiers

curl -O https://example.com/data.csv           # garde le nom
curl -o local.csv https://example.com/data.csv
curl -L -o file.zip https://example.com/dl     # suit la redirection
curl -F "file=@photo.jpg" -F "note=test" https://api.example.com/upload

-F fait du multipart, -d fait du form-urlencoded ou du brut : ce ne sont pas les mêmes requêtes.

Le pipe vers bash

curl -LsSf https://astral.sh/uv/install.sh | sh

Correct pour un installeur officiel. Pour tout le reste, lire d'abord :

curl -sSL https://exemple.com/install.sh | less

Équivalences avec Python

curl requests
-X POST requests.post(...)
-H "K: V" headers={"K": "V"}
-d '{"a":1}' json={"a": 1}
-G --data-urlencode "q=x" params={"q": "x"}
-u user:pass auth=("user", "pass")
--max-time 30 timeout=30
-L allow_redirects=True (défaut)

Astuce navigateur : dans l'onglet Réseau des DevTools, clic droit sur une requête puis « Copy as cURL » reproduit l'appel complet, cookies et headers inclus.

See also

DeepSeek-OCR

page dédiée →

Trait distinctif : un modèle OCR qui traite la vision comme un problème de compression, pas de perception. 97 % de précision avec 10× moins de tokens que les pipelines classiques.

Architecture DeepEncoder

Trois étages avec un goulot explicite entre le traitement local et global.

# Flux conceptuel
DeepEncoder = KnowledgeHead(Compressor(PerceptionHead(Image)))
Étage Rôle Coût attention
Perception Head Attention par fenêtres 8×8 (détails locaux) O(N·M²), M=64
Conv2D 16× kernel=4, stride=4 : 4096 → 256 tokens -
Knowledge Head Attention globale (structure document) O(256²) au lieu de O(4096²)

Résultat : ~250× de réduction du coût d'attention globale.

Modes de résolution

Le budget de tokens est un paramètre de design de premier ordre.

Modes natifs (vue unique)

Mode Résolution Tokens Cas d'usage
Tiny 512×512 64 Tickets, formulaires simples
Small 640×640 100 Documents standards
Base 1024×1024 256 Documents denses
Large 1280×1280 400 Layouts complexes

Modes dynamiques (tuiles + vue globale)

# Gundam : n tuiles à 640 (100 tokens/tuile) + 1 global à 1024 (256 tokens)
tokens_total = n * 100 + 256  # n ∈ [2,9]

# Gundam-Master : n tuiles à 1024 (256 tokens/tuile) + 1 global à 1280 (400 tokens)
tokens_total = n * 256 + 400

Tokens valides après padding (documents non carrés)

# Exemple page A4 (2480×3508, ratio ≈ 0.707)
N_valid = floor(N_actual * (min(w,h) / max(w,h)))

# Base mode : floor(256 * 0.707) = 181 tokens valides
# Large mode : floor(400 * 0.707) = 282 tokens valides

Métriques de performance

Comparaison avec les pipelines classiques.

Système Tokens/page Précision OCR Throughput (1×A100-40G)
DeepSeek-OCR (Small) ~100 ~97 % 200k pages/jour
DeepSeek-OCR (Base) ~256 ~97 % 200k pages/jour
GOT-OCR2.0 256 - -
MinerU 2.0 ~6000 - -
Qwen-VL 4000-6000 - -

Compromis compression/précision

  • 9-10× compression : ~97 % précision
  • 20× compression : ~60 % précision

DeepSeek-OCR vs Qwen-VL

Objectifs différents, architectures différentes.

Aspect Qwen-VL DeepSeek-OCR
Philosophie Comprendre tout Compresser intelligemment
Tokens/image 4k-16k 64-400
Compression Aucune (encodeur unique) Goulot 16× explicite
Meilleur pour Raisonnement multimodal, vidéo Archives documents, OCR
Usage contexte LLM Lourd Léger

Quand choisir quoi

  • Qwen-VL : compréhension multimodale générale, UI, vidéo longue (1M tokens)
  • DeepSeek-OCR : extraction de texte à partir de documents, compression pour LLM

Entraînement et fonction de perte

Le décodeur est un petit MoE autorégressif.

# Teacher forcing : à chaque pas t, on donne les vrais tokens précédents
L = -Σ(t=1..T) log p_θ(y_t | y_<t, T_vision)

# Gradient pour logit z_tk
∂L/∂z_tk = p_tk - 1[k == y_t]

# Token correct : (probabilité - 1) → pousse vers le haut si p < 1
# Tokens incorrects : probabilité → pousse vers le bas

Le gradient remonte à travers :

  1. Projection de sortie
  2. Couches du décodeur (self-attention + MLP)
  3. Cross-attention vers les tokens vision
  4. Tous les poids de DeepEncoder

Rôle de CLIP dans DeepEncoder

On réutilise uniquement l'encodeur image de CLIP (ViT), pas l'encodeur texte ni la perte contrastive.

# Flux après compression
[256 tokens compressés]
  ↓ Adaptateur linéaire (→ d_clip)
  ↓ + Embeddings positionnels 2D
  ↓ ViT CLIP (attention globale)
  ↓ Adaptateur linéaire (→ d_decoder)
[256 tokens vision pour décodeur]

Initialisation forte, architecture éprouvée, fine-tuné par cross-entropie OCR.

Vision-as-compression pour LLM

Utiliser des tokens visuels pour compresser l'information textuelle.

Workflow

  1. Renderer : Convertir texte/PDF long → images 2D (préserve layout)
  2. DeepEncoder : Compresser en K tokens vision (100-800 par page)
  3. LLM : Cross-attend aux tokens compressés au lieu du texte complet

Quand ça gagne

  • PDFs longs avec tableaux, colonnes multiples
  • 100-800 tokens vision au lieu de 2000-10000 tokens texte
  • Préserve la structure 2D que le texte linéaire perd

Compression hiérarchique pour texte pur (direction future)

# Architecture proposée
1. Attention complète sur fenêtre glissante (W = 4-8k tokens)
2. Conv1D stride-2 sur tokens anciens (KV seulement, pas Q)
3. Keep-gate apprise : préserver tokens importants (entités, nombres)
4. Goulot latent : petit ensemble de latents cross-attend au passé lointain
5. Tokens globaux : quelques-uns par couche pour chemins longue portée

# Entraînement
- Distillation depuis teacher full-attention (perte KL)
- Supervision de gate depuis attention maps du teacher
- Perte de reconstruction sur tokens anciens masqués

Comparaison avec d'autres pipelines

Pour une page 1024×1024.

Approche Tokens Avantages Inconvénients
Two-stage (détecteur → reconnaisseur) 5k-50k - Pipeline complexe, difficile à déployer
Multi-crop low-res (InternVL) ~9800 - Explosion de tokens, vue globale faible
Qwen-VL (NaViT + MRoPE) 4k-6k Excellent pour multimodal général Coût activation élevé, gros contexte LLM
DeepSeek-OCR 256 Budget prévisible, rapide, faible mémoire Compression lourde peut manquer micro-détails

Implications pratiques

Quand utiliser DeepSeek-OCR

  • Archives documentaires à grande échelle
  • Pipelines avec budget de tokens strict
  • Besoin de throughput prévisible (200k pages/jour sur 1 GPU)

Pattern architectural réutilisable

  1. Traitement local (détails fins)
  2. Compression agressive (goulot explicite)
  3. Raisonnement global (sur tokens compressés)

Ce pattern s'applique au-delà de l'OCR : tout domaine nécessitant un résumé efficace d'informations spatiales denses.

Systèmes hybrides futurs

Input Router
├─ Documents denses → DeepEncoder path (compression-first)
├─ Images/vidéo générales → Qwen-VL path (perception-first)
└─ Texte pur → Hierarchical text compression

Architecture détaillée de DeepEncoder

Le flux complet pour une image 1024×1024 :

Étape 1 : Patch embedding

# 1024×1024 divisé en patches 16×16
patches = (1024/16)² = 4096 patches
X₀ ∈ ℝ^(4096 × 768)  # projection linéaire

Étape 2 : Perception Head (window attention)

  • Fenêtres locales de 8×8 patches (64 tokens chacune)
  • Coût : O(4096 × 64) au lieu de O(4096²)
  • Capture les détails locaux : traits de caractères, géométrie des fontes

Étape 3 : Compresseur convolutif 16×

Conv2D(kernel=4, stride=4)
# 64×64 grid → 16×16 grid
# 4096 tokens → 256 tokens

# Agrégation par région 4×4
Y[c,i,j] = Σ W[c,d,u,v] · X[d, 4i+u, 4j+v] + b[c]

Fusionne les tokens caractères en tokens régions (mots, lignes).

Étape 4 : Knowledge Head (attention globale)

  • Attention sur 256 tokens seulement
  • Coût : O(256²) vs O(4096²) → économie de ~256×
  • Apprend la structure documentaire : colonnes, tables, hiérarchies

Étape 5 : Projection finale

T_vision ∈ ℝ^(256 × d_decoder)

Entraînement détaillé

Fonction de perte (cross-entropy autorégressif)

L = -Σ(t=1 to T) log p_θ(y_t | y_<t, T_vision)

Teacher forcing : on donne toujours les vrais tokens précédents (y_{<t}), jamais les prédictions du modèle.

Gradient du softmax

∂L/∂z_tk = p_tk - 1[k = y_t]
# Token correct : pousse vers 1
# Tokens incorrects : pousse vers 0

Le gradient remonte à travers :

  1. Projection de sortie
  2. Décodeur MoE (self-attention + MLP)
  3. Cross-attention vers tokens vision
  4. Tout DeepEncoder (y compris la conv 16×)

Rôle de CLIP : seul l'encodeur image ViT est réutilisé (pas le contrastif), comme backbone global préentraîné pour le Knowledge Head. Fine-tuné end-to-end avec la perte OCR.

Tokens valides après padding

Pour documents non carrés (ex: A4 2480×3508, ratio R ≈ 0.707) :

N_valid = ⌊N_actual × (min(w,h) / max(w,h))⌋

# Exemples A4
Base (256 tokens)  → ⌊256 × 0.707⌋ = 181 valides
Large (400 tokens) → ⌊400 × 0.707⌋ = 282 valides

Comparaison pipelines documentaires

Approche Tokens (1024×1024) Avantages Limites
Détecteur + reconnaisseur 5k-50k - Pipeline complexe, déploiement lourd
Multi-crop 384×384 (InternVL) ~9.8k - Explosion tokens, vue globale faible
Qwen-VL (NaViT + MRoPE) 4k-6k Excellente compréhension multimodale Coût activation élevé
DeepSeek-OCR 256 Budget prévisible, rapide Compression agressive perd micro-détails

Vision-as-compression pour LLM

Utiliser les tokens visuels pour compresser du texte long :

# Workflow
renderer(long_text) → image_2D
DeepEncoder(image) → 100-800 vision tokens
LLM.cross_attend(vision_tokens)  # au lieu de 2k-10k text tokens

Quand utiliser :

  • PDFs longs avec tables, multi-colonnes
  • Préserver la structure 2D perdue en linéaire
  • Économiser 5-10× sur le contexte LLM

Compression hiérarchique pour texte pur

Pattern transposable au texte :

# Architecture proposée
attention_locale(fenêtre W=4-8k tokens)
conv1d_stride2(tokens anciens, KV seulement)
keep_gate(préserver entités, nombres importants)
latent_bottleneck(quelques latents cross-attendent le passé lointain)

Entraînement :

  • Distillation depuis teacher full-attention (KL loss)
  • Supervision gate depuis attention maps du teacher
  • Perte reconstruction sur tokens masqués

Objectif : 40-70% du coût attention pour QA long document.

Systèmes hybrides

Router vers spécialistes :

if dense_document:
    path = DeepEncoder  # compression-first
elif image_or_video:
    path = Qwen_VL      # perception-first
else:
    path = hierarchical_text_compression

Questions ouvertes

  • Fallback haute résolution : rajouter des tokens pour régions ambiguës ?
  • Budget appris : réseau de politique choisit K par page selon entropie ?
  • Fusion multimodale : mixer optimalement tokens vision compressés et texte ?
  • Zero-shot compression : appliquer à audio, code, données structurées ?

See also

keras-tensorflow pytorch langchain numpy

Le cycle quotidien

docker build -t monapp:dev .
docker run --rm -p 8000:8000 --env-file .env monapp:dev
docker ps
docker logs -f <container>
docker exec -it <container> bash
docker stop <container>
docker ps
CONTAINER ID   IMAGE        COMMAND         STATUS         PORTS                    NAMES
a3f1c9e21b04   monapp:dev   "uvicorn ap…"   Up 2 minutes   0.0.0.0:8000->8000/tcp   brave_liskov

docker ps -a inclut les conteneurs arrêtés. --rm supprime le conteneur à sa sortie, ce qui évite d'en accumuler des dizaines.

Flags de run à connaître

Flag Effet
-p 8000:8000 port hôte:conteneur
-v $(pwd):/app monte le dossier courant, code à chaud
--env-file .env charge les variables
-e KEY=value une variable à la volée
-d détaché, en arrière-plan
-it interactif + tty, pour un shell
--rm nettoie à la sortie
--name api nom fixe au lieu d'un nom généré

Dockerfile Python typique

FROM python:3.12-slim

WORKDIR /app

# Couche de dépendances séparée : elle n'est reconstruite que si les
# requirements changent, pas à chaque édition du code.
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

--host 0.0.0.0 est obligatoire : sur 127.0.0.1 le serveur n'écoute que l'intérieur du conteneur et le port publié ne sert à rien.

Un .dockerignore évite d'envoyer 1 Go de contexte au démon :

.venv
node_modules
.git
__pycache__
*.pyc
.env

Compose

services:
  api:
    build: .
    ports: ["8000:8000"]
    env_file: .env
    volumes: ["./app:/app/app"]      # code à chaud en dev
    depends_on: [db]

  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: dev
    ports: ["5432:5432"]
    volumes: ["pgdata:/var/lib/postgresql/data"]

volumes:
  pgdata:
docker compose up              # au premier plan
docker compose up -d --build   # reconstruit puis détache
docker compose logs -f api
docker compose exec api bash
docker compose down            # -v pour supprimer aussi les volumes

--build n'est nécessaire que si le Dockerfile ou les dépendances changent. Avec un volume monté et un reload actif, le code est déjà à jour sans rebuild.

Nettoyer

docker system df               # où part la place
docker system prune -f         # conteneurs et réseaux inutilisés
docker system prune -a -f      # + toutes les images non utilisées
docker builder prune -f        # cache de build
docker system df
TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          24        3         18.4GB    15.2GB (82%)
Containers      5         1         120MB     98MB (81%)
Local Volumes   7         2         2.1GB     1.4GB (66%)
Build Cache     183       0         9.8GB     9.8GB

Les erreurs qu'on rencontre vraiment

port is already allocated — un autre processus tient le port.

lsof -i :8000
kill -9 $(lsof -t -i :8000)

no space left on device pendant un build — le disque de la VM Docker est plein, faire un prune progressif avant d'envisager un reset.

Le conteneur démarre puis s'arrête aussitôt — le processus principal a rendu la main. docker logs <container> donne toujours la raison.

Build lent à chaque fois — l'ordre des couches est mauvais : COPY . . avant pip install invalide le cache à la moindre modification de code.

Inspecter

docker images
docker inspect <container> | rg -i "ipaddress|mounts" -A 5
docker stats                   # CPU / RAM en direct
docker history monapp:dev      # poids par couche

See also

Flow Matching

page dédiée →

Apprendre un champ de vélocité qui transforme du bruit en données via une trajectoire continue, alternative plus rapide aux modèles de diffusion.

L'idée centrale

Au lieu d'ajouter puis retirer du bruit par étapes discrètes (diffusion), Flow Matching apprend le champ de vitesse v(x, t) qui pousse directement les points d'une distribution source p₀ (bruit gaussien) vers une distribution cible p₁ (données réelles).

ODE qui gouverne le flux

dx(t)/dt = v(x(t), t)

À l'inférence, on intègre cette ODE de t=0 à t=1 pour générer un échantillon.

Entraînement

Paires et interpolation

Pour chaque paire (x₀, x₁) où x₀ ~ N(0, I) et x₁ vient des données :

# Interpolation linéaire
x(t) = (1 - t) * x₀ + t * x₁

# Vélocité de référence (constante)
v_gt = x₁ - x₀

Loss de régression simple

import torch

def flow_matching_loss(model, x0, x1):
    t = torch.rand(x0.shape[0], 1)  # t ~ Uniform[0,1]
    x_t = (1 - t) * x0 + t * x1
    v_gt = x1 - x0
    v_pred = model(x_t, t)
    return ((v_pred - v_gt) ** 2).mean()

Pas de schedule de bruit, pas de KL : juste un MSE sur la vélocité.

Inférence (sampling)

Intégration ODE

from torchdiffeq import odeint

# x0 : bruit initial [batch, dim]
x0 = torch.randn(batch_size, dim)

# Résoudre dx/dt = v_θ(x, t)
def ode_func(t, x):
    return model(x, t)

t_span = torch.linspace(0, 1, steps=20)
trajectory = odeint(ode_func, x0, t_span, method='dopri5')
x1 = trajectory[-1]  # Échantillon final

Méthode d'Euler simple (10-50 steps suffisent souvent)

x = torch.randn(batch_size, dim)
dt = 1.0 / n_steps

for i in range(n_steps):
    t = i * dt
    v = model(x, torch.full((batch_size, 1), t))
    x = x + dt * v

Comparaison avec diffusion

Aspect Diffusion Flow Matching
Processus Ajouter/retirer du bruit Chemin direct via vélocité
Cible d'entraînement Prédire le bruit ε Prédire la vitesse v
Steps d'inférence 50-1000 10-50
Déterminisme Peut être stochastique Déterministe (ODE)
Loss MSE + éventuellement KL MSE simple

Robotique

Pourquoi Flow Matching pour les actions robot

  • Rapidité : 10-20 évaluations d'ODE suffisent pour du temps réel
  • Lissité : les trajectoires robotiques sont naturellement continues
  • Conditionnement : facile d'ajouter état actuel, but, obstacles

Exemple minimal

# v_θ conditionné sur l'état
class ConditionalFlowModel(nn.Module):
    def __init__(self, state_dim, action_dim, hidden=256):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(action_dim + 1 + state_dim, hidden),
            nn.SiLU(),
            nn.Linear(hidden, hidden),
            nn.SiLU(),
            nn.Linear(hidden, action_dim)
        )
    
    def forward(self, x, t, state):
        inp = torch.cat([x, t, state], dim=-1)
        return self.net(inp)

# Génération d'action
state = get_robot_state()
x0 = torch.randn(1, action_dim)
action = odeint(lambda t, x: model(x, t, state), x0, t_span)[-1]

Flow vs gradient : ne pas confondre

Concept Définition Notation
Gradient Direction de plus forte variation d'une fonction scalaire ∇f(x)
Flow (vélocité) Comment un point se déplace dans l'espace-temps v(x, t)

Un flow peut être un gradient (gradient flow : dx/dt = -∇f(x)), mais en Flow Matching le champ de vélocité est appris indépendamment.

Techniques liées

  • Continuous Normalizing Flows (CNF) : utilisent des ODE mais calculent des log-vraisemblances coûteuses (trace du Jacobien)
  • Rectified Flow : variante qui apprend des trajectoires plus droites via itérations successives
  • Score-Based Models : apprennent ∇ log p(x) au lieu de v(x, t), liés par v = -∇ log p

Quand l'utiliser

Flow Matching si :

  • ✅ Inférence rapide critique (robotique, interactif)
  • ✅ Données continues sur variétés lisses (trajectoires, mouvements)
  • ✅ Besoin de déterminisme

Diffusion si :

  • Images (écosystème mature, modèles pré-entraînés)
  • Variation stochastique souhaitée
  • La vitesse d'inférence n'est pas bloquante

title: Flow Matching category: cheatsheets created: 2025-01-15 updated: 2025-01-15 tags: [flow-matching, generative-models, deep-learning, diffusion, ode, cnf, pytorch] confidence: high publish: true

Flow matching transforme une distribution simple (bruit gaussien) en distribution complexe (données) via un champ de vitesse déterministe, offrant la qualité de diffusion avec 7-100× moins d'étapes d'inférence et un entraînement plus simple.

L'idée centrale

On apprend un champ de vitesse v_θ(x, t) qui décrit comment transformer du bruit en données :

# ODE à intégrer de t=0 à t=1
dx_t/dt = v_θ(x_t, t)

À l'entraînement, on régresse directement ce champ de vitesse (MSE simple) au lieu de prédire du bruit comme en diffusion. Mathématiquement équivalent à la diffusion gaussienne, mais plus intuitif.

Conditional Flow Matching (CFM)

Le trick qui rend l'entraînement tractable : conditionner sur un point de données x₁.

import torch
import torch.nn as nn

def cfm_loss(model, x1, sigma_min=0.001):
    """
    x1: batch de données réelles, shape (B, D)
    """
    B, D = x1.shape
    
    # Échantillonner t uniformément
    t = torch.rand(B, 1, device=x1.device)
    
    # Chemin conditionnel gaussien
    mu_t = t * x1
    sigma_t = 1 - t + t * sigma_min
    
    # Échantillonner x_t sur le chemin
    x0 = torch.randn_like(x1)
    x_t = mu_t + sigma_t * x0
    
    # Vitesse conditionnelle (forme close)
    u_t = (x1 - (1 - sigma_min) * x0) / (1 - (1 - sigma_min) * t)
    
    # Prédire et comparer
    v_pred = model(x_t, t)
    loss = ((v_pred - u_t) ** 2).mean()
    
    return loss

Pas de calcul de posteriors. Pas d'intégration d'ODE à l'entraînement. Juste de la régression.

Optimal Transport coupling (OT-CFM)

Le gain le plus simple : matcher les paires bruit-données avec Sinkhorn au lieu de les coupler aléatoirement.

from torchdyn.core import NeuralODE
import ot  # POT library

def ot_cfm_loss(model, x1, reg=0.05):
    B = x1.shape[0]
    x0 = torch.randn_like(x1)
    
    # Matrice de coût L2
    C = torch.cdist(x0, x1) ** 2
    
    # Plan de transport optimal (Sinkhorn)
    pi = ot.sinkhorn(torch.ones(B)/B, torch.ones(B)/B, 
                     C.cpu().numpy(), reg)
    pi = torch.from_numpy(pi).to(x1.device)
    
    # Rééchantillonner x0 selon le plan
    indices = torch.multinomial(pi, 1).squeeze()
    x0_coupled = x0[indices]
    
    # CFM loss standard avec couplage OT
    t = torch.rand(B, 1, device=x1.device)
    x_t = t * x1 + (1 - t) * x0_coupled
    u_t = x1 - x0_coupled
    
    v_pred = model(x_t, t)
    return ((v_pred - u_t) ** 2).mean()

Réduit la variance, rend les chemins plus droits. Speedup 4.4× en pratique.

Sampling

Intégrer l'ODE avec n'importe quel solveur.

from torchdiffeq import odeint

def sample(model, batch_size, dim, steps=50, method='dopri5'):
    # Partir du bruit
    x0 = torch.randn(batch_size, dim)
    t_span = torch.linspace(0, 1, steps)
    
    # Définir l'ODE
    def ode_func(t, x):
        t_batch = t.expand(x.shape[0], 1)
        return model(x, t_batch)
    
    # Intégrer
    trajectory = odeint(ode_func, x0, t_span, method=method)
    return trajectory[-1]  # x_1

Solveurs courants :

Méthode Steps typiques Qualité
euler 50-100 Acceptable
rk4 20-50 Bon
dopri5 (adaptatif) 10-30 Excellent

Équivalence avec diffusion

Flow matching et diffusion gaussienne sont mathématiquement identiques (DeepMind 2024).

Aspect Diffusion Flow Matching
Cible d'entraînement Bruit ε Vitesse v
Loss MSE pondérée (SNR) MSE simple
Trajectoire Stochastique (SDE) Déterministe (ODE)
Mental model "Nettoyer le bruit" "Suivre le flux"
Convergence Baseline 4.4× plus rapide

Choisir flow matching pour :

  • Formulation plus simple
  • Chemins plus droits (moins d'étapes)
  • Optimal transport naturel
  • Contraintes géométriques (SE(3), SO(3))

Rectified Flow

Rendr les chemins encore plus droits via "reflow" : ré-entraîner le modèle sur ses propres générations.

def reflow(model, dataloader, epochs=10):
    """
    Génère (x0, x1) via le modèle actuel,
    puis ré-entraîne pour apprendre les chemins droits.
    """
    synthetic_pairs = []
    
    with torch.no_grad():
        for x1_real in dataloader:
            x0 = torch.randn_like(x1_real)
            x1_gen = sample(model, x0)  # via ODE
            synthetic_pairs.append((x0, x1_gen))
    
    # Ré-entraîner avec paires synthétiques
    for x0, x1 in synthetic_pairs:
        t = torch.rand(B, 1)
        x_t = t * x1 + (1 - t) * x0
        u_t = x1 - x0  # Vitesse droite
        loss = ((model(x_t, t) - u_t) ** 2).mean()
        # backward, step...

Après 1-2 reflows : génération en 1-5 étapes seulement.

Production : Stable Diffusion 3

SD3 utilise flow matching + DiT (Diffusion Transformer) :

# Architecture simplifiée
class FlowMatchingDiT(nn.Module):
    def __init__(self, dim=1024, depth=28, heads=16):
        self.pos_embed = ...
        self.blocks = nn.ModuleList([
            TransformerBlock(dim, heads) for _ in range(depth)
        ])
        self.final = nn.Linear(dim, dim)
    
    def forward(self, x, t, text_embed):
        # x: latents, t: timestep, text_embed: CLIP/T5
        h = self.pos_embed(x) + self.time_embed(t)
        
        for block in self.blocks:
            h = block(h, text_embed)  # cross-attention
        
        return self.final(h)  # prédire vitesse

Clés du succès :

  • Latent space (VAE 8× compression)
  • Classifier-free guidance (CFG)
  • OT coupling
  • 50 steps d'inférence (vs 1000 pour DDPM initial)

TorchCFM

Librairie officielle de référence.

pip install torchdyn torchcfm
from torchcfm.conditional_flow_matching import (
    ConditionalFlowMatcher,
    TargetConditionalFlowMatcher,  # OT version
)

# Setup
fm = TargetConditionalFlowMatcher(sigma=0.0)  # sigma=0: chemins droits

# Training loop
for x1 in dataloader:
    x0 = torch.randn_like(x1)
    t, xt, ut = fm.sample_location_and_conditional_flow(x0, x1)
    
    vt = model(xt, t)
    loss = (vt - ut).pow(2).mean()
    
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

Cas d'usage

Où flow matching excelle :

  • Images : SD3, Flux.1 (12B params)
  • Vidéo : Pyramidal Flow (10s, 768p, 24fps)
  • Robotique : policies 50Hz, contraintes géométriques
  • Protéines : génération SE(3)-équivariante
  • Molécules : conformères 3D avec symétrie E(3)
  • Parole : synthèse haute fidélité

Limites actuelles :

  • Données discrètes (texte) : encore derrière l'autoregressif
  • Écosystème moins mature que diffusion
  • One-step generation : léger retard sur diffusion distillée

Diagnostic

# Vérifier la qualité du flow
def plot_trajectories(model, x1, steps=50):
    x0 = torch.randn_like(x1)
    t_span = torch.linspace(0, 1, steps)
    trajectory = []
    
    x = x0
    for i in range(len(t_span) - 1):
        dt = t_span[i+1] - t_span[i]
        v = model(x, t_span[i].expand(x.shape[0], 1))
        x = x + v * dt
        trajectory.append(x.clone())
    
    # Mesurer la courbure
    curvature = sum([
        torch.norm(trajectory[i+1] - 2*trajectory[i] + trajectory[i-1])
        for i in range(1, len(trajectory)-1)
    ])
    print(f"Courbure totale: {curvature:.4f}")  # Plus bas = mieux

Chemins droits = moins d'étapes nécessaires.

See also

pytorch keras-tensorflow world-models

See also

keras-tensorflow pytorch world-models

Interfaces de démo pour modèles ML. Contrairement à streamlit, le modèle est événementiel : on déclare des composants et on branche des fonctions dessus, sans réexécution complète du script.

uv add gradio
python app.py          # sert sur http://127.0.0.1:7860

Le plus court chemin

import gradio as gr

def check(site: str, produit: str) -> str:
    return f"{produit} trouvé sur {site} en 3 clics"

gr.Interface(
    fn=check,
    inputs=["text", "text"],
    outputs="text",
    title="Findability checker",
).launch()
* Running on local URL:  http://127.0.0.1:7860
* To create a public link, set `share=True` in `launch()`.

share=True ouvre un tunnel public valable 72 h — pratique pour montrer une démo à un client sans rien déployer.

Blocks, pour tout le reste

Interface couvre le cas « une fonction, des entrées, des sorties ». Dès qu'il faut de la mise en page ou plusieurs interactions, passer à Blocks.

with gr.Blocks(title="Findability") as demo:
    gr.Markdown("## Test d'accessibilité produit")

    with gr.Row():
        site = gr.Textbox(label="Site", value="versace.com")
        produit = gr.Textbox(label="Produit")

    with gr.Row():
        budget = gr.Slider(10, 200, value=60, step=10, label="Budget d'étapes")
        mode = gr.Radio(["fast", "default"], value="default", label="Mode")

    run = gr.Button("Lancer", variant="primary")
    out = gr.JSON(label="Résultat")
    gallery = gr.Gallery(label="Captures", columns=3)

    run.click(fn=check, inputs=[site, produit, budget, mode], outputs=[out, gallery])

demo.launch()

inputs et outputs sont des listes de composants, et la fonction doit renvoyer autant de valeurs qu'il y a de sorties, dans le même ordre.

Composants courants

Composant Usage
gr.Textbox(lines=5) texte court ou long
gr.Number, gr.Slider valeurs numériques
gr.Dropdown(choices=[...], multiselect=True) listes
gr.Checkbox, gr.Radio booléens et choix exclusifs
gr.Image(type="pil") image en entrée ou sortie
gr.File, gr.Audio, gr.Video fichiers
gr.Dataframe tableau éditable
gr.JSON, gr.Label sorties structurées
gr.Plot figure matplotlib ou plotly

Événements

run.click(fn, inputs=..., outputs=...)
site.change(fn, inputs=site, outputs=out)
site.submit(fn, ...)          # touche Entrée
demo.load(fn, ...)            # au chargement de la page

Enchaîner des étapes en gardant l'interface réactive :

run.click(prepare, [site], [status]).then(execute, [site], [out])

Streaming

def stream(prompt):
    partial = ""
    for chunk in llm.stream(prompt):
        partial += chunk
        yield partial            # un yield = une mise à jour de l'UI

gr.Interface(fn=stream, inputs="text", outputs="text").launch()

Un générateur suffit : chaque yield pousse une nouvelle valeur au composant de sortie.

Chat

def respond(message, history):
    return f"Reçu : {message}"

gr.ChatInterface(respond, type="messages").launch()

history arrive en liste de dicts {"role", "content"} avec type="messages". C'est le format aligné sur les API LLM, à préférer à l'ancien format en tuples.

État par session

with gr.Blocks() as demo:
    state = gr.State([])          # propre à chaque visiteur

    def add(item, current):
        current = current + [item]
        return current, current

    box = gr.Textbox()
    out = gr.JSON()
    box.submit(add, [box, state], [state, out])

gr.State n'est jamais rendu, il transporte des données Python entre événements.

Files d'attente et progression

def long_task(x, progress=gr.Progress()):
    for i in progress.tqdm(range(10), desc="Sessions"):
        run(i)
    return "ok"

demo.queue(max_size=20).launch()

queue() est nécessaire dès que plusieurs personnes utilisent la démo en même temps.

Déployer sur Hugging Face Spaces

Un repo avec app.py, requirements.txt, et un README.md à en-tête :

---
title: Findability Checker
sdk: gradio
sdk_version: "5.0.0"
app_file: app.py
---

Les secrets se règlent dans les paramètres du Space et se lisent par os.environ.

Gradio ou Streamlit

Gradio pour exposer un modèle ou une fonction, surtout avec image, audio ou chat, et pour publier sur Spaces. Streamlit pour un tableau de bord de données avec des filtres. Les deux sont des outils de démo : au-delà, une vraie application.

See also

Jupyter Notebook

page dédiée →

Jupyter fonctionne en deux modes : Édition (dans une cellule) et Commande (entre cellules). Échap bascule en mode commande, Entrée revient en édition.

Raccourcis essentiels

Le réflexe à prendre : Échap, B, puis Shift + Entrée — nouvelle cellule, exécution, passage à la suivante.

Action Raccourci
Exécuter et aller à la cellule suivante Shift + Entrée
Exécuter sans bouger Cmd + Entrée
Exécuter et créer une cellule dessous Option + Entrée
Créer une cellule au-dessus Échap, puis A
Créer une cellule en dessous Échap, puis B
Supprimer la cellule Échap, puis D, D
Copier / couper / coller une cellule Échap, puis C / X / V
Annuler la suppression Échap, puis Z
Transformer en cellule Markdown Échap, puis M
Transformer en cellule Code Échap, puis Y
Naviguer entre cellules Échap, puis ↑ / ↓

Magics utiles

%time ma_fonction()          # Temps d'une instruction
%timeit ma_fonction()        # Moyenne sur plusieurs exécutions
%%time                       # Temps de toute la cellule
%load_ext autoreload
%autoreload 2                # Recharger les modules modifiés
%matplotlib inline           # Afficher les plots inline
%pdb                         # Debugger automatique sur erreur
!pip install package         # Commande shell

Afficher plusieurs résultats

Par défaut, seule la dernière expression s'affiche.

from IPython.display import display

display(df.head())
display(df.describe())

Éviter le scroll infini

import pandas as pd
pd.set_option('display.max_rows', 50)

Redémarrer le kernel

Échap, puis 0, 0 redémarre le kernel. Utile quand l'état est cassé.

Convertir en script

jupyter nbconvert --to script notebook.ipynb

Extensions JupyterLab

pip install jupyterlab-code-formatter black isort

Permet de formater le code avec Cmd + Shift + F.

Variables d'environnement et secrets

from dotenv import load_dotenv, dotenv_values, find_dotenv
import os

# Charger automatiquement le .env trouvé
load_dotenv()

# Charger un fichier précis
load_dotenv(".env")
load_dotenv("/chemin/vers/.env")

# Recharger en remplaçant les valeurs déjà présentes dans le kernel
load_dotenv(".env", override=True)

.env = fichier sur disque ; os.environ = variables actuellement en mémoire dans le kernel. load_dotenv(..., override=True) copie le premier vers le second en remplaçant les valeurs existantes.

# Trouver le .env réellement utilisé
env_path = find_dotenv(usecwd=True)
print(env_path)

# Lire le fichier sans modifier l'environnement du notebook
env_vars = dotenv_values(".env")
print(env_vars.keys())

# Vérifier les noms et la présence des valeurs sans afficher les secrets
for name, value in dotenv_values(".env").items():
    print(f"{name}: {'définie' if value else 'vide'}")
# Lire une variable chargée
api_key = os.environ.get("GOOGLE_API_KEY")

# Vérifier une variable sans révéler sa valeur
print(bool(os.environ.get("GOOGLE_API_KEY")))

# Définir ou modifier temporairement une variable dans le notebook
os.environ["GOOGLE_API_KEY"] = "nouvelle_valeur"

# Supprimer une variable du kernel actuel
os.environ.pop("GOOGLE_API_KEY", None)

Variables d'environnement

%env

Liste toutes les variables d'environnement. À éviter si tu as des clés secrètes.

%env GOOGLE_API_KEY

Affiche la valeur d'une variable précise — à éviter aussi pour une clé.

%env MODE=development

Crée ou remplace une variable dans le kernel.

%env MODE development

Même résultat, avec un espace au lieu de =.

my_value = "development"
%env MODE=$my_value

Copie la valeur d'une variable Python dans une variable d'environnement.

%env?

Affiche l'aide exacte de ta version.

Introspection avec ? et ??

objet?

Affiche l'aide : documentation, signature et type.

len?
get_current_weather?
PolygonToolkit?

Pour une fonction, tu verras ses arguments et sa docstring.

objet??

Fait la même chose, mais essaie aussi d'afficher le code source Python.

get_current_weather??

Très pratique pour tes propres fonctions, ou pour comprendre une fonction d'une librairie.

Le code source peut ne pas apparaître si l'objet est écrit en C, compilé, ou si le package ne fournit pas les sources (list?? par exemple).

Recherche

# Recherche les objets dont le nom contient "embed"
*embed*?
# Avec un import récent
from langchain_community.agent_toolkits.polygon.toolkit import PolygonToolkit

PolygonToolkit?
PolygonToolkit.from_polygon_api_wrapper?

À retenir : ? = comprendre comment utiliser ; ?? = essayer de voir comment c'est construit.

See also

python-requests pandas numpy pytorch keras-tensorflow

Keras & TensorFlow

page dédiée →

Keras 3 fonctionne au-dessus de TensorFlow, JAX ou PyTorch. Le backend se choisit avant l'import.

import os
os.environ["KERAS_BACKEND"] = "tensorflow"   # ou "jax", "torch"
import keras

Construire un modèle

from keras import layers

model = keras.Sequential([
    keras.Input(shape=(128,)),
    layers.Dense(256, activation="relu"),
    layers.Dropout(0.2),
    layers.Dense(10, activation="softmax"),
])
model.summary()
Model: "sequential"
┌─────────────────────────────────┬────────────────────────┬───────────────┐
│ Layer (type)                    │ Output Shape           │       Param # │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense (Dense)                   │ (None, 256)            │        33,024 │
│ dropout (Dropout)               │ (None, 256)            │             0 │
│ dense_1 (Dense)                 │ (None, 10)             │         2,570 │
└─────────────────────────────────┴────────────────────────┴───────────────┘
 Total params: 35,594 (139.04 KB)

summary() est le premier outil de debug : une forme de sortie inattendue s'y voit immédiatement.

API fonctionnelle, dès qu'il y a plusieurs entrées ou une branche :

inp = keras.Input(shape=(128,))
x = layers.Dense(256, activation="relu")(inp)
x = layers.Dropout(0.2)(x)
out = layers.Dense(10, activation="softmax")(x)
model = keras.Model(inp, out)

Compiler et entraîner

model.compile(
    optimizer=keras.optimizers.AdamW(learning_rate=3e-4),
    loss="sparse_categorical_crossentropy",
    metrics=["accuracy"],
)

history = model.fit(
    X_train, y_train,
    validation_data=(X_val, y_val),
    epochs=20,
    batch_size=32,
    callbacks=[
        keras.callbacks.EarlyStopping(patience=3, restore_best_weights=True),
        keras.callbacks.ReduceLROnPlateau(factor=0.5, patience=2),
        keras.callbacks.ModelCheckpoint("best.keras", save_best_only=True),
    ],
)
Epoch 1/20
188/188 ━━━━━━━━━━━━━━━━━━━━ 2s 7ms/step - accuracy: 0.4123 - loss: 1.8342 - val_accuracy: 0.6210
Epoch 2/20
188/188 ━━━━━━━━━━━━━━━━━━━━ 1s 6ms/step - accuracy: 0.6890 - loss: 1.0021 - val_accuracy: 0.7455

Choisir la bonne perte

Cible Perte Dernière couche
Entiers de classe (0, 1, 2) sparse_categorical_crossentropy Dense(n, softmax)
One-hot categorical_crossentropy Dense(n, softmax)
Binaire binary_crossentropy Dense(1, sigmoid)
Régression mse / mae Dense(1) sans activation

Confondre sparse_ et la version one-hot est l'erreur la plus fréquente : elle produit un message sur les formes plutôt qu'un mauvais score, donc elle se repère vite.

Évaluer et prédire

loss, acc = model.evaluate(X_test, y_test, verbose=0)
proba = model.predict(X_test)
classes = proba.argmax(axis=1)

Courbes d'apprentissage

import matplotlib.pyplot as plt

plt.plot(history.history["loss"], label="train")
plt.plot(history.history["val_loss"], label="val")
plt.legend(); plt.xlabel("epoch"); plt.ylabel("loss")

La val_loss qui remonte pendant que la loss descend est la signature du surapprentissage. EarlyStopping(restore_best_weights=True) récupère automatiquement le meilleur état.

Sauvegarder

model.save("model.keras")                 # format natif, tout inclus
model = keras.models.load_model("model.keras")

model.save_weights("poids.weights.h5")    # poids seuls
model.load_weights("poids.weights.h5")

Pipeline de données

import tensorflow as tf

ds = (
    tf.data.Dataset.from_tensor_slices((X, y))
    .shuffle(10_000)
    .batch(32)
    .prefetch(tf.data.AUTOTUNE)
)
model.fit(ds, epochs=10)

prefetch(AUTOTUNE) recouvre la préparation des données et le calcul — souvent le gain le plus simple quand le GPU attend.

Transfer learning

base = keras.applications.EfficientNetB0(include_top=False, weights="imagenet", pooling="avg")
base.trainable = False                    # gel

model = keras.Sequential([base, layers.Dense(5, activation="softmax")])
model.compile(optimizer=keras.optimizers.Adam(1e-3), loss="sparse_categorical_crossentropy")
model.fit(train_ds, epochs=5)

base.trainable = True                     # dégel pour le fine-tuning
model.compile(optimizer=keras.optimizers.Adam(1e-5), loss="sparse_categorical_crossentropy")
model.fit(train_ds, epochs=5)

Le second compile avec un learning rate cent fois plus petit est obligatoire : dégeler sans le baisser détruit les poids pré-entraînés dès le premier batch.

GPU

print(tf.config.list_physical_devices("GPU"))
[PhysicalDevice(name='/physical_device:GPU:0', device_type='GPU')]

Liste vide alors qu'un GPU existe : c'est presque toujours une incompatibilité entre les versions de TensorFlow, CUDA et cuDNN.

See also

API en mouvement rapide. Ce qui suit couvre le cœur stable — LCEL, modèles de chat, prompts, parsers, retrievers. Vérifier contre docs.langchain.com avant de s'appuyer sur un détail.

Installation

uv add langchain langchain-openai langchain-anthropic langchain-community

Les intégrations sont dans des paquets séparés depuis la 0.1 : le cœur ne dépend d'aucun fournisseur.

Appeler un modèle

from langchain_anthropic import ChatAnthropic
from langchain_core.messages import HumanMessage, SystemMessage

llm = ChatAnthropic(model="claude-sonnet-4-5", temperature=0, max_tokens=1024)

resp = llm.invoke([
    SystemMessage("Tu réponds en une phrase."),
    HumanMessage("Qu'est-ce qu'un agent computer use ?"),
])
print(resp.content)
Un agent computer use est un système qui perçoit l'écran et agit via souris et clavier
pour accomplir des tâches à la place d'un utilisateur.

Quatre méthodes sur tout composant : invoke, batch, stream, et leurs variantes asynchrones ainvoke, abatch, astream.

LCEL, l'opérateur |

from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser

prompt = ChatPromptTemplate.from_messages([
    ("system", "Tu es un analyste e-commerce concis."),
    ("human", "Le produit {produit} est-il trouvable sur {site} ?"),
])

chain = prompt | llm | StrOutputParser()
print(chain.invoke({"produit": "Najim 100 ml", "site": "versace.com"}))

Chaque maillon reçoit la sortie du précédent. StrOutputParser extrait .content du message, sinon on manipule un objet AIMessage.

Sortie structurée

from pydantic import BaseModel, Field

class Findability(BaseModel):
    findable: bool = Field(description="Produit atteignable par la navigation")
    clicks: int = Field(description="Nombre de clics jusqu'à la fiche produit")
    path: list[str] = Field(default_factory=list)

structured = llm.with_structured_output(Findability)
result = structured.invoke("Sur versace.com, combien de clics pour Najim 100 ml ?")
print(result.clicks, result.findable)
3 True

with_structured_output s'appuie sur le function calling natif du fournisseur. C'est plus fiable qu'un JsonOutputParser derrière un prompt qui supplie de rendre du JSON.

Tools et agents

from langchain_core.tools import tool

@tool
def get_price(sku: str) -> float:
    """Retourne le prix TTC d'un SKU."""
    return 129.0

llm_with_tools = llm.bind_tools([get_price])
msg = llm_with_tools.invoke("Quel est le prix du SKU AB-12 ?")
print(msg.tool_calls)
[{'name': 'get_price', 'args': {'sku': 'AB-12'}, 'id': 'toolu_01X…', 'type': 'tool_call'}]

Le décorateur @tool dérive le schéma des annotations de type et la description de la docstring — donc la docstring est un élément fonctionnel, pas un commentaire.

bind_tools ne fait que proposer l'appel : c'est à la boucle d'exécuter l'outil et de renvoyer un ToolMessage. Pour la boucle complète, passer à langgraph.

RAG minimal

from langchain_community.document_loaders import PyPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import FAISS

docs = PyPDFLoader("manuel.pdf").load()

splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=150)
chunks = splitter.split_documents(docs)

store = FAISS.from_documents(chunks, OpenAIEmbeddings(model="text-embedding-3-small"))
retriever = store.as_retriever(search_kwargs={"k": 4})

RecursiveCharacterTextSplitter coupe d'abord sur les paragraphes, puis les phrases, puis les mots. C'est le défaut raisonnable ; chunk_overlap évite de trancher une idée en deux.

from langchain_core.runnables import RunnablePassthrough

template = ChatPromptTemplate.from_template(
    "Réponds uniquement à partir du contexte.\n\nContexte:\n{context}\n\nQuestion: {question}"
)

def format_docs(docs):
    return "\n\n".join(d.page_content for d in docs)

rag = (
    {"context": retriever | format_docs, "question": RunnablePassthrough()}
    | template
    | llm
    | StrOutputParser()
)
print(rag.invoke("Quelle est la garantie ?"))

RunnablePassthrough laisse passer l'entrée telle quelle pendant que l'autre branche va chercher les documents. Les deux branches du dict s'exécutent en parallèle.

Streaming

for chunk in chain.stream({"produit": "Najim", "site": "versace.com"}):
    print(chunk, end="", flush=True)

Observabilité

export LANGCHAIN_TRACING_V2=true
export LANGCHAIN_API_KEY=ls__...
export LANGCHAIN_PROJECT=mon-projet

Toutes les exécutions apparaissent alors dans LangSmith, avec les prompts réels, les latences et les tokens. C'est le principal argument pour rester dans l'écosystème.

Quand ne pas l'utiliser

Pour un simple appel à un modèle avec un prompt, le SDK du fournisseur suffit et se debugge mieux. LangChain se justifie quand on veut changer de fournisseur sans réécrire, brancher des retrievers existants, ou tracer dans LangSmith.

See also

API en mouvement. Le cœur — StateGraph, nœuds, arêtes conditionnelles, checkpointer — est stable. Vérifier les détails sur docs.langchain.com.

Orchestration d'agents comme graphe d'états. Là où une chaîne LCEL est un pipeline linéaire, LangGraph autorise les cycles, les branchements et la reprise après interruption.

L'idée

Un état partagé, des nœuds qui le transforment, des arêtes qui décident du nœud suivant. Chaque nœud reçoit l'état et renvoie les clés qu'il modifie, pas l'état entier.

uv add langgraph langchain-anthropic

Graphe minimal

from typing import Annotated, TypedDict
from operator import add
from langgraph.graph import StateGraph, START, END

class State(TypedDict):
    site: str
    steps: Annotated[list[str], add]     # les mises à jour s'accumulent
    verdict: str | None

def navigate(state: State) -> dict:
    return {"steps": [f"ouverture de {state['site']}"]}

def judge(state: State) -> dict:
    return {"verdict": "findable" if len(state["steps"]) < 5 else "unreachable"}

builder = StateGraph(State)
builder.add_node("navigate", navigate)
builder.add_node("judge", judge)
builder.add_edge(START, "navigate")
builder.add_edge("navigate", "judge")
builder.add_edge("judge", END)

graph = builder.compile()
print(graph.invoke({"site": "versace.com", "steps": [], "verdict": None}))
{'site': 'versace.com', 'steps': ['ouverture de versace.com'], 'verdict': 'findable'}

Annotated[list, add] est le mécanisme central : sans le réducteur, chaque nœud écraserait steps. Avec, les listes se concatènent. Pour les messages, langgraph.graph.message.add_messages gère aussi la déduplication par id.

Branchement conditionnel

def should_retry(state: State) -> str:
    if state["verdict"] == "unreachable" and len(state["steps"]) < 20:
        return "navigate"
    return END

builder.add_conditional_edges("judge", should_retry, ["navigate", END])

La fonction renvoie le nom du nœud suivant. C'est ce qui crée les cycles, et donc les boucles d'agent.

Boucle agent avec outils

from langgraph.prebuilt import create_react_agent
from langchain_core.tools import tool

@tool
def click(selector: str) -> str:
    """Clique sur un élément et retourne le nouvel état de la page."""
    return f"cliqué sur {selector}"

agent = create_react_agent(llm, tools=[click])
out = agent.invoke({"messages": [("user", "Trouve Najim 100 ml sur versace.com")]})
print(out["messages"][-1].content)

create_react_agent monte la boucle standard : le modèle propose un appel, un nœud ToolNode l'exécute, le résultat repart au modèle, jusqu'à une réponse sans tool call. Pour tout contrôle fin — budget d'étapes, vérification, sous-agents — écrire le graphe à la main.

Persistance et reprise

from langgraph.checkpoint.memory import MemorySaver

graph = builder.compile(checkpointer=MemorySaver())
config = {"configurable": {"thread_id": "session-42"}}

graph.invoke({"site": "versace.com", "steps": [], "verdict": None}, config)
graph.invoke({"site": "dior.com"}, config)      # reprend le même fil

print(graph.get_state(config).values["steps"])

Le thread_id identifie une conversation. En production, remplacer MemorySaver par un checkpointer SQLite ou Postgres pour survivre au redémarrage.

Interruption humaine

graph = builder.compile(checkpointer=MemorySaver(), interrupt_before=["judge"])

graph.invoke(initial, config)              # s'arrête avant "judge"
state = graph.get_state(config)
graph.update_state(config, {"verdict": "override"})
graph.invoke(None, config)                 # None = reprendre où on s'est arrêté

C'est le mécanisme des portes d'approbation avant une action irréversible — indispensable dès qu'un agent écrit quelque part.

Suivre l'exécution

for event in graph.stream(initial, config, stream_mode="values"):
    print(event["steps"][-1] if event["steps"] else "…")

stream_mode : values (l'état complet à chaque étape), updates (seulement les deltas), messages (les tokens du LLM).

Budget d'étapes

graph.invoke(initial, {"recursion_limit": 25, **config})

Au-delà, GraphRecursionError. Un agent qui boucle sans progresser est le mode d'échec par défaut — le budget est une sécurité, pas une optimisation.

Visualiser

print(graph.get_graph().draw_mermaid())
graph TD;
	__start__ --> navigate;
	navigate --> judge;
	judge -.-> navigate;
	judge -.-> __end__;

Utile en démo client : le diagramme se colle tel quel dans un document.

LangChain ou LangGraph

Chaîne linéaire, un aller-retour, pas d'état : LCEL suffit. Cycles, outils, reprise après échec, validation humaine : LangGraph. Les deux se composent — un nœud de graphe peut être une chaîne LCEL.

See also

Next.js (App Router)

page dédiée →

Démarrer

npx create-next-app@latest mon-app --ts --tailwind --app --no-src-dir
cd mon-app && npm run dev
▲ Next.js 16.3.0 (Turbopack)
- Local:   http://localhost:3000
✓ Ready in 812ms

Routage par fichiers

app/
  layout.tsx              → enveloppe toutes les pages
  page.tsx                → /
  globals.css
  blog/
    page.tsx              → /blog
    [slug]/page.tsx       → /blog/:slug
  api/
    items/route.ts        → /api/items
  (marketing)/            → groupe, n'apparaît pas dans l'URL
  loading.tsx             → état de chargement automatique
  error.tsx               → frontière d'erreur (doit être "use client")
  not-found.tsx           → 404

Server Components par défaut

Tout composant est serveur sauf mention contraire. Il peut être async, lire le système de fichiers, appeler une base — et son code n'est jamais envoyé au navigateur.

// app/items/page.tsx  — server component
import { readFileSync } from "node:fs";

export default async function Items() {
  const data = JSON.parse(readFileSync("content/items.json", "utf8"));
  return <ul>{data.map((i) => <li key={i.id}>{i.title}</li>)}</ul>;
}

"use client" en première ligne bascule un fichier côté navigateur. Nécessaire dès qu'on utilise useState, useEffect, un gestionnaire d'événement ou une API du DOM.

"use client";
import { useState } from "react";

export function Counter() {
  const [n, setN] = useState(0);
  return <button onClick={() => setN(n + 1)}>{n}</button>;
}

Règle de composition : un composant serveur peut importer un composant client, l'inverse est impossible. Donc on descend "use client" le plus bas possible dans l'arbre.

Params et search params sont asynchrones

export default async function Page({
  params,
  searchParams,
}: {
  params: Promise<{ slug: string }>;
  searchParams: Promise<{ tag?: string }>;
}) {
  const { slug } = await params;
  const { tag } = await searchParams;
  return <h1>{slug} {tag}</h1>;
}

C'est le changement qui casse le plus de code venu des anciennes versions : params était un objet simple, il faut maintenant l'attendre.

Génération statique

export function generateStaticParams() {
  return getAllSlugs().map((slug) => ({ slug }));
}

export async function generateMetadata({ params }): Promise<Metadata> {
  const { slug } = await params;
  return { title: `${slug} — Mon site` };
}

Avec generateStaticParams, chaque route est prérendue au build. Sans, elle est rendue à la demande.

Routes API

// app/api/items/route.ts
import { NextResponse } from "next/server";

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  return NextResponse.json({ q: searchParams.get("q") });
}

export async function POST(request: Request) {
  const body = await request.json();
  return NextResponse.json({ ok: true, body }, { status: 201 });
}

Server Actions

// app/actions.ts
"use server";

export async function createItem(formData: FormData) {
  const title = formData.get("title") as string;
  await db.insert({ title });
  revalidatePath("/items");
}
import { createItem } from "./actions";

export default function Form() {
  return (
    <form action={createItem}>
      <input name="title" />
      <button type="submit">Créer</button>
    </form>
  );
}

Une mutation serveur appelée depuis le client sans écrire de route API.

import Link from "next/link";
import Image from "next/image";

<Link href="/blog/hello" prefetch>Article</Link>
<Image src="/photo.jpg" alt="" width={800} height={600} priority />
"use client";
import { useRouter, usePathname, useSearchParams } from "next/navigation";

const router = useRouter();
router.push("/blog");
router.refresh();     // recharge les données serveur sans perdre l'état client

next/navigation, pas next/router — ce dernier appartient au Pages Router.

Variables d'environnement

DATABASE_URL=postgres://...      # serveur uniquement
NEXT_PUBLIC_API_URL=https://...  # exposée au navigateur

Seul le préfixe NEXT_PUBLIC_ traverse vers le client. Tout le reste reste serveur — c'est la protection à ne pas contourner par confort.

Build et déploiement

npm run build        # vérifie les types et prérend
npm start            # sert le build de production
npx vercel --prod    # déploie
Route (app)
┌ ○ /                    142 B    102 kB
├ ● /blog/[slug]         1.2 kB   118 kB
└ ƒ /api/items           0 B      0 B

○ Static  ● SSG  ƒ Dynamic

Lire cette table à chaque build : une route passée en ƒ alors qu'elle devrait être statique signale un appel dynamique involontaire — cookies(), headers() ou un fetch non caché.

See also

Créer

import numpy as np

np.array([[1, 2], [3, 4]])
np.zeros((2, 3))
np.ones((2, 3), dtype=np.float32)
np.full((2, 2), 7)
np.eye(3)
np.arange(0, 10, 2)          # 0 2 4 6 8, borne exclue
np.linspace(0, 1, 5)         # 5 points, bornes incluses
rng = np.random.default_rng(42)     # API moderne, préférée à np.random.seed
rng.normal(0, 1, size=(2, 3))
rng.integers(0, 10, size=5)
rng.choice([1, 2, 3], size=4, replace=True)
a = np.arange(6).reshape(2, 3)
print(a)
print(a.shape, a.dtype, a.ndim, a.size)
[[0 1 2]
 [3 4 5]]
(2, 3) int64 2 6

Formes

a.reshape(3, 2)
a.reshape(-1)          # aplatit, -1 = "déduis la dimension"
a.T                    # transposée
a[:, np.newaxis]       # ajoute un axe -> (2, 1, 3)
np.squeeze(a)          # retire les axes de taille 1
np.concatenate([a, a], axis=0)
np.stack([a, a])       # crée un nouvel axe

reshape renvoie une vue quand c'est possible : modifier le résultat modifie l'original. .copy() pour couper le lien.

Indexer

a[0, 1]
a[:, 1]            # toute la colonne 1
a[1:, :2]
a[a > 2]           # masque booléen -> tableau 1D
a[[0, 1], [2, 0]]  # fancy indexing -> éléments (0,2) et (1,0)
a = np.arange(6).reshape(2, 3)
print(a > 2)
print(a[a > 2])
[[False False False]
 [ True  True  True]]
[3 4 5]

Le masque booléen est le pattern à avoir : np.where(cond, x, y) pour choisir élément par élément, a[cond] = valeur pour affecter.

Agréger

a.sum()            # tout
a.sum(axis=0)      # somme des lignes -> un résultat par colonne
a.sum(axis=1)      # un résultat par ligne
a.mean(), a.std(), a.min(), a.max()
a.argmax(), a.argmin()      # position, pas valeur
np.median(a), np.percentile(a, 95)

Le sens d'axis est le piège classique : axis=0 fait disparaître l'axe 0, donc agrège les lignes et laisse une valeur par colonne.

a = np.arange(6).reshape(2, 3)
print(a.sum(axis=0), a.sum(axis=1))
[3 5 7] [ 3 12]

Avec des NaN : np.nanmean, np.nansum, sinon tout devient NaN.

Broadcasting

a = np.ones((3, 4))
b = np.arange(4)          # (4,)
a + b                     # (3, 4) : b est étiré sur les lignes

Règle : on aligne les shapes par la droite, et deux dimensions sont compatibles si elles sont égales ou si l'une vaut 1.

(3, 4)  +  (4,)     ->  (3, 4)   ✅
(3, 4)  +  (3,)     ->  erreur   ❌   (3 ≠ 4 sur le dernier axe)
(3, 4)  +  (3, 1)   ->  (3, 4)   ✅

Pour corriger le cas d'erreur : a + b[:, np.newaxis].

Algèbre linéaire

A @ B                       # produit matriciel
A * B                       # produit terme à terme
np.dot(u, v)                # produit scalaire
np.linalg.norm(v)
np.linalg.inv(A)
np.linalg.solve(A, b)       # résout Ax = b, mieux que inv(A) @ b
np.linalg.eig(A)

Vectoriser au lieu de boucler

# lent
out = [x ** 2 + 1 for x in data]

# rapide
out = data ** 2 + 1

Une boucle Python sur un tableau NumPy annule tout l'intérêt de NumPy. Si la logique semble imposer une boucle, chercher du côté de np.where, np.select, des masques, ou d'un reshape + agrégation par axe.

Similarité cosinus, en pratique

def cosine(a: np.ndarray, b: np.ndarray) -> np.ndarray:
    a = a / np.linalg.norm(a, axis=-1, keepdims=True)
    b = b / np.linalg.norm(b, axis=-1, keepdims=True)
    return a @ b.T

keepdims=True conserve la dimension pour que la division broadcaste correctement — sans lui, la shape passe de (n, 1) à (n,) et la division échoue ou donne un résultat faux.

Sauvegarder

np.save("arr.npy", a)
a = np.load("arr.npy")
np.savez("plusieurs.npz", x=a, y=b)
np.savetxt("arr.csv", a, delimiter=",")

See also

Charger et regarder

import pandas as pd

df = pd.read_csv("ventes.csv")
df = pd.read_csv("ventes.csv", sep=";", parse_dates=["date"], dtype={"code": str})
df = pd.read_parquet("ventes.parquet")
df = pd.read_json("ventes.json")
df.head(3)
df.info()
df.describe()
df.shape
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1240 entries, 0 to 1239
Data columns (total 4 columns):
 #   Column   Non-Null Count  Dtype
---  ------   --------------  -----
 0   date     1240 non-null   datetime64[ns]
 1   produit  1240 non-null   object
 2   region   1198 non-null   object
 3   montant  1240 non-null   float64

info() est le premier réflexe : il donne d'un coup les types et les valeurs manquantes. dtype: object sur une colonne censée être numérique signale presque toujours un problème de parsing.

Sélectionner

df["montant"]                 # une Series
df"produit", "montant"    # un DataFrame
df.loc[3, "montant"]          # par label
df.iloc[3, 2]                 # par position
df.loc[df["montant"] > 100, ["produit", "montant"]]

Filtres combinés : parenthèses obligatoires, et & | ~ au lieu de and or not.

df[(df["montant"] > 100) & (df["region"] == "IDF")]
df[df["produit"].isin(["A", "B"])]
df[df["produit"].str.contains("chaise", case=False, na=False)]
df.query("montant > 100 and region == 'IDF'")

Valeurs manquantes

df.isna().sum()
df.dropna(subset=["region"])
df["region"] = df["region"].fillna("inconnue")
df.isna().sum()
date        0
produit     0
region     42
montant     0
dtype: int64

Grouper et agréger

df.groupby("region")["montant"].sum()
df.groupby("region")["montant"].agg(["sum", "mean", "count"])
df.groupby(["region", "produit"], as_index=False).agg(
    total=("montant", "sum"),
    n=("montant", "size"),
)
df.groupby("region")["montant"].agg(["sum", "mean", "count"])
              sum    mean  count
region
IDF      184203.5  312.55    589
PACA      92310.0  287.29    321
Bretagne  61044.2  254.35    240

as_index=False évite d'avoir à faire .reset_index() juste après.

Transformer

df["ttc"] = df["montant"] * 1.2
df["mois"] = df["date"].dt.to_period("M")
df["cat"] = df["montant"].apply(lambda x: "gros" if x > 500 else "petit")

df = df.rename(columns={"montant": "ht"})
df = df.drop(columns=["temp"])
df = df.sort_values("ht", ascending=False)
df = df.astype({"code": "string", "n": "int32"})

apply sur une Series est lent. Préférer les opérations vectorisées, ou np.where / pd.cut pour les cas conditionnels :

import numpy as np
df["cat"] = np.where(df["ht"] > 500, "gros", "petit")

Joindre et empiler

pd.merge(cmd, clients, on="client_id", how="left")
pd.merge(a, b, left_on="id", right_on="ref", how="inner")
pd.concat([df_jan, df_fev], ignore_index=True)     # empile verticalement

how : left, right, inner, outer. Après un merge, vérifier len(df) — une explosion du nombre de lignes révèle une clé non unique.

Pivots

df.pivot_table(index="region", columns="mois", values="ht", aggfunc="sum", fill_value=0)
df.melt(id_vars=["region"], var_name="mois", value_name="ht")   # l'inverse

Le SettingWithCopyWarning

sub = df[df["ht"] > 100]
sub["remise"] = 0.1          # ⚠️ warning : sub peut être une vue

Corriger avec une copie explicite :

sub = df[df["ht"] > 100].copy()
sub["remise"] = 0.1

Ou modifier en place sur l'original : df.loc[df["ht"] > 100, "remise"] = 0.1.

Exporter

df.to_csv("out.csv", index=False)
df.to_parquet("out.parquet")            # plus rapide et typé, à préférer
df.to_json("out.json", orient="records")

index=False presque toujours, sinon on récupère une colonne Unnamed: 0 au rechargement.

Options d'affichage

pd.set_option("display.max_columns", None)
pd.set_option("display.width", 200)
pd.set_option("display.float_format", "{:.2f}".format)

See also

Validation de données par annotations de type. La v2 a un cœur en Rust et une API qui diffère de la v1 sur plusieurs noms.

Modèle de base

from pydantic import BaseModel, Field

class Item(BaseModel):
    id: int
    title: str
    price: float = Field(gt=0, description="Prix TTC en euros")
    tags: list[str] = []

item = Item(id="42", title="Chaise", price=19.9)   # "42" est converti en int
print(item)
print(item.model_dump())
id=42 title='Chaise' price=19.9 tags=[]
{'id': 42, 'title': 'Chaise', 'price': 19.9, 'tags': []}

La coercition est volontaire : "42" devient 42. Pour l'interdire, model_config = ConfigDict(strict=True).

Les méthodes v2, et leurs anciens noms

v2 v1
model_dump() .dict()
model_dump_json() .json()
model_validate(obj) parse_obj()
model_validate_json(s) parse_raw()
model_json_schema() schema()
model_copy(update=...) .copy()

Erreurs de validation

from pydantic import ValidationError

try:
    Item(id="abc", title="Chaise", price=-1)
except ValidationError as exc:
    print(exc)
2 validation errors for Item
id
  Input should be a valid integer, unable to parse string as an integer
    [type=int_parsing, input_value='abc', input_type=str]
price
  Input should be greater than 0
    [type=greater_than, input_value=-1, input_type=int]

exc.errors() renvoie la même chose en liste de dicts — c'est la forme à renvoyer à un modèle quand on valide ses tool calls.

Contraintes utiles

from typing import Literal, Annotated
from pydantic import BaseModel, Field, HttpUrl, EmailStr

class Agent(BaseModel):
    name: Annotated[str, Field(min_length=1, max_length=64)]
    url: HttpUrl
    email: EmailStr | None = None            # pip install pydantic[email]
    mode: Literal["fast", "default"] = "default"
    budget: int = Field(default=60, ge=1, le=500)

Field(gt, ge, lt, le) pour les nombres, min_length / max_length pour chaînes et listes, pattern= pour une regex.

Valeurs par défaut mutables

from pydantic import BaseModel

class Bad(BaseModel):
    tags: list[str] = []          # sûr ici : pydantic copie la valeur par défaut

Contrairement aux dataclasses et aux fonctions Python, un défaut mutable ne fuit pas entre instances. Pour un défaut calculé : Field(default_factory=list).

Validateurs

from pydantic import BaseModel, field_validator, model_validator

class Range(BaseModel):
    low: int
    high: int

    @field_validator("low", "high")
    @classmethod
    def positive(cls, v: int) -> int:
        if v < 0:
            raise ValueError("doit être positif")
        return v

    @model_validator(mode="after")
    def ordered(self):
        if self.low > self.high:
            raise ValueError("low doit être <= high")
        return self

mode="before" s'exécute sur la valeur brute avant conversion, mode="after" sur l'objet déjà validé.

Modèles imbriqués et parsing JSON

class Result(BaseModel):
    findable: bool
    clicks: int
    notes: str | None = None

class Report(BaseModel):
    site: str
    results: list[Result]

payload = '{"site":"versace.com","results":[{"findable":true,"clicks":3}]}'
report = Report.model_validate_json(payload)
print(report.results[0].clicks)
3

C'est le pattern des sorties structurées de LLM : on impose un schéma, on parse, et une réponse hors format lève au lieu de contaminer la suite.

Schéma JSON pour un tool call

print(Result.model_json_schema())
{'properties': {'findable': {'title': 'Findable', 'type': 'boolean'},
                'clicks': {'title': 'Clicks', 'type': 'integer'},
                'notes': {'anyOf': [{'type': 'string'}, {'type': 'null'}],
                          'default': None, 'title': 'Notes'}},
 'required': ['findable', 'clicks'],
 'title': 'Result',
 'type': 'object'}

C'est exactement ce qu'attend un tools=[...] d'API LLM.

Configuration et settings

from pydantic import ConfigDict
from pydantic_settings import BaseSettings   # pip install pydantic-settings

class Item(BaseModel):
    model_config = ConfigDict(extra="forbid", frozen=True, strict=False)

class Settings(BaseSettings):
    api_key: str                              # lit API_KEY dans l'environnement
    base_url: str = "https://api.example.com"

settings = Settings()                         # lève si API_KEY manque

extra="forbid" refuse les champs inconnus — précieux pour attraper une faute de frappe dans une config plutôt que de l'ignorer en silence.

See also

Gère des versions de Python, pas des paquets. Fonctionne par shims : de faux exécutables placés très tôt dans le PATH, qui redirigent vers la version active.

Voir et installer

pyenv versions        # installées, l'active marquée d'une étoile
pyenv version         # l'active ICI, et qui l'a décidée
pyenv install --list | rg "^\s*3\.12"
pyenv install 3.12.9
pyenv version
3.12.9 (set by /Users/edouard/code/mon-projet/.python-version)

La deuxième partie est la plus utile : elle dit quel fichier impose la version.

Choisir une version

pyenv global 3.12.9      # défaut de la machine  -> ~/.pyenv/version
pyenv local 3.12.9       # ce dossier            -> ./.python-version
pyenv shell 3.12.9       # ce shell seulement    -> $PYENV_VERSION
pyenv local --unset

Ordre de priorité : shell > local > global.

Environnements nommés (plugin pyenv-virtualenv)

pyenv virtualenv 3.12.9 mon-env    # -> ~/.pyenv/versions/3.12.9/envs/mon-env
pyenv local mon-env                # activation automatique en entrant dans le dossier
pyenv activate mon-env
pyenv deactivate
pyenv virtualenv-delete mon-env

Ils vivent dans ~/.pyenv/versions/, pas dans le projet. C'est la différence de fond avec un .venv local.

venv standard, sans le plugin

pyenv local 3.12.9
python -m venv .venv
source .venv/bin/activate
pip install httpx pydantic
pip freeze > requirements.txt
deactivate

Les deux pièges qui coûtent des minutes

Le shim répond à la place du venv. Un paquet installé dans le venv, et pourtant la commande résolue est celle de pyenv :

which uvicorn
/Users/edouard/.pyenv/versions/lewagon/bin/uvicorn

Contournement immédiat : python -m uvicorn main:app passe par le Python courant et ignore le PATH. Sinon hash -r (zsh : rehash) vide le cache de commandes du shell.

La commande existe ailleurs. Message classique :

pyenv: jupyter: command not found

The `jupyter' command exists in these Python versions:
  3.12.9/envs/lewagon
  3.10.6/envs/taxifare-env

Le shim cherche dans la version active, qui n'a pas le paquet. Soit on active le bon environnement, soit on passe par uv run.

Diagnostic

which -a python python3     # tous les candidats, dans l'ordre du PATH
pyenv which python          # ce que pyenv résoudrait
pyenv doctor                # si le plugin est installé
PYENV_VERSION=system python -V   # contournement ponctuel

Rapport avec uv

uv est indépendant : il télécharge ses propres interpréteurs dans ~/.local/share/uv/python/ et ne consulte jamais pyenv. Seul point de friction : les deux lisent .python-version.

See also

Python requests

page dédiée →

Client HTTP synchrone. Pas de timeout par défaut — toujours le passer explicitement.

Le squelette à écrire de mémoire

import os
import requests

BASE = "https://api.example.com/v1"

r = requests.post(
    f"{BASE}/environments",
    headers={
        "Authorization": f"Bearer {os.environ['API_KEY']}",
        "Content-Type": "application/json",
    },
    json={"name": "demo", "url": "https://example.com"},
    timeout=30,
)
r.raise_for_status()
print(r.status_code, r.json())

json= sérialise le dict et pose Content-Type: application/json tout seul. data= envoie du form-urlencoded ou des bytes bruts — c'est la confusion la plus fréquente.

Réponse

Attribut Contenu
r.status_code entier, 200, 404…
r.ok True si status_code < 400
r.json() corps parsé, lève JSONDecodeError si ce n'est pas du JSON
r.text corps brut décodé
r.headers dict insensible à la casse
r.raise_for_status() lève HTTPError sur 4xx/5xx, ne fait rien sinon

Session : réutiliser la connexion et les headers

with requests.Session() as s:
    s.headers.update({"Authorization": f"Bearer {os.environ['API_KEY']}"})
    for page in range(1, 6):
        r = s.get(f"{BASE}/items", params={"page": page}, timeout=30)
        r.raise_for_status()
        print(r.json()["items"])

params= construit la query string, pas besoin de la concaténer à la main.

Polling d'un job asynchrone

import time

def wait_for(session_id: str, timeout_s: int = 300, every_s: int = 3) -> dict:
    deadline = time.time() + timeout_s
    while time.time() < deadline:
        r = requests.get(f"{BASE}/sessions/{session_id}", timeout=30)
        r.raise_for_status()
        payload = r.json()
        if payload["status"] in {"completed", "failed"}:
            return payload
        time.sleep(every_s)
    raise TimeoutError(f"session {session_id} toujours en cours après {timeout_s}s")

Erreurs à distinguer

try:
    r = requests.get(url, timeout=10)
    r.raise_for_status()
except requests.Timeout:              # dépassement du timeout
    ...
except requests.ConnectionError:      # DNS, refus de connexion, réseau
    ...
except requests.HTTPError as exc:     # 4xx / 5xx après raise_for_status
    print(exc.response.status_code, exc.response.text[:200])

Toutes héritent de requests.RequestException.

Charger la clé depuis un .env

from dotenv import load_dotenv   # pip install python-dotenv
load_dotenv()
key = os.environ["API_KEY"]      # KeyError explicite si absente

os.environ["X"] lève si la variable manque, os.getenv("X") renvoie None en silence. Préférer le premier pour une clé obligatoire.

Debug rapide

print(r.request.method, r.request.url)
print(r.request.headers)
print(r.request.body)

Équivalences avec curl

curl requests
-X POST requests.post(...)
-H "K: V" headers={"K": "V"}
-d '{"a":1}' json={"a": 1}
-G --data-urlencode "q=x" params={"q": "x"}
-u user:pass auth=("user", "pass")
--max-time 30 timeout=30

See also

  • httpx — même API, plus l'async et un timeout par défaut

python-dotenv

page dédiée →

Charge des variables d'environnement depuis un fichier .env sans polluer le shell parent, utile en notebook et pour les secrets locaux.

Charger

from dotenv import load_dotenv
import os

# Cherche .env dans le répertoire courant et les parents
load_dotenv()
# Charger un fichier précis
load_dotenv(".env.local")
load_dotenv("/chemin/absolu/.env")
# Recharger en écrasant les valeurs déjà présentes dans le kernel
load_dotenv(override=True)

Lire sans modifier l'environnement

from dotenv import dotenv_values

# Retourne un dict, sans toucher à os.environ
env_vars = dotenv_values(".env")
print(env_vars.keys())
# Vérifier les noms et la présence des valeurs sans afficher les secrets
for name, value in dotenv_values(".env").items():
    print(f"{name}: {'définie' if value else 'vide'}")

Diagnostic

from dotenv import find_dotenv

# Trouver le .env réellement utilisé
env_path = find_dotenv(usecwd=True)
print(env_path)  # '' si aucun fichier trouvé
# Vérifier qu'une variable est chargée sans révéler sa valeur
print(bool(os.environ.get("GOOGLE_API_KEY")))

Manipuler l'environnement du kernel

# Lire une variable chargée
api_key = os.environ.get("GOOGLE_API_KEY")
# Définir ou modifier temporairement
os.environ["GOOGLE_API_KEY"] = "nouvelle_valeur"
# Supprimer du kernel actuel
os.environ.pop("GOOGLE_API_KEY", None)

Distinction à retenir

Objet Portée
.env fichier sur disque
os.environ variables en mémoire dans le kernel Python actuel
load_dotenv() copie le fichier vers os.environ
override=True remplace les valeurs déjà présentes

Le fichier .env n'est jamais modifié par load_dotenv(). Les modifications de os.environ ne persistent pas après redémarrage du kernel.


title: python-dotenv category: cheatsheets created: 2025-01-21 updated: 2025-01-21 tags: [python, dotenv, env, secrets, configuration, jupyter] confidence: high publish: true

Charger des variables d'environnement depuis un fichier .env sans les commiter, surtout utile en notebook où l'environnement shell n'est pas lu au démarrage.

Import et chargement

from dotenv import load_dotenv, dotenv_values, find_dotenv
import os

# Charger automatiquement le .env trouvé
load_dotenv()
# Charger un fichier précis
load_dotenv(".env")
load_dotenv("/chemin/vers/.env")
# Recharger en remplaçant les valeurs déjà présentes dans le kernel
load_dotenv(".env", override=True)

Lire sans charger

# Lire le fichier sans modifier l'environnement du notebook
env_vars = dotenv_values(".env")
print(env_vars.keys())
# Vérifier les noms et la présence des valeurs sans afficher les secrets
for name, value in dotenv_values(".env").items():
    print(f"{name}: {'définie' if value else 'vide'}")

Utiliser les variables

# Lire une variable chargée
api_key = os.environ.get("GOOGLE_API_KEY")
# Vérifier une variable sans révéler sa valeur
print(bool(os.environ.get("GOOGLE_API_KEY")))

Modifier temporairement

# Définir ou modifier temporairement une variable dans le notebook
os.environ["GOOGLE_API_KEY"] = "nouvelle_valeur"
# Supprimer une variable du kernel actuel
os.environ.pop("GOOGLE_API_KEY", None)

Diagnostic

# Trouver le .env réellement utilisé
env_path = find_dotenv(usecwd=True)
print(env_path)

À retenir

.env = fichier sur disque ; os.environ = variables actuellement en mémoire dans le kernel. load_dotenv(..., override=True) copie le premier vers le second en remplaçant les valeurs existantes.

Saisir un secret sans l'afficher avec getpass

from getpass import getpass

api_key = getpass("Entre ta clé API : ")

La saisie est masquée. Utile dans un notebook pour tester sans écrire la clé dans le fichier.

Charger directement dans l'environnement :

import os
from getpass import getpass

os.environ["GOOGLE_API_KEY"] = getpass("Clé Gemini : ")

Puis lire normalement :

api_key = os.environ.get("GOOGLE_API_KEY")

Vérifier qu'elle existe, sans l'afficher :

print(bool(api_key))

Récupérer ton nom d'utilisateur système :

from getpass import getuser
print(getuser())

À retenir : la valeur vit en mémoire dans le kernel, disparaît au redémarrage. Ne pas faire print(api_key). Pratique pour tester sans .env ni écriture dans le notebook.

See also

See also

  • python-requests — charger une clé API depuis .env
  • jupyter — redémarrer le kernel pour effacer os.environ
  • pydantic — Settings avec model_config = SettingsConfigDict(env_file=".env")

Tenseurs et device

import torch

x = torch.tensor([[1., 2.], [3., 4.]])
torch.zeros(2, 3); torch.ones(2, 3); torch.randn(2, 3)
torch.arange(0, 10, 2); torch.linspace(0, 1, 5)
device = (
    "cuda" if torch.cuda.is_available()
    else "mps" if torch.backends.mps.is_available()
    else "cpu"
)
print(device, torch.__version__)
mps 2.9.1
x = x.to(device)
model = model.to(device)

Erreur la plus fréquente : Expected all tensors to be on the same device. Les données et le modèle doivent être sur le même device, à chaque batch.

Formes

x.shape, x.dtype, x.device
x.view(-1, 4)         # nécessite un tenseur contigu
x.reshape(-1, 4)      # marche toujours, copie si besoin
x.permute(0, 2, 1)    # réordonne les axes
x.unsqueeze(0)        # ajoute un axe -> (1, ...)
x.squeeze()           # retire les axes de taille 1
torch.cat([a, b], dim=0)
torch.stack([a, b])   # crée un nouvel axe

torch.einsum("bij,bjk->bik", a, b) remplace avantageusement les enchaînements de permute + matmul quand la manipulation d'axes devient illisible.

Autograd

w = torch.randn(3, requires_grad=True)
loss = (w ** 2).sum()
loss.backward()
print(w.grad)
tensor([ 1.4832, -0.6210,  2.0044])
with torch.no_grad():        # désactive le graphe : inférence, évaluation
    preds = model(x)

x.detach()                   # coupe un tenseur du graphe

Un modèle

import torch.nn as nn

class MLP(nn.Module):
    def __init__(self, d_in: int, d_hidden: int, d_out: int):
        super().__init__()                    # obligatoire, avant tout le reste
        self.net = nn.Sequential(
            nn.Linear(d_in, d_hidden),
            nn.GELU(),
            nn.Dropout(0.1),
            nn.Linear(d_hidden, d_out),
        )

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        return self.net(x)

model = MLP(128, 512, 10).to(device)
print(sum(p.numel() for p in model.parameters()) / 1e6, "M paramètres")
0.13 M paramètres

Boucle d'entraînement

from torch.utils.data import DataLoader, TensorDataset

loader = DataLoader(TensorDataset(X, y), batch_size=32, shuffle=True, num_workers=4)
opt = torch.optim.AdamW(model.parameters(), lr=3e-4, weight_decay=0.01)
sched = torch.optim.lr_scheduler.CosineAnnealingLR(opt, T_max=epochs)
criterion = nn.CrossEntropyLoss()

for epoch in range(epochs):
    model.train()
    for xb, yb in loader:
        xb, yb = xb.to(device), yb.to(device)

        opt.zero_grad(set_to_none=True)     # sinon les gradients s'accumulent
        loss = criterion(model(xb), yb)
        loss.backward()
        torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
        opt.step()

    sched.step()

    model.eval()
    with torch.no_grad():
        acc = (model(X_val.to(device)).argmax(1) == y_val.to(device)).float().mean()
    print(f"epoch {epoch}  loss {loss.item():.4f}  val_acc {acc:.3f}")
epoch 0  loss 1.8342  val_acc 0.412
epoch 1  loss 1.1907  val_acc 0.638
epoch 2  loss 0.8455  val_acc 0.741

Les quatre oublis classiques : zero_grad absent, model.train() / model.eval() non basculés (dropout et batchnorm se comportent différemment), no_grad manquant en évaluation, et données restées sur le CPU.

Pertes

Tâche Perte Entrée attendue
Classification multi-classe nn.CrossEntropyLoss logits bruts, pas de softmax
Classification binaire nn.BCEWithLogitsLoss logits bruts
Régression nn.MSELoss, nn.L1Loss valeurs

CrossEntropyLoss applique le log-softmax en interne. Y ajouter un softmax dans le modèle est une erreur silencieuse qui dégrade l'apprentissage sans lever.

Précision mixte

scaler = torch.amp.GradScaler(device)

with torch.autocast(device_type=device, dtype=torch.bfloat16):
    loss = criterion(model(xb), yb)

scaler.scale(loss).backward()
scaler.step(opt)
scaler.update()

En bf16 le GradScaler est facultatif ; il reste nécessaire en fp16.

Sauvegarder

torch.save(model.state_dict(), "model.pt")
model.load_state_dict(torch.load("model.pt", map_location=device))
model.eval()

Toujours sauvegarder le state_dict, jamais l'objet modèle : la sérialisation directe casse dès que le code de la classe change.

Checkpoint complet pour reprendre un entraînement :

torch.save({"model": model.state_dict(), "opt": opt.state_dict(), "epoch": epoch}, "ckpt.pt")

Diagnostic mémoire GPU

print(torch.cuda.memory_allocated() / 1e9, "Go")
torch.cuda.empty_cache()

CUDA out of memory : réduire le batch, activer l'accumulation de gradients, passer en précision mixte, ou activer le gradient checkpointing.

Accélérer

model = torch.compile(model)       # gains réels sur GPU récents
torch.backends.cuda.matmul.allow_tf32 = True

See also

Raccourcis Mac

page dédiée →

Le terminal et l'éditeur de code partagent des raccourcis proches, mais pas identiques. Le meilleur trio à mémoriser : Option pour les mots, Cmd pour les lignes/fichier, et Shift pour sélectionner pendant le déplacement.

Terminal (zsh / bash)

Action Raccourci
Début de la ligne Ctrl + A
Fin de la ligne Ctrl + E
Reculer / avancer d'un mot Option + ← / Option + →
Supprimer le mot précédent Option + Retour arrière
Supprimer jusqu'au début de ligne Ctrl + U
Supprimer jusqu'à la fin de ligne Ctrl + K
Effacer tout l'écran Ctrl + L
Historique des commandes ↑ / ↓
Rechercher une commande passée Ctrl + R, puis tape ta recherche
Annuler l'édition courante Ctrl + C

Le terminal ne permet généralement pas de sélectionner/manipuler des lignes comme un éditeur : on sélectionne à la souris, puis Cmd + C.

Éditeur de code

Déplacement et sélection

Action Raccourci
Déplacer le curseur d'un caractère ← / →
Déplacer le curseur d'un mot Option + ← / Option + →
Début / fin de ligne Cmd + ← / Cmd + →
Début / fin du fichier Cmd + ↑ / Cmd + ↓
Ligne suivante / précédente ↑ / ↓
Sélectionner caractère par caractère Shift + flèches
Sélectionner mot par mot Option + Shift + ← / →
Sélectionner jusqu'au début / à la fin de ligne Cmd + Shift + ← / →
Sélectionner jusqu'au début / à la fin du fichier Cmd + Shift + ↑ / ↓
Sélectionner tout Cmd + A
Sélectionner un mot Double-clic
Sélectionner une ligne Triple-clic

Édition de base

Action Raccourci
Copier / couper / coller Cmd + C / X / V
Annuler / refaire Cmd + Z / Cmd + Shift + Z
Indenter / désindenter les lignes sélectionnées Tab / Shift + Tab
Commenter les lignes sélectionnées Cmd + /

Manipulation de lignes (VS Code, Cursor)

Action Raccourci
Dupliquer la ligne / sélection Option + Shift + ↓ ou ↑
Déplacer la ligne / sélection Option + ↓ ou ↑
Supprimer une ligne Cmd + Shift + K
Ajouter un curseur sur la ligne suivante Option + Cmd + ↓
Ajouter un curseur sur la ligne précédente Option + Cmd + ↑

title: Raccourcis Mac category: cheatsheets created: 2025-01-XX updated: 2025-01-XX tags: [mac, shortcuts, terminal, zsh, vscode, cursor] confidence: high publish: true

Distinguer terminal et éditeur de code : les raccourcis de base sont proches, mais déplacer une ligne dépend de l'éditeur.

Terminal (zsh / bash)

Action Raccourci
Début de la ligne Ctrl + A
Fin de la ligne Ctrl + E
Reculer / avancer d'un mot Option + ← / Option + →
Supprimer le mot précédent Option + Retour arrière
Supprimer jusqu'au début de ligne Ctrl + U
Supprimer jusqu'à la fin de ligne Ctrl + K
Effacer tout l'écran Ctrl + L
Historique des commandes ↑ / ↓
Rechercher une commande passée Ctrl + R, puis tape ta recherche
Annuler l'édition courante Ctrl + C

Le terminal ne permet généralement pas de sélectionner/manipuler des lignes comme un éditeur : on sélectionne à la souris, puis Cmd + C.

Éditeur de code

Action Raccourci
Déplacer le curseur d'un caractère ← / →
Déplacer le curseur d'un mot Option + ← / Option + →
Début / fin de ligne Cmd + ← / Cmd + →
Début / fin du fichier Cmd + ↑ / Cmd + ↓
Ligne suivante / précédente ↑ / ↓
Sélectionner caractère par caractère Shift + flèches
Sélectionner mot par mot Option + Shift + ← / →
Sélectionner jusqu'au début / à la fin de ligne Cmd + Shift + ← / →
Sélectionner jusqu'au début / à la fin du fichier Cmd + Shift + ↑ / ↓
Sélectionner tout Cmd + A
Sélectionner un mot Double-clic
Sélectionner une ligne Triple-clic
Copier / couper / coller Cmd + C / X / V
Annuler / refaire Cmd + Z / Cmd + Shift + Z
Indenter / désindenter les lignes sélectionnées Tab / Shift + Tab
Commenter les lignes sélectionnées Cmd + /

Dans VS Code, Cursor et beaucoup d'éditeurs modernes :

Action Raccourci
Dupliquer la ligne / sélection Option + Shift + ↓ ou ↑
Déplacer la ligne / sélection Option + ↓ ou ↑
Supprimer une ligne Cmd + Shift + K
Ajouter un curseur sur la ligne suivante Option + Cmd + ↓
Ajouter un curseur sur la ligne précédente Option + Cmd + ↑

Le meilleur trio à mémoriser : Option pour les mots, Cmd pour les lignes/fichier, et Shift pour sélectionner pendant le déplacement.

Presse-papiers système

Copier la sortie d'une commande directement dans le presse-papiers :

pwd | pbcopy

Ensuite Cmd + V pour coller.

Lire le contenu du presse-papiers :

pbpaste

Copier le chemin absolu d'un fichier :

realpath mon_fichier.py | pbcopy

Pipe typique : copier le résultat d'un cat, d'un git log --oneline, etc.

See also

bash jupyter vscode

See also

bash jupyter vscode

scikit-learn

page dédiée →

Une API uniforme : tout estimateur a fit, tout transformateur a transform, tout prédicteur a predict. Le reste en découle.

Découper

from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)

stratify=y conserve la proportion des classes — indispensable en classification déséquilibrée. random_state rend le découpage reproductible.

Le pipeline, à utiliser systématiquement

from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.impute import SimpleImputer
from sklearn.ensemble import RandomForestClassifier

numeric = ["age", "montant"]
categorical = ["region", "segment"]

pre = ColumnTransformer([
    ("num", Pipeline([
        ("impute", SimpleImputer(strategy="median")),
        ("scale", StandardScaler()),
    ]), numeric),
    ("cat", Pipeline([
        ("impute", SimpleImputer(strategy="most_frequent")),
        ("onehot", OneHotEncoder(handle_unknown="ignore")),
    ]), categorical),
])

model = Pipeline([("pre", pre), ("clf", RandomForestClassifier(random_state=42))])
model.fit(X_train, y_train)

Le pipeline n'est pas une élégance : il empêche la fuite de données. Un StandardScaler ajusté sur tout le jeu avant le split fait fuiter la moyenne du test dans l'entraînement, et le score devient un mensonge.

handle_unknown="ignore" évite un crash quand une catégorie inconnue apparaît en production.

Évaluer

from sklearn.metrics import classification_report, confusion_matrix, roc_auc_score

y_pred = model.predict(X_test)
print(classification_report(y_test, y_pred))
              precision    recall  f1-score   support

           0       0.91      0.95      0.93       248
           1       0.78      0.66      0.71        72

    accuracy                           0.89       320
   macro avg       0.84      0.80      0.82       320
weighted avg       0.88      0.89      0.88       320

Sur un jeu déséquilibré, l'accuracy ment : ici 89 % semble bon alors que la classe minoritaire n'est rattrapée qu'à 66 %. Regarder le rappel par classe, la matrice de confusion, et l'AUC.

proba = model.predict_proba(X_test)[:, 1]
print(roc_auc_score(y_test, proba))

Validation croisée

from sklearn.model_selection import cross_val_score, StratifiedKFold

cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
scores = cross_val_score(model, X, y, cv=cv, scoring="f1", n_jobs=-1)
print(scores.mean().round(3), scores.std().round(3))
0.712 0.031

L'écart-type compte autant que la moyenne : un modèle à 0.71 ± 0.03 est utilisable, à 0.71 ± 0.18 il est instable.

Recherche d'hyperparamètres

from sklearn.model_selection import GridSearchCV, RandomizedSearchCV

grid = {
    "clf__n_estimators": [100, 300],
    "clf__max_depth": [None, 10, 20],
    "clf__min_samples_leaf": [1, 5],
}

search = GridSearchCV(model, grid, cv=cv, scoring="f1", n_jobs=-1, verbose=1)
search.fit(X_train, y_train)
print(search.best_params_, search.best_score_)

Le double underscore adresse un paramètre à travers le pipeline : clf__max_depth vise le max_depth de l'étape nommée clf. Au-delà d'une centaine de combinaisons, RandomizedSearchCV(n_iter=50) donne presque le même résultat bien plus vite.

Estimateurs courants

Tâche Point de départ Ensuite
Classification tabulaire LogisticRegression HistGradientBoostingClassifier, RandomForest
Régression tabulaire Ridge HistGradientBoostingRegressor
Clustering KMeans DBSCAN, AgglomerativeClustering
Réduction de dimension PCA TSNE, UMAP (hors sklearn)

HistGradientBoosting* gère les valeurs manquantes nativement et bat presque toujours une forêt aléatoire sur du tabulaire.

Toujours établir une référence triviale avant de comparer :

from sklearn.dummy import DummyClassifier
DummyClassifier(strategy="most_frequent").fit(X_train, y_train).score(X_test, y_test)

Classes déséquilibrées

RandomForestClassifier(class_weight="balanced")
LogisticRegression(class_weight="balanced")

Et ajuster le seuil de décision plutôt que d'accepter 0.5 par défaut :

from sklearn.metrics import precision_recall_curve
p, r, seuils = precision_recall_curve(y_test, proba)

Sauvegarder

import joblib

joblib.dump(model, "model.joblib")
model = joblib.load("model.joblib")

Le pipeline entier est sérialisé, préprocessing compris — c'est le second intérêt majeur du pipeline : le même objet sert en entraînement et en production.

See also

Interface web en Python pur. Le meilleur rapport temps/effet pour une démo client interne.

Le modèle d'exécution, à comprendre en premier

Le script entier est réexécuté de haut en bas à chaque interaction. Pas de callbacks, pas de composants. C'est ce qui rend Streamlit simple et ce qui provoque toutes ses surprises.

Deux conséquences : tout état doit vivre dans st.session_state, et tout calcul coûteux doit être mis en cache.

pip install streamlit
streamlit run app.py
  You can now view your Streamlit app in your browser.

  Local URL: http://localhost:8501
  Network URL: http://192.168.1.24:8501

Afficher

import streamlit as st

st.title("Test d'accessibilité produit")
st.header("Résultats")
st.subheader("Par site")
st.write("Accepte à peu près tout : texte, DataFrame, figure, dict")
st.markdown("**gras**, `code`, [lien](https://example.com)")
st.code("requests.post(url, json=payload)", language="python")
st.json({"findable": True, "clicks": 3})
st.dataframe(df)            # interactif, triable
st.table(df.head())         # statique
st.metric("Clics moyens", 3.2, delta=-0.4)

Saisir

site = st.text_input("Site", value="versace.com")
produit = st.text_area("Produit recherché")
budget = st.slider("Budget d'étapes", 10, 200, 60)
n = st.number_input("Répétitions", min_value=1, max_value=10, value=3)
mode = st.selectbox("Mode", ["fast", "default"])
tags = st.multiselect("Tests", ["navigation", "panier", "checkout"])
strict = st.checkbox("Mode strict")
fichier = st.file_uploader("CSV", type=["csv"])

if st.button("Lancer"):
    ...

Chaque widget renvoie sa valeur courante. Le if st.button(...) n'est vrai que sur le rerun déclenché par le clic.

Mise en page

col1, col2, col3 = st.columns(3)
with col1:
    st.metric("Sessions", 12)

with st.sidebar:
    api_key = st.text_input("Clé API", type="password")

tab1, tab2 = st.tabs(["Résultats", "Logs"])
with tab1:
    st.dataframe(df)

with st.expander("Détails techniques"):
    st.code(trace)

with st.container():
    st.write("bloc regroupé")

État

if "runs" not in st.session_state:
    st.session_state.runs = []

if st.button("Ajouter"):
    st.session_state.runs.append({"site": site})

st.write(f"{len(st.session_state.runs)} exécutions")

Sans session_state, la liste serait recréée vide à chaque interaction.

Cache

@st.cache_data                     # pour des données : DataFrame, JSON, réponses API
def charger(path: str):
    return pd.read_csv(path)

@st.cache_resource                 # pour des objets vivants : client, modèle, connexion
def get_client():
    return SomeClient(api_key=os.environ["API_KEY"])

La distinction compte : cache_data sérialise et renvoie une copie, cache_resource renvoie le même objet partagé entre sessions. Un client HTTP dans cache_data casse.

Retour visuel pendant un traitement long

with st.spinner("Session en cours…"):
    result = run_agent(site, produit)

bar = st.progress(0)
for i, test in enumerate(tests):
    run(test)
    bar.progress((i + 1) / len(tests))

st.success("Terminé")
st.warning("2 tests non concluants")
st.error("Clé API invalide")

with st.status("Navigation…", expanded=True) as s:
    st.write("Ouverture du site")
    st.write("Recherche du produit")
    s.update(label="Terminé", state="complete")

Streaming d'un LLM

def tokens():
    for chunk in client.stream(prompt):
        yield chunk.text

st.write_stream(tokens)

Pour un chat complet, st.chat_message("user" | "assistant") et st.chat_input().

Secrets et configuration

.streamlit/secrets.toml :

API_KEY = "sk-..."
key = st.secrets["API_KEY"]

.streamlit/config.toml pour le thème et le port :

[server]
port = 8501
[theme]
base = "light"

Limites à connaître avant de s'engager

Pas de routing multi-pages fin (juste un dossier pages/), peu de contrôle sur le CSS, et le modèle de rerun devient pénible dès qu'on veut une vraie interactivité. Au-delà de la démo, passer à Next.js avec une API FastAPI derrière.

Déploiement : Streamlit Community Cloud gratuit depuis un repo GitHub, ou un conteneur Docker n'importe où. Pas sur Vercel — Vercel ne fait pas tourner de processus Python long.

See also

Infrastructure décrite en fichiers, appliquée de façon idempotente. On décrit l'état voulu, Terraform calcule les opérations pour y arriver.

Le cycle

terraform init       # télécharge les providers, configure le backend
terraform fmt        # reformate le HCL
terraform validate   # vérifie la syntaxe et les références
terraform plan       # ce qui va changer, sans rien faire
terraform apply      # applique après confirmation
terraform destroy    # supprime tout ce qui est géré
terraform plan
Terraform will perform the following actions:

  # aws_s3_bucket.artifacts will be created
  + resource "aws_s3_bucket" "artifacts" {
      + bucket = "monprojet-artifacts"
      + id     = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.

Toujours lire le plan avant d'appliquer. La ligne à surveiller est le nombre de destroy : un changement anodin peut provoquer un remplacement de ressource.

Structure d'un projet

main.tf         ressources
variables.tf    entrées
outputs.tf      sorties
providers.tf    providers et versions
terraform.tfvars   valeurs (à ne pas committer si secrets)
terraform {
  required_version = ">= 1.9"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"        # >= 5.0, < 6.0
    }
  }
  backend "s3" {
    bucket = "mon-tfstate"
    key    = "prod/terraform.tfstate"
    region = "eu-west-3"
  }
}

provider "aws" {
  region = var.region
}

Le backend distant est le premier réflexe en équipe : sans lui, le fichier d'état vit sur un poste et deux personnes qui appliquent en même temps se marchent dessus.

Variables et sorties

variable "region" {
  type        = string
  default     = "eu-west-3"
  description = "Région AWS"
}

variable "instance_count" {
  type = number
  validation {
    condition     = var.instance_count > 0
    error_message = "Il faut au moins une instance."
  }
}

output "bucket_url" {
  value = aws_s3_bucket.artifacts.bucket_domain_name
}

output "db_password" {
  value     = random_password.db.result
  sensitive = true      # masqué dans les logs
}
terraform apply -var="region=eu-west-1"
terraform apply -var-file="prod.tfvars"
TF_VAR_region=eu-west-1 terraform apply

Ressources et références

resource "aws_s3_bucket" "artifacts" {
  bucket = "${var.project}-artifacts"

  tags = {
    Environment = var.env
    ManagedBy   = "terraform"
  }
}

resource "aws_s3_bucket_versioning" "artifacts" {
  bucket = aws_s3_bucket.artifacts.id     # crée la dépendance implicite
  versioning_configuration {
    status = "Enabled"
  }
}

Le graphe de dépendances se déduit des références. depends_on ne sert que pour les dépendances invisibles.

Boucles et conditions

resource "aws_instance" "worker" {
  count         = var.instance_count
  ami           = data.aws_ami.ubuntu.id
  instance_type = "t3.micro"
  tags = { Name = "worker-${count.index}" }
}

resource "aws_s3_bucket" "per_env" {
  for_each = toset(["dev", "staging", "prod"])
  bucket   = "monprojet-${each.key}"
}

locals {
  is_prod = var.env == "prod"
  size    = local.is_prod ? "t3.large" : "t3.micro"
}

Préférer for_each à count : avec count, supprimer un élément au milieu décale les index et Terraform détruit puis recrée tout ce qui suit.

Data sources

data "aws_ami" "ubuntu" {
  most_recent = true
  owners      = ["099720109477"]
  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
  }
}

État

terraform state list
terraform state show aws_s3_bucket.artifacts
terraform import aws_s3_bucket.artifacts mon-bucket-existant
terraform state rm aws_s3_bucket.artifacts     # oublie sans détruire
terraform refresh

import sert à reprendre la main sur une ressource créée à la main dans la console.

Modules

module "network" {
  source = "./modules/network"
  cidr   = "10.0.0.0/16"
}

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "5.8.1"
  name    = "prod"
}

# module.network.subnet_ids pour lire une sortie

Cibler et forcer

terraform plan -target=aws_s3_bucket.artifacts     # dépannage, pas une habitude
terraform apply -replace=aws_instance.worker[0]    # recrée une ressource
terraform apply -auto-approve                      # CI uniquement

Pièges

Le .tfstate contient des secrets en clair : jamais dans git, toujours dans un backend chiffré. Un terraform destroy sur le mauvais workspace est irréversible. Et le plan n'est valide qu'à l'instant où il est produit — si quelqu'un modifie l'infra à la main entre-temps, l'apply diverge.

See also

Gestionnaire de projets Python en Rust. Remplace pyenv + venv + pip + pip-tools d'un seul coup, et télécharge ses propres interpréteurs.

Démarrer un projet

uv init --python 3.12 mon-projet
cd mon-projet
uv add httpx pydantic pytest
uv run python -V
Using CPython 3.12.11
Creating virtual environment at: .venv
Python 3.12.11

Toujours passer --python. Sans lui, uv prend sa version par défaut — souvent une 3.13 ou 3.14 — et écrit requires-python = ">=3.13" dans le pyproject.toml.

uv init crée pyproject.toml, .python-version, main.py, README.md, .gitignore et un dépôt git. Le .venv/ n'apparaît qu'au premier add ou run.

Variante Résultat
uv init projet application, fichiers à plat
uv init --package src/<nom>/__init__.py + [build-system]
uv init --lib idem plus py.typed

Le quotidien

uv add httpx                  # ajoute et installe
uv add --dev pytest ruff      # dépendance de dev
uv remove httpx
uv sync                       # aligne .venv sur uv.lock
uv lock                       # regénère le lock sans installer
uv tree                       # arbre des dépendances
uv run <commande>             # exécute dans le venv, sans activation

uv add et uv run synchronisent implicitement. uv sync sert surtout après un clone, ou quand le .venv est cassé.

uv sync désinstalle ce qui n'est pas dans le lock, contrairement à pip install -r.

uv sync
Resolved 100 packages in 4ms
Uninstalled 3 packages in 8ms
 - markdown-it-py==4.2.0
 - mdurl==0.1.2
 - rich==15.0.0

Reprendre un repo existant

git clone <url> && cd <repo> && ls
Ce qu'on trouve Ce qu'on lance
uv.lock uv sync
pyproject.toml seul uv sync (le lock est généré)
requirements.txt uv venv --python 3.12 puis uv pip install -r requirements.txt
poetry.lock poetry install, ou uv pip install -e .
rien uv init --python 3.12

uv venv + uv pip ne modifient aucun fichier du repo — c'est la voie propre pour accélérer l'installation chez un client sans imposer son outillage.

Le piège .python-version

uv et pyenv lisent ce fichier. Si uv y écrit 3.13 et que pyenv n'a pas de 3.13, pyenv affiche une erreur à chaque commande dans ce dossier. C'est du bruit, uv continue de fonctionner, mais c'est pénible.

# corriger requires-python dans pyproject.toml D'ABORD
uv python pin 3.12
Updated `.python-version` from `3.13` -> `3.12`

Dans l'autre ordre, uv refuse : The requested Python version 3.12 is incompatible with the project requires-python value of >=3.13.

Outils globaux

uv tool install ruff          # installé isolément, disponible partout
uv tool list
uvx ruff check .              # exécute sans installer

Diagnostic

uv run python -c "import sys; print(sys.executable)"
/Users/edouard/code/mon-projet/.venv/bin/python3

Le chemin doit contenir /.venv/ et être dans le projet. S'il pointe vers .pyenv/shims, /opt/homebrew ou /usr/bin, on n'est pas où on croit.

uv python list        # interpréteurs connus
uv cache clean        # vider le cache

Pour un live coding sous pression, activer une fois et oublier le préfixe :

source .venv/bin/activate

See also

Vision Encoders

page dédiée →

Le vrai défi des vision encoders pour LLM n'est pas la perception mais la compression : un document de 50 pages à 4000 tokens/page sature un contexte de 256k avant toute analyse.

Le problème de budget de tokens

Les encoders classiques (CLIP, SigLIP) produisent des représentations coûteuses :

Résolution Tokens (classique) Budget consommé (50 pages)
1024×1024 4,096 204,800
1280×1280 6,400 320,000

Une image devient un texte de plusieurs milliers de mots. Pour les tâches long-contexte (analyse de documents, archives, vidéos), cette inefficience bloque des classes entières d'applications.

Architecture à deux étages

La clé : comprimer avant l'attention globale, pas après.

# Ordre conventionnel (inefficace)
patches = patchify(image)           # 4096 tokens
features = global_attention(patches) # O(4096²) mémoire
compressed = project(features)       # 256 tokens

# Ordre optimisé (DeepSeek-OCR)
patches = patchify(image)            # 4096 tokens
local = window_attention(patches)    # O(4096 × window²)
compressed = conv_4x4_stride4(local) # 256 tokens (16× compression)
features = global_attention(compressed) # O(256²) — 256× moins de mémoire

La compression spatiale (stride convolution) réduit la grille de 64×64 à 16×16 avant que l'attention quadratique ne s'applique.

Modes de fonctionnement explicites

Traiter les tokens comme un budget de première classe :

Mode Tokens/page Résolution Précision OCR Cas d'usage
Tiny 64 512×512 ~85% Screening haute-volume
Small 100 640×640 ~92% Documents standards
Base 256 1024×1024 ~97% Articles techniques
Large 400 1280×1280 ~98.5% Détails fins

Pour les grandes pages, stratégie « Gundam » : n_tiles × 100 + 256 (vue globale).

Entraînement bout-en-bout

Le CLIP-style transformer sert de backbone mais sans contrastive learning.

# Gradient depuis la reconstruction de texte
loss = cross_entropy(decoder_output, ground_truth_text)
loss.backward()  # Flow : text loss → cross-attention → vision encoder

# Le goulot de compression force la sélection de features
# Seules les features prédictives du texte survivent

Le bottleneck de 16× agit comme un filtre d'information : l'encoder apprend à garder ce qui prédit les caractères, écarter le reste (texture, style).

Résultats empiriques

  • Fox benchmark : 97% de précision OCR à 9-10× compression
  • Throughput : 200,000 pages/jour sur A100
  • Mémoire : 256× moins pour l'attention globale (256² vs 4096²)

Compression vs perception

Le choix n'est pas binaire mais dépend de la tâche :

Approche Tokens/image Quand l'utiliser
Compressée (DeepSeek-OCR) 64-400 Documents longs, OCR, archivage, génération de données
Généraliste (Qwen2.5-VL) 1024-4096 Raisonnement visuel complexe, diagrammes, agents
Haute fidélité (Qwen3-VL) 4096-16384 Vidéos longues, spatial reasoning, détails géométriques

La compression sacrifie la richesse du raisonnement visuel au profit de l'efficience contextuelle.

Placement de la compression

Principe de design : identifier où réduire la dimensionnalité.

# Anti-pattern : opérations quadratiques sur haute résolution
expensive = global_attention(high_res_tokens)  # Puis compresser

# Pattern optimal : comprimer avant scaling quadratique
compressed = spatial_downsample(high_res_tokens)
cheap = global_attention(compressed)

La position du compresseur dans le pipeline détermine les caractéristiques d'efficience du système.

Questions ouvertes

Limite de compression : Le régime 20× montre ~60% de précision. Où se situe l'effondrement sémantique ?

Préservation géométrique : Les RoPE multi-résolution (Qwen3-VL) maintiennent les relations spatiales. La compression agressive casse-t-elle les priors géométriques pour les tables/diagrammes ?

Budgeting adaptatif : Allouer plus de tokens aux régions denses (équations, tableaux) et moins au texte courant ?

Frontière de Pareto : Quelle est la surface d'échange compression/taille/précision ?

Compress before global attention

Le placement de la compression dans le pipeline change radicalement l'efficacité mémoire.

Architecture DeepEncoder :

  1. Perception head : window attention locale (64×64 patches → 4,096 tokens, pas de réduction)
  2. Compression : convolution 4×4 stride 4 (4,096 → 256 tokens, 16× reduction)
  3. Knowledge head : CLIP ViT avec global self-attention sur les 256 tokens compressés

Gain mémoire : O(256²) vs O(4,096²) = 256× moins de mémoire pour l'attention globale.

# Principe architectural
local_features = window_attention(patches)      # 4096 tokens
compressed = conv_4x4_stride4(local_features)  # 256 tokens (16×)
global_features = global_attention(compressed)  # attention quadratique ici

Le point critique : compresser avant les opérations quadratiques, pas après.

Modes de résolution comme paramètres budgétaires

Token budget explicite au lieu de propriété émergente.

Mode Tokens/page Résolution OCR Precision Use case
Tiny 64 512×512 ~85% High-volume screening
Small 100 640×640 ~92% Standard documents
Base 256 1024×1024 ~97% Technical papers
Large 400 1280×1280 ~98.5% Fine detail preservation

Tiling "Gundam" pour documents larges : n × 100 + 256 tokens (n tuiles locales + 1 vue globale).

Le budget devient prévisible et schedulable.

Entraînement end-to-end sur reconstruction de texte

Pas de contrastive learning CLIP, malgré l'architecture CLIP.

  • Loss : cross-entropy du décodeur sur les tokens de texte ground-truth
  • Gradient : ∂L/∂logits = p(predicted) − 𝟙_ground_truth remonte jusqu'au vision encoder
  • Effet du bottleneck : la compression 16× force l'encoder à ne garder que les features pertinentes pour reconstruire le texte (caractères, mise en page), pas la texture

Training insight : le ratio de compression agit comme information bottleneck, sélectionnant automatiquement les features OCR-relevant.

Résultats empiriques DeepSeek-OCR

  • 97% OCR precision à 9-10× compression (Fox benchmark)
  • SOTA accuracy/token sur OmniDocBench
  • Throughput : ~200,000 pages/jour sur 1× A100

Dégradation : à 20× compression, précision ~60% (encore utile pour certaines tâches).

Trade-off : compression vs perception breadth

Compression-focused (DeepSeek-OCR) :

  • Document processing à grande échelle
  • Data generation
  • Archival applications
  • Token efficiency prime

Generalist VLM (Qwen2.5-VL, Qwen3-VL) :

  • Visual reasoning complexe
  • Long-form video understanding
  • Diagram parsing fin
  • Préservent plus de tokens pour reasoning riche

Le choix dépend du contexte d'usage, pas d'une supériorité absolue.

Questions ouvertes

  • Compression limits : à quel ratio la semantic collapse survient-elle ?
  • Geometric preservation : l'aggressive compression sacrifie-t-elle les priors spatiaux (tables, diagrammes) ?
  • Adaptive budgeting : allouer plus de tokens aux régions denses (formules, tables) ?
  • Cross-modal compression : le principe s'étend-il à video, audio, 3D point clouds ?
  • Architecture search : frontière de Pareto compression/qualité/coût ?

See also

numpy pour la manipulation de patches et grilles spatiales
pytorch pour implémenter les architectures de compression
langchain pour intégrer les encoders dans des pipelines multimodaux
keras-tensorflow pour les stratégies de training bout-en-bout

World Models

page dédiée →

Le terme « world model » désigne techniquement un modèle qui prédit comment l'état du monde évolue en réponse à des actions. En 2025-2026, le marché confond sous ce label quatre technologies distinctes avec des dynamiques et des menaces complètement différentes.

Les quatre catégories confondues

Catégorie Définition Exemples Vraiment un world model ?
Learned world models Réseau de neurones entraîné à prédire l'état futur en réponse à une action DreamDojo, DreamZero (NVIDIA), V-JEPA 2-AC (AMI Labs), GAIA (Wayve) Oui
Simulation analytique Moteur physique déterministe (Newton, MuJoCo) NVIDIA Isaac Sim + Newton, Genesis, MuJoCo Non, c'est un simulateur
Génération 3D Modèle génératif créant des environnements 3D statiques Marble (World Labs), HunyuanWorld, Genie 3 (Google DeepMind) Non, génère un instantané spatial
Génération vidéo Diffusion générant des séquences vidéo 2D Sora, Veo, Runway, Kling Seulement si conditionné par actions

La confusion coûte des milliards : un investisseur qui entend « world model » investit dans un concept unifié alors que chaque catégorie a ses propres risques de commoditisation.

Learned world models : JEPA vs génératif

Thèse JEPA (AMI Labs, LeCun)

Prédire en pixels gaspille de la capacité sur du bruit (feuilles qui bougent, reflets). Mieux vaut prédire en représentation abstraite où seule la structure causale est capturée.

Forces : V-JEPA 2 atteint 77,3% sur Something-Something v2, SOTA en anticipation d'actions (Epic-Kitchens-100), représentations efficaces (VL-JEPA surpasse CLIP avec 50% de paramètres en moins).

Faiblesses :

  • Vitesse : V-JEPA 2-AC nécessite 16 secondes de planning MPC par action (800 candidats échantillonnés, 10 itérations). Vs 10,81 FPS pour DreamDojo.
  • Données : post-entraîné sur 62 heures de données robot (DROID) vs 44 711 heures de vidéo humaine pour DreamDojo. Désavantage de scaling structurel.
  • Contrôle : excellent en compréhension vidéo passive, s'effondre en contrôle robotique actif.

DreamZero/DreamDojo (NVIDIA GEAR)

World Action Model de 14B paramètres prédisant conjointement vidéo future et actions dans un seul forward pass.

Résultats : 2× la généralisation zero-shot des VLAs, 10,81 FPS après distillation (vs 16s/action pour JEPA), corrélation r=0.995 entre qualité de prédiction et performance de contrôle.

Leçon : les pixels ne sont pas du bruit pour le contrôle moteur. Texture = friction, reflet = angle, ombre = profondeur. L'information que JEPA jette est ce dont un robot a besoin.

EgoScale (NVIDIA)

Retargeting de vidéo humaine égocentrique vers robots. 20 854 heures de vidéo, +54% de performance, scaling log-linéaire (R²=0.9983). La courbe ne plafonne pas.

Implication : la vidéo humaine égocentrique est quasi-infinie. Le bottleneck n'est pas l'architecture du world model, c'est l'accès aux données et la qualité du pipeline de transfert.

Simulation analytique : le monopole brisé

Genesis : 105M$ levés sur la thèse « simulation trop lente, pas différentiable ». USP : moteur unifié, différentiable, 43M FPS.

Trois fronts d'érosion :

  1. NVIDIA Newton rend Isaac Sim différentiable → USP absorbé
  2. Learned world models apprennent la physique depuis la vidéo → contournent le problème
  3. Résultats sim-to-real existants (DoorMan 83% vs 80% humain, VIRAL, Unitree parkour) prouvent que le bottleneck n'était pas la vitesse du moteur

La simulation analytique reste indispensable (safety-critical, RL haute fréquence, domain randomization), mais le monopole comme seul pipeline de données robotiques est brisé.

Génération 3D : marchés surestimés

Marble (World Labs) : 1B$ levé, 5B$ de valorisation.

Marché Promesse Réalité
Robotique Génération de scènes d'entraînement Nice-to-have. Cosmos Transfer fait déjà l'augmentation visuelle. EgoScale/DreamDojo exploitent directement la vidéo humaine.
VFX Remplacement workflow 3D Mid-tier migre vers vidéo gen pure (SeedDance, Veo). Haut de gamme : contrôle caméra déjà conditionnable dans la vidéo gen (SeedDance, Wan 2.1).
AEC Architecture/construction Hallucine de la géométrie. Pas de précision géométrique (murs pas exactement 3,20m). 60% du marché = rénovation → LiDAR iPhone/ARCore capture l'existant.

Commoditisation : HunyuanWorld (Tencent) open-source, 4 itérations en 7 mois. Genie 3 (Google DeepMind) en production. Blender intègre génération 3D native.

Survie : course contre la commoditisation. Si World Labs capture 20-30% du marché outils 3D en 2-3 ans, justifié par distribution (Autodesk), sinon avalé.

NVIDIA : le hedge ultime

NVIDIA maintient six approches parallèles sans en choisir une :

  • Isaac Sim + Newton (simulation différentiable)
  • DreamDojo, DreamZero (learned world models)
  • EgoScale (retargeting vidéo humaine)
  • Cosmos (génération synthétique)
  • GROOTDream (simulation procédurale)
  • Isaac GR00T N1.6 (contrôle humanoid)

Quand le plus gros acteur maintient six approches parallèles, le signal est clair : aucune couche n'est le goulot d'étranglement. NVIDIA traite chaque couche comme une commodité et construit l'orchestration.

Intégration verticale complète : GPU → training (DGX) → simulation (Omniverse) → inférence embarquée (Jetson/DRIVE). Les startups qui construisent une couche intermédiaire sont structurellement vulnérables.

Cas Wayve : le label qui cache l'actif

8,6B$ de valorisation, présenté comme « embodied AI + world models ».

Réalité :

  • Produit : modèle de conduite end-to-end (capteurs → décisions). C'est ça qui conduit en zero-shot dans 500+ villes.
  • GAIA : outil interne de simulation (diffusion latent 15B, génération scénarios synthétiques). Équivalent à Cosmos → commoditisable.
  • Moat réel : distribution (Uber, Nissan, Mercedes) + données de conduite depuis 2017.

Le label « world model » est du marketing de levée. Tesla fait du end-to-end sans ce label. XPeng/Huawei aussi à échelle massive en Chine.

Tableau de survie

Acteur Levée Menace principale Survie dépend de
World Labs $1B, $5B valo Commoditisation open-source + vidéo gen Vitesse de capture marché (2-3 ans)
AMI Labs ~€500M, €3B valo DreamZero contredit thèse JEPA pour robotique Pivot vers niche (industrie, surveillance)
Genesis $105M Newton + learned models Pivot marché chinois ou acquisition
Wayve $1.2B, $8.6B valo N/A (vrai actif = distribution) Déjà solide
Runway $315M, $5.3B valo Vidéo gen commoditisée Exécution produit et UX

Biais révélés par l'IA

Claude (Anthropic) reproduit initialement le consensus VC :

  • Défend business case par réflexe (VFX, AEC, gaming)
  • Maintient exceptions (Wayve) sans examiner concrètement
  • Poids démesuré aux Turing Awards (Fei-Fei Li, LeCun)
  • Connaît chaque papier individuellement mais ne fait pas la synthèse des convergences

Si une IA entraînée sur toute la littérature ne fait pas spontanément cette synthèse, comment attendre des VCs qu'ils la fassent ? Herding effect (a16z investit → Fidelity suit), FOMO narratif, asymétrie d'expertise.

L'humanoïde : la seule forme généraliste

L'essai de Dandjinou (2026) défend une thèse radicale : la forme humanoïde n'est pas un choix anthropomorphique, c'est une convergence inévitable pour la robotique généraliste.

Les trois arguments fondateurs

  1. Co-évolution infrastructure-morphologie : notre monde (portes à 90 cm, marches de 17 cm, outils) a été construit pour un corps bipède à deux bras. Modifier l'infrastructure coûte plus cher que d'adopter la morphologie.

  2. Généralité > performance de pointe : un guépard court plus vite, mais l'humain court, nage, grimpe, manipule. La vraie métrique est la résilience dans l'imprévu, pas la performance en conditions contrôlées.

  3. Le bottleneck était le logiciel : jusqu'en 2015, contrôler un humanoïde nécessitait de programmer chaque mouvement à la main (cinématique inverse). Le reinforcement learning, l'imitation learning et le sim-to-real ont effacé cette limite.

L'humanoïde augmenté

La forme est le point de départ, pas la limite. Un robot hérite de la morphologie sans hériter des contraintes biologiques :

  • Pas de fatigue musculaire → opération continue 24h/24
  • Degrés de liberté augmentés → articulations à 360°, hypermobilité systématique
  • Force et vitesse décuplées → actionneurs dépassant les limites biochimiques
  • Modularité : roues escamotables pour terrain plat, torse télescopique pour l'atteinte verticale, capteurs infrarouges pour l'obscurité

Les zones d'ombre des formes spécialisées

Chaque forme spécialisée (AGV, bras fixe, drone) excelle dans 80 % des cas et échoue structurellement dans les 20 % restants (obstacle imprévu, escalier, manipulation fine). Le coût total, incluant les interventions humaines pour couvrir ces zones d'ombre, dépasse celui d'une plateforme généraliste.

Pourquoi les critiques ont tort en 2026

Les objections classiques (instabilité posturale, complexité du contrôle, consommation énergétique) décrivent la robotique de 2015. Les robots comme Unitree G1 (< 20 k$) ou Atlas de Boston Dynamics exécutent des mouvements qui étaient impensables il y a cinq ans. Maintenir le scepticisme face aux données actuelles n'est pas de la rigueur, c'est de l'inertie cognitive.

Position de survie : la forme humanoïde devient la plateforme standard pour tout environnement humain non reconstruit. Les formes spécialisées survivent uniquement dans des infrastructures conçues de zéro pour elles (usines, entrepôts autonomes).

Le spectre VLM ↔ world model (janvier 2025)

Le débat n'est plus « LLM ou world model », mais où placer la prédiction dans la pile :

Position Où prédit-on ? Exemple Trade-off
Language causality Chaîne de causalité en texte Alpamayo-R1 (NVIDIA) Auditable, mais certains phénomènes physiques refusent d'être résumés
Latent grounding Prédiction de représentations futures FLARE + GR00T N1.5 Apprend depuis vidéo sans actions ; opaque
Joint video + action Diffusion simultanée vidéo/action UWM, UnifoLM Un modèle, quatre modes (policy/forward/inverse/génération)
Planning via JEPA Représentation abstraite pour MPC V-JEPA 2 16 s/étape de planif. → pas un contrôleur 50 Hz
VL-JEPA Prédiction d'embeddings texte VL-JEPA (Meta) 2,85× moins de décodage ; perd l'open-ended text

Deux têtes : raisonnement + contrôle

Le pattern 2024–2025 répété (Pi0, Alpamayo, GR00T, WALL-OSS) :

  • Tête raisonnement : autorégressif, CoT, interprétable, lent (secondes)
  • Tête contrôle : flow matching, trajectoires continues, 50 Hz (20 ms)

Pourquoi ? Parce que raisonnement et contrôle ont des physiques différentes. Le VLM garde les priors sémantiques, mais n'écrase pas le contrôleur temps-réel.

# Schéma conceptuel Pi0
backbone = PaliGemma_3B()  # raisonnement
action_expert = FlowMatchingHead(300M)  # contrôle

# Génère 50 actions en une passe (73 ms, RTX GPU)
actions = action_expert(backbone_features, proprio, noise)

Le goulot autorégessif et flow matching

RT-2 (2023) discrétisait chaque angle en 256 bins, générait token par token → trop lent, manque de précision.

Flow matching (2024–2025) raffine tout le chunk d'actions en parallèle :

  • Pi0 : 50 actions/chunk, 50 Hz, 73 ms d'inférence
  • Alpamayo : trajectoires de conduite via diffusion
  • GR00T : DiT-based flow matching

Pipeline de données

Source Technique Gain
Simulation Isaac GR00T → 100k+ trajectoires synthétiques +40 % perf. (sim+real vs real seul)
Vidéo sans actions FLARE latent future, UWM masked actions +26 % (FLARE), 60 % → 80 % succès (1→10 démos)
Cross-embodiment Open X-Embodiment + embodiment tokens Transfert inter-robots

Runtime de sécurité (le layer manquant)

Un MMLM natif-action à 50 Hz n'est pas juste un modèle, c'est une pile :

Capteurs → Modèle → [Safety Runtime] → Moteurs
                      ↑
                      gate, constrain, verify, log, fallback

Le runtime doit être model-agnostic : chaque labo sort son modèle (GR00T, Pi0, V-JEPA), mais la couche qui empêche le bras de clipper le comptoir doit être commune.

Blueprint convergent 2026–2027

La trajectoire copie texte → image :

  • 2023 : séparés (LLM + diffusion)
  • 2024 : collés (LLM orchestre)
  • 2025 : fusionnés (GPT-4o génère images nativement)
  • 2026–2027 (prédiction) : action-native MMLM → même forward pass pour chat, image, et commandes moteur
Input Output Cas d'usage
Text Text Chat
Text + Image Text VQA
Text + Image Image Édition
Text + Image Text + Action Raisonner puis agir
Video + Text Action Contrôle robot 50 Hz

Ce qui reste dur

  • Garanties temps-réel : 20 ms/décision, toutes les décisions
  • Distribution shift : lumière, usure, objets hors-distribution
  • Vérification : la simulation a un reality gap, le monde réel est lent/cher/dangereux
  • Opacité des pannes : perception ? raisonnement ? exécution ? hardware ?

Falsifiable

La thèse 2025 : on ne remplace pas le VLM, on l'arme. Prédiction testable d'ici 2027 : les robots de production auront une tête VLM gelée (raisonnement) + une tête flow matching fine-tunée (contrôle) + un runtime de sécurité qui gate les deux.

Le spectre des world models : où placer la prédiction

La question n'est pas « LLM ou world model », mais où placer la prédiction. Trois espaces possibles :

Espace Méthode Exemple Avantage Limite
Langage Raisonnement causal en texte Alpamayo CoC Auditable, interprétable Certaines physiques refusent la verbalisation (déformables, fluides)
Latent Prédire les états futurs compressés FLARE, V-JEPA Efficace, apprend sans labels d'action Opaque
Pixel/Vidéo Prédire les frames suivantes UWM, Cosmos Ancré visuellement Coûteux

Le cinquième angle, souvent manqué : l'interface reasoning→control. WALL-OSS, Pi0.5, Alpamayo partagent une même forme architecturale, arrivée de plusieurs directions en 2024-2025.

Le bottleneck autorégressif et le shift vers flow matching

Pourquoi RT-2 a plafonné (2023) :

  • Discrétiser chaque angle en 256 bins → tokens 128 91 241 5 101 127
  • Générer séquentiellement, un token à la fois
  • Trois murs : vitesse (trop lent pour 50 Hz), résolution (manipulation fine impossible), multimodalité (un seul chemin, alors que plusieurs sont valides)

Flow matching résout les trois :

# Au lieu de
token → token → token → token  # séquentiel

# Flow matching fait
bruit_aléatoire → étape_1 → ... → étape_10 → trajectoire_complète
# parallèle sur toutes les dimensions, continu, multimodal naturel

Pi0 (Physical Intelligence, oct. 2024) : 3.3B params, 50 Hz via flow matching, 73 ms d'inférence pour un chunk de 50 actions. 5× plus rapide à entraîner qu'un VLA à diffusion. Plie du linge, monte des cartons.

Pattern émergent : reasoning head + control head

Tous les systèmes performants 2024-2025 ont convergé vers la même architecture à deux têtes :

┌─────────────────────────────────┐
│   Backbone VLM partagé          │  ← PaliGemma, Gemma, Eagle
│   (encode images + language)    │
└──────────┬──────────────────────┘
           ├─────────────┬─────────────┐
           ▼             ▼             |
  ┌────────────────┐  ┌──────────────┐ │
  │ Reasoning Head │  │ Control Head │ │
  │ autorégressif  │  │ flow matching│ │
  │ tokens         │  │ continu      │ │
  │ lent (s)       │  │ rapide (20ms)│ │
  └────────────────┘  └──────────────┘ │
       text              actions        │

Exemples :

  • Alpamayo-R1 (NVIDIA, oct. 2025) : Cosmos-Reason 10B + décodeur flow matching. Chain of Causation (700K traces) → trajectoires. 80K heures de conduite.
  • GR00T N1.5 (NVIDIA, juin 2025) : Eagle-2 1.34B + DiT flow matching. 2.2B params total, 63.9 ms/inférence (L40). FLARE ajoute des tokens de futur pour apprendre de vidéo humaine sans labels.
  • Pi0 : PaliGemma 3B + Action Expert 300M. 73 ms pour 50 actions.
  • Pi0.5 : ajoute planification langage explicite avant exécution.
  • WALL-OSS (sept. 2025) : traite le problème comme un mismatch objectif/interface, keep les priors VLM, control head diffusion/flow.

Pourquoi deux têtes : raisonnement et contrôle ont des physiques incompatibles. Ne pas forcer les actions à passer par next-token generation.

Unified World Models : prédire vidéo et action simultanément

UWM (Univ. Washington / Toyota, avril 2025) : une seule diffusion, timesteps indépendants par modalité.

t_video t_action Mode Fonction
Bruité Clean Policy actions depuis observations
Clean Bruité Forward Dynamics futur depuis action
Bruité Bruité Inverse Dynamics action depuis goal
Clean — Video Generator prédire vidéo future

Quand on entraîne sur YouTube (pas de labels action), on bruite complètement t_action → le modèle apprend la dynamique visuelle. Ce savoir se transfère quand les actions arrivent.

UnifoLM-WMA-0 (Unitree, sept. 2025) : extension open-source, fine-tuné sur Open-X + datasets Unitree. Intégré au robot G1 (1.3 m, >1000 unités livrées).

V-JEPA 2 : apprendre la physique, puis planifier

V-JEPA 2 (Meta, juin 2025) : prédit des portions masquées de vidéo dans l'espace d'embeddings (pas pixels). Pré-entraîné sur VideoMix22M (>1M heures). Apprend gravité, permanence d'objets, trajectoires sans supervision.

Planification (variante action-conditionnée V-JEPA 2-AC) :

  1. Encoder observation actuelle → z_now
  2. Encoder image goal → z_goal
  3. Imaginer plusieurs séquences d'actions → embeddings futurs prédits
  4. Choisir la séquence dont l'embedding final est le plus proche de z_goal
  5. Exécuter (ou juste le premier pas, puis replan)

Post-entraîné sur <62h de vidéo robot (DROID), zéro-shot sur bras Franka. 16 secondes par étape de planification (vs 4 min pour Cosmos). Parfait pour "pense puis agis", pas pour la boucle de contrôle à 50 Hz.

VL-JEPA : prédire des embeddings de texte, pas des tokens

VL-JEPA (Meta FAIR & HKUST, déc. 2025) : prédit l'embedding de la réponse, pas chaque token.

  • 50 % moins de paramètres entraînables vs VLM génératifs équivalents
  • 2.85× moins d'opérations de décodage (selective decoding)
  • 1.6B params, performances comparables à des VLM plus gros sur VQA

Pour un robot : flux continu d'embeddings sémantiques. Décodeur texte invoqué seulement si besoin. Trade-off : moins de flexibilité générative ouverte, gain en vitesse et stabilité.

Le blueprint : MMLM action-natif

On a déjà vu ce film :

  • 2023 : séparé (LLM écrit des prompts DALL-E)
  • 2024 : collé (LLM orchestre diffusion, mais distincts)
  • 2025 : fusionné (GPT-4o génère images nativement)
  • 2026-2027 (mon pari) : action-natif. Le modèle qui chat et génère des images produit aussi des commandes moteur quand embodied.
┌────────────────────────────────────────┐
│      MMLM Action-Natif                 │
│  (texte, image, vidéo, actions)        │
└────────────┬───────────────────────────┘
             ├─ texte → texte (conversation)
             ├─ texte+image → texte (VQA)
             ├─ texte+image → image (édition)
             ├─ texte+image → texte+action (raison puis agit)
             └─ vidéo+texte → action (contrôle robot)

Données : simulation, vidéo sans actions, cross-embodiment

Le gap d'échelle : trillions de tokens texte, milliards d'images, milliers d'heures d'actions robot (×1M moins).

Trois approches :

  1. Données synthétiques : NVIDIA Isaac GR00T Blueprint génère centaines de milliers de trajectoires à partir d'un petit seed de démos humaines. +40 % de perf en combinant synthétique et réel.
  2. Vidéo sans actions : FLARE, UWM, V-JEPA 2 apprennent de YouTube, films, vidéo égocentrique humaine.
  3. Transfert cross-embodiment : Open X-Embodiment agrège données de robots différents. Embodiment tokens disent au modèle quel robot il contrôle.

Runtime safety layer : l'interface manquante

Le problème d'interface : quand un MMLM action-natif produit 50 commandes/s vers un robot réel, on a besoin de :

  • Fusion de capteurs (caméras, proprio)
  • Monitoring de sécurité temps réel
  • Enforcement de contraintes (limites joints, évitement collisions, caps de force)
  • Dégradation gracieuse
  • Logs et audit
  • Gestion des tools/skills disponibles
  • Mémoire des interactions passées

Ce n'est pas un problème de modèle. C'est un problème de runtime/systèmes.

Exemple : le modèle génère une trajectoire parfaite — sauf que le coude clip le bord du comptoir à l'étape 17/50. Un runtime de sécurité n'évalue pas "l'intelligence", il enforce la physique.

Layer Fonction Pourquoi
Gate Bloquer actions dangereuses avant exécution Prévenir dégâts/blessures
Constrain Limites physiques continues Joint limits, collisions, forces
Verify Simuler l'action optionnellement Catcher erreurs en virtuel
Log Enregistrer chaque action/résultat Debug, responsabilité, amélioration
Fallback Procédures de récupération sûres Dégradation gracieuse

Doit être model-agnostic : le paysage est fragmenté (NVIDIA, Physical Intelligence, Meta, etc.). La couche de sécurité doit se placer entre n'importe quelle policy AI et n'importe quel hardware robot, comme les OS, Docker, ou les APIs cloud.

Ce que les autres ratent : c'est une question d'interfaces

Les gens débattent familles de modèles ("LLMs can't spatial reason", "world models are the future"). Les vraies questions sont d'interfaces :

Interface 1 : entre raisonnement et contrôle. Comment la planification langage se connecte à l'exécution moteur ? L'architecture à deux têtes est une réponse.

Interface 2 : entre policy et hardware. Comment garantir sécurité, auditabilité, agnosticisme matériel ? Le runtime safety layer.

Interface 3 : entre training et deployment. Comment combler le gap simulation→réel, multi-embodiment, action-free video → robot control ?

La convergence 2024-2025 n'est pas "quel modèle gagne". C'est : où placer la prédiction, comment séparer reasoning/control, et quelle couche système protège l'exécution.

The two-head pattern : reasoning + control

La plupart des VLA performants de 2024–2025 suivent la même architecture :

  • Reasoning head : autorégressif, génère du texte interprétable (chain-of-thought, planification)
  • Control head : flow matching, génère des trajectoires continues en parallèle, sortie 50 Hz

Les deux têtes partagent un backbone (VLM), mais se spécialisent :

Exigence Reasoning Control
Latence secondes OK <20 ms impératif
Format tokens discrets angles/forces continus
Interprétabilité critique accessoire
Sorties valides une réponse multiples trajectoires

Exemples :

  • Alpamayo-R1 : CoC language reasoning + flow matching trajectory decoder
  • GR00T N1.x : VLM Eagle-2 + DiT action expert
  • Pi0/Pi0.5 : PaliGemma 3B + 300M action expert (73 ms pour 50 actions @ 50 Hz)
  • WALL-OSS : diffusion control head pour éviter le goulot autorégressif

L'erreur RT-2 (2023) : forcer les actions dans des tokens discrets (256 bins/joint). Ça marche mal dès qu'on veut de la dextérité ou du réactif.

Runtime safety layer : l'interface manquante

Le passage à l'action-native MMLM ne demande pas seulement un meilleur modèle, mais une couche runtime entre modèle et moteurs :

Couche Rôle
Gate Bloquer les actions dangereuses avant exécution
Constrain Limites articulaires, évitement collision, seuil de force
Verify Simulation optionnelle pré-exécution
Log Traçabilité (debug, responsabilité)
Fallback Dégradation gracieuse en cas d'échec

Une erreur texte coûte une phrase. Une erreur moteur coûte du matériel ou blesse quelqu'un.

Ce runtime doit être model-agnostic : NVIDIA (GR00T, Alpamayo), Physical Intelligence (Pi0), Meta (V-JEPA) auront tous leurs modèles. La couche de sécurité doit s'intercaler entre n'importe quel policy et n'importe quel robot.

C'est le même pattern qu'un OS (apps ↔ hardware) ou Docker (apps ↔ infra).

Blueprint : action-native MMLM

La trajectoire suit celle de l'image :

  • 2023 : LLM et diffusion séparés (GPT écrit des prompts pour DALL-E)
  • 2024 : collés (DALL-E 3, Midjourney)
  • 2025 : mergés (GPT-4o génère nativement des images)
  • 2026–2027 : action-native (le modèle qui chat/génère des images sort aussi des commandes moteur quand il est embodié)
Input Output Usage
Texte Texte Conversation
Texte + Image Texte VQA
Texte + Image Image Édition
Texte + Image Texte + Action Raisonner puis agir
Vidéo + Texte Action Contrôle robot

Le modèle reste un MMLM. L'action devient une modalité de sortie first-class, au même titre que le texte ou l'image.

Données : trois leviers pour combler l'écart

Le texte se compte en trillions de tokens, l'image en milliards. Les actions robot ? Des milliers d'heures (×10⁶ moins).

  1. Synthétique : Isaac GR00T Blueprint génère des centaines de milliers de trajectoires par randomisation de domaine à partir d'un seed set de démos humaines. +40 % de perf en combinant synthétique + réel.

  2. Vidéo sans actions : apprendre de YouTube/vidéo égocentrique humaine

    • FLARE : prédiction de futurs latents → +26 % en sim, 37,5 % → 60 % de succès avec 1 démo + vidéo humaine
    • UWM : masquer les actions pendant l'entraînement
    • V-JEPA 2 : 62 h de vidéo robot DROID non labellisées → zero-shot control
  3. Cross-embodiment : Open X-Embodiment agrège des données de robots différents ; un token d'embodiment indique au modèle quel robot il contrôle.

Ce qui reste difficile : sécurité, évaluation, déploiement

  • Garanties temps réel : 50 Hz = 20 ms/décision, non négociable. Les VLM actuels tournent en centaines de ms. Flow matching aide, mais les garanties dures manquent.
  • Distribution shift : éclairage, objets, usure matérielle, humains imprévisibles.
  • Vérification : sim rapide mais reality gap ; tests réels lents/dangereux/coûteux ; vérification formelle intraitable.
  • Opacité des échecs : perception ? raisonnement ? exécution ? hardware ? Les modèles actuels ne séparent pas proprement ces modes.

Une mauvaise commande de couple casse un préhenseur. Une contrainte de collision ratée envoie un bras de 20 kg dans un poignet humain. C'est pourquoi la robotique avance moins vite que l'IA logicielle.

See also

pytorch keras-tensorflow docker bash