Machine Learning Spécialisé¶
Lorsque l'on travaille sur des données non tabulaires, il faut pouvoir adapter sa chaîne de traitement à des données non tabulaires.
C'est le cas lorsque l'on travaille sur des données relevant de domaines spécifiques !!
En machine learning classique (contrairemen au deep learning), le data scientist doit faire de l'extraction de feature pour transformer chaque variable en feature numérique.
Domaines abordés¶
| Domaine | Type de données | Applications typiques |
|---|---|---|
| Vision par ordinateur | Images, vidéos | Classification, détection d'objets |
| Traitement du langage | Texte brut | Sentiment, classification, NER |
| Séries temporelles | Données séquentielles | Prévision, détection d'anomalies |
Principe commun¶
scikit-learn travaille sur des matrices 2D (n_samples, n_features)
Pour chaque domaine, l'enjeu est de transformer les données brutes en ce format, puis d'appliquer la chaîne de traitement habituelle.
Introduction — Pourquoi c'est différent ?¶
Une image n'est pas un tableau de features : c'est un tenseur 3D (hauteur, largeur, canaux)
Image couleur 128×128 px → shape (128, 128, 3) → 49 152 valeurs
Image niveaux de gris → shape (128, 128) → 16 384 valeurs
Applications classiques¶
- Classification : identifier ce que contient une image (chien / chat, tumeur / sain…)
- Détection d'objets : localiser des éléments dans une image
- Segmentation : classer chaque pixel
- Reconnaissance de caractères (OCR)
⚠️ Pour les tâches complexes, les réseaux de neurones convolutifs (CNN) sont aujourd'hui incontournables. Ici, on couvre les approches ML classiques.
Représentation et chargement des images¶
from PIL import Image
import numpy as np
# Charger une image
img = Image.open("chat.jpg")
img_array = np.array(img) # shape: (H, W, 3) — valeurs 0-255
# Convertir en niveaux de gris
img_gray = img.convert("L")
img_gray_array = np.array(img_gray) # shape: (H, W)
# Normaliser les valeurs de pixels
img_normalized = img_array / 255.0 # valeurs dans [0, 1]
Mise à l'échelle (preprocessing indispensable)¶
# Toutes les images doivent avoir la même taille avant l'entraînement
target_size = (64, 64)
img_resized = img.resize(target_size)
# Aplatir pour scikit-learn : (H, W) → (H*W,)
img_flat = np.array(img_resized).flatten() # shape: (4096,)
# Pour un dataset complet :
X = np.array([np.array(img.resize(target_size).convert("L")).flatten()
for img in images]) # shape: (n_samples, H*W)
Extraction de features : HOG¶
HOG (Histogram of Oriented Gradients) : résumé des contours et formes d'une image
→ Beaucoup plus compact et informatif que les pixels bruts
Image (64×128 px) → HOG → vecteur de ~3780 features
from skimage.feature import hog
from skimage import color
import numpy as np
def extract_hog(image_array):
"""Extrait les features HOG d'une image."""
img_gray = color.rgb2gray(image_array) # HOG fonctionne en niveaux de gris
features = hog(
img_gray,
orientations=9, # nombre de directions de gradient
pixels_per_cell=(8, 8), # taille des cellules
cells_per_block=(2, 2), # normalisation par blocs
visualize=False
)
return features
# Extraire HOG pour tout le dataset
X_hog = np.array([extract_hog(img) for img in images]) # (n_samples, n_hog_features)
Exemple complet : classification de chiffres manuscrits¶
Dataset digits de scikit-learn : images 8×8 px, 10 classes (0 à 9)
from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.svm import SVC
from sklearn.metrics import classification_report
import matplotlib.pyplot as plt
# Chargement
digits = load_digits()
X, y = digits.data, digits.target # X déjà aplati : (1797, 64)
# Visualisation d'un échantillon
fig, axes = plt.subplots(2, 5, figsize=(10, 4))
for i, ax in enumerate(axes.flat):
ax.imshow(digits.images[i], cmap='gray')
ax.set_title(f"Label: {digits.target[i]}")
ax.axis('off')
plt.suptitle("Exemples du dataset digits")
plt.tight_layout()
plt.show()
Vérification de la dimension du dataset :
print(f"Taille originale des images {digits.images[i].shape}",
f"\nNombre d'images : {X.shape[1]}",
f"\nLongeur des image aplaties : {X.shape[0]}")
Taille originale des images (8, 8) Nombre d'images : 64 Longeur des image aplaties : 1797
# Pipeline : normalisation + SVM
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
pipe = make_pipeline(
StandardScaler(),
SVC(kernel='rbf', C=10, gamma=0.001)
)
pipe.fit(X_train, y_train)
print(classification_report(y_test, pipe.predict(X_test)))
precision recall f1-score support
0 0.97 1.00 0.99 33
1 1.00 1.00 1.00 28
2 1.00 1.00 1.00 33
3 1.00 0.97 0.99 34
4 1.00 1.00 1.00 46
5 0.98 0.98 0.98 47
6 0.97 0.97 0.97 35
7 1.00 0.97 0.99 34
8 0.94 0.97 0.95 30
9 0.95 0.95 0.95 40
accuracy 0.98 360
macro avg 0.98 0.98 0.98 360
weighted avg 0.98 0.98 0.98 360
Comparaison : pixels bruts vs features HOG¶
Les images digits font 8×8 px → 64 features brutes.
HOG sur ces mêmes images (cellules 4×4) → 36 features seulement.
L'enjeu : HOG capture les contours et formes, plus robuste et compact que les valeurs de pixels.
from skimage.feature import hog
import numpy as np
# --- Extraction HOG ---
# Les images digits sont déjà en niveaux de gris (valeurs 0–16), shape (8, 8)
def extract_hog_digits(image_2d):
return hog(
image_2d,
orientations=9,
pixels_per_cell=(4, 4), # cellules 4×4 px → grille 2×2
cells_per_block=(1, 1), # pas de normalisation par bloc
visualize=False
)
X_hog = np.array([extract_hog_digits(img) for img in digits.images])
print(f"Pixels bruts : {X.shape[1]} features par image")
print(f"HOG : {X_hog.shape[1]} features par image")
# --- Même split train/test (random_state=42 → indices identiques) ---
X_hog_train, X_hog_test, y_hog_train, y_hog_test = train_test_split(
X_hog, y, test_size=0.2, random_state=42
)
# Pipeline HOG + SVM
pipe_hog = make_pipeline(
StandardScaler(),
SVC(kernel='rbf', C=10, gamma='scale')
)
pipe_hog.fit(X_hog_train, y_hog_train)
# --- Comparaison des scores ---
score_brut = pipe.score(X_test, y_test)
score_hog = pipe_hog.score(X_hog_test, y_hog_test)
print(f"\nAccuracy — pixels bruts (64 features) : {score_brut:.4f}")
print(f"Accuracy — HOG (36 features) : {score_hog:.4f}")
# --- Visualisation détaillée par classe ---
from sklearn.metrics import classification_report
print("\n=== Pixels bruts ===")
print(classification_report(y_test, pipe.predict(X_test)))
print("=== HOG ===")
print(classification_report(y_hog_test, pipe_hog.predict(X_hog_test)))
Pixels bruts : 64 features par image
HOG : 36 features par image
Accuracy — pixels bruts (64 features) : 0.9806
Accuracy — HOG (36 features) : 0.8611
=== Pixels bruts ===
precision recall f1-score support
0 0.97 1.00 0.99 33
1 1.00 1.00 1.00 28
2 1.00 1.00 1.00 33
3 1.00 0.97 0.99 34
4 1.00 1.00 1.00 46
5 0.98 0.98 0.98 47
6 0.97 0.97 0.97 35
7 1.00 0.97 0.99 34
8 0.94 0.97 0.95 30
9 0.95 0.95 0.95 40
accuracy 0.98 360
macro avg 0.98 0.98 0.98 360
weighted avg 0.98 0.98 0.98 360
=== HOG ===
precision recall f1-score support
0 0.85 0.88 0.87 33
1 0.97 1.00 0.98 28
2 0.91 0.91 0.91 33
3 0.77 0.88 0.82 34
4 0.82 0.87 0.84 46
5 0.91 0.89 0.90 47
6 0.94 0.89 0.91 35
7 0.91 0.85 0.88 34
8 0.78 0.70 0.74 30
9 0.79 0.75 0.77 40
accuracy 0.86 360
macro avg 0.86 0.86 0.86 360
weighted avg 0.86 0.86 0.86 360
Réduction de dimension pour les images¶
Les images aplaties produisent des vecteurs très longs → la PCA est quasi-indispensable
from sklearn.decomposition import PCA
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.svm import SVC
# Pipeline complet avec PCA
pipe = make_pipeline(
StandardScaler(),
PCA(n_components=0.95), # garder 95% de la variance
SVC(kernel='rbf', C=10)
)
pipe.fit(X_train, y_train)
print(f"Dimensions après PCA : {pipe['pca'].n_components_} composantes")
print(f"Score : {pipe.score(X_test, y_test):.3f}")
Tableau récapitulatif — Analyse d'images¶
| Étape | Outil | Remarque |
|---|---|---|
| Chargement | PIL.Image, skimage.io |
Uniformiser la taille en amont |
| Aplatissement | np.flatten() |
Pixels bruts → vecteur 1D |
| Normalisation | StandardScaler ou /255 |
Toujours nécessaire |
| Extraction features | HOG (skimage.feature.hog) |
Plus robuste que pixels bruts |
| Réduction dim | PCA |
Indispensable si beaucoup de pixels |
| Modèle | SVC, RandomForest |
SVM + HOG = combo classique |
Introduction — Le défi du texte¶
Le texte est une séquence de symboles — pas de nombres, pas de taille fixe, vocabulaire potentiellement infini
"Ce film est excellent !" → ??? → modèle ML
Applications classiques¶
- Analyse de sentiment : positif / négatif / neutre
- Classification de documents : spam, catégorisation d'articles
- Extraction d'information : noms, lieux, dates (NER)
- Similarité de textes : documents dupliqués, plagiat
- Recherche d'information : moteurs de recherche internes
Étape 1 : Prétraitement du texte¶
Nettoyer le texte avant vectorisation :
import re
def preprocess_text(text):
text = text.lower() # minuscules
text = re.sub(r'[^a-zàâçéèêëîïôùûü\s]', '', text) # supprimer ponctuation
text = re.sub(r'\s+', ' ', text).strip() # espaces multiples
return text
corpus = [preprocess_text(doc) for doc in raw_texts]
Opérations optionnelles selon le contexte :
| Opération | Description | Quand l'utiliser |
|---|---|---|
| Stop words | Supprimer mots très fréquents (le, et…) |
Toujours en classification |
| Stemming | Racine du mot (manger → mang) |
Texte court, vocabulaire limité |
| Lemmatisation | Forme canonique (mangeait → manger) |
Texte long, besoin de précision |
Étape 2 : Vectorisation — Bag of Words¶
Principe : chaque document devient un vecteur de comptage des mots
Vocabulaire : ["bon", "film", "nul", "excellent"]
"ce film est bon" → [1, 1, 0, 0]
"film nul" → [0, 1, 1, 0]
"excellent excellent" → [0, 0, 0, 2]
from sklearn.feature_extraction.text import CountVectorizer
vectorizer = CountVectorizer(
max_features=5000, # limiter la taille du vocabulaire
stop_words='english', # ou liste personnalisée
ngram_range=(1, 2), # inclure les bigrammes ("très bon", "pas cher")
min_df=2 # ignorer les mots très rares
)
X_train_bow = vectorizer.fit_transform(corpus_train) # sparse matrix
X_test_bow = vectorizer.transform(corpus_test)
Étape 2 (bis) : TF-IDF — pondérer l'importance des mots¶
TF-IDF = Term Frequency × Inverse Document Frequency
- TF : fréquence du mot dans le document (mots fréquents = importants)
- IDF : inverse de la fréquence dans le corpus (mots rares = discriminants)
from sklearn.feature_extraction.text import TfidfVectorizer
tfidf = TfidfVectorizer(
max_features=5000,
stop_words='english',
ngram_range=(1, 2),
sublinear_tf=True, # log(TF) — atténue les mots très répétés
min_df=2,
max_df=0.95 # ignorer les mots présents dans >95% des docs
)
X_train_tfidf = tfidf.fit_transform(corpus_train)
X_test_tfidf = tfidf.transform(corpus_test)
TF-IDF est généralement préférable à Bag of Words — il pondère mieux l'importance des mots
Exemple complet : classification de nouvelles (20newsgroups)¶
from sklearn.datasets import fetch_20newsgroups
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.naive_bayes import MultinomialNB
from sklearn.pipeline import make_pipeline
from sklearn.metrics import classification_report
# Chargement (4 catégories pour simplifier)
categories = ['sci.space', 'rec.sport.hockey', 'comp.graphics', 'talk.politics.misc']
train = fetch_20newsgroups(subset='train', categories=categories, remove=('headers', 'footers'))
test = fetch_20newsgroups(subset='test', categories=categories, remove=('headers', 'footers'))
# Pipeline TF-IDF + Naive Bayes (modèle de référence pour le texte)
pipe = make_pipeline(
TfidfVectorizer(max_features=10000, stop_words='english', ngram_range=(1, 2)),
MultinomialNB(alpha=0.1)
)
pipe.fit(train.data, train.target)
y_pred = pipe.predict(test.data)
print(classification_report(test.target, y_pred, target_names=train.target_names))
precision recall f1-score support
comp.graphics 0.94 0.95 0.94 389
rec.sport.hockey 0.97 0.98 0.98 399
sci.space 0.91 0.90 0.90 394
talk.politics.misc 0.91 0.90 0.91 310
accuracy 0.93 1492
macro avg 0.93 0.93 0.93 1492
weighted avg 0.93 0.93 0.93 1492
from sklearn.svm import LinearSVC
# SVM linéaire — souvent plus performant que Naive Bayes pour le texte
pipe_svm = make_pipeline(
TfidfVectorizer(max_features=10000, stop_words='english', sublinear_tf=True),
LinearSVC(C=1.0, max_iter=2000)
)
pipe_svm.fit(train.data, train.target)
print(f"Score SVM : {pipe_svm.score(test.data, test.target):.3f}")
Score SVM : 0.948
Inspecter les features les plus importantes¶
import numpy as np
# Récupérer les mots les plus discriminants par classe (SVM)
feature_names = pipe_svm['tfidfvectorizer'].get_feature_names_out()
coefs = pipe_svm['linearsvc'].coef_
for i, category in enumerate(train.target_names):
top_indices = np.argsort(coefs[i])[-10:]
top_words = feature_names[top_indices]
print(f"\n{category}: {', '.join(top_words)}")
comp.graphics: code, card, 3do, polygon, hi, 3d, computer, file, image, graphics rec.sport.hockey: season, games, players, playoff, play, ca, nhl, game, team, hockey sci.space: flight, dietz, solar, shuttle, spacecraft, launch, moon, nasa, orbit, space talk.politics.misc: narrative, state, government, house, tax, drugs, law, people, com, clinton
Tableau récapitulatif — Analyse textuelle¶
| Étape | Outil | Remarque |
|---|---|---|
| Nettoyage | re, str.lower() |
Minuscules, ponctuation, stop words |
| Vectorisation | CountVectorizer |
Bag of Words, simple |
| Vectorisation | TfidfVectorizer |
Préférable — pondération intelligente |
| Modèle | MultinomialNB |
Baseline rapide, bon pour texte sparse |
| Modèle | LinearSVC |
Souvent le plus performant sur texte |
| Modèle | LogisticRegression |
Interprétable, probabilités disponibles |
Introduction — Ce qui change avec les séries temporelles¶
Les observations ne sont pas indépendantes : la valeur au temps $t$ dépend des valeurs passées
Données classiques : x₁, x₂, x₃ ... indépendants
Série temporelle : x_t dépend de x_{t-1}, x_{t-2}, ...
Challenges spécifiques¶
| Challenge | Description |
|---|---|
| Dépendance temporelle | Les observations passées prédisent le futur |
| Non-stationnarité | La moyenne et la variance changent dans le temps (tendance, saisonnalité) |
| Train/test split | ⚠️ Ne jamais mélanger les données — toujours couper dans le temps |
| Data leakage | Ne pas utiliser de valeurs futures pour prédire le passé |
Train/test split temporel — point critique ⚠️¶
# ❌ NE PAS FAIRE — mélange passé et futur
from sklearn.model_selection import train_test_split
X_train, X_test = train_test_split(X, test_size=0.2, shuffle=True) # FAUX !
# ✅ CORRECT — couper dans le temps
split = int(len(X) * 0.8)
X_train, X_test = X[:split], X[split:]
y_train, y_test = y[:split], y[split:]
# ✅ Cross-validation temporelle avec TimeSeriesSplit
from sklearn.model_selection import TimeSeriesSplit
tscv = TimeSeriesSplit(n_splits=5)
# → les folds respectent l'ordre chronologique
Feature engineering — transformer la série en tableau 2D¶
L'idée clé : créer des features à partir du passé pour prédire le futur
import pandas as pd
df = pd.DataFrame({'valeur': serie})
# 1. Features de lag (valeurs passées)
df['lag_1'] = df['valeur'].shift(1) # valeur d'il y a 1 pas
df['lag_7'] = df['valeur'].shift(7) # valeur d'il y a 7 pas (semaine)
df['lag_30'] = df['valeur'].shift(30) # valeur d'il y a 1 mois
# 2. Statistiques glissantes (rolling statistics)
df['mean_7'] = df['valeur'].rolling(window=7).mean() # moyenne sur 7 jours
df['std_7'] = df['valeur'].rolling(window=7).std() # écart-type glissant
df['max_30'] = df['valeur'].rolling(window=30).max() # max du mois
# 3. Features temporelles (date)
df['heure'] = df.index.hour
df['jour_semaine'] = df.index.dayofweek
df['mois'] = df.index.month
df['is_weekend'] = df.index.dayofweek >= 5
# Supprimer les lignes avec NaN (dues aux lags)
df = df.dropna()
Décomposition d'une série temporelle¶
Une série = tendance + saisonnalité + résidus
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from statsmodels.tsa.seasonal import seasonal_decompose
# Créer une série synthétique : tendance + saisonnalité + bruit
np.random.seed(42)
t = np.arange(200)
serie = 0.05 * t + 10 * np.sin(2 * np.pi * t / 52) + np.random.randn(200) * 2
ts = pd.Series(serie, index=pd.date_range('2020-01-01', periods=200, freq='W'))
# Décomposition
decomp = seasonal_decompose(ts, model='additive', period=52)
fig, axes = plt.subplots(4, 1, figsize=(10, 8))
decomp.observed.plot(ax=axes[0], title='Série originale')
decomp.trend.plot(ax=axes[1], title='Tendance')
decomp.seasonal.plot(ax=axes[2], title='Saisonnalité')
decomp.resid.plot(ax=axes[3], title='Résidus')
plt.tight_layout()
plt.show()
Exemple complet : prévision avec un modèle sklearn¶
from sklearn.ensemble import GradientBoostingRegressor
from sklearn.metrics import mean_absolute_error
# Construire le dataset de features à partir de la série
df = pd.DataFrame({'valeur': ts})
# Features : lags + statistiques glissantes + composantes temporelles
for lag in [1, 4, 8, 52]:
df[f'lag_{lag}'] = df['valeur'].shift(lag)
df['mean_4'] = df['valeur'].rolling(4).mean()
df['std_4'] = df['valeur'].rolling(4).std()
df['semaine'] = df.index.isocalendar().week.astype(int)
df = df.dropna()
# Split temporel
split = int(len(df) * 0.8)
X_train = df.drop('valeur', axis=1).iloc[:split]
X_test = df.drop('valeur', axis=1).iloc[split:]
y_train = df['valeur'].iloc[:split]
y_test = df['valeur'].iloc[split:]
# Entraînement
model = GradientBoostingRegressor(n_estimators=200, learning_rate=0.05, max_depth=4)
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
print(f"MAE : {mean_absolute_error(y_test, y_pred):.3f}")
MAE : 1.767
# Visualisation des prédictions
plt.figure(figsize=(12, 4))
plt.plot(y_test.index, y_test.values, label='Réel', linewidth=2)
plt.plot(y_test.index, y_pred, label='Prédit', linestyle='--', linewidth=2)
plt.legend()
plt.title("Prévision — GradientBoosting sur features temporelles")
plt.tight_layout()
plt.show()
Cross-validation temporelle¶
from sklearn.model_selection import TimeSeriesSplit, cross_val_score
import matplotlib.pyplot as plt
tscv = TimeSeriesSplit(n_splits=5)
# Visualiser les folds
X_all = df.drop('valeur', axis=1)
y_all = df['valeur']
fig, axes = plt.subplots(5, 1, figsize=(10, 6), sharex=True)
for fold, (train_idx, test_idx) in enumerate(tscv.split(X_all)):
axes[fold].plot(train_idx, [1]*len(train_idx), 'b|', label='Train')
axes[fold].plot(test_idx, [2]*len(test_idx), 'r|', label='Test')
axes[fold].set_ylabel(f'Fold {fold+1}', fontsize=8)
axes[fold].set_yticks([])
plt.suptitle("TimeSeriesSplit — les folds respectent l'ordre chronologique")
plt.tight_layout()
plt.show()
# Score cross-validé
scores = cross_val_score(model, X_all, y_all, cv=tscv, scoring='neg_mean_absolute_error')
print(f"MAE cross-validé : {-scores.mean():.3f} ± {scores.std():.3f}")
MAE cross-validé : 2.447 ± 0.869
Tableau récapitulatif — Séries temporelles¶
| Étape | Outil | Remarque |
|---|---|---|
| Split temporel | iloc[:split] / iloc[split:] |
⚠️ Jamais de shuffle |
| Cross-validation | TimeSeriesSplit |
Respecte l'ordre chronologique |
| Lag features | df.shift(n) |
Features fondamentales |
| Rolling stats | df.rolling(w).mean/std |
Capturer la tendance locale |
| Décomposition | seasonal_decompose |
Comprendre la structure |
| Modèle | GradientBoosting, RandomForest |
Très bons résultats avec bonnes features |
| Modèle | LinearRegression |
Baseline simple et interprétable |
| Domaine | Transformation clé | Modèle recommandé |
|---|---|---|
| Images | Aplatir + normaliser (ou HOG) + PCA | SVM avec kernel RBF |
| Texte | TF-IDF vectorisation | LinearSVC ou LogisticRegression |
| Séries temp. | Lag features + rolling stats | GradientBoosting + TimeSeriesSplit |
Principes communs¶
- Toujours transformer en matrice 2D
(n_samples, n_features)avant sklearn - Apprendre les transformations sur le train set — jamais sur les données de test
- Encapsuler dans une Pipeline pour éviter le data leakage
- Choisir un modèle baseline simple (Naive Bayes, LinearSVC, Ridge) avant d'optimiser
- Pour des performances maximales : se tourner vers le Deep Learning (CNN, Transformers, LSTM)