Club D.I.A.M · Formation

Deep Learning

Quatre semaines pour aller de la couche d'entrée jusqu'au déploiement — chaque concept ancré dans un problème africain concret : cacao, maladies des plantes, prix des marchés du Sahel, qualité de l'eau, imagerie drone, NLP multilingue.

4 semaines Niveau intermédiaire Python · TF · PyTorch Contexte africain
la formation, vue comme un réseau — survolez une couche
Le parcours

Quatre couches, un réseau complet

La progression suit l'architecture d'un réseau : on pose les fondations (le neurone, les frameworks), on apprend à « voir » (CNN), à « lire » (NLP), puis on met le tout en production.

01
Semaine 1

Fondations du Deep Learning

On construit l'intuition de zéro : un neurone, puis un réseau entier, en NumPy pur — avant de passer aux frameworks industriels TensorFlow/Keras et PyTorch. On comprend la rétropropagation dans ses moindres détails.

J1

Du neurone biologique au neurone artificiel

analogie · combinaison linéaire · fonctions d'activation
▶ Ouvrir
L'analogie biologique
Neurone biologiqueNeurone artificiel
Dendrites (reçoivent les signaux)Inputs x₁, x₂, …, xₙ
Synapses (force de connexion)Poids w₁, w₂, …, wₙ
Corps cellulaire (intègre)z = Σ wᵢxᵢ + b
Seuil d'activationFonction f(z)
Axone (transmet le signal)Sortie a = f(z)
Les deux étapes clés
Calcul d'un neurone
Étape 1 — Combinaison linéaire z = w₁x₁ + w₂x₂ + … + wₙxₙ + b z = wᵀx + b (forme vectorielle) Étape 2 — Activation a = f(z) (non-linéarité)
w ∈ ℝⁿ = vecteur de poids · b ∈ ℝ = biais · x ∈ ℝⁿ = features
Contexte — Côte d'Ivoire Prédire si une récolte de cacao sera bonne à partir de la pluviométrie, la température, l'humidité et le type de sol → le neurone sort une probabilité entre 0 et 1.
Fonctions d'activation — Rôle et Choix
Pourquoi activer ? Sans f(z), un réseau de 100 couches reste une simple régression linéaire. Les activations introduisent la non-linéarité qui permet d'approximer n'importe quelle fonction.
Sigmoid
σ(z) = 1/(1+e⁻ᶻ)
sortie: [0,1]
ReLU
max(0, z)
sortie: [0,+∞[
Tanh
(eᶻ−e⁻ᶻ)/(eᶻ+e⁻ᶻ)
sortie: [−1,1]
Softmax
eᶻⁱ / Σeᶻʲ
classification multi
Leaky ReLU
max(0.01z, z)
évite dying ReLU
ActivationUtilisation recommandéeProblème connu
SigmoidSortie binaire (0/1)Vanishing gradient
TanhRNN, couches cachéesVanishing gradient
ReLUCouches cachées CNN/MLPDying ReLU (z<0)
Leaky ReLUAlternative à ReLURobuste
SoftmaxSortie multi-classes
Implémentation from scratch
neurone.py
import numpy as np

class Neurone:
    """Un seul neurone artificiel."""
    def __init__(self, n_inputs, activation='relu'):
        # He initialization (recommandé pour ReLU)
        self.W = np.random.randn(n_inputs) * np.sqrt(2 / n_inputs)
        self.b = 0.0
        self.act = activation

    def _activation(self, z):
        if self.act == 'sigmoid': return 1 / (1 + np.exp(-z))
        if self.act == 'relu':    return np.maximum(0, z)
        if self.act == 'tanh':    return np.tanh(z)
        return z  # linéaire

    def forward(self, x):
        z = np.dot(self.W, x) + self.b   # combinaison linéaire
        a = self._activation(z)           # activation
        return a, z

# Exemple : prédiction cacao (Côte d'Ivoire)
# x = [pluviométrie, température, humidité, type_sol]
x = np.array([0.72, 0.65, 0.80, 0.40])
neurone = Neurone(n_inputs=4, activation='sigmoid')
a, z = neurone.forward(x)
print(f"z = {z:.4f} | a = {a:.4f}")
print("Bonne récolte probable" if a > 0.5 else "Risque faible")
J2

Réseau de neurones (MLP) & Rétropropagation

architecture · forward pass · loss · backward · gradient descent
▶ Ouvrir
Architecture d'un MLP — Vue d'ensemble
Couche d'entrée Couches cachées Sortie x₁ ──┐ ┌── h₁₁ ──┐ x₂ ──┤ [W¹, b¹] ┤── h₁₂ ──┤ [W², b²] ┌── h₂₁ ──┐ [W³] ┌── ŷ₁ x₃ ──┤ ─────────► ┤── h₁₃ ──┤ ─────────► ┤── h₂₂ ──┤ ────► ┤── ŷ₂ x₄ ──┘ └── h₁₄ ──┘ └── h₂₃ ──┘ └── ŷ₃ n=4 features 4 neurones (ReLU) 3 neurones Softmax Couche 1 Couche 2
Les 4 étapes du forward pass
Forward pass — couche ℓ
1. Combinaison linéaire Z[ℓ] = W[ℓ] · A[ℓ⁻¹] + b[ℓ] 2. Activation A[ℓ] = f(Z[ℓ]) 3. Prédiction finale (L couches) ŷ = A[L] = softmax(Z[L])
W[ℓ] ∈ ℝ(nₗ × nₗ₋₁) · A[0] = X · f = fonction d'activation
Fonctions de perte
Loss functions
Binary Cross-Entropy (classification binaire) L = −[y · log(ŷ) + (1−y) · log(1−ŷ)] Categorical Cross-Entropy (multi-classes) L = −Σᵢ yᵢ · log(ŷᵢ) Mean Squared Error (régression) L = (1/m) · Σᵢ (yᵢ − ŷᵢ)² MAE — Mean Absolute Error L = (1/m) · Σᵢ |yᵢ − ŷᵢ|
Contexte — Afrique subsaharienne Classer 5 maladies de plantes (mildiou du manioc, rouille du maïs, anthracnose du manguier, pourriture du cacao, mouche blanche) : 5 sorties softmax. Loss = CCE.
Rétropropagation — Règle de la chaîne
Backpropagation — gradients
Règle de la chaîne ∂L/∂W[ℓ] = ∂L/∂A[ℓ] · ∂A[ℓ]/∂Z[ℓ] · ∂Z[ℓ]/∂W[ℓ] Gradient sur les poids dW[ℓ] = (1/m) · dZ[ℓ] · A[ℓ⁻¹]ᵀ Gradient sur les biais db[ℓ] = (1/m) · Σ dZ[ℓ] Gradient propagé à la couche précédente dA[ℓ⁻¹] = W[ℓ]ᵀ · dZ[ℓ] Mise à jour des poids (Gradient Descent) W[ℓ] ← W[ℓ] − η · dW[ℓ] b[ℓ] ← b[ℓ] − η · db[ℓ]
η = learning rate · m = taille du batch · dZ[ℓ] = dA[ℓ] * f'(Z[ℓ])
Implémentation d'une couche Dense
couche_dense.py — forward + backward
class CoucheDense:
    def __init__(self, n_in, n_out, activation='relu'):
        # He initialization
        self.W = np.random.randn(n_out, n_in) * np.sqrt(2/n_in)
        self.b = np.zeros((n_out, 1))
        self.act = activation

    def forward(self, A_prev):
        self.A_prev = A_prev                  # cache pour backward
        self.Z = self.W @ A_prev + self.b     # combinaison linéaire
        self.A = self._activate(self.Z)       # activation
        return self.A

    def _activate(self, Z):
        if self.act == 'relu':    return np.maximum(0, Z)
        if self.act == 'sigmoid': return 1 / (1 + np.exp(-Z))
        if self.act == 'softmax':
            e = np.exp(Z - Z.max(axis=0, keepdims=True))
            return e / e.sum(axis=0, keepdims=True)
        return Z

    def _activate_deriv(self, Z):
        if self.act == 'relu':    return (Z > 0).astype(float)
        if self.act == 'sigmoid':
            s = 1 / (1 + np.exp(-Z)); return s * (1 - s)
        return np.ones_like(Z)  # linéaire

    def backward(self, dA, lr=1e-3):
        m   = self.A_prev.shape[1]
        dZ  = dA * self._activate_deriv(self.Z)  # ∂L/∂Z
        dW  = (dZ @ self.A_prev.T) / m            # ∂L/∂W
        db  = dZ.sum(axis=1, keepdims=True) / m   # ∂L/∂b
        dA_prev = self.W.T @ dZ                   # gradient propagé
        self.W -= lr * dW                          # mise à jour
        self.b -= lr * db
        return dA_prev
Optimiseurs — Au-delà du Gradient Descent de base
Comparaison des optimiseurs
SGD (Stochastic Gradient Descent) W ← W − η · ∇L(W) → simple mais lent, oscille SGD + Momentum v ← β · v + (1−β) · ∇L(W) W ← W − η · v → accumule la direction (β ≈ 0.9) Adam (recommandé par défaut) m ← β₁·m + (1−β₁)·∇L → 1ᵉʳ moment (moyenne) v ← β₂·v + (1−β₂)·(∇L)² → 2ᵉ moment (variance) m̂ = m / (1−β₁ᵗ) → correction du biais v̂ = v / (1−β₂ᵗ) W ← W − η · m̂ / (√v̂ + ε) → β₁=0.9, β₂=0.999, ε=1e-8
mlp_maladies.py — MLP complet PyTorch
import torch, torch.nn as nn, torch.optim as optim

class MLP(nn.Module):
    """MLP pour classification de maladies de plantes (5 classes)."""
    def __init__(self, n_in, couches=[128, 64, 32], n_classes=5):
        super().__init__()
        blocs, n = [], n_in
        for n_out in couches:
            blocs += [
                nn.Linear(n, n_out),
                nn.BatchNorm1d(n_out),   # stabilise l'entraînement
                nn.ReLU(inplace=True),
                nn.Dropout(0.3),         # régularisation
            ]
            n = n_out
        blocs.append(nn.Linear(n, n_classes))
        self.net = nn.Sequential(*blocs)

    def forward(self, x): return self.net(x)

# Entraînement
model     = MLP(n_in=20, n_classes=5)
optimizer = optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-4)
scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.5)
criterion = nn.CrossEntropyLoss()

for epoch in range(50):
    model.train()
    for X_batch, y_batch in train_loader:
        optimizer.zero_grad()
        loss = criterion(model(X_batch), y_batch)
        loss.backward()
        torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)  # gradient clipping
        optimizer.step()
    scheduler.step()
J3

TensorFlow & Keras — Production Ready

Sequential · Functional · callbacks · regularisation · tuning
▶ Ouvrir
Philosophie Keras — 4 étapes
Définir
Compiler
Entraîner
Évaluer
Régularisations disponibles
TechniqueRôleQuand
Dropout(p)Éteint p% des neuronesSurajustement
L2 (weight decay)Pénalise les grands poidsDonnées limitées
BatchNormNormalise les activationsRéseaux profonds
EarlyStoppingArrête si pas d'améliorationToujours
DataAugmentationAugmente les donnéesImages, textes
Contexte — Sahel Prédire le prix du mil sur les marchés (Sénégal/Mali) à partir des autres céréales, de la pluviométrie et de la saison de soudure. TF Lite permet l'inférence hors-ligne sur smartphone.
Modèle complet avec callbacks
keras_prix_mil.py
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers, regularizers

# ── Architecture ─────────────────────────────────────
def construire_modele(n_features, lr=1e-3):
    inputs = keras.Input(shape=(n_features,), name='features')
    x = layers.Dense(256, activation='relu',
                     kernel_regularizer=regularizers.l2(1e-4))(inputs)
    x = layers.BatchNormalization()(x)
    x = layers.Dropout(0.4)(x)
    x = layers.Dense(128, activation='relu')(x)
    x = layers.BatchNormalization()(x)
    x = layers.Dropout(0.3)(x)
    x = layers.Dense(64, activation='relu')(x)
    output = layers.Dense(1, name='prix_predit')(x)

    model = keras.Model(inputs, output)
    model.compile(
        optimizer=keras.optimizers.Adam(lr),
        loss='mse',
        metrics=['mae', 'mape']
    )
    return model

model = construire_modele(n_features=12)
model.summary()

# ── Callbacks professionnels ──────────────────────────
callbacks = [
    keras.callbacks.EarlyStopping(
        monitor='val_loss', patience=15,
        restore_best_weights=True, verbose=1
    ),
    keras.callbacks.ReduceLROnPlateau(
        monitor='val_loss', factor=0.3,
        patience=7, min_lr=1e-6, verbose=1
    ),
    keras.callbacks.ModelCheckpoint(
        'meilleur_modele.keras',
        monitor='val_loss', save_best_only=True
    ),
    keras.callbacks.TensorBoard(
        log_dir='./logs', histogram_freq=1
    ),
]

history = model.fit(
    X_train, y_train,
    validation_data=(X_val, y_val),
    epochs=200, batch_size=32,
    callbacks=callbacks
)

# ── Sauvegarde TFLite (mobile) ────────────────────────
converter = tf.lite.TFLiteConverter.from_keras_model(model)
tflite_model = converter.convert()
with open('prix_mil.tflite', 'wb') as f:
    f.write(tflite_model)
API Fonctionnelle — Réseaux complexes (connexions résiduelles)
resnet_like.py — skip connections avec Keras Functional
def bloc_residuel(x, n_units, dropout=0.3):
    """Bloc ResNet adapté aux données tabulaires."""
    residual = x
    x = layers.Dense(n_units, activation='relu')(x)
    x = layers.BatchNormalization()(x)
    x = layers.Dropout(dropout)(x)
    x = layers.Dense(n_units)(x)
    x = layers.BatchNormalization()(x)
    # Skip connection : ajoute l'entrée à la sortie
    if residual.shape[-1] != n_units:
        residual = layers.Dense(n_units)(residual)
    x = layers.Add()([x, residual])  # ← connexion résiduelle
    return layers.Activation('relu')(x)

# Construction
inp = keras.Input(shape=(n_features,))
x   = layers.Dense(128, activation='relu')(inp)
x   = bloc_residuel(x, 128)
x   = bloc_residuel(x, 64)
out = layers.Dense(n_classes, activation='softmax')(x)
model = keras.Model(inp, out)
J4

PyTorch — Autograd, nn.Module et boucle d'entraînement

tenseurs · autograd · Dataset · DataLoader · training loop
▶ Ouvrir
TensorFlow vs PyTorch — Comparaison
CritèreTF / KerasPyTorch
ParadigmeDéclaratif (define-and-run)Impératif (define-by-run)
DébogagePlus complexeTrès pythonique
RechercheProductionStandard académique
AutogradGradientTapeAutomatique (.backward)
MobileTF Lite ✓TorchScript / ONNX
CommunautéGoogle / enterpriseMeta / recherche
Autograd — Différentiation automatique
Comment fonctionne autograd
PyTorch construit un graphe de calcul dynamique. Chaque opération est enregistrée dans un nœud. .backward() parcourt le graphe en sens inverse et calcule les gradients via la règle de la chaîne. Exemple : f(x) = x² + 3x x.grad = df/dx = 2x + 3
autograd.py
import torch

# Tenseur avec suivi du gradient
x = torch.tensor([2.0, 3.0], requires_grad=True)
y = x**2 + 3*x            # f(x) = x² + 3x
z = y.sum()               # scalaire pour backward
z.backward()              # calcule les gradients
print(x.grad)             # tensor([7., 9.]) = 2x+3

# Arrêter le suivi (inférence)
with torch.no_grad():
    pred = model(X_test)
Contexte — OMS / Afrique Classer la qualité de l'eau potable (pH, turbidité, coliformes, nitrates, fluorure, conductivité, chlore, température) en potable / non potable — déployé sur tablette dans les villages.
Dataset, DataLoader et Training Loop complet
eau_qualite_pytorch.py
import torch, torch.nn as nn
from torch.utils.data import Dataset, DataLoader
import numpy as np

# ── Dataset personnalisé ─────────────────────────────
class EauDataset(Dataset):
    def __init__(self, X, y):
        self.X = torch.FloatTensor(X)
        self.y = torch.LongTensor(y)
    def __len__(self):  return len(self.y)
    def __getitem__(self, i): return self.X[i], self.y[i]

train_ds = EauDataset(X_train, y_train)
val_ds   = EauDataset(X_val,   y_val)
train_dl = DataLoader(train_ds, batch_size=64, shuffle=True,
                      num_workers=2, pin_memory=True)
val_dl   = DataLoader(val_ds,  batch_size=128)

# ── Modèle nn.Module ─────────────────────────────────
class EauNet(nn.Module):
    def __init__(self, n_in, couches=[128,64,32], n_out=2):
        super().__init__()
        blocs, n = [], n_in
        for h in couches:
            blocs += [nn.Linear(n, h), nn.BatchNorm1d(h),
                      nn.ReLU(inplace=True), nn.Dropout(0.3)]
            n = h
        blocs.append(nn.Linear(n, n_out))
        self.net = nn.Sequential(*blocs)
    def forward(self, x): return self.net(x)

device = 'cuda' if torch.cuda.is_available() else 'cpu'
model  = EauNet(n_in=8).to(device)

# ── Boucle d'entraînement complète ───────────────────
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-3,
                               weight_decay=1e-4)
scheduler = torch.optim.lr_scheduler.OneCycleLR(
    optimizer, max_lr=1e-2,
    steps_per_epoch=len(train_dl), epochs=50)
criterion = nn.CrossEntropyLoss()

best_val_acc, best_state = 0, None
for epoch in range(50):
    # ─ train ─
    model.train()
    for X_b, y_b in train_dl:
        X_b, y_b = X_b.to(device), y_b.to(device)
        optimizer.zero_grad()
        loss = criterion(model(X_b), y_b)
        loss.backward()
        nn.utils.clip_grad_norm_(model.parameters(), 1.0)
        optimizer.step()
        scheduler.step()
    # ─ validation ─
    model.eval()
    correct = 0
    with torch.no_grad():
        for X_b, y_b in val_dl:
            X_b, y_b = X_b.to(device), y_b.to(device)
            correct += (model(X_b).argmax(1)==y_b).sum().item()
    val_acc = correct / len(val_ds)
    if val_acc > best_val_acc:
        best_val_acc = val_acc
        best_state   = {k:v.clone() for k,v in model.state_dict().items()}
    if (epoch+1) % 10 == 0:
        print(f"Epoch {epoch+1:3d} | Val Acc: {val_acc:.3f}")

model.load_state_dict(best_state)  # restaurer le meilleur modèle
print(f"Meilleure val acc : {best_val_acc:.3f}")
02
Semaine 2

Vision par Ordinateur — CNN

Les images ont une structure spatiale qu'un MLP ignore complètement. Les CNN l'exploitent grâce au partage de poids et à la détection de features hiérarchiques. Puis le transfert d'apprentissage permet d'obtenir d'excellents résultats avec peu de données et peu de GPU.

CNN

Fondamentaux des réseaux convolutifs

convolution · stride · padding · pooling · feature maps
▶ Ouvrir
Pourquoi pas un MLP pour les images ?
Le problème du MLP sur images Une image 224×224×3 = 150 528 paramètres d'entrée. Une seule couche cachée de 1 000 neurones = 150 millions de poids → ingérable, pas de prise en compte de la structure spatiale.
L'opération de Convolution
Convolution 2D
Définition (I * K)[i,j] = Σₘ Σₙ I[i+m, j+n] · K[m, n] Taille de la feature map résultante H_out = ⌊(H_in + 2·pad − kernel) / stride⌋ + 1 W_out = ⌊(W_in + 2·pad − kernel) / stride⌋ + 1 Exemple : image 28×28, kernel 3×3, pad=1, stride=1 H_out = ⌊(28 + 2·1 − 3) / 1⌋ + 1 = 28
I = image d'entrée · K = filtre/kernel · pad = zero-padding · stride = pas de déplacement
Schéma : Un filtre convolutif
Image d'entrée (5×5) Filtre (3×3) Feature Map (3×3) ┌─────────────────┐ ┌─────────┐ ┌───────────┐ │ 1 2 3 0 1 │ │ 1 0 1 │ │ 4 3 4 │ │ 0 1 2 1 0 │ * │ 0 1 0 │ = │ 2 4 3 │ │ 1 0 1 2 1 │ │ 1 0 1 │ │ 2 3 4 │ │ 2 1 0 1 2 │ └─────────┘ └───────────┘ │ 1 0 1 0 1 │ └─────────────────┘ Partage de poids : le même filtre est appliqué en tous points → translation invariance
Types de Pooling
TypeOpérationUsage
MaxPoolMaximum du voisinageLe plus courant (détection)
AvgPoolMoyenne du voisinageLissage, GAP en sortie
GlobalAvgPoolMoyenne sur H×W entierRemplacement de Flatten
AdaptivePoolTaille de sortie fixéeInputs de taille variable
Architecture CNN type — Hiérarchie des features
Ce qu'apprend chaque couche Couche 1 : bords, coins, gradients simples │ (ex: filtre de Sobel horizontal) Couche 2 : textures, motifs, courbes │ (ex: œil, oreille partielle) Couche 3 : parties d'objets complexes │ (ex: roue, feuille, fleur) Couche 4+ : objets entiers, combinaisons │ (ex: voiture, feuille malade, visage) FC + Softmax : classification finale
CNN Complet pour classification de plantes
plant_cnn.py — 3 blocs convolutifs
import torch.nn as nn

class PlanteCNN(nn.Module):
    """CNN pour classification de 5 maladies de plantes."""
    def __init__(self, n_classes=5, img_size=64):
        super().__init__()

        # ── Blocs convolutifs (feature extraction) ────────
        self.features = nn.Sequential(
            # Bloc 1 : 3 → 32 filtres | 64×64 → 32×32
            nn.Conv2d(3, 32, 3, padding=1), nn.BatchNorm2d(32),
            nn.ReLU(inplace=True),
            nn.Conv2d(32, 32, 3, padding=1), nn.BatchNorm2d(32),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(2, 2), nn.Dropout2d(0.25),

            # Bloc 2 : 32 → 64 filtres | 32×32 → 16×16
            nn.Conv2d(32, 64, 3, padding=1), nn.BatchNorm2d(64),
            nn.ReLU(inplace=True),
            nn.Conv2d(64, 64, 3, padding=1), nn.BatchNorm2d(64),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(2, 2), nn.Dropout2d(0.25),

            # Bloc 3 : 64 → 128 filtres | 16×16 → 8×8
            nn.Conv2d(64, 128, 3, padding=1), nn.BatchNorm2d(128),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(2, 2), nn.Dropout2d(0.25),
        )

        # ── Tête de classification ────────────────────────
        self.classifier = nn.Sequential(
            nn.AdaptiveAvgPool2d(1),  # Global Average Pooling
            nn.Flatten(),
            nn.Linear(128, 256), nn.ReLU(), nn.Dropout(0.5),
            nn.Linear(256, n_classes)
        )

    def forward(self, x):
        return self.classifier(self.features(x))

# Vérification des dimensions
model  = PlanteCNN(n_classes=5)
sample = torch.randn(8, 3, 64, 64)   # batch de 8 images
output = model(sample)
print(f"Sortie : {output.shape}")     # [8, 5]
Data Augmentation — Essentielle avec peu de données
augmentation.py — transforms PyTorch
from torchvision import transforms

# Transformations d'entraînement (augmentation agressive)
train_transform = transforms.Compose([
    transforms.RandomResizedCrop(64, scale=(0.7, 1.0)),
    transforms.RandomHorizontalFlip(p=0.5),
    transforms.RandomVerticalFlip(p=0.2),
    transforms.RandomRotation(degrees=30),
    transforms.ColorJitter(brightness=0.3, contrast=0.3,
                           saturation=0.2, hue=0.1),
    transforms.RandomGrayscale(p=0.1),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406],   # stats ImageNet
                         std=[0.229, 0.224, 0.225])
])

# Transformations de validation (seulement normalisation)
val_transform = transforms.Compose([
    transforms.Resize(72),
    transforms.CenterCrop(64),
    transforms.ToTensor(),
    transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
])
Architectures CNN majeures — Évolution historique
ModèleAnnéeParamètresTop-1 ImageNetUtilisation
LeNet-5199860KChiffres manuscrits, référence pédagogique
AlexNet201261M63.3%Première révolution CNN + GPU
VGGNet-162014138M74.4%Feature extraction simple et robuste
ResNet-50201525M80.4%Skip connections — réseau très profond
EfficientNet-B020195.3M84.0%Meilleur ratio perf/taille — Mobile
MobileNetV320195.4M75.2%Smartphones, edge computing
ViT-Base202086M85.5%Transformers pour vision — état de l'art
TL

Transfer Learning — Apprendre avec peu de données

feature extraction · fine-tuning · mobilenet · grad-cam
▶ Ouvrir
Principe du Transfer Learning
Deux stratégies principales
Feature Extraction ┌────────────────────────────┐ GELÉ │ Backbone (ResNet pré-en.) │ (poids figés) └────────────────────────────┘ ↓ ┌────────────────────────────┐ ENTRAÎNÉ │ Nouvelle tête (MLP) │ (poids libres) └────────────────────────────┘ Fine-Tuning (partiel ou complet) ┌─────────────────────┐ GELÉ (~70%) │ Premières couches │ (features bas niveau) ├─────────────────────┤ ENTRAÎNÉ (~30%) │ Dernières couches │ (features haut niveau) ├─────────────────────┤ ENTRAÎNÉ │ Nouvelle tête │ (petite lr : 1e-5) └─────────────────────┘
Règle : <1000 images → extraction. 1000-10000 → fine-tuning partiel. >10000 → fine-tuning complet.
StratégieDonnéesGPURecommandé
Extraction<500CPU ok✓ Débuter
Fine-tune partiel500–5K1 GPU✓ Standard
Fine-tune complet>10KMulti-GPUAvancé
Contexte — Drone rural africain Classer des cultures vues par drone : manioc, maïs, coton, sorgho, arachide. 200 images par classe → feature extraction avec MobileNetV3 (5 MB, tourne sur smartphone).
Pipeline Transfer Learning complet
transfer_drone.py — ResNet + Fine-tuning
import torch, torch.nn as nn
from torchvision import models
from torchvision.models import ResNet50_Weights

CLASSES    = ['manioc','maïs','coton','sorgho','arachide']
N_CLASSES  = len(CLASSES)

# ── 1. Charger le backbone pré-entraîné ──────────────
backbone = models.resnet50(weights=ResNet50_Weights.IMAGENET1K_V2)

# ── 2. Geler toutes les couches ───────────────────────
for p in backbone.parameters():
    p.requires_grad = False

# ── 3. Remplacer la tête (fc) ─────────────────────────
n_features    = backbone.fc.in_features  # 2048 pour ResNet50
backbone.fc   = nn.Sequential(
    nn.Linear(n_features, 512),
    nn.BatchNorm1d(512),
    nn.ReLU(inplace=True),
    nn.Dropout(0.5),
    nn.Linear(512, 256),
    nn.ReLU(inplace=True),
    nn.Dropout(0.3),
    nn.Linear(256, N_CLASSES)
)

# ── 4. Phase 1 : Feature Extraction (5 epochs) ────────
optimizer_ft = torch.optim.Adam(
    backbone.fc.parameters(), lr=1e-3)

# ── 5. Phase 2 : Dégeler les 2 derniers blocs ─────────
for name, p in backbone.named_parameters():
    if 'layer4' in name or 'layer3' in name:
        p.requires_grad = True

optimizer_full = torch.optim.Adam([
    {'params': backbone.fc.parameters(),    'lr': 1e-3},
    {'params': backbone.layer4.parameters(),'lr': 1e-5},
    {'params': backbone.layer3.parameters(),'lr': 1e-6},
])
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(
    optimizer_full, T_max=20)

# ── 6. Grad-CAM — Visualiser ce que regarde le modèle ─
def grad_cam(model, img_tensor, target_layer):
    """Carte de chaleur sur la feature map de target_layer."""
    gradients, activations = [], []
    target_layer.register_forward_hook(
        lambda m,i,o: activations.append(o))
    target_layer.register_full_backward_hook(
        lambda m,gi,go: gradients.append(go[0]))
    out  = model(img_tensor.unsqueeze(0))
    pred = out.argmax(1)
    model.zero_grad()
    out[0, pred].backward()
    grad = gradients[0].mean(dim=[2,3], keepdim=True)
    cam  = (grad * activations[0]).sum(1).squeeze()
    cam  = torch.relu(cam)
    return (cam / cam.max()).detach().numpy()
03
Semaine 3

Traitement du Langage Naturel — NLP Complet

Du sac-de-mots aux Transformers, en passant par les embeddings, LSTM et l'attention — avec une vraie spécificité africaine : code-switching, langues locales (wolof, lingala, hausa, bambara…) et peu de données labellisées.

PRÉ

Prétraitement du texte — Pipeline NLP

tokenisation · stopwords · stemming · lemmatisation · TF-IDF · bag-of-words
▶ Ouvrir
Pipeline de prétraitement — Vue d'ensemble
Texte brut
Nettoyage
Tokenisation
Stop words
Lemmatisation
Vectorisation
1. Tokenisation

Découper le texte en unités (tokens) : mots, sous-mots ou caractères.

TypeExempleModèle
Mots"je mange" → ["je","mange"]NLTK, spaCy
Sous-mots (BPE)"manger" → ["mang","er"]BERT, GPT
Caractères"chat" → ["c","h","a","t"]Rare
PhrasesDécoupe par ponctuationspaCy
2. Suppression des Stop Words
Définition Les stop words sont des mots très fréquents portant peu de sens : "le", "la", "de", "et", "est", "que"… Les supprimer réduit le bruit et la dimensionnalité.
stopwords.py
import nltk
              from nltk.corpus import stopwords
              from nltk.tokenize import word_tokenize
              import re, string

              nltk.download('punkt')
              nltk.download('stopwords')
              nltk.download('wordnet')

              STOP_FR = set(stopwords.words('french'))
              # Stop words africains supplémentaires (wolof mêlé de français)
              STOP_EXTRA = {'wakh', 'xam', 'bi', 'yi', 'la', 'nga', 'ci'}
              STOP_FR.update(STOP_EXTRA)

              def nettoyer_texte(texte):
                  """Pipeline de nettoyage complet."""
                  # 1. Minuscules
                  texte = texte.lower()
                  # 2. Supprimer URLs, mentions, hashtags
                  texte = re.sub(r'http\S+|www\S+', '', texte)
                  texte = re.sub(r'@\w+|#\w+', '', texte)
                  # 3. Supprimer ponctuation et chiffres
                  texte = re.sub(r'[^\w\s]', ' ', texte)
                  texte = re.sub(r'\d+', '', texte)
                  # 4. Tokeniser
                  tokens = word_tokenize(texte, language='french')
                  # 5. Supprimer stop words + tokens courts
                  tokens = [t for t in tokens
                            if t not in STOP_FR and len(t) > 2]
                  return tokens

              # Exemple
              avis = "C'est vraiment bon wakh ! La qualité du mil est très bonne cette année."
              tokens = nettoyer_texte(avis)
              print(tokens)
              # → ['vraiment', 'bon', 'qualité', 'mil', 'bonne', 'année']
3. Stemming vs Lemmatisation
TechniquePrincipe"mangeons", "mangé", "mangeait"
StemmingTroncature brute (règles)→ "mang" (radicalement)
LemmatisationForme canonique (lexique)→ "manger" (correct)
RecommandationPréférer la lemmatisation (spaCy, TreeTagger) plus précise. Le stemming (SnowballStemmer) est plus rapide mais bruyant.
4. TF-IDF — Term Frequency × Inverse Document Frequency
TF-IDF — Formule complète
TF — Term Frequency (fréquence locale) TF(t, d) = nombre d'occurrences de t dans d ──────────────────────────────── nombre total de mots dans d IDF — Inverse Document Frequency (rareté) IDF(t, D) = log( |D| / (1 + |{d ∈ D : t ∈ d}|) ) TF-IDF (importance globale) TF-IDF(t, d, D) = TF(t, d) × IDF(t, D) Interprétation Élevé → mot fréquent dans ce doc, rare ailleurs Faible → mot fréquent partout (stop word) OU rare dans ce doc
|D| = nombre de documents dans le corpus · t = terme · d = document
5. Bag-of-Words vs TF-IDF vs N-grams
vectorisation.py — sklearn
from sklearn.feature_extraction.text import (
                  CountVectorizer, TfidfVectorizer)
              from nltk.stem import SnowballStemmer
              from nltk.stem.cistem import Cistem
              import spacy

              # ── Lemmatisation avec spaCy (français) ──────────────
              nlp = spacy.load('fr_core_news_sm')

              def lemmatiser(texte):
                  doc = nlp(texte.lower())
                  return ' '.join([token.lemma_ for token in doc
                                   if not token.is_stop and not token.is_punct
                                   and len(token.text) > 2])

              corpus = [
                  "Le mil est cher sur le marché de Dakar",
                  "La qualité du cacao de Côte d'Ivoire est excellente",
                  "Les pluies tardives menacent la récolte de sorgho",
              ]
              corpus_lem = [lemmatiser(t) for t in corpus]

              # ── Bag-of-Words ──────────────────────────────────────
              bow = CountVectorizer(max_features=500)
              X_bow = bow.fit_transform(corpus_lem).toarray()

              # ── TF-IDF (1-grammes + 2-grammes) ────────────────────
              tfidf = TfidfVectorizer(
                  ngram_range=(1, 2),    # unigrammes + bigrammes
                  max_features=1000,
                  min_df=1,              # minimum 1 doc
                  sublinear_tf=True,     # log(TF+1) pour réduire l'impact
                  norm='l2'              # normalisation L2 de chaque document
              )
              X_tfidf = tfidf.fit_transform(corpus_lem).toarray()
              print(f"TF-IDF shape : {X_tfidf.shape}")

              # ── Top features ──────────────────────────────────────
              vocab = tfidf.get_feature_names_out()
              scores = X_tfidf.mean(axis=0)
              top5 = sorted(zip(vocab, scores), key=lambda x: -x[1])[:5]
              print("Top features:", top5)
6. Similarité cosinus — Trouver des textes proches
Similarité cosinus entre documents
Définition cos(A, B) = (A · B) / (‖A‖ · ‖B‖) = Σᵢ AᵢBᵢ / √(Σᵢ Aᵢ²) · √(Σᵢ Bᵢ²) Interprétation cos = 1 → documents identiques cos = 0 → documents orthogonaux (rien en commun) cos = -1 → documents opposés (rare en NLP)
similarite.py
from sklearn.metrics.pairwise import cosine_similarity
                import numpy as np

                # Matrice de similarité entre tous les documents
                sim_matrix = cosine_similarity(X_tfidf)
                print("Matrice similarité :")
                print(np.round(sim_matrix, 3))

                # Trouver le document le plus proche d'une requête
                requete = "prix élevé marché sorgho"
                q_vec   = tfidf.transform([lemmatiser(requete)])
                scores  = cosine_similarity(q_vec, X_tfidf)[0]
                plus_proche = np.argmax(scores)
                print(f"\nRequête : '{requete}'")
                print(f"Document le plus proche : '{corpus[plus_proche]}'")
                print(f"Score : {scores[plus_proche]:.3f}")
EMB

Word Embeddings — Représentations denses

Word2Vec · GloVe · FastText · embeddings contextuels
▶ Ouvrir
Limites du TF-IDF → Besoin des embeddings
Problème de TF-IDF "mil" et "céréale" sont traités comme totalement différents par TF-IDF alors qu'ils sont sémantiquement proches. Les embeddings placent les mots similaires proches dans l'espace vectoriel.
Word2Vec — Deux architectures
CBOW vs Skip-Gram
CBOW — Prédire le mot central à partir du contexte Contexte : ["Le", "est", "cher"] → Prédit : "mil" Entrée : moyenne des vecteurs du contexte Sortie : softmax sur le vocabulaire Skip-Gram — Prédire le contexte à partir du mot Entrée : "mil" → Prédit : ["Le","est","cher"] Plus lent mais meilleur sur mots rares Propriétés algébriques fascinantes roi - homme + femme ≈ reine Paris - France + Sénégal ≈ Dakar
FastText — Gestion des mots inconnus (OOV)
Avantage crucial pour les langues africaines FastText décompose les mots en n-grams de caractères. "mangé", "mangeant", "mangeons" partagent le n-gram "mang". Les mots inconnus sont représentés par leurs n-grams → parfait pour le wolof, le bambara, les langues à morphologie riche.
Entraîner Word2Vec sur corpus agricole africain
word2vec_agri.py — gensim
from gensim.models import Word2Vec, FastText
import numpy as np

# Corpus agricole africain (simplifié)
corpus = [
    ["mil", "cher", "marché", "dakar", "sénégal"],
    ["cacao", "côte", "ivoire", "qualité", "bonne"],
    ["manioc", "récolte", "maladie", "feuille", "rouille"],
    ["sorgho", "pluie", "sahel", "burkina", "sécheresse"],
    ["maïs", "fertilisant", "rendement", "ghana", "cultivateur"],
]

# ── Word2Vec Skip-Gram ────────────────────────────────
w2v = Word2Vec(
    sentences=corpus,
    vector_size=100,    # dimension des embeddings
    window=5,           # taille du contexte
    min_count=1,        # fréquence minimum
    sg=1,               # 1=Skip-Gram, 0=CBOW
    epochs=50,
    workers=4,
    seed=42
)

# Mots similaires
similaires = w2v.wv.most_similar('cacao', topn=3)
print("Mots proches de 'cacao' :", similaires)

# Opérations vectorielles
v = w2v.wv['sorgho'] - w2v.wv['sahel'] + w2v.wv['côte']
res = w2v.wv.similar_by_vector(v, topn=1)
print("sorgho - sahel + côte =", res)

# ── FastText (mots inconnus) ──────────────────────────
ft = FastText(sentences=corpus, vector_size=100,
              window=5, min_count=1, epochs=50)
# Même les mots non vus dans le corpus !
v_inconnu = ft.wv["agriculteurs"]  # OOV → géré via n-grams
print(f"Vecteur OOV shape : {v_inconnu.shape}")

# ── Matrice d'embeddings pour PyTorch ─────────────────
def texte_vers_vecteur(tokens, model, dim=100):
    """Moyenne des vecteurs des tokens."""
    vecs = [model.wv[t] for t in tokens if t in model.wv]
    return np.mean(vecs, axis=0) if vecs else np.zeros(dim)
Couche Embedding dans PyTorch/Keras
embedding_layer.py
import torch.nn as nn
import numpy as np

# Charger les poids pré-entraînés (Word2Vec/FastText)
def charger_embedding_matrix(vocab, w2v_model, embed_dim=100):
    matrix = np.zeros((len(vocab)+1, embed_dim))
    for mot, idx in vocab.items():
        if mot in w2v_model.wv:
            matrix[idx] = w2v_model.wv[mot]
        else:
            matrix[idx] = np.random.normal(0, 0.01, embed_dim)
    return matrix

# Intégrer dans un modèle PyTorch
class TexteModel(nn.Module):
    def __init__(self, vocab_size, embed_dim,
                 pretrained_weights=None):
        super().__init__()
        self.embedding = nn.Embedding(
            vocab_size, embed_dim, padding_idx=0)
        if pretrained_weights is not None:
            # Initialiser avec Word2Vec
            self.embedding.weight = nn.Parameter(
                torch.FloatTensor(pretrained_weights))
            self.embedding.weight.requires_grad = True  # fine-tunable

    def forward(self, x):
        return self.embedding(x)  # [batch, seq_len, embed_dim]
SEQ

RNN, LSTM, GRU & Mécanisme d'Attention

vanishing gradient · LSTM gates · BiLSTM · self-attention
▶ Ouvrir
Problème du RNN simple — Vanishing Gradient
Vanishing Gradient Dans un RNN, le gradient est multiplié à chaque pas temporel par les poids W. Si |W|<1, il disparaît exponentiellement. Le réseau oublie les dépendances lointaines → LSTM résout ce problème.
Les 4 portes du LSTM
LSTM — Long Short-Term Memory
1. Forget Gate — Que faut-il oublier ? f_t = σ( W_f · [h_{t-1}, x_t] + b_f ) 2. Input Gate — Quoi mémoriser ? i_t = σ( W_i · [h_{t-1}, x_t] + b_i ) c̃_t = tanh( W_c · [h_{t-1}, x_t] + b_c ) 3. Cell State — Mise à jour de la mémoire c_t = f_t ⊙ c_{t-1} + i_t ⊙ c̃_t 4. Output Gate — Quoi transmettre ? o_t = σ( W_o · [h_{t-1}, x_t] + b_o ) h_t = o_t ⊙ tanh(c_t)
σ = sigmoid · tanh = hyperbolic tangent · ⊙ = produit élément par élément
GRU — Version simplifiée du LSTM
GRU — Gated Recurrent Unit
Reset Gate r_t = σ( W_r · [h_{t-1}, x_t] ) Update Gate z_t = σ( W_z · [h_{t-1}, x_t] ) Candidate hidden state h̃_t = tanh( W · [r_t ⊙ h_{t-1}, x_t] ) Final hidden state h_t = (1 − z_t) ⊙ h_{t-1} + z_t ⊙ h̃_t
GRU = 2 portes vs 3 pour LSTM → 25% moins de paramètres. Souvent aussi bon en pratique.
Mécanisme d'Attention
Attention de Bahdanau (2015)
Score d'alignement e_{t,s} = vᵀ · tanh( W_a · h_s + U_a · s_t ) Poids d'attention (softmax) α_{t,s} = softmax( e_{t,s} ) = exp(e_{t,s}) / Σₛ exp(e_{t,s}) Vecteur de contexte c_t = Σₛ α_{t,s} · h_s Intuition Chaque mot "regarde" tous les autres et attribue un poids d'importance à chacun.
Visualisation de l'attention
Le
mil
coûte
très
cher
en
ce
moment

Le modèle se concentre sur "coûte", "très", "cher" pour prédire → sentiment négatif (prix).

BiLSTM + Attention pour analyse de sentiment
bilstm_attention.py
class BiLSTMAttention(nn.Module):
    """BiLSTM avec mécanisme d'attention pour classification."""
    def __init__(self, vocab_size, embed_dim=128,
                 hidden_dim=256, n_layers=2, n_classes=3):
        super().__init__()
        self.embedding = nn.Embedding(vocab_size, embed_dim,
                                      padding_idx=0)
        self.bilstm = nn.LSTM(
            embed_dim, hidden_dim, n_layers,
            batch_first=True,
            bidirectional=True,   # ← lit dans les 2 sens
            dropout=0.3 if n_layers > 1 else 0
        )
        # Couche d'attention
        self.attention = nn.Linear(hidden_dim * 2, 1)
        # Couche de classification
        self.classifier = nn.Sequential(
            nn.Linear(hidden_dim * 2, 128),
            nn.ReLU(), nn.Dropout(0.4),
            nn.Linear(128, n_classes)
        )

    def forward(self, x, lengths=None):
        # x : [batch, seq_len] (indices de mots)
        embedded = self.embedding(x)            # [B, L, E]
        out, _   = self.bilstm(embedded)        # [B, L, 2H]

        # ── Attention ────────────────────────────────────
        energy  = self.attention(out).squeeze(-1)  # [B, L]
        if lengths is not None:
            # Masquer les tokens de padding
            mask = torch.arange(x.size(1))[None,:] >= lengths[:,None]
            energy = energy.masked_fill(mask.to(x.device), -1e9)
        weights = torch.softmax(energy, dim=1)    # [B, L]
        context = (weights.unsqueeze(-1) * out).sum(1)  # [B, 2H]

        return self.classifier(context), weights
TRF

Transformers & BERT — Architecture complète

self-attention · multi-head · positional encoding · BERT · fine-tuning
▶ Ouvrir
Self-Attention — Au cœur du Transformer
Scaled Dot-Product Attention
Matrices Q, K, V (Query, Key, Value) Q = X · W_Q (ce qu'on cherche) K = X · W_K (index de recherche) V = X · W_V (information à extraire) Attention Score Attention(Q, K, V) = softmax( Q·Kᵀ / √d_k ) · V Division par √d_k pour éviter les gradients trop petits Multi-Head Attention head_i = Attention(Q·W_Q^i, K·W_K^i, V·W_V^i) MultiHead = Concat(head₁, …, head_h) · W_O Encodage Positionnel (sinusoïdal) PE(pos, 2i) = sin(pos / 10000^(2i/d_model)) PE(pos, 2i+1) = cos(pos / 10000^(2i/d_model))
d_k = dimension des clés · h = nombre de têtes d'attention · W_Q, W_K, W_V = matrices de projection apprises
Architecture Transformer complète
Encodeur (BERT) Décodeur (GPT) Input Tokens Output Tokens (décalé) ↓ ↓ [CLS] + tokens + [SEP] Embedding + Positional Enc. ↓ ↓ Embedding + Pos. Enc. Masked Self-Attention ↓ ↓ ┌──────────────────┐ Cross-Attention ← Encodeur │ Self-Attention │ ↓ │ Feed-Forward │ Feed-Forward Network │ LayerNorm │ ↓ └──────────────────┘ Linear + Softmax × N couches (BERT: 12) × N couches (GPT: 12-96)
Modèles pour langues africaines
ModèleLanguesParamètres
CamemBERTFrançais (dont africain)110M
AfroXLMR17 langues africaines270M
AfriBERTa11 langues africaines125M
Aya-101101 langues (dont 28 africaines)13B
XLM-RoBERTa100 langues125M–560M
Défis NLP africain Code-switching : « C'est vraiment bon wakh ! Le mil bi est frais. » (français + wolof). Solution : modèles multilingues + fine-tuning sur corpus locaux (AfriSenti, MasakhaNER, OPUS-MT).
Fine-tuning BERT/CamemBERT — Pipeline complet
finetune_bert.py — classification alertes agricoles
from transformers import (
    AutoTokenizer, AutoModelForSequenceClassification,
    TrainingArguments, Trainer, DataCollatorWithPadding
)
from datasets import Dataset
import torch, numpy as np
from sklearn.metrics import f1_score, accuracy_score

# ── Labels ────────────────────────────────────────────
LABELS = ['urgence', 'opportunite', 'information']
ID2LABEL = {i:l for i,l in enumerate(LABELS)}
LABEL2ID = {l:i for i,l in ID2LABEL.items()}

# ── Tokenizer ─────────────────────────────────────────
MODEL_NAME = "camembert-base"  # ou "Davlan/afro-xlmr-mini"
tokenizer  = AutoTokenizer.from_pretrained(MODEL_NAME)

def tokeniser(exemples):
    return tokenizer(
        exemples['text'],
        truncation=True,
        max_length=128,
        padding=False  # dynamic padding avec DataCollator
    )

# Charger et tokeniser
train_ds = Dataset.from_dict({'text': texts_train, 'label': labels_train})
val_ds   = Dataset.from_dict({'text': texts_val,   'label': labels_val})
train_tok = train_ds.map(tokeniser, batched=True)
val_tok   = val_ds.map(tokeniser,   batched=True)

# ── Modèle ────────────────────────────────────────────
model = AutoModelForSequenceClassification.from_pretrained(
    MODEL_NAME, num_labels=3,
    id2label=ID2LABEL, label2id=LABEL2ID,
    ignore_mismatched_sizes=True
)

# ── Métriques ─────────────────────────────────────────
def compute_metrics(pred):
    y_pred = pred.predictions.argmax(-1)
    y_true = pred.label_ids
    return {
        'accuracy': accuracy_score(y_true, y_pred),
        'f1_macro': f1_score(y_true, y_pred, average='macro')
    }

# ── Arguments d'entraînement ──────────────────────────
args = TrainingArguments(
    output_dir="./camembert_alerte",
    num_train_epochs=5,
    per_device_train_batch_size=16,
    per_device_eval_batch_size=32,
    learning_rate=2e-5,
    weight_decay=0.01,
    warmup_ratio=0.1,       # warmup sur 10% des steps
    evaluation_strategy="epoch",
    save_strategy="epoch",
    load_best_model_at_end=True,
    metric_for_best_model="f1_macro",
    fp16=torch.cuda.is_available(),  # mixed precision
    report_to="none"
)

trainer = Trainer(
    model=model, args=args,
    train_dataset=train_tok,
    eval_dataset=val_tok,
    compute_metrics=compute_metrics,
    data_collator=DataCollatorWithPadding(tokenizer)
)
trainer.train()
results = trainer.evaluate()
print(f"F1-macro : {results['eval_f1_macro']:.3f}")

# ── Pipeline d'inférence rapide ───────────────────────
from transformers import pipeline
classifier = pipeline(
    "text-classification",
    model=model, tokenizer=tokenizer,
    device=0 if torch.cuda.is_available() else -1
)
pred = classifier("Invasion de criquets signalée à Agadez !")
print(pred)  # [{'label': 'urgence', 'score': 0.94}]
Analyse de sentiment multilingue — Étapes prétraitement pour le wolof
wolof_preprocess.py
import re

# Stop words wolof (liste partielle)
STOP_WOLOF = {
    'la', 'nga', 'na', 'mu', 'nu', 'yi', 'bi', 'gi',
    'ci', 'ko', 'leen', 'fi', 'fu', 'bu', 'du', 'dafa'
}

def nettoyer_wolof(texte):
    """Nettoyage adapté au code-switching français-wolof."""
    texte = texte.lower()
    # Normaliser les variantes phonétiques
    texte = re.sub(r'kh', 'x', texte)   # "khol" → "xol"
    texte = texte.replace('ou', 'u')
    # Supprimer caractères spéciaux sauf apostrophe wolof
    texte = re.sub(r"[^\w\s']", ' ', texte)
    tokens = texte.split()
    # Filtrer stop words (bilingue)
    tokens = [t for t in tokens
              if t not in STOP_WOLOF
              and t not in STOP_FR
              and len(t) > 1]
    return tokens

# Exemple de texte code-switch
txt = "Le mil bi dafa chér wakh ! Jerejef pour l'info."
print(nettoyer_wolof(txt))
# → ['mil', 'chér', 'jerejef', 'info']
APP

Applications NLP avancées — NER, Génération, Résumé

NER · résumé automatique · traduction · génération de texte
▶ Ouvrir
NER — Reconnaissance d'entités nommées
Définition La NER identifie et classifie les entités dans le texte : LOC (lieu), ORG (organisation), PER (personne), PROD (produit), DAT (date), QUANT (quantité).
Exemple NER agricole "Le MINAGRI annonce que Dakar connaît une pénurie de mil depuis janvier 2024." MINAGRI → ORG (organisation) Dakar → LOC (lieu) mil → PROD (produit agricole) janvier 2024 → DAT (date)
Ressources NLP africaines
RessourceLanguesTâche
MasakhaNER21 africainesNER
AfriSenti14 africainesSentiment
MasakhaNews16 africainesClassification
OPUS-MTmultilingueTraduction
NLLB-200200 languesTraduction
Pipeline NLP complet avec spaCy
nlp_pipeline_complet.py
import spacy
from transformers import pipeline

# ── NER avec spaCy ────────────────────────────────────
nlp = spacy.load('fr_core_news_lg')

texte = """Le MINAGRI Sénégal annonce que la récolte de mil
           dans la région de Louga sera déficitaire en 2024
           à cause des pluies tardives de juillet."""

doc = nlp(texte)
for ent in doc.ents:
    print(f"{ent.text:<25} → {ent.label_}")

# ── Résumé automatique ────────────────────────────────
resumeur = pipeline("summarization",
                    model="facebook/mbart-large-cc25",
                    tokenizer="facebook/mbart-large-cc25")

rapport_long = """La campagne agricole 2024 au Sénégal présente
    des caractéristiques contrastées... [500 mots]"""

resume = resumeur(rapport_long, max_length=60,
                  min_length=20, do_sample=False)
print("Résumé :", resume[0]['summary_text'])

# ── Traduction FR → Wolof (expérimental) ─────────────
from transformers import MarianMTModel, MarianTokenizer
model_name = "Helsinki-NLP/opus-mt-fr-en"  # FR→EN comme étape
trad_tokenizer = MarianTokenizer.from_pretrained(model_name)
trad_model     = MarianMTModel.from_pretrained(model_name)

def traduire(texte, src_lang="fr", tgt_lang="en"):
    tokens = trad_tokenizer([texte], return_tensors="pt",
                             padding=True, truncation=True)
    out    = trad_model.generate(**tokens)
    return trad_tokenizer.decode(out[0], skip_special_tokens=True)

# ── Extraction de features textuelles ─────────────────
from sentence_transformers import SentenceTransformer

encoder  = SentenceTransformer('sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2')
phrases  = ["Le mil coûte cher", "Le prix du mil est élevé",
            "La pluie manque cette année"]
vecteurs = encoder.encode(phrases)  # [3, 384]

from sklearn.metrics.pairwise import cosine_similarity
sim = cosine_similarity(vecteurs)
print("\nSimilarité sémantique :")
print(sim.round(2))
04
Semaine 4

Déploiement & Projet Final

Un modèle qui reste dans un notebook ne sert personne. On assemble vision et NLP dans une application complète : Teranga AI — l'agriculture intelligente pour l'Afrique. On couvre aussi MLflow, FastAPI et les bonnes pratiques MLOps.

OPS

MLOps — Du notebook à la production

MLflow · FastAPI · Docker · monitoring · CI/CD
▶ Ouvrir
Pipeline MLOps complet
📊 Data Pipeline (nettoyage, augmentation)
🧪 Expérimentation & Tracking (MLflow)
📦 Packaging (ONNX, TorchScript, TFLite)
🚀 Serving (FastAPI / Streamlit)
📈 Monitoring (drift, performance)
MLflow — Tracking des expériences
mlflow_tracking.py
import mlflow, mlflow.pytorch
import torch

mlflow.set_experiment("Teranga-AI-PlantesClassif")

with mlflow.start_run(run_name="ResNet50-finetune-v2"):
    # Log hyperparamètres
    mlflow.log_params({
        "model":          "resnet50",
        "lr":             1e-4,
        "batch_size":     32,
        "n_epochs":       30,
        "augmentation":   True,
        "optimizer":      "AdamW",
    })
    # Entraînement ...
    best_acc, best_f1 = entraîner(model, ...)

    # Log métriques
    mlflow.log_metrics({
        "val_accuracy": best_acc,
        "val_f1_macro": best_f1,
    })
    # Log du modèle
    mlflow.pytorch.log_model(
        model, "model",
        registered_model_name="TenaragaPlanteCNN"
    )
    print(f"Acc={best_acc:.3f} | F1={best_f1:.3f}")
FastAPI — API REST pour l'inférence
api_teranga.py — FastAPI production
from fastapi import FastAPI, File, UploadFile, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
import torch
from PIL import Image
import io, time

app = FastAPI(
    title="Teranga AI — API Agricole",
    description="Détection de maladies + analyse de sentiment",
    version="1.0.0"
)
app.add_middleware(CORSMiddleware, allow_origins=["*"])

# ── Chargement au démarrage ───────────────────────────
@app.on_event("startup")
async def charger_modeles():
    app.state.cnn = torch.jit.load("plantes_scripted.pt")
    app.state.cnn.eval()
    app.state.nlp = pipeline("text-classification",
                             model="./camembert_alerte")

# ── Endpoint : classification de plantes ──────────────
@app.post("/predict/plante")
async def predire_maladie(file: UploadFile = File(...)):
    if file.content_type not in ["image/jpeg","image/png"]:
        raise HTTPException(400, "Format non supporté")
    t0     = time.time()
    img    = Image.open(io.BytesIO(await file.read())).convert("RGB")
    tensor = val_transform(img).unsqueeze(0)
    with torch.no_grad():
        probs = torch.softmax(app.state.cnn(tensor), 1)[0]
    pred_idx = probs.argmax().item()
    return {
        "maladie":    CLASSES[pred_idx],
        "confiance":  round(float(probs[pred_idx])*100, 1),
        "traitement": TRAITEMENTS[CLASSES[pred_idx]],
        "duree_ms":   round((time.time()-t0)*1000, 1)
    }

# ── Endpoint : analyse d'alerte agricole ──────────────
class AlerteRequest(BaseModel):
    message: str

@app.post("/predict/alerte")
async def analyser_alerte(req: AlerteRequest):
    result = app.state.nlp(req.message)[0]
    return {"type": result["label"], "score": round(result["score"],3)}

# Démarrage : uvicorn api_teranga:app --host 0.0.0.0 --port 8000
APP

Teranga AI — Application Streamlit Complète

3 modules · multilingue · mobile-friendly · dashboard
▶ Ouvrir
Les 3 modules de l'application
🌿

Diagnostic Plantes

Photo de feuille → détection de la maladie + traitement recommandé + coût estimé (XOF/CFA)

📢

Analyse d'Alertes

Message en français/wolof/hausa → classification urgence/opportunité/information

📊

Dashboard Prix

Suivi temps réel des prix des céréales (mil, sorgho, maïs) + prévisions ML

Pensé pour le terrain africain Interface multilingue (français, wolof, hausa, lingala, anglais), modèles légers (<20 MB), utilisables sur smartphone 4G en zone semi-rurale, mode hors-ligne partiel (TFLite).
Lancement streamlit run app_teranga_ai.py --server.port 8501
Code principal — Module diagnostic
app_teranga_ai.py
import streamlit as st
import torch
from PIL import Image
from transformers import pipeline
import plotly.graph_objects as go

st.set_page_config(page_title="🌿 Teranga AI", layout="wide",
                   page_icon="🌿")

# ── Chargement des modèles ────────────────────────────
@st.cache_resource
def charger_modeles():
    cnn = torch.jit.load("plantes_scripted.pt")
    cnn.eval()
    nlp = pipeline("text-classification",
                   model="./camembert_alerte")
    return cnn, nlp

cnn_model, nlp_model = charger_modeles()

# ── Sidebar navigation ────────────────────────────────
st.sidebar.image("logo_teranga.png", width=120)
module = st.sidebar.radio("🗂️ Module", [
    "🌿 Diagnostic Plantes",
    "📢 Analyse d'Alertes",
    "📊 Prix des Céréales"
])

CLASSES = ['Saine','Mildiou manioc','Rouille maïs',
           'Anthracnose manguier','Pourriture cacao']
TRAITEMENTS = {
    'Saine':                "Aucun traitement nécessaire ✅",
    'Mildiou manioc':       "Fongicide à base de cuivre — 2500 XOF/ha",
    'Rouille maïs':         "Propiconazole 250 EC — 3200 XOF/ha",
    'Anthracnose manguier': "Thirame + Manèbe — 4100 XOF/ha",
    'Pourriture cacao':     "Cuivre oxychloride — 5500 XOF/ha",
}

# ── Module 1 : Diagnostic ─────────────────────────────
if module == "🌿 Diagnostic Plantes":
    st.title("🔬 Diagnostic de Maladies des Plantes")
    col1, col2 = st.columns([1, 1.2])
    with col1:
        photo = st.file_uploader("📷 Photo de la feuille",
                                  type=["jpg","jpeg","png"])
        if photo:
            img    = Image.open(photo).convert("RGB")
            tensor = val_transform(img).unsqueeze(0)
            with torch.no_grad():
                probs = torch.softmax(cnn_model(tensor), 1)[0]
            pred_cls  = CLASSES[probs.argmax().item()]
            confiance = float(probs.max()) * 100
    with col2:
        if photo:
            st.image(img, caption="Image analysée", width=280)
            st.metric("🔬 Diagnostic", pred_cls,
                      f"{confiance:.1f}% de confiance")
            st.info(f"💊 {TRAITEMENTS[pred_cls]}")
            # Graphique de probabilités
            fig = go.Figure(go.Bar(
                x=CLASSES, y=probs.tolist(),
                marker_color=['#2f7d5c' if c==pred_cls
                              else '#bcd3c4' for c in CLASSES]))
            fig.update_layout(title="Probabilités par classe",
                             height=300)
            st.plotly_chart(fig, use_container_width=True)
Mise en pratique

Exercices — Toutes semaines

Exercices de difficulté croissante, ancrés dans des cas africains réels. Les solutions des exercices débutants sont données en détail.

Semaine 1 — Fondations
Débutant

Neurone d'irrigation

Café éthiopien — décider si une parcelle nécessite une irrigation d'urgence à partir de 3 features.

  • Calculer z = wᵀx + b (sans NumPy)
  • Appliquer la sigmoïde manuellement
  • Décider : a > 0.6 → irriguer
  • Tester 3 combinaisons de poids
Intermédiaire

MLP qualité du lait

Tanzanie — classer le lait en Bon / Moyen / Mauvais à partir de pH, gras, protéines.

  • Générer 600 échantillons synthétiques, 3 classes
  • MLP Keras (128→64→32→3) + BatchNorm + Dropout
  • Courbes loss/accuracy + matrice de confusion
  • Comparer avec/sans BatchNorm
Avancé

Régression solaire + PyTorch

Panneaux ruraux — prédire la production (kWh) à partir de 6 features météo.

  • Dataset PyTorch personnalisé + DataLoader
  • nn.Module avec 3 architectures différentes
  • AdamW + OneCycleLR scheduler
  • Visualiser RMSE par architecture
Semaine 2 — Vision CNN
Débutant

Convolution manuelle

Calculer à la main la feature map résultante d'une image 5×5 avec un filtre de Sobel 3×3.

  • Implémenter conv2d from scratch en NumPy
  • Tester les filtres : flou, contour, sobel
  • Visualiser l'image originale vs filtrée
  • Calculer la taille de sortie pour différents strides
Intermédiaire

CNN maladies de plantes

Afrique — classer 5 maladies de feuilles à partir d'images 64×64.

  • Dataset synthétique de 1000 images (PIL)
  • CNN 3 blocs PyTorch + data augmentation
  • Confusion matrix + rapport de classification
  • Grad-CAM pour interpréter les prédictions
Avancé

Transfer Learning drone

Classer 5 cultures vues par drone (manioc, maïs, coton, sorgho, arachide) avec 200 images/classe.

  • Feature extraction ResNet50 → fine-tuning partiel
  • Comparer 3 modèles (ResNet18, MobileNetV3, EfficientNet)
  • Courbes ROC + AUC par classe
  • Exporter en ONNX pour déploiement
Semaine 3 — NLP
Débutant

Pipeline de prétraitement

Nettoyer et vectoriser 50 avis de marchés africains (français + quelques mots wolof).

  • Tokenisation + stop words bilingues
  • Lemmatisation avec spaCy fr_core_news_sm
  • Vectorisation TF-IDF + bigrammes
  • Trouver les 10 features les plus discriminantes
Intermédiaire

BiLSTM analyse de sentiment

Classer des tweets en français africain (pos/neu/neg) sur les prix alimentaires.

  • Pipeline complet : tokenisation → padding → BiLSTM
  • Embeddings FastText pré-entraînés
  • Mécanisme d'attention + visualisation des poids
  • Comparer TF-IDF+LR vs BiLSTM
Avancé

Fine-tuning CamemBERT

Classifier des alertes agricoles multilingues (français/wolof) en 3 catégories.

  • Fine-tuning CamemBERT avec HuggingFace Trainer
  • Gestion du déséquilibre de classes (class_weight)
  • Évaluation : F1-macro + rapport complet
  • Pipeline d'inférence en production (FastAPI)

Solution complète — Exercice 1 (Neurone d'irrigation)

x = [sécheresse, température, humidité] · w = [−0.7, 0.5, 0.8] · b = −0.1
▶ Voir solution
Le calcul détaillé
Forward pass — étape par étape
Données d'entrée x = [0.20, 0.85, 0.75] w = [−0.70, 0.50, 0.80] b = −0.10 Combinaison linéaire z = (−0.70)(0.20) + (0.50)(0.85) + (0.80)(0.75) + (−0.10) z = −0.140 + 0.425 + 0.600 + (−0.100) z = 0.785 Activation sigmoïde a = σ(0.785) = 1 / (1 + e⁻⁰·⁷⁸⁵) a = 1 / (1 + 0.456) = 1 / 1.456 a ≈ 0.687 Décision a ≈ 0.687 > 0.60 → 🚿 IRRIGUER Probabilité d'urgence : 68.7%
Interprétation agronomique L'humidité du sol (0.75) et la température élevée (0.85) dominent la décision. Le poids négatif sur la sécheresse (−0.70 × 0.20 = −0.14) atténue légèrement le signal.
Solution Python complète avec tests
import math

                def sigmoid(z):
                    """Fonction sigmoïde : σ(z) = 1 / (1 + e⁻ᶻ)"""
                    return 1 / (1 + math.exp(-z))

                class NeuroneIrrigation:
                    """Neurone décideur d'irrigation."""
                    def __init__(self, weights, bias, seuil=0.6):
                        self.w      = weights
                        self.b      = bias
                        self.seuil  = seuil

                    def forward(self, x):
                        # Combinaison linéaire (sans NumPy)
                        z = sum(wi*xi for wi,xi in zip(self.w, x)) + self.b
                        a = sigmoid(z)
                        return a, z

                    def decider(self, x):
                        a, z = self.forward(x)
                        decision = "🚿 IRRIGUER" if a > self.seuil else "⏳ Pas urgent"
                        print(f"z = {z:.4f} | a = {a:.4f} | → {decision}")
                        return a

                # Configuration du neurone
                # Features : [sécheresse_score, température_norm, humidité_inverse]
                neurone = NeuroneIrrigation(
                    weights=[-0.7, 0.5, 0.8],
                    bias=-0.1,
                    seuil=0.6
                )

                # Tests sur 3 parcelles
                print("=== Parcelle 1 (cas d'urgence) ===")
                neurone.decider([0.20, 0.85, 0.75])  # → 0.687 ✓ Irriguer

                print("\n=== Parcelle 2 (sol humide) ===")
                neurone.decider([0.10, 0.60, 0.20])  # → probabilité basse

                print("\n=== Parcelle 3 (stress hydrique) ===")
                neurone.decider([0.90, 0.95, 0.95])  # → probabilité très haute

Solution — Exercice NLP S3 (Pipeline de prétraitement complet)

tokenisation · lemmatisation · TF-IDF · similarité cosinus
▶ Voir solution
solution_nlp_pipeline.py
"""
Solution complète — Pipeline NLP pour avis marchés africains.
Prétraitement → TF-IDF → Analyse de similarité.
"""
import re, string
import numpy as np
import pandas as pd
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
from collections import Counter

# ── Données (corpus d'avis marchés) ──────────────────
corpus = [
    "Le prix du mil est très élevé au marché de Thiès cette semaine",
    "La qualité du cacao de Côte d'Ivoire est excellente cette année",
    "Pénurie de sorgho signalée dans la région de Louga au Sénégal",
    "Les pluies tardives menacent la récolte de maïs au Burkina Faso",
    "Bonne récolte d'arachide attendue en Gambie, prix stables",
    "Le marché de Bamako affiche des prix record pour le mil rouge",
    "Alerte: sécheresse sévère au Sahel, récoltes compromises",
    "Coopérative de cacao: formation des agriculteurs sur les maladies",
]

# ── Stop words bilingues (fr + mots wolof courants) ───
STOP_FR_BASE = {
    'le','la','les','de','des','du','un','une','au','aux',
    'et','est','en','ce','qui','que','à','se','sur','pour',
    'par','avec','dans','il','elle','ils','elles','on',
    'très','cette','ces',
}
STOP_WOLOF = {'bi','gi','yi','la','nga','ci','ko','bu','du','na','mu'}
STOPS      = STOP_FR_BASE | STOP_WOLOF

def normaliser(texte):
    """Nettoyage complet du texte."""
    texte = texte.lower()
    texte = re.sub(r'[^\w\s]', ' ', texte)    # ponctuation
    texte = re.sub(r'\d+', '', texte)           # chiffres
    texte = re.sub(r'\s+', ' ', texte).strip() # espaces
    return texte

def tokeniser_simple(texte):
    """Tokenisation basique (sans spaCy pour la démo)."""
    tokens = normaliser(texte).split()
    return [t for t in tokens if t not in STOPS and len(t) > 2]

# ── Lemmatisation basique par règles (fr) ─────────────
LEMMES = {
    'prix': 'prix', 'élevé': 'élevé', 'élevée': 'élevé',
    'récoltes': 'récolte', 'récolte': 'récolte',
    'marchés': 'marché', 'marché': 'marché',
    'pluies': 'pluie', 'pluie': 'pluie',
    'agriculteurs': 'agriculteur', 'cultivateurs': 'cultivateur',
    'maladies': 'maladie', 'maladie': 'maladie',
    'sécheresses': 'sécheresse',
}

def lemmatiser_simple(tokens):
    return [LEMMES.get(t, t) for t in tokens]

# ── Pipeline complet ───────────────────────────────────
def pipeline_nlp(texte):
    tokens = tokeniser_simple(texte)
    tokens = lemmatiser_simple(tokens)
    return ' '.join(tokens)

corpus_traité = [pipeline_nlp(t) for t in corpus]
print("=== Corpus traité ===")
for orig, trt in zip(corpus[:3], corpus_traité[:3]):
    print(f"\nOrigin : {orig[:60]}...")
    print(f"Traité : {trt}")

# ── TF-IDF avec bigrammes ─────────────────────────────
tfidf = TfidfVectorizer(
    ngram_range=(1, 2),
    max_features=200,
    min_df=1,
    sublinear_tf=True,
    norm='l2'
)
X = tfidf.fit_transform(corpus_traité)
vocab = tfidf.get_feature_names_out()

print(f"\n=== Matrice TF-IDF : {X.shape} ===")
print(f"    ({X.shape[0]} documents × {X.shape[1]} features)")

# ── Top 10 features discriminantes ───────────────────
scores_moyens = X.toarray().mean(axis=0)
top10_idx     = scores_moyens.argsort()[::-1][:10]
print("\n=== Top 10 features (TF-IDF moyen) ===")
for i, idx in enumerate(top10_idx):
    print(f"  {i+1:2d}. {vocab[idx]:<25} {scores_moyens[idx]:.4f}")

# ── Similarité cosinus entre documents ───────────────
sim = cosine_similarity(X)
print("\n=== Matrice de similarité (extrait) ===")
df_sim = pd.DataFrame(sim.round(2))
print(df_sim.iloc[:4, :4].to_string())

# ── Recherche par requête ──────────────────────────────
requete     = "prix élevé mil marché Sénégal"
requete_trt = pipeline_nlp(requete)
q_vec       = tfidf.transform([requete_trt])
scores_req  = cosine_similarity(q_vec, X)[0]
plus_proche = scores_req.argmax()

print(f"\n=== Recherche ===")
print(f"Requête : '{requete}'")
print(f"Plus proche : '{corpus[plus_proche]}'")
print(f"Score : {scores_req[plus_proche]:.3f}")

# ── Fréquences globales ───────────────────────────────
tous_tokens = []
for t in corpus_traité:
    tous_tokens.extend(t.split())
freq = Counter(tous_tokens).most_common(10)
print("\n=== Mots les plus fréquents (après nettoyage) ===")
for mot, n in freq:
    print(f"  {mot:<20} {n}×")