Lesson 0: Setup Environnement Complet
Objectifs d'apprentissage
- Installer Python 3.11+ et configurer un environnement virtuel
- Configurer Conda/Miniconda pour la gestion de packages
- Installer les drivers GPU (CUDA, cuDNN) pour l'entraînement accéléré
- Configurer WSL2 sur Windows pour des workloads IA optimaux
- Installer VS Code avec les extensions essentielles pour l'IA/ML
Introduction
Un environnement de développement bien configuré est la fondation de tout projet IA réussi. Cette leçon vous guide à travers l'installation complète des outils nécessaires.
1. Installation de Python 3.11+
Python 3.11 apporte des améliorations significatives de performance (10-60% plus rapide) essentielles pour l'IA.
# Ubuntu/Debian
sudo apt update
sudo apt install software-properties-common
sudo add-apt-repository ppa:deadsnakes/ppa
sudo apt install python3.11 python3.11-venv python3.11-dev
# Vérifier l'installation
python3.11 --version
# Télécharger depuis python.org et installer
# Ou utiliser winget
winget install Python.Python.3.11
# Vérifier
python --version
2. Installation de Conda/Miniconda
Conda est essentiel pour gérer les dépendances complexes des projets IA, notamment les bibliothèques binaires comme PyTorch avec CUDA.
# Télécharger Miniconda
wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh
# Installer
bash Miniconda3-latest-Linux-x86_64.sh
# Créer un environnement pour l'IA
conda create -n ia-env python=3.11
conda activate ia-env
# Vérifier
conda list
3. Configuration GPU: CUDA & cuDNN
L'entraînement de modèles IA sur GPU est 10-100x plus rapide que sur CPU. CUDA est indispensable.
# Vérifier la carte NVIDIA
nvidia-smi
# Installer CUDA Toolkit 12.1 (pour PyTorch 2.x)
wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64/cuda-keyring_1.0-1_all.deb
sudo dpkg -i cuda-keyring_1.0-1_all.deb
sudo apt-get update
sudo apt-get -y install cuda
# Ajouter au PATH
echo 'export PATH=/usr/local/cuda/bin:$PATH' >> ~/.bashrc
echo 'export LD_LIBRARY_PATH=/usr/local/cuda/lib64:$LD_LIBRARY_PATH' >> ~/.bashrc
source ~/.bashrc
# Vérifier CUDA
nvcc --version
# Installer PyTorch avec support CUDA
conda install pytorch torchvision torchaudio pytorch-cuda=12.1 -c pytorch -c nvidia
# Tester CUDA dans Python
import torch
print(f"CUDA disponible: {torch.cuda.is_available()}")
print(f"Nombre de GPUs: {torch.cuda.device_count()}")
print(f"GPU actuel: {torch.cuda.get_device_name(0)}")
# Test de calcul GPU
x = torch.rand(5, 3).cuda()
print(x)
4. Configuration WSL2 (Windows uniquement)
WSL2 permet d'exécuter un véritable noyau Linux sur Windows, offrant de meilleures performances pour les workloads IA.
# Installer WSL2
wsl --install
# Installer Ubuntu
wsl --install -d Ubuntu-22.04
# Configurer WSL2 comme version par défaut
wsl --set-default-version 2
# Accéder à WSL
wsl
# Dans WSL, installer les outils IA
sudo apt update && sudo apt upgrade -y
sudo apt install build-essential git wget curl
5. VS Code et Extensions
Visual Studio Code est l'IDE le plus populaire pour l'IA grâce à son écosystème d'extensions.
- Python (ms-python.python) - Support complet Python
- Pylance (ms-python.vscode-pylance) - IntelliSense rapide
- Jupyter (ms-toolsai.jupyter) - Notebooks intégrés
- GitHub Copilot - Assistant IA pour le code
- Python Indent - Indentation correcte automatique
# Installer les extensions via CLI
code --install-extension ms-python.python
code --install-extension ms-python.vscode-pylance
code --install-extension ms-toolsai.jupyter
code --install-extension GitHub.copilot
6. Architecture de l'Environnement
┌─────────────────────────────────────────────────────┐
│ ENVIRONNEMENT IA COMPLET │
├─────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ Python │──────│ Conda │ │
│ │ 3.11+ │ │ Miniconda │ │
│ └─────────────┘ └─────────────┘ │
│ │ │ │
│ └──────────┬──────────┘ │
│ │ │
│ ┌──────────▼──────────┐ │
│ │ Environnements │ │
│ │ Virtuels │ │
│ │ (ia-env, dl-env) │ │
│ └──────────┬──────────┘ │
│ │ │
│ ┌──────────▼──────────┐ │
│ │ Packages IA │ │
│ │ PyTorch, NumPy, │ │
│ │ Pandas, Jupyter │ │
│ └──────────┬──────────┘ │
│ │ │
│ ┌──────────▼──────────┐ │
│ │ CUDA Toolkit │ │
│ │ cuDNN Libraries │ │
│ └──────────┬──────────┘ │
│ │ │
│ ┌──────────▼──────────┐ │
│ │ GPU NVIDIA │ │
│ │ (GeForce/Tesla) │ │
│ └─────────────────────┘ │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ VS Code IDE │ │
│ │ Extensions: Python, Jupyter │ │
│ └─────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────┘
7. Vérification Finale
Script Python complet pour vérifier que tout fonctionne correctement:
import sys
import torch
import numpy as np
import pandas as pd
import matplotlib
import jupyter
def check_environment():
"""Vérification complète de l'environnement IA"""
print("="*50)
print("VÉRIFICATION ENVIRONNEMENT IA")
print("="*50)
# Python version
print(f"\n✓ Python: {sys.version}")
# PyTorch et CUDA
print(f"\n✓ PyTorch: {torch.__version__}")
print(f" - CUDA disponible: {torch.cuda.is_available()}")
if torch.cuda.is_available():
print(f" - GPU: {torch.cuda.get_device_name(0)}")
print(f" - CUDA version: {torch.version.cuda}")
# NumPy
print(f"\n✓ NumPy: {np.__version__}")
# Pandas
print(f"\n✓ Pandas: {pd.__version__}")
# Matplotlib
print(f"\n✓ Matplotlib: {matplotlib.__version__}")
# Test de calcul GPU
if torch.cuda.is_available():
print("\n[TEST GPU]")
x = torch.rand(1000, 1000).cuda()
y = torch.rand(1000, 1000).cuda()
import time
start = time.time()
z = torch.matmul(x, y)
torch.cuda.synchronize()
elapsed = time.time() - start
print(f"✓ Multiplication matricielle 1000x1000: {elapsed*1000:.2f}ms")
print("\n" + "="*50)
print("ENVIRONNEMENT PRÊT POUR L'IA!")
print("="*50)
if __name__ == "__main__":
check_environment()
Lab: Configuration Pratique
- Créer un environnement conda nommé ia-foundation
- Installer PyTorch avec support CUDA
- Installer NumPy, Pandas, Matplotlib, Jupyter
- Créer un notebook Jupyter et tester l'import de toutes les bibliothèques
- Exécuter le script de vérification ci-dessus
- Prendre une capture d'écran de nvidia-smi montrant votre GPU
Lesson 1: Python Essentiel pour l'IA
Objectifs d'apprentissage
- Maîtriser les fonctions avancées, décorateurs et générateurs
- Utiliser les compréhensions de liste/dict pour un code efficace
- Appliquer les concepts OOP essentiels pour le ML
- Écrire du code Python idiomatique et performant
1. Fonctions Avancées et Type Hints
Les type hints améliorent la lisibilité et permettent la détection précoce d'erreurs.
from typing import List, Dict, Tuple, Optional, Union
import numpy as np
def preprocess_data(
data: np.ndarray,
normalize: bool = True,
mean: Optional[float] = None,
std: Optional[float] = None
) -> Tuple[np.ndarray, Dict[str, float]]:
"""
Prétraite les données pour l'entraînement ML.
Args:
data: Array NumPy des données brutes
normalize: Si True, normalise les données
mean: Moyenne pour la normalisation (calculée si None)
std: Écart-type pour la normalisation (calculé si None)
Returns:
Tuple contenant les données traitées et les statistiques
"""
stats = {}
if normalize:
if mean is None:
mean = data.mean()
if std is None:
std = data.std()
data = (data - mean) / (std + 1e-8) # epsilon pour éviter division par 0
stats = {"mean": float(mean), "std": float(std)}
return data, stats
# Utilisation
data = np.random.randn(1000, 10)
processed_data, stats = preprocess_data(data, normalize=True)
print(f"Stats: {stats}")
2. Décorateurs pour le ML
Les décorateurs permettent d'ajouter des fonctionnalités sans modifier le code original.
import time
import functools
from typing import Callable
def timing_decorator(func: Callable) -> Callable:
"""Décorateur pour mesurer le temps d'exécution"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
elapsed = time.time() - start
print(f"{func.__name__} exécuté en {elapsed:.4f}s")
return result
return wrapper
def cache_decorator(func: Callable) -> Callable:
"""Décorateur pour mettre en cache les résultats"""
cache = {}
@functools.wraps(func)
def wrapper(*args):
if args in cache:
print(f"Cache hit pour {args}")
return cache[args]
result = func(*args)
cache[args] = result
return result
return wrapper
# Utilisation en ML
@timing_decorator
def train_model(epochs: int, batch_size: int):
"""Simule l'entraînement d'un modèle"""
time.sleep(0.1) # Simule le calcul
return {"loss": 0.05, "accuracy": 0.95}
@cache_decorator
def expensive_computation(n: int) -> int:
"""Calcul coûteux avec cache"""
time.sleep(0.5)
return sum(range(n))
# Test
model_metrics = train_model(10, 32)
result1 = expensive_computation(1000000)
result2 = expensive_computation(1000000) # Utilise le cache
3. Générateurs pour les Gros Datasets
Les générateurs permettent de traiter de grandes quantités de données sans tout charger en mémoire.
from typing import Generator
import numpy as np
def data_generator(
file_path: str,
batch_size: int = 32,
shuffle: bool = True
) -> Generator[Tuple[np.ndarray, np.ndarray], None, None]:
"""
Générateur de batches pour l'entraînement.
Simule la lecture d'un gros fichier.
"""
# En pratique, vous liriez depuis un fichier
total_samples = 10000
while True: # Loop infini pour les epochs
indices = np.arange(total_samples)
if shuffle:
np.random.shuffle(indices)
for start_idx in range(0, total_samples, batch_size):
batch_indices = indices[start_idx:start_idx + batch_size]
# Simuler le chargement des données
X = np.random.randn(len(batch_indices), 784) # Images 28x28
y = np.random.randint(0, 10, len(batch_indices)) # Labels
yield X, y
# Utilisation
gen = data_generator(batch_size=32)
# Entraîner sur 3 batches
for i, (X_batch, y_batch) in enumerate(gen):
print(f"Batch {i}: X shape = {X_batch.shape}, y shape = {y_batch.shape}")
if i >= 2: # Arrêter après 3 batches
break
4. Compréhensions de Liste et Dictionnaire
# Liste comprehension - plus rapide que les boucles
# Filtrer et transformer des données
data = list(range(1000))
squared_evens = [x**2 for x in data if x % 2 == 0]
# Dict comprehension - créer des mappings
label_to_idx = {label: idx for idx, label in enumerate(['cat', 'dog', 'bird'])}
# {'cat': 0, 'dog': 1, 'bird': 2}
# Nested comprehension - matrices
matrix = [[i * j for j in range(5)] for i in range(5)]
# Set comprehension - valeurs uniques
unique_classes = {label for _, label in dataset}
# Exemple ML: normaliser un batch
batch = np.random.randn(32, 784)
normalized = [(sample - sample.mean()) / sample.std() for sample in batch]
# One-hot encoding avec compréhension
num_classes = 10
labels = [3, 1, 5, 2]
one_hot = [[1 if i == label else 0 for i in range(num_classes)] for label in labels]
5. Classes et OOP pour le ML
from abc import ABC, abstractmethod
import numpy as np
class BaseModel(ABC):
"""Classe de base pour tous les modèles ML"""
def __init__(self, name: str):
self.name = name
self.is_trained = False
@abstractmethod
def fit(self, X: np.ndarray, y: np.ndarray):
"""Entraîner le modèle"""
pass
@abstractmethod
def predict(self, X: np.ndarray) -> np.ndarray:
"""Faire des prédictions"""
pass
def __repr__(self):
return f"{self.__class__.__name__}(name='{self.name}')"
class LinearRegression(BaseModel):
"""Régression linéaire simple"""
def __init__(self, name: str = "LinearReg"):
super().__init__(name)
self.weights = None
self.bias = None
def fit(self, X: np.ndarray, y: np.ndarray):
"""Entraîner avec la méthode des moindres carrés"""
# Ajouter une colonne de 1 pour le bias
X_b = np.c_[np.ones((X.shape[0], 1)), X]
# Solution analytique: (X^T X)^-1 X^T y
theta = np.linalg.inv(X_b.T @ X_b) @ X_b.T @ y
self.bias = theta[0]
self.weights = theta[1:]
self.is_trained = True
return self
def predict(self, X: np.ndarray) -> np.ndarray:
"""Prédictions"""
if not self.is_trained:
raise ValueError("Modèle non entraîné! Appelez fit() d'abord.")
return X @ self.weights + self.bias
# Utilisation
X = np.random.randn(100, 5)
y = X @ np.array([1, 2, -1, 0.5, 3]) + 2 + np.random.randn(100) * 0.1
model = LinearRegression("MonModele")
model.fit(X, y)
predictions = model.predict(X[:5])
print(f"Modèle: {model}")
print(f"Prédictions: {predictions}")
6. Lambda et Fonctions d'Ordre Supérieur
# Lambda pour des transformations rapides
data = [1, 2, 3, 4, 5]
squared = list(map(lambda x: x**2, data))
evens = list(filter(lambda x: x % 2 == 0, data))
# Cas d'usage ML: appliquer des augmentations
from typing import Callable
augmentations = [
lambda img: img * 0.8, # Diminuer luminosité
lambda img: np.flip(img), # Flip horizontal
lambda img: img + np.random.randn(*img.shape) * 0.01 # Bruit
]
def apply_augmentations(image: np.ndarray, augs: List[Callable]) -> np.ndarray:
"""Applique une liste d'augmentations"""
for aug in augs:
image = aug(image)
return image
# Test
image = np.random.rand(28, 28)
augmented = apply_augmentations(image, augmentations[:2])
Quiz: Python pour l'IA
1. Pourquoi utiliser des générateurs plutôt que des listes pour les gros datasets?
2. Quel est l'avantage principal des type hints?
Lesson 2: NumPy & Calcul Matriciel
Objectifs d'apprentissage
- Maîtriser les arrays NumPy et leurs opérations vectorisées
- Comprendre le broadcasting pour des calculs efficaces
- Effectuer des multiplications matricielles pour le ML
- Optimiser les performances avec NumPy
1. Arrays, Shapes et Dtypes
import numpy as np
# Créer des arrays
a = np.array([1, 2, 3, 4, 5])
b = np.zeros((3, 4)) # 3x4 de zéros
c = np.ones((2, 3, 4)) # 2x3x4 de uns
d = np.eye(5) # Matrice identité 5x5
e = np.arange(0, 10, 0.5) # Séquence de 0 à 10 par pas de 0.5
f = np.linspace(0, 1, 100) # 100 valeurs entre 0 et 1
# Shapes et dimensions
print(f"Shape de a: {a.shape}") # (5,)
print(f"Dimensions de c: {c.ndim}") # 3
print(f"Taille totale: {c.size}") # 24
# Dtypes - critique pour la mémoire et la précision
float32_arr = np.array([1.0, 2.0], dtype=np.float32) # 4 bytes/élément
float64_arr = np.array([1.0, 2.0], dtype=np.float64) # 8 bytes/élément
int8_arr = np.array([1, 2], dtype=np.int8) # 1 byte/élément
# En ML, float32 est le standard (balance précision/mémoire)
weights = np.random.randn(1000, 1000).astype(np.float32)
print(f"Mémoire utilisée: {weights.nbytes / 1e6:.2f} MB")
# Reshape - essentiel pour le ML
flat = np.arange(12)
matrix = flat.reshape(3, 4)
tensor = flat.reshape(2, 2, 3)
# Attention: reshape retourne une vue, pas une copie!
matrix[0, 0] = 999
print(flat[0]) # 999 aussi!
2. Broadcasting - La Magie de NumPy
RÈGLES DE BROADCASTING:
1. Si les arrays ont des dimensions différentes,
préfixer avec des 1 jusqu'à égalisation
2. Les dimensions sont compatibles si:
- Elles sont égales
- L'une d'elles est 1
Exemples:
A: (3, 4) B: (4,)
B devient: (1, 4)
Résultat: (3, 4)
A: (3, 1, 4) B: (3, 5, 1)
Résultat: (3, 5, 4)
import numpy as np
# Exemple 1: Normalisation de batch
# Shape: (batch_size, features)
batch = np.random.randn(32, 128)
# Calculer mean et std par feature
mean = batch.mean(axis=0, keepdims=True) # Shape: (1, 128)
std = batch.std(axis=0, keepdims=True) # Shape: (1, 128)
# Broadcasting: (32, 128) - (1, 128) -> (32, 128)
normalized = (batch - mean) / (std + 1e-8)
# Exemple 2: Calcul de distances
points = np.random.randn(100, 2) # 100 points 2D
center = np.array([[0, 0]]) # Centre
# Broadcasting: (100, 2) - (1, 2) -> (100, 2)
distances = np.sqrt(((points - center) ** 2).sum(axis=1))
# Exemple 3: Softmax avec numerical stability
logits = np.random.randn(10, 5) # 10 échantillons, 5 classes
# Max par ligne pour la stabilité numérique
max_logits = logits.max(axis=1, keepdims=True) # (10, 1)
# Broadcasting dans l'exponentielle
exp_logits = np.exp(logits - max_logits) # (10, 5)
softmax = exp_logits / exp_logits.sum(axis=1, keepdims=True)
print(f"Softmax shape: {softmax.shape}")
print(f"Somme par ligne: {softmax.sum(axis=1)}") # Toutes égales à 1
3. Multiplication Matricielle
import numpy as np
# Dot product (produit scalaire)
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
dot = np.dot(a, b) # 1*4 + 2*5 + 3*6 = 32
# Matrix multiplication
A = np.random.randn(3, 4)
B = np.random.randn(4, 5)
C = A @ B # Opérateur @ (Python 3.5+)
# ou: C = np.matmul(A, B)
# ou: C = np.dot(A, B)
# Forward pass d'une couche neuronale
batch_size = 32
input_dim = 784 # 28x28 image aplatie
hidden_dim = 128
X = np.random.randn(batch_size, input_dim)
W = np.random.randn(input_dim, hidden_dim) * 0.01
b = np.zeros((1, hidden_dim))
# Linear: Y = XW + b
# Shapes: (32, 784) @ (784, 128) + (1, 128) = (32, 128)
Y = X @ W + b
# ReLU activation
Y_relu = np.maximum(0, Y)
print(f"Input shape: {X.shape}")
print(f"Weights shape: {W.shape}")
print(f"Output shape: {Y_relu.shape}")
# Batch matrix multiplication avec einsum
# Plus flexible et lisible pour des opérations complexes
A = np.random.randn(10, 3, 4) # 10 matrices 3x4
B = np.random.randn(10, 4, 5) # 10 matrices 4x5
# Multiplier chaque paire de matrices
C = np.einsum('bij,bjk->bik', A, B) # Résultat: (10, 3, 5)
# Attention mechanism simplifié
Q = np.random.randn(32, 10, 64) # Queries: (batch, seq_len, dim)
K = np.random.randn(32, 10, 64) # Keys
V = np.random.randn(32, 10, 64) # Values
# Attention scores: Q @ K^T / sqrt(d)
scores = np.einsum('bqd,bkd->bqk', Q, K) / np.sqrt(64)
attn = np.exp(scores) / np.exp(scores).sum(axis=-1, keepdims=True)
# Apply attention to values
output = np.einsum('bqk,bkd->bqd', attn, V)
print(f"Attention output: {output.shape}")
4. Opérations Vectorisées vs Boucles
import numpy as np
import time
# Comparaison de performance
n = 1000000
a = np.random.randn(n)
b = np.random.randn(n)
# Méthode 1: Boucle Python (LENT!)
start = time.time()
result_loop = []
for i in range(n):
result_loop.append(a[i] * b[i])
loop_time = time.time() - start
# Méthode 2: Vectorisé NumPy (RAPIDE!)
start = time.time()
result_vec = a * b
vec_time = time.time() - start
print(f"Boucle Python: {loop_time:.4f}s")
print(f"Vectorisé NumPy: {vec_time:.4f}s")
print(f"Speedup: {loop_time/vec_time:.0f}x plus rapide!")
# Cas pratique: calculer les distances euclidiennes
points = np.random.randn(1000, 100) # 1000 points de dim 100
# MAUVAIS: Double boucle
start = time.time()
distances_loop = np.zeros((1000, 1000))
for i in range(1000):
for j in range(1000):
distances_loop[i, j] = np.sqrt(((points[i] - points[j]) ** 2).sum())
loop_time = time.time() - start
# BON: Vectorisé avec broadcasting
start = time.time()
# Expand dimensions: (1000, 1, 100) - (1, 1000, 100)
diff = points[:, np.newaxis, :] - points[np.newaxis, :, :]
distances_vec = np.sqrt((diff ** 2).sum(axis=2))
vec_time = time.time() - start
print(f"\nDistances - Boucle: {loop_time:.4f}s")
print(f"Distances - Vectorisé: {vec_time:.4f}s")
print(f"Speedup: {loop_time/vec_time:.0f}x!")
5. Random Generation pour le ML
import numpy as np
# Seed pour la reproductibilité
np.random.seed(42)
# Distributions courantes en ML
uniform = np.random.rand(100) # Uniforme [0, 1)
normal = np.random.randn(100) # Normale N(0, 1)
normal_scaled = np.random.normal(5, 2, 100) # N(5, 2^2)
# Initialisation de poids (Xavier/Glorot)
fan_in = 784
fan_out = 128
xavier_weights = np.random.randn(fan_in, fan_out) * np.sqrt(2.0 / (fan_in + fan_out))
# Initialisation He (pour ReLU)
he_weights = np.random.randn(fan_in, fan_out) * np.sqrt(2.0 / fan_in)
# Génération de données synthétiques
X = np.random.randn(1000, 20)
true_weights = np.random.randn(20, 1)
noise = np.random.randn(1000, 1) * 0.1
y = X @ true_weights + noise
# Dropout mask
keep_prob = 0.8
dropout_mask = np.random.rand(32, 128) < keep_prob
# Appliquer: activations * dropout_mask / keep_prob
# Data augmentation - random flips
batch = np.random.randn(32, 28, 28)
flip_mask = np.random.rand(32) > 0.5
batch[flip_mask] = batch[flip_mask, :, ::-1] # Flip horizontal
Lab Pratique: Implémentation d'une Couche Dense
- Créer une classe DenseLayer avec forward pass
- Initialiser les poids avec Xavier initialization
- Implémenter le forward pass: Y = XW + b
- Ajouter l'activation ReLU
- Tester avec un batch de 64 échantillons
- Mesurer le temps d'exécution pour 1000 forward passes
Lesson 3: Pandas & Manipulation de Données
Objectifs d'apprentissage
- Maîtriser les DataFrames et Series pour l'analyse de données
- Nettoyer et transformer des données réelles
- Utiliser groupby, merge et pivot pour l'agrégation
- Préparer des données pour l'entraînement ML
1. DataFrames et Series Fondamentaux
import pandas as pd
import numpy as np
# Créer un DataFrame
data = {
'user_id': range(1, 101),
'age': np.random.randint(18, 70, 100),
'revenue': np.random.exponential(100, 100),
'country': np.random.choice(['US', 'UK', 'FR', 'DE'], 100),
'subscribed': np.random.choice([True, False], 100, p=[0.3, 0.7])
}
df = pd.DataFrame(data)
# Inspection rapide
print(df.head())
print(df.info())
print(df.describe())
# Indexing et slicing
print(df.loc[0:5, ['age', 'revenue']]) # Par label
print(df.iloc[0:5, 1:3]) # Par position
print(df[df['age'] > 40]) # Filtrage booléen
# Ajouter des colonnes calculées
df['age_group'] = pd.cut(df['age'], bins=[0, 30, 50, 100],
labels=['young', 'middle', 'senior'])
df['revenue_log'] = np.log1p(df['revenue']) # log(1+x) pour éviter log(0)
df['high_value'] = (df['revenue'] > df['revenue'].median()) & df['subscribed']
print(df.head())
2. Nettoyage des Données
import pandas as pd
import numpy as np
# Créer des données avec des problèmes typiques
df = pd.DataFrame({
'id': range(1, 1001),
'temperature': np.random.randn(1000) * 10 + 20,
'humidity': np.random.rand(1000) * 100,
'label': np.random.choice(['A', 'B', 'C'], 1000)
})
# Introduire des valeurs manquantes
df.loc[np.random.choice(1000, 50, replace=False), 'temperature'] = np.nan
df.loc[np.random.choice(1000, 30, replace=False), 'humidity'] = np.nan
# Introduire des outliers
df.loc[np.random.choice(1000, 10, replace=False), 'temperature'] = 1000
print(f"Valeurs manquantes:\n{df.isnull().sum()}")
# Stratégie 1: Supprimer les lignes avec NaN
df_dropped = df.dropna()
print(f"Lignes après suppression: {len(df_dropped)}")
# Stratégie 2: Imputation
df_filled = df.copy()
df_filled['temperature'].fillna(df_filled['temperature'].mean(), inplace=True)
df_filled['humidity'].fillna(df_filled['humidity'].median(), inplace=True)
# Stratégie 3: Forward/Backward fill (pour séries temporelles)
df_ffill = df.fillna(method='ffill')
# Détection d'outliers avec IQR
Q1 = df['temperature'].quantile(0.25)
Q3 = df['temperature'].quantile(0.75)
IQR = Q3 - Q1
outliers = (df['temperature'] < Q1 - 1.5*IQR) | (df['temperature'] > Q3 + 1.5*IQR)
print(f"Outliers détectés: {outliers.sum()}")
# Traiter les outliers
df_clean = df.copy()
df_clean.loc[outliers, 'temperature'] = df_clean['temperature'].median()
# Conversion de types
df_clean['label'] = df_clean['label'].astype('category')
print(f"\nDataFrame nettoyé:\n{df_clean.info()}")
- Mean/Median: Pour données continues sans tendance temporelle
- Mode: Pour données catégorielles
- Forward fill: Pour séries temporelles
- Prédiction: Utiliser un modèle ML pour imputer (MICE, KNN)
- Feature engineering: Créer une colonne booléenne "was_missing"
3. GroupBy et Agrégation
import pandas as pd
import numpy as np
# Dataset de transactions
transactions = pd.DataFrame({
'user_id': np.random.randint(1, 101, 1000),
'product': np.random.choice(['A', 'B', 'C', 'D'], 1000),
'amount': np.random.exponential(50, 1000),
'timestamp': pd.date_range('2024-01-01', periods=1000, freq='H')
})
# GroupBy simple
user_stats = transactions.groupby('user_id').agg({
'amount': ['sum', 'mean', 'count'],
'product': lambda x: x.nunique()
})
user_stats.columns = ['total_spent', 'avg_transaction', 'num_transactions', 'num_products']
print(user_stats.head())
# GroupBy multiple
product_user_stats = transactions.groupby(['product', 'user_id'])['amount'].sum()
print(product_user_stats.head())
# Apply custom function
def user_segment(group):
total = group['amount'].sum()
if total > 500:
return 'high'
elif total > 200:
return 'medium'
else:
return 'low'
segments = transactions.groupby('user_id').apply(user_segment)
transactions['segment'] = transactions['user_id'].map(segments)
# Feature engineering avec groupby
# RFM analysis (Recency, Frequency, Monetary)
now = transactions['timestamp'].max()
rfm = transactions.groupby('user_id').agg({
'timestamp': lambda x: (now - x.max()).days, # Recency
'user_id': 'count', # Frequency
'amount': 'sum' # Monetary
})
rfm.columns = ['recency', 'frequency', 'monetary']
print(rfm.head())
# Rolling window pour séries temporelles
transactions = transactions.sort_values('timestamp')
transactions['rolling_avg'] = transactions.groupby('user_id')['amount'].transform(
lambda x: x.rolling(window=10, min_periods=1).mean()
)
4. Merge, Join et Concatenation
import pandas as pd
# Tables d'exemple
users = pd.DataFrame({
'user_id': [1, 2, 3, 4, 5],
'name': ['Alice', 'Bob', 'Charlie', 'David', 'Eve'],
'country': ['US', 'UK', 'FR', 'DE', 'US']
})
orders = pd.DataFrame({
'order_id': [101, 102, 103, 104, 105],
'user_id': [1, 2, 2, 3, 6], # Note: user 6 n'existe pas
'amount': [100, 200, 150, 300, 50]
})
# Inner join (par défaut)
inner = pd.merge(users, orders, on='user_id', how='inner')
print(f"Inner join: {len(inner)} lignes")
# Left join (garder tous les users)
left = pd.merge(users, orders, on='user_id', how='left')
print(f"Left join: {len(left)} lignes")
# Right join (garder tous les orders)
right = pd.merge(users, orders, on='user_id', how='right')
print(f"Right join: {len(right)} lignes")
# Outer join (garder tout)
outer = pd.merge(users, orders, on='user_id', how='outer')
print(f"Outer join: {len(outer)} lignes")
# Merge sur des colonnes différentes
df1 = pd.DataFrame({'id1': [1, 2], 'val': ['a', 'b']})
df2 = pd.DataFrame({'id2': [1, 2], 'val': ['c', 'd']})
merged = pd.merge(df1, df2, left_on='id1', right_on='id2')
# Concatenation verticale
df_a = pd.DataFrame({'x': [1, 2], 'y': [3, 4]})
df_b = pd.DataFrame({'x': [5, 6], 'y': [7, 8]})
concatenated = pd.concat([df_a, df_b], ignore_index=True)
# Concatenation horizontale
concat_h = pd.concat([df_a, df_b], axis=1)
5. Pipeline de Préparation pour le ML
import pandas as pd
import numpy as np
from sklearn.preprocessing import LabelEncoder, StandardScaler
from sklearn.model_selection import train_test_split
# Charger des données
np.random.seed(42)
df = pd.DataFrame({
'age': np.random.randint(18, 70, 1000),
'income': np.random.exponential(50000, 1000),
'education': np.random.choice(['HS', 'BS', 'MS', 'PhD'], 1000),
'city': np.random.choice(['NYC', 'LA', 'SF', 'CHI'], 1000),
'target': np.random.choice([0, 1], 1000, p=[0.7, 0.3])
})
# Introduire des valeurs manquantes
df.loc[np.random.choice(1000, 50, replace=False), 'income'] = np.nan
print("Pipeline de préparation ML:")
print("="*50)
# 1. Gestion des valeurs manquantes
df['income'].fillna(df['income'].median(), inplace=True)
print(f"✓ Valeurs manquantes traitées")
# 2. Encodage des variables catégorielles
# Label Encoding pour l'éducation (ordinale)
education_map = {'HS': 0, 'BS': 1, 'MS': 2, 'PhD': 3}
df['education_encoded'] = df['education'].map(education_map)
# One-Hot Encoding pour la ville (nominale)
df = pd.get_dummies(df, columns=['city'], prefix='city')
print(f"✓ Variables catégorielles encodées")
# 3. Feature engineering
df['income_log'] = np.log1p(df['income'])
df['age_squared'] = df['age'] ** 2
df['age_group'] = pd.cut(df['age'], bins=[0, 30, 50, 100], labels=[0, 1, 2])
print(f"✓ Features engineered")
# 4. Normalisation des features numériques
numeric_cols = ['age', 'income_log', 'age_squared']
scaler = StandardScaler()
df[numeric_cols] = scaler.fit_transform(df[numeric_cols])
print(f"✓ Features normalisées")
# 5. Split train/test
feature_cols = [col for col in df.columns if col not in ['target', 'education', 'age_group']]
X = df[feature_cols].values
y = df['target'].values
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
print(f"✓ Split effectué")
print(f"\nX_train shape: {X_train.shape}")
print(f"X_test shape: {X_test.shape}")
print(f"Distribution des classes (train): {np.bincount(y_train)}")
- Split AVANT preprocessing: Éviter le data leakage
- Fit sur train only: scaler.fit(X_train), puis scaler.transform(X_test)
- Stratify: Toujours utiliser stratify pour les classes déséquilibrées
- Seed fixe: Pour la reproductibilité
Lesson 23: Examen Final - Phase 1
Objectifs d'évaluation
- Démontrer la maîtrise de Python, NumPy, Pandas
- Appliquer les concepts mathématiques pour l'IA
- Construire et entraîner un modèle deep learning complet
- Évaluer et améliorer les performances d'un modèle
Partie 1: Quiz Théorique (40 points)
Questions à Choix Multiples
1. Pourquoi utilise-t-on float32 plutôt que float64 en deep learning?
2. Quelle est la fonction de perte appropriée pour une classification multi-classe?
3. Quel problème résout l'attention dans les Transformers?
4. Que signifie un learning rate trop élevé?
5. Pourquoi normalise-t-on les features avant l'entraînement?
6. Quelle métrique utiliser pour un problème déséquilibré (1% de classe positive)?
7. Qu'est-ce que le broadcasting en NumPy?
8. Pourquoi utilise-t-on dropout pendant l'entraînement?
9. Quelle est la différence principale entre RNN et Transformers?
10. Que fait le positional encoding dans les Transformers?
11. Pourquoi PyTorch utilise-t-il autograd?
12. Que signifie "epoch" en deep learning?
Partie 2: Projet Pratique (60 points)
Projet: Classificateur d'Images CIFAR-10
Construisez un pipeline ML complet de A à Z.
- Chargement des données (5 pts): Utiliser torchvision pour charger CIFAR-10
- Prétraitement (10 pts): Normalisation, augmentation (flip, crop), DataLoader
- Architecture (15 pts): Concevoir un CNN avec au moins 3 couches conv, batch norm, dropout
- Entraînement (15 pts): Loop d'entraînement avec loss tracking, validation, early stopping
- Évaluation (10 pts): Accuracy, matrice de confusion, courbes loss/accuracy
- Optimisation (5 pts): Essayer différents hyperparamètres, documenter les résultats
Code de démarrage:
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader
import torchvision
import torchvision.transforms as transforms
import matplotlib.pyplot as plt
import numpy as np
# 1. Configuration
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f"Utilisation de: {device}")
# 2. Préparation des données
transform_train = transforms.Compose([
transforms.RandomCrop(32, padding=4),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
transforms.Normalize((0.4914, 0.4822, 0.4465),
(0.2023, 0.1994, 0.2010))
])
transform_test = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.4914, 0.4822, 0.4465),
(0.2023, 0.1994, 0.2010))
])
train_dataset = torchvision.datasets.CIFAR10(
root='./data', train=True, download=True, transform=transform_train
)
test_dataset = torchvision.datasets.CIFAR10(
root='./data', train=False, download=True, transform=transform_test
)
train_loader = DataLoader(train_dataset, batch_size=128, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=128, shuffle=False)
# 3. Définir votre architecture CNN
class CIFAR10Net(nn.Module):
def __init__(self):
super(CIFAR10Net, self).__init__()
# TODO: Définir vos couches
# Exemple de structure:
# - Conv2d, BatchNorm2d, ReLU, MaxPool2d
# - Conv2d, BatchNorm2d, ReLU, MaxPool2d
# - Conv2d, BatchNorm2d, ReLU, MaxPool2d
# - Flatten, Linear, Dropout, Linear
pass
def forward(self, x):
# TODO: Définir le forward pass
pass
# 4. Initialiser le modèle
model = CIFAR10Net().to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=200)
# 5. Fonction d'entraînement
def train_epoch(model, loader, criterion, optimizer, device):
model.train()
total_loss = 0
correct = 0
total = 0
for inputs, targets in loader:
inputs, targets = inputs.to(device), targets.to(device)
# TODO: Forward, backward, optimize
pass
return total_loss / len(loader), 100. * correct / total
# 6. Fonction d'évaluation
def evaluate(model, loader, criterion, device):
model.eval()
total_loss = 0
correct = 0
total = 0
with torch.no_grad():
for inputs, targets in loader:
inputs, targets = inputs.to(device), targets.to(device)
# TODO: Évaluer
pass
return total_loss / len(loader), 100. * correct / total
# 7. Boucle d'entraînement principale
num_epochs = 100
best_acc = 0
train_losses, test_losses = [], []
train_accs, test_accs = [], []
for epoch in range(num_epochs):
train_loss, train_acc = train_epoch(model, train_loader,
criterion, optimizer, device)
test_loss, test_acc = evaluate(model, test_loader,
criterion, device)
train_losses.append(train_loss)
test_losses.append(test_loss)
train_accs.append(train_acc)
test_accs.append(test_acc)
if test_acc > best_acc:
best_acc = test_acc
torch.save(model.state_dict(), 'best_model.pth')
if (epoch + 1) % 10 == 0:
print(f'Epoch [{epoch+1}/{num_epochs}]')
print(f'Train Loss: {train_loss:.4f}, Train Acc: {train_acc:.2f}%')
print(f'Test Loss: {test_loss:.4f}, Test Acc: {test_acc:.2f}%')
scheduler.step()
print(f'\nMeilleure précision test: {best_acc:.2f}%')
# 8. Visualisation des résultats
# TODO: Tracer les courbes de loss et accuracy
- Code propre et bien commenté (10%)
- Architecture appropriée (25%)
- Entraînement fonctionnel (25%)
- Évaluation complète (20%)
- Analyse des résultats (20%)
Soumission
Votre soumission doit inclure:
- Notebook Jupyter complet avec tout le code
- README expliquant vos choix d'architecture et d'hyperparamètres
- Graphiques de loss et accuracy (train/test)
- Matrice de confusion sur le test set
- Analyse critique: ce qui a marché, ce qui n'a pas marché, améliorations possibles