Formation ML / Deep learning

Réseau de neurones from scratch

Intermédiaire 50 min 17 sections

Forward, perte, rétropropagation et descente de gradient écrits en NumPy pur, sans framework pour cacher le calcul.

Objectifs d'apprentissage

  • Comprendre l'architecture d'un réseau de neurones
  • Implémenter le forward pass (propagation avant)
  • Comprendre et coder la rétropropagation
  • Visualiser l'apprentissage du réseau
  • Maîtriser les fonctions d'activation (sigmoid, ReLU)

Prérequis

Notions de Python, algèbre linéaire de base (matrices, produit matriciel)

Le code de cette page s'exécute dans votre navigateur. Le premier lancement télécharge Python et ses bibliothèques.

Théorie

Qu'est-ce qu'un réseau de neurones?

Un réseau de neurones artificiel est un modèle inspire du cerveau humain. Il est composé de neurones organises en couches.

Architecture basique:

  • Couche d'entrée: reçoit les données (ex: pixels d'une image, features)
  • Couches cachées: transforment les données (apprentissage des patterns)
  • Couche de sortie: produit la prédiction (ex: probabilité de classe)

Le neurone artificiel:

Chaque neurone effectue 3 opérations:

  1. Somme pondérée: $z = w_1 \cdot x_1 + w_2 \cdot x_2 + ... + b$
  2. Fonction d'activation: $a = f(z)$
  3. Transmet le résultat aux neurones suivants

L'apprentissage consiste à ajuster les poids (w) et biais (b) pour minimiser l'erreur.

Théorie

Schéma: Architecture d'un réseau de neurones

Architecture du réseau (2 entrées --> 4 neurones caches --> 1 sortie):

flowchart LR subgraph Input["COUCHE D'ENTRÉE"] x1((x1)) x2((x2)) end subgraph Hidden["COUCHE CACHÉE"] h1((h1)) h2((h2)) h3((h3)) h4((h4)) end subgraph Output["COUCHE DE SORTIE"] y((y)) end x1 --> h1 & h2 & h3 & h4 x2 --> h1 & h2 & h3 & h4 h1 & h2 & h3 & h4 --> y y --> P["Prédiction"]

Le neurone artificiel en détail:

flowchart LR subgraph Entrees ["Entrées"] x1[x1] x2[x2] x3[x3] end subgraph Poids w1[w1] w2[w2] w3[w3] b[biais b] end x1 --- w1 x2 --- w2 x3 --- w3 w1 & w2 & w3 & b --> S["z = Σ wi·xi + b"] S --> F["f(z)"] F --> A[a = sortie]

Exemple concret avec code couleur - Calcul d'un neurone:

Entrées: $\textcolor{#3498db}{x_1 = 0.5}$, $\textcolor{#e67e22}{x_2 = 0.8}$, $\textcolor{#27ae60}{x_3 = 0.2}$ Poids: $\textcolor{#3498db}{w_1 = 0.4}$, $\textcolor{#e67e22}{w_2 = 0.3}$, $\textcolor{#27ae60}{w_3 = 0.9}$, $\textcolor{#9B7AC4}{b = 0.1}$

Étape 1 - Somme pondérée z:

$z = \textcolor{#3498db}{w_1 \cdot x_1} + \textcolor{#e67e22}{w_2 \cdot x_2} + \textcolor{#27ae60}{w_3 \cdot x_3} + \textcolor{#9B7AC4}{b}$

$z = \textcolor{#3498db}{0.4 \times 0.5} + \textcolor{#e67e22}{0.3 \times 0.8} + \textcolor{#27ae60}{0.9 \times 0.2} + \textcolor{#9B7AC4}{0.1}$

$z = \textcolor{#3498db}{0.20} + \textcolor{#e67e22}{0.24} + \textcolor{#27ae60}{0.18} + \textcolor{#9B7AC4}{0.10} = \textcolor{#F7E64D}{\mathbf{0.72}}$

Étape 2 - Activation sigmoid:

$a = \sigma(\textcolor{#F7E64D}{0.72}) = \frac{1}{1 + e^{-\textcolor{#F7E64D}{0.72}}} = \textcolor{#e74c3c}{\mathbf{0.67}}$

Interprétation: Le neurone "s'active" à $\textcolor{#e74c3c}{67\%}$

Légende des couleurs:

  • $\textcolor{#3498db}{Bleu}$: Entrée 1 et son poids ($\textcolor{#3498db}{x_1, w_1}$)
  • $\textcolor{#e67e22}{Orange}$: Entrée 2 et son poids ($\textcolor{#e67e22}{x_2, w_2}$)
  • $\textcolor{#27ae60}{Vert}$: Entrée 3 et son poids ($\textcolor{#27ae60}{x_3, w_3}$)
  • $\textcolor{#9B7AC4}{Violet}$: Biais ($\textcolor{#9B7AC4}{b}$)
  • $\textcolor{#F7E64D}{Jaune}$: Somme pondérée z
  • $\textcolor{#e74c3c}{Rouge}$: Activation finale a

Forward Pass (propagation avant):

flowchart LR A["Données X"] --> B["Couche 1
z1=X·W1+b1
a1=relu(z1)"] B --> C["Couche 2
z2=a1·W2+b2
y=sigmoid(z2)"] C --> D["Prédiction y"] class A ml-node-secondary class D ml-node-accent-soft

Backpropagation (retropropagation du gradient):

flowchart RL E["Erreur
(y - ŷ)"] --> G2["Gradient W2
dL/dW2"] G2 --> G1["Gradient W1
dL/dW1"] G1 --> U["Mise à jour
W, b"] class E ml-node-brand class U ml-node-accent

Cycle d'apprentissage:

flowchart TD F["1. FORWARD
X → Couches → y_pred"] F --> L["2. LOSS
Erreur = diff(y_pred, y_vrai)"] L --> B["3. BACKWARD
dL/dW = contribution à l'erreur"] B --> U["4. UPDATE
W = W - lr × gradient"] U --> R{"Convergence?"} R -->|Non| F R -->|Oui| D["Modèle entraîné!"] class F ml-node-secondary class L ml-node-warning class B ml-node-brand class U ml-node-main class D ml-node-accent
Avancé Exercice manuel: À vous de calculer!

Objectif: Maîtriser les calculs d'un réseau de neurones à la main (forward pass, activation, gradient).

Prenez une feuille et un stylo. Résolvez chaque partie AVANT de regarder la solution !

CONTEXTE

Vous avez un réseau de neurones simple avec :

  • 2 entrées : $x_1$, $x_2$
  • 1 neurone cache avec activation sigmoid
  • 1 neurone de sortie avec activation sigmoid

Poids de la couche cachée :

  • $w_1 = 0.5$ (poids de $x_1$)
  • $w_2 = -0.3$ (poids de $x_2$)
  • $b_1 = 0.1$ (biais)

Poids de la couche de sortie :

  • $w_3 = 0.8$ (poids de la sortie du neurone cache)
  • $b_2 = -0.2$ (biais)

Fonction sigmoid : $\sigma(z) = \frac{1}{1 + e^{-z}}$

Valeurs utiles : $\sigma(0) = 0.5$, $\sigma(0.2) \approx 0.55$

Données d'entrée : $x_1 = 1.0$, $x_2 = 2.0$, Cible : $y_{vrai} = 1$

PARTIE 1 : Forward Pass - Couche cachée

1.1) Calculez $z_1 = w_1 \cdot x_1 + w_2 \cdot x_2 + b_1$ 1.2) Calculez $a_1 = \sigma(z_1)$

PARTIE 2 : Forward Pass - Couche de sortie

2.1) Calculez $z_2 = w_3 \cdot a_1 + b_2$ 2.2) Calculez $\hat{y} = \sigma(z_2)$

PARTIE 3 : Calcul de l'erreur

$L = (y_{vrai} - \hat{y})^2$

3.1) Calculez l'erreur $L$

PARTIE 4 : Gradient et mise à jour

$\frac{\partial L}{\partial w_3} = -2(y_{vrai} - \hat{y}) \cdot \hat{y}(1-\hat{y}) \cdot a_1$

Learning rate : $\eta = 0.1$

4.1) Calculez le gradient de $w_3$ 4.2) Calculez $w_3^{new} = w_3 - \eta \cdot gradient$

Avancé Solution de l'exercice manuel

SOLUTION DÉTAILLÉE

PARTIE 1 : Forward Pass - Couche cachée

1.1) $z_1 = 0.5 \times 1.0 + (-0.3) \times 2.0 + 0.1 = 0.5 - 0.6 + 0.1 = \textcolor{#F7E64D}{\mathbf{0.0}}$

1.2) $a_1 = \sigma(0) = \frac{1}{1+1} = \textcolor{#F7E64D}{\mathbf{0.5}}$

PARTIE 2 : Forward Pass - Couche de sortie

2.1) $z_2 = 0.8 \times 0.5 + (-0.2) = 0.4 - 0.2 = \textcolor{#F7E64D}{\mathbf{0.2}}$

2.2) $\hat{y} = \sigma(0.2) \approx \textcolor{#e74c3c}{\mathbf{0.55}}$

PARTIE 3 : Calcul de l'erreur

3.1) $L = (1 - 0.55)^2 = (0.45)^2 = \textcolor{#e74c3c}{\mathbf{0.2025}}$

PARTIE 4 : Gradient et mise à jour

4.1) Gradient :

$$\frac{\partial L}{\partial w_3} = -2(1 - 0.55) \times 0.55 \times 0.45 \times 0.5$$

$$= -2 \times 0.45 \times 0.2475 \times 0.5 = \textcolor{#e74c3c}{-0.111}$$

4.2) Nouveau poids :

$$w_3^{new} = 0.8 - 0.1 \times (-0.111) = 0.8 + 0.011 = \textcolor{#27ae60}{\mathbf{0.811}}$$

Le poids augmente car le modèle sous-estimait (0.55 < 1).

Légende : $\textcolor{#F7E64D}{Jaune}$: activations, $\textcolor{#e74c3c}{Rouge}$: erreur/gradients, $\textcolor{#27ae60}{Vert}$: nouveau poids

Code

Charger et visualiser les données

print("=" * 70)
print("   EXPLORATION DES DONNEES DE CLASSIFICATION BINAIRE")
print("   Pour l'entrainement de notre reseau de neurones")
print("=" * 70)

# =================================================================
# 1. APERCU DES DONNEES
# =================================================================
print("\n" + "-" * 40)
print("1. APERCU DU DATASET")
print("-" * 40)
print("""
Ce dataset contient des exemples pour un probleme de classification binaire.
Chaque exemple a 2 features et appartient a une classe (0 ou 1).
Notre reseau de neurones devra apprendre a distinguer les deux classes.
""")
display(df.head(10), title="Premiers exemples du dataset")

# =================================================================
# 2. DIMENSIONS ET STRUCTURE
# =================================================================
print("\n" + "-" * 40)
print("2. DIMENSIONS ET STRUCTURE")
print("-" * 40)
n_samples, n_features = df.shape[0], 2
print(f"""
Nombre total d'exemples : {n_samples}
Nombre de features      : {n_features} (feature1, feature2)
Variable cible          : label (0 ou 1)

En termes de reseau de neurones:
- Couche d'entree  : {n_features} neurones (un par feature)
- Couche de sortie : 1 neurone (probabilite de classe 1)
""")

# =================================================================
# 3. DISTRIBUTION DES CLASSES
# =================================================================
print("\n" + "-" * 40)
print("3. EQUILIBRE DES CLASSES")
print("-" * 40)
class_counts = df['label'].value_counts().sort_index()
total = len(df)
print(f"""
Classe 0 : {class_counts[0]:3d} exemples ({class_counts[0]/total*100:.1f}%)
Classe 1 : {class_counts[1]:3d} exemples ({class_counts[1]/total*100:.1f}%)
""")

ratio = min(class_counts) / max(class_counts)
if ratio > 0.8:
    print("  [OK] Classes bien equilibrees!")
    print("       Le reseau ne sera pas biaise vers une classe.")
elif ratio > 0.5:
    print("  [ATTENTION] Leger desequilibre.")
    print("              Surveiller la precision par classe.")
else:
    print("  [ATTENTION] Desequilibre significatif!")
    print("              Considerer des techniques de reequilibrage.")

# =================================================================
# 4. STATISTIQUES DES FEATURES
# =================================================================
print("\n" + "-" * 40)
print("4. STATISTIQUES DES FEATURES")
print("-" * 40)
print("""
Avant d'entrainer un reseau de neurones, il est crucial de comprendre
l'echelle des donnees. Des features avec des echelles tres differentes
peuvent ralentir ou empecher la convergence.
""")
for col in ['feature1', 'feature2']:
    data = df[col]
    print(f"  {col}:")
    print(f"    Min = {data.min():.3f} | Max = {data.max():.3f}")
    print(f"    Moyenne = {data.mean():.3f} | Ecart-type = {data.std():.3f}")
    print()

# =================================================================
# 5. VISUALISATION 2D
# =================================================================
print("\n" + "-" * 40)
print("5. VISUALISATION DES CLASSES")
print("-" * 40)
print("""
Le graphique ci-dessous montre la repartition des deux classes.
Si les classes sont bien separees, le reseau apprendra facilement.
Si elles se chevauchent beaucoup, la tache sera plus difficile.
""")

plt.figure(figsize=(10, 6))
colors = ['#9B7AC4' if l == 0 else '#F7E64D' for l in df['label']]
plt.scatter(df['feature1'], df['feature2'], c=colors, alpha=0.6,
            edgecolors='black', s=60)
plt.xlabel('Feature 1', fontsize=12)
plt.ylabel('Feature 2', fontsize=12)
plt.title('Donnees de classification binaire\n(Violet=Classe 0, Jaune=Classe 1)',
          fontsize=14)
plt.grid(True, alpha=0.3)
plt.legend(handles=[
    plt.scatter([], [], c='#9B7AC4', edgecolors='black', label='Classe 0'),
    plt.scatter([], [], c='#F7E64D', edgecolors='black', label='Classe 1')
], loc='upper right')
plt.tight_layout()
plt.show()

# =================================================================
# CONCLUSION
# =================================================================
print("\n" + "=" * 70)
print("SYNTHESE POUR L'ENTRAINEMENT")
print("=" * 70)
print(f"""
- {n_samples} exemples disponibles pour l'apprentissage
- 2 features en entree --> 1 probabilite en sortie
- Classes equilibrees: {'OUI' if ratio > 0.8 else 'NON'}
- Les features ont des echelles similaires (normalisation recommandee)

Prochaine etape: Normaliser les donnees et diviser en train/test.
""")
Ctrl+Entrée

Cliquez sur « Exécuter » pour voir le résultat.

Théorie

Les fonctions d'activation

Les fonctions d'activation introduisent la non-linéarité dans le réseau. Sans elles, même un réseau profond serait équivalent à une simple régression linéaire.

Sigmoid (pour couche de sortie binaire):

$$\sigma(z) = \frac{1}{1 + e^{-z}}$$

  • Sortie entre 0 et 1 (probabilité)
  • Utile pour classification binaire

ReLU (pour couches cachées):

$$\text{relu}(z) = \max(0, z)$$

  • Simple et efficace
  • Résout le problème du gradient qui disparaît

Dérivées (pour backpropagation):

  • $\sigma'(z) = \sigma(z) \cdot (1 - \sigma(z))$
  • $\text{relu}'(z) = 1$ si $z > 0$, sinon $0$
Code

Implémenter les fonctions d'activation

print("=" * 70)
print("   FONCTIONS D'ACTIVATION: LE COEUR DE LA NON-LINEARITE")
print("=" * 70)

# =================================================================
# 1. DEFINITION DES FONCTIONS
# =================================================================
print("\n" + "-" * 40)
print("1. IMPLEMENTATION DES FONCTIONS")
print("-" * 40)

def sigmoid(z):
    """Fonction sigmoid: compresse les valeurs entre 0 et 1"""
    return 1 / (1 + np.exp(-np.clip(z, -500, 500)))

def sigmoid_derivative(z):
    """Derivee de sigmoid pour le backpropagation"""
    s = sigmoid(z)
    return s * (1 - s)

def relu(z):
    """Fonction ReLU: max(0, z)"""
    return np.maximum(0, z)

def relu_derivative(z):
    """Derivee de ReLU"""
    return (z > 0).astype(float)

print("""
Nous avons defini 4 fonctions essentielles:

SIGMOID σ(z) = 1/(1+e^(-z))
- Compresse toute valeur entre 0 et 1
- Utile en sortie pour obtenir une probabilite
- Probleme: gradient tres faible aux extremites

RELU relu(z) = max(0, z)
- Simple: garde les positifs, annule les negatifs
- Efficace pour les couches cachees
- Resout le probleme du "vanishing gradient"

DERIVEES (pour backpropagation):
- sigmoid'(z) = sigmoid(z) * (1 - sigmoid(z))
- relu'(z) = 1 si z > 0, sinon 0
""")

# =================================================================
# 2. DEMONSTRATION AVEC DES VALEURS
# =================================================================
print("\n" + "-" * 40)
print("2. EXEMPLES DE CALCULS")
print("-" * 40)

test_values = [-2, -1, 0, 1, 2, 5]
print("""
Voyons comment chaque fonction transforme differentes valeurs:
""")
print(f"{'z':>8} | {'sigmoid(z)':>12} | {'relu(z)':>10} | {'d_sigmoid':>12} | {'d_relu':>10}")
print("-" * 60)
for z in test_values:
    sig = sigmoid(z)
    rel = relu(z)
    sig_d = sigmoid_derivative(z)
    rel_d = relu_derivative(np.array([z]))[0]
    print(f"{z:>8} | {sig:>12.4f} | {rel:>10.1f} | {sig_d:>12.4f} | {rel_d:>10.1f}")

print("""
Observations importantes:
- Sigmoid: z=-2 donne 0.12, z=+2 donne 0.88 (compression!)
- ReLU: z negatif donne 0, z positif reste inchange
- Derivee sigmoid max a z=0 (0.25), decroit aux extremites
- Derivee ReLU: 0 ou 1 seulement (gradient constant ou nul)
""")

# =================================================================
# 3. VISUALISATION GRAPHIQUE
# =================================================================
print("\n" + "-" * 40)
print("3. VISUALISATION DES FONCTIONS")
print("-" * 40)
print("""
Les graphiques ci-dessous montrent les fonctions et leurs derivees.
Comprendre ces courbes est essentiel pour le backpropagation!
""")

z = np.linspace(-5, 5, 100)

fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# Sigmoid
axes[0].plot(z, sigmoid(z), 'b-', linewidth=2.5, label='sigmoid(z)')
axes[0].plot(z, sigmoid_derivative(z), 'r--', linewidth=2, label="sigmoid'(z)")
axes[0].axhline(y=0.5, color='gray', linestyle=':', alpha=0.5, label='y=0.5')
axes[0].axhline(y=0, color='k', linewidth=0.5)
axes[0].axhline(y=1, color='gray', linestyle=':', alpha=0.5, label='y=1')
axes[0].axvline(x=0, color='k', linewidth=0.5)
axes[0].fill_between(z, sigmoid_derivative(z), alpha=0.2, color='red')
axes[0].set_title('Fonction Sigmoid', fontsize=14, fontweight='bold')
axes[0].set_xlabel('z (pre-activation)')
axes[0].set_ylabel('Valeur')
axes[0].legend(loc='upper left')
axes[0].grid(True, alpha=0.3)
axes[0].set_ylim(-0.1, 1.1)

# ReLU
axes[1].plot(z, relu(z), 'b-', linewidth=2.5, label='relu(z)')
axes[1].plot(z, relu_derivative(z), 'r--', linewidth=2, label="relu'(z)")
axes[1].axhline(y=0, color='k', linewidth=0.5)
axes[1].axvline(x=0, color='k', linewidth=0.5)
axes[1].set_title('Fonction ReLU', fontsize=14, fontweight='bold')
axes[1].set_xlabel('z (pre-activation)')
axes[1].set_ylabel('Valeur')
axes[1].legend(loc='upper left')
axes[1].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

# =================================================================
# INTERPRETATION PEDAGOGIQUE
# =================================================================
print("\n" + "-" * 40)
print("INTERPRETATION")
print("-" * 40)
print("""
SIGMOID:
- Zone centrale (|z| < 2): gradient significatif, apprentissage actif
- Zones extremes (|z| > 4): gradient quasi-nul ("saturation")
--> Probleme: les neurones peuvent "mourir" (arret d'apprentissage)

RELU:
- z > 0: gradient constant de 1, apprentissage stable
- z < 0: gradient nul, neurone inactif
--> Solution au vanishing gradient, mais "dying ReLU" possible

EN PRATIQUE:
- Couches cachees: ReLU (rapide, efficace)
- Sortie binaire: Sigmoid (probabilite entre 0 et 1)
- Sortie multi-classe: Softmax (non couvert ici)
""")
Ctrl+Entrée

Cliquez sur « Exécuter » pour voir le résultat.

Contenu verrouillé
7 / 17

Continuez votre apprentissage

Vous avez exploré 7 sections de ce module. Connectez-vous pour débloquer le reste du cours, incluant les exercices pratiques et les solutions.

Ctrl ou Cmd plus Entrée exécute le code. Tab indente ; Échap puis Tab quitte l'éditeur.