Cheatsheets — vue longue
retour à la listeToutes 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
curl
page dédiée →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 :
- Projection de sortie
- Couches du décodeur (self-attention + MLP)
- Cross-attention vers les tokens vision
- 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
- Renderer : Convertir texte/PDF long → images 2D (préserve layout)
- DeepEncoder : Compresser en K tokens vision (100-800 par page)
- 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
- Traitement local (détails fins)
- Compression agressive (goulot explicite)
- 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 :
- Projection de sortie
- Décodeur MoE (self-attention + MLP)
- Cross-attention vers tokens vision
- 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
Docker
page dédiée →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 dev(x, t), liés parv = -∇ 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
Gradio
page dédiée →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
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
LangChain
page dédiée →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
LangGraph
page dédiée →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.
Navigation et images
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
NumPy
page dédiée →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
pandas
page dédiée →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
Pydantic v2
page dédiée →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
pyenv
page dédiée →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 —
Settingsavecmodel_config = SettingsConfigDict(env_file=".env")
PyTorch
page dédiée →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
See also
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
Streamlit
page dédiée →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
Terraform
page dédiée →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 :
- Perception head : window attention locale (64×64 patches → 4,096 tokens, pas de réduction)
- Compression : convolution 4×4 stride 4 (4,096 → 256 tokens, 16× reduction)
- 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 :
- NVIDIA Newton rend Isaac Sim différentiable → USP absorbé
- Learned world models apprennent la physique depuis la vidéo → contournent le problème
- 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
-
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.
-
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.
-
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) :
- Encoder observation actuelle → z_now
- Encoder image goal → z_goal
- Imaginer plusieurs séquences d'actions → embeddings futurs prédits
- Choisir la séquence dont l'embedding final est le plus proche de z_goal
- 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 :
- 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.
- Vidéo sans actions : FLARE, UWM, V-JEPA 2 apprennent de YouTube, films, vidéo égocentrique humaine.
- 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).
-
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.
-
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
-
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.