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.
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.
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.
É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
Activation
Utilisation recommandée
Problème connu
Sigmoid
Sortie binaire (0/1)
Vanishing gradient
Tanh
RNN, couches cachées
Vanishing gradient
ReLU
Couches cachées CNN/MLP
Dying ReLU (z<0)
Leaky ReLU
Alternative à ReLU
Robuste
Softmax
Sortie 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")
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, oscilleSGD + 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()
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ère
TF / Keras
PyTorch
Paradigme
Déclaratif (define-and-run)
Impératif (define-by-run)
Débogage
Plus complexe
Très pythonique
Recherche
Production
Standard académique
Autograd
GradientTape
Automatique (.backward)
Mobile
TF Lite ✓
TorchScript / ONNX
Communauté
Google / enterprise
Meta / 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.
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.
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.
Découper le texte en unités (tokens) : mots, sous-mots ou caractères.
Type
Exemple
Modèle
Mots
"je mange" → ["je","mange"]
NLTK, spaCy
Sous-mots (BPE)
"manger" → ["mang","er"]
BERT, GPT
Caractères
"chat" → ["c","h","a","t"]
Rare
Phrases
Découpe par ponctuation
spaCy
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
Technique
Principe
"mangeons", "mangé", "mangeait"
Stemming
Troncature brute (règles)
→ "mang" (radicalement)
Lemmatisation
Forme 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}")
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]
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.
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).
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
Ressource
Langues
Tâche
MasakhaNER
21 africaines
NER
AfriSenti
14 africaines
Sentiment
MasakhaNews
16 africaines
Classification
OPUS-MT
multilingue
Traduction
NLLB-200
200 langues
Traduction
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.
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.785Activation sigmoïde
a = σ(0.785) = 1 / (1 + e⁻⁰·⁷⁸⁵)
a = 1 / (1 + 0.456) = 1 / 1.456
a ≈ 0.687Dé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)
"""
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}×")