~/wiki

Cheatsheets — vue longue

retour à la liste

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

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

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