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.

En 30 ans de carrière, j'ai vu des centaines de projets échouer avant même de commencer à cause d'un environnement mal configuré. Un développeur passe en moyenne 20% de son temps à résoudre des problèmes d'environnement. Prenez le temps de bien configurer maintenant, vous gagnerez des semaines plus tard.

1. Installation de Python 3.11+

Python 3.11 apporte des améliorations significatives de performance (10-60% plus rapide) essentielles pour l'IA.

Bash (Linux/Mac)
# 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
                        
PowerShell (Windows)
# 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.

Bash
# 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
                        
Tip: Utilisez toujours des environnements virtuels séparés pour chaque projet. Cela évite les conflits de dépendances qui peuvent ruiner des jours de travail.

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.

Bash
# 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
                        
Python
# 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)
                        
Attention: La version de CUDA doit correspondre à la version PyTorch. PyTorch 2.x nécessite CUDA 11.8 ou 12.1. Vérifiez toujours la compatibilité sur pytorch.org.

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.

PowerShell (Administrateur)
# 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.

Extensions essentielles:
  • 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
Bash
# 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:

Python
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
Le code que vous écrivez aujourd'hui, vous le relirez dans 6 mois. En IA, la reproductibilité est critique. Un code propre et bien structuré n'est pas un luxe, c'est une nécessité. J'ai vu trop de modèles prometteurs abandonnés car personne ne comprenait le code spaghetti original.

1. Fonctions Avancées et Type Hints

Les type hints améliorent la lisibilité et permettent la détection précoce d'erreurs.

Python
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.

Python
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.

Python
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
                        
Tip: Utilisez toujours des générateurs pour les datasets qui ne tiennent pas en mémoire. C'est la norme pour l'entraînement de LLMs et de modèles de vision sur de grandes échelles.

4. Compréhensions de Liste et Dictionnaire

Python
# 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

Python
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

Python
# 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])
                        
Important: En IA/ML, la qualité du code Python impacte directement la vitesse d'itération. Un code bien structuré avec des classes réutilisables vous permet d'expérimenter 10x plus vite.

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
NumPy est le fondement de tout le calcul scientifique en Python. Chaque framework ML - PyTorch, TensorFlow, JAX - est construit sur les mêmes principes. Maîtriser NumPy, c'est comprendre 80% des opérations que vous ferez en IA. Une opération vectorisée est typiquement 100x plus rapide qu'une boucle Python.

1. Arrays, Shapes et Dtypes

Python
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)
                        
Python
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
                        
Tip: Utilisez toujours keepdims=True lors des réductions (mean, sum, max) pour préserver les dimensions et faciliter le broadcasting.

3. Multiplication Matricielle

Python
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

Python
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!")
                        
Attention: Évitez les boucles Python sur des arrays NumPy à tout prix! Une règle simple: si vous écrivez for i in range(len(array)), vous faites probablement quelque chose de mal.

5. Random Generation pour le ML

Python
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
En production, 80% du temps d'un projet ML est consacré au nettoyage et à la préparation des données. Pandas est votre couteau suisse. J'ai vu des data scientists perdre des semaines à cause de données mal nettoyées qui produisaient des modèles qui semblaient marcher en dev mais échouaient en prod. La qualité des données détermine le plafond de performance de votre modèle.

1. DataFrames et Series Fondamentaux

Python
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

Python
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()}")
                        
Tip: Pour les valeurs manquantes en ML, considérez ces stratégies selon le contexte:
  • 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

Python
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

Python
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

Python
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)}")
                        
Important - Ordre des opérations:
  1. Split AVANT preprocessing: Éviter le data leakage
  2. Fit sur train only: scaler.fit(X_train), puis scaler.transform(X_test)
  3. Stratify: Toujours utiliser stratify pour les classes déséquilibrées
  4. 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
Cet examen est votre opportunité de synthétiser tout ce que vous avez appris. En 30 ans, j'ai constaté que les meilleurs praticiens ML ne sont pas ceux qui connaissent toutes les techniques, mais ceux qui savent choisir et appliquer les bonnes techniques au bon moment. Montrez que vous comprenez le "pourquoi" derrière chaque décision.

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:

Python
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
                        
Critères d'évaluation:
  • Code propre et bien commenté (10%)
  • Architecture appropriée (25%)
  • Entraînement fonctionnel (25%)
  • Évaluation complète (20%)
  • Analyse des résultats (20%)
Objectif de performance: Atteindre au moins 70% d'accuracy sur le test set.

Soumission

Votre soumission doit inclure:

  1. Notebook Jupyter complet avec tout le code
  2. README expliquant vos choix d'architecture et d'hyperparamètres
  3. Graphiques de loss et accuracy (train/test)
  4. Matrice de confusion sur le test set
  5. Analyse critique: ce qui a marché, ce qui n'a pas marché, améliorations possibles
Félicitations d'avoir atteint cette étape! Vous avez acquis les fondations essentielles de l'IA. La Phase 2 vous attend avec les LLMs, le fine-tuning et les techniques avancées. Souvenez-vous: l'IA n'est pas une destination mais un voyage d'apprentissage continu. Bonne chance!