Anatomie d'un réseau de neurones
Ce document contient le code et les explications pour le notebook d'exploration interactive d'un réseau de neurones. Vous pouvez copier-coller chaque section dans une cellule Google Colab.
Cellule 1 (Markdown) - Introduction
# Anatomie d'un réseau de neurones
## Exploration interactive du fonctionnement interne d'un réseau de neurones
Dans ce notebook, nous allons explorer de manière interactive le fonctionnement interne d'un réseau de neurones. Vous pourrez manipuler directement les composants fondamentaux (neurones, poids, biais) et observer leur impact sur les prédictions.
### Objectifs :
- Comprendre le fonctionnement d'un neurone artificiel
- Visualiser l'effet des poids et du biais sur les décisions
- Explorer le flux d'information dans un réseau multicouche
- Observer l'évolution des poids pendant l'entraînement
Cellule 2 (Code) - Configuration initiale
# Partie 1: Configuration initiale
import numpy as np
import matplotlib.pyplot as plt
import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense
from google.colab import output
output.enable_custom_widget_manager()
import ipywidgets as widgets
from IPython.display import display, clear_output
from matplotlib.colors import LinearSegmentedColormap
print("Configuration terminée!")
Cellule 3 (Markdown) - Exploration d'un neurone unique
## Exploration d'un neurone unique
Dans cette partie, nous allons observer le fonctionnement d'un neurone artificiel, l'unité fondamentale des réseaux de neurones.
Un neurone artificiel effectue deux opérations principales :
1. Une **somme pondérée** des entrées (z = w₁x₁ + w₂x₂ + ... + b)
2. L'application d'une **fonction d'activation** qui introduit la non-linéarité (a = f(z))
Utilisez les contrôles interactifs ci-dessous pour observer comment un neurone traite l'information.
Cellule 4 (Code) - Fonctions du neurone
# Fonction pour calculer la sortie d'un neurone
def neuron_output(x1, x2, w1, w2, b, activation="relu"):
# Calcul de la somme pondérée
z = x1 * w1 + x2 * w2 + b
# Application de la fonction d'activation
if activation == "relu":
a = max(0, z)
elif activation == "sigmoid":
a = 1 / (1 + np.exp(-z))
elif activation == "tanh":
a = np.tanh(z)
else:
a = z # Linéaire
return z, a
# Fonction pour visualiser un neurone
def visualize_neuron(x1, x2, w1, w2, b, activation="relu"):
# Calculer la sortie
z, a = neuron_output(x1, x2, w1, w2, b, activation)
# Créer la figure
fig, axes = plt.subplots(1, 3, figsize=(18, 5))
# 1. Représentation du neurone
ax = axes[0]
ax.set_xlim(-0.5, 2.5)
ax.set_ylim(-0.5, 2.5)
# Dessiner le neurone
circle = plt.Circle((1, 1), 0.4, fill=True, color='lightblue', alpha=0.7)
ax.add_artist(circle)
# Dessiner les entrées
ax.plot(0, 0.7, 'ro', markersize=10)
ax.plot(0, 1.3, 'ro', markersize=10)
# Dessiner la sortie
ax.plot(2, 1, 'go', markersize=10)
# Ajouter les connexions
ax.arrow(0, 0.7, 0.6, 0.1, head_width=0.1, head_length=0.1, fc='black', ec='black', linewidth=2)
ax.arrow(0, 1.3, 0.6, -0.1, head_width=0.1, head_length=0.1, fc='black', ec='black', linewidth=2)
ax.arrow(1.4, 1, 0.6, 0, head_width=0.1, head_length=0.1, fc='black', ec='black', linewidth=2)
# Ajouter les textes
ax.text(-0.1, 0.7, f"x₁ = {x1:.2f}", fontsize=12, ha='right')
ax.text(-0.1, 1.3, f"x₂ = {x2:.2f}", fontsize=12, ha='right')
ax.text(1, 1, f"z = {z:.2f}\na = {a:.2f}", fontsize=12, ha='center')
ax.text(0.5, 0.95, f"w₁ = {w1:.2f}", fontsize=10, rotation=15)
ax.text(0.5, 1.15, f"w₂ = {w2:.2f}", fontsize=10, rotation=-15)
ax.text(2.1, 1, f"Sortie = {a:.2f}", fontsize=12, ha='left')
ax.text(1, 0.5, f"Biais = {b:.2f}", fontsize=10)
ax.set_title("Neurone artificiel", fontsize=14)
ax.set_axis_off()
# 2. Représentation de la fonction d'activation
ax = axes[1]
x = np.linspace(-5, 5, 100)
if activation == "relu":
y = np.maximum(0, x)
title = "Fonction d'activation: ReLU"
elif activation == "sigmoid":
y = 1 / (1 + np.exp(-x))
title = "Fonction d'activation: Sigmoid"
elif activation == "tanh":
y = np.tanh(x)
title = "Fonction d'activation: Tanh"
else:
y = x
title = "Fonction d'activation: Linéaire"
ax.plot(x, y, 'b-', linewidth=2)
ax.axhline(y=0, color='k', linestyle='-', alpha=0.3)
ax.axvline(x=0, color='k', linestyle='-', alpha=0.3)
# Marquer le point correspondant à z
ax.plot(z, a, 'ro', markersize=8)
ax.plot([z, z], [0, a], 'r--', alpha=0.5)
ax.plot([0, z], [a, a], 'r--', alpha=0.5)
ax.set_xlim(-5, 5)
ax.set_ylim(-1.5, 1.5)
ax.set_xlabel("z (somme pondérée)")
ax.set_ylabel("a (activation)")
ax.set_title(title, fontsize=14)
ax.grid(True, alpha=0.3)
# 3. Visualisation de la frontière de décision
ax = axes[2]
# Créer des points pour former une grille
grid_size = 20
x1_values = np.linspace(0, 1, grid_size)
x2_values = np.linspace(0, 1, grid_size)
x1_grid, x2_grid = np.meshgrid(x1_values, x2_values)
# Calculer la sortie pour chaque point de la grille
z_grid = x1_grid * w1 + x2_grid * w2 + b
if activation == "relu":
a_grid = np.maximum(0, z_grid)
elif activation == "sigmoid":
a_grid = 1 / (1 + np.exp(-z_grid))
elif activation == "tanh":
a_grid = np.tanh(z_grid)
else:
a_grid = z_grid
# Créer une carte de couleur
cmap = plt.get_cmap('coolwarm')
# Tracer la heatmap
im = ax.imshow(a_grid, origin='lower', extent=[0, 1, 0, 1],
cmap=cmap, vmin=0, vmax=1)
plt.colorbar(im, ax=ax, label="Activation")
# Ajouter le point actuel
ax.plot(x1, x2, 'ko', markersize=8)
# Tracer la frontière de décision (a = 0.5)
if activation in ["sigmoid", "tanh"]:
threshold = 0.5
CS = ax.contour(x1_grid, x2_grid, a_grid, levels=[threshold],
colors='k', linestyles='--')
ax.clabel(CS, inline=True, fontsize=10, fmt={threshold: "a = 0.5"})
ax.set_xlim(0, 1)
ax.set_ylim(0, 1)
ax.set_xlabel("x₁")
ax.set_ylabel("x₂")
ax.set_title("Carte d'activation", fontsize=14)
plt.tight_layout()
plt.show()
return a
Cellule 5 (Code) - Interface interactive pour un neurone
# Créer des widgets interactifs pour le neurone
w1_slider = widgets.FloatSlider(value=1.0, min=-3.0, max=3.0, step=0.1, description='Poids w₁:')
w2_slider = widgets.FloatSlider(value=1.0, min=-3.0, max=3.0, step=0.1, description='Poids w₂:')
b_slider = widgets.FloatSlider(value=0.0, min=-3.0, max=3.0, step=0.1, description='Biais:')
x1_slider = widgets.FloatSlider(value=0.5, min=0.0, max=1.0, step=0.05, description='Entrée x₁:')
x2_slider = widgets.FloatSlider(value=0.5, min=0.0, max=1.0, step=0.05, description='Entrée x₂:')
activation_dropdown = widgets.Dropdown(
options=['relu', 'sigmoid', 'tanh', 'linear'],
value='relu',
description='Activation:'
)
# Fonction pour mettre à jour la visualisation
def update_neuron_visualization(w1, w2, b, x1, x2, activation):
clear_output(wait=True)
output = visualize_neuron(x1, x2, w1, w2, b, activation)
print(f"Sortie du neurone: {output:.4f}")
# Expliquer le calcul
z = x1 * w1 + x2 * w2 + b
print(f"\nCalcul détaillé:")
print(f"z = (x₁ × w₁) + (x₂ × w₂) + b")
print(f"z = ({x1:.2f} × {w1:.2f}) + ({x2:.2f} × {w2:.2f}) + {b:.2f}")
print(f"z = {x1*w1:.2f} + {x2*w2:.2f} + {b:.2f}")
print(f"z = {z:.2f}")
if activation == "relu":
print(f"a = ReLU(z) = max(0, z) = max(0, {z:.2f}) = {max(0, z):.2f}")
elif activation == "sigmoid":
sig_z = 1 / (1 + np.exp(-z))
print(f"a = Sigmoid(z) = 1 / (1 + e^(-z)) = 1 / (1 + e^(-{z:.2f})) = {sig_z:.2f}")
elif activation == "tanh":
tanh_z = np.tanh(z)
print(f"a = tanh(z) = tanh({z:.2f}) = {tanh_z:.2f}")
else:
print(f"a = z = {z:.2f}") # Linéaire
# Interface interactive pour le neurone
neuron_output = widgets.interactive_output(
update_neuron_visualization,
{'w1': w1_slider, 'w2': w2_slider, 'b': b_slider,
'x1': x1_slider, 'x2': x2_slider, 'activation': activation_dropdown}
)
# Afficher les widgets
print("Utilisez les contrôles ci-dessous pour modifier les propriétés du neurone:")
display(widgets.VBox([
widgets.HBox([x1_slider, x2_slider]),
widgets.HBox([w1_slider, w2_slider]),
widgets.HBox([b_slider, activation_dropdown])
]))
display(neuron_output)
Cellule 6 (Markdown) - De l'unique au réseau
## De l'unique au réseau
Maintenant que nous avons exploré un neurone unique, passons à un réseau simple. Un réseau de neurones est composé de plusieurs neurones organisés en couches, où l'information se propage de l'entrée vers la sortie.
Le réseau ci-dessous contient :
- Une couche d'entrée (2 neurones)
- Une couche cachée (nombre ajustable de neurones)
- Une couche de sortie (1 neurone)
Observez comment l'information circule à travers le réseau et comment les différents poids affectent les activations.
Cellule 7 (Code) - Fonctions du réseau
# Fonction pour créer et visualiser un réseau simple
def create_simple_network(hidden_units=3, activation='relu'):
# Créer un modèle séquentiel
model = Sequential([
Dense(hidden_units, activation=activation, input_shape=(2,)),
Dense(1, activation='sigmoid')
])
# Compiler le modèle (bien que nous ne l'entraînerons pas)
model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])
return model
# Fonction pour visualiser un réseau simple
def visualize_network(inputs, weights1=None, biases1=None, weights2=None, biases2=None, hidden_units=3, activation='relu'):
# Créer le modèle si non fourni
model = create_simple_network(hidden_units, activation)
# Si des poids sont fournis, les appliquer
if weights1 is not None and biases1 is not None and weights2 is not None and biases2 is not None:
model.layers[0].set_weights([weights1, biases1])
model.layers[1].set_weights([weights2, biases2])
# Convertir les entrées pour prédiction
x = np.array([inputs])
# Obtenir les activations intermédiaires
intermediate_layer_model = tf.keras.Model(inputs=model.input,
outputs=model.layers[0].output)
intermediate_activations = intermediate_layer_model.predict(x)[0]
# Obtenir les activations de sortie
output_activation = model.predict(x)[0][0]
# Extraire les poids et biais
weights1, biases1 = model.layers[0].get_weights()
weights2, biases2 = model.layers[1].get_weights()
# Créer la figure pour visualiser le réseau
plt.figure(figsize=(12, 8))
# Définir les positions des neurones
input_layer_y = np.array([0.2, 0.8])
hidden_layer_y = np.linspace(0.1, 0.9, hidden_units)
output_layer_y = np.array([0.5])
input_layer_x = 0.1
hidden_layer_x = 0.5
output_layer_x = 0.9
# Dessiner les neurones d'entrée
for i, y in enumerate(input_layer_y):
plt.scatter(input_layer_x, y, s=200, c='blue', alpha=0.7)
plt.text(input_layer_x, y, f"x{i+1}={inputs[i]:.2f}", fontsize=12, ha='center', va='center', color='white')
# Dessiner les neurones cachés
for i, y in enumerate(hidden_layer_y):
# Calculer la somme pondérée
z = np.dot(inputs, weights1[:, i]) + biases1[i]
# Appliquer l'activation
if activation == 'relu':
a = max(0, z)
elif activation == 'sigmoid':
a = 1 / (1 + np.exp(-z))
elif activation == 'tanh':
a = np.tanh(z)
else:
a = z
# Couleur basée sur l'activation
color = plt.cm.viridis(a)
plt.scatter(hidden_layer_x, y, s=200, c=[color], alpha=0.7)
plt.text(hidden_layer_x, y, f"{a:.2f}", fontsize=12, ha='center', va='center', color='white')
# Dessiner le neurone de sortie
plt.scatter(output_layer_x, output_layer_y, s=200, c='red', alpha=0.7)
plt.text(output_layer_x, output_layer_y, f"{output_activation:.2f}", fontsize=12, ha='center', va='center', color='white')
# Dessiner les connexions entre couches d'entrée et cachée
for i, y_in in enumerate(input_layer_y):
for j, y_hid in enumerate(hidden_layer_y):
# Couleur et épaisseur basées sur le poids
weight = weights1[i, j]
width = abs(weight) * 3
color = 'red' if weight < 0 else 'green'
alpha = min(abs(weight), 1.0)
plt.plot([input_layer_x, hidden_layer_x], [y_in, y_hid],
c=color, linewidth=width, alpha=alpha)
# Dessiner les connexions entre couche cachée et sortie
for i, y_hid in enumerate(hidden_layer_y):
# Couleur et épaisseur basées sur le poids
weight = weights2[i, 0]
width = abs(weight) * 3
color = 'red' if weight < 0 else 'green'
alpha = min(abs(weight), 1.0)
plt.plot([hidden_layer_x, output_layer_x], [y_hid, output_layer_y],
c=color, linewidth=width, alpha=alpha)
# Étiquettes
plt.text(input_layer_x, 0.03, "Couche d'entrée", fontsize=14, ha='center')
plt.text(hidden_layer_x, 0.03, "Couche cachée", fontsize=14, ha='center')
plt.text(output_layer_x, 0.03, "Couche de sortie", fontsize=14, ha='center')
# Enlever les axes
plt.axis('off')
plt.title(f"Réseau de neurones - Activation cachée: {activation}", fontsize=16)
plt.tight_layout()
plt.show()
# Afficher les calculs détaillés
print("\nCalculs détaillés pour chaque neurone de la couche cachée:")
for i in range(hidden_units):
z = np.dot(inputs, weights1[:, i]) + biases1[i]
print(f"\nNeurone caché {i+1}:")
print(f"z = (x₁ × w₁,{i+1}) + (x₂ × w₂,{i+1}) + b{i+1}")
print(f"z = ({inputs[0]:.2f} × {weights1[0, i]:.2f}) + ({inputs[1]:.2f} × {weights1[1, i]:.2f}) + {biases1[i]:.2f}")
print(f"z = {inputs[0] * weights1[0, i]:.2f} + {inputs[1] * weights1[1, i]:.2f} + {biases1[i]:.2f} = {z:.2f}")
if activation == 'relu':
a = max(0, z)
print(f"a = ReLU(z) = max(0, {z:.2f}) = {a:.2f}")
elif activation == 'sigmoid':
a = 1 / (1 + np.exp(-z))
print(f"a = Sigmoid(z) = 1 / (1 + e^(-{z:.2f})) = {a:.2f}")
elif activation == 'tanh':
a = np.tanh(z)
print(f"a = tanh(z) = tanh({z:.2f}) = {a:.2f}")
else:
a = z
print(f"a = z = {z:.2f}")
print("\nCalcul pour le neurone de sortie:")
z_out = np.dot(intermediate_activations, weights2[:, 0]) + biases2[0]
print(f"z = Σ(a_caché × w_sortie) + b_sortie = {z_out:.2f}")
print(f"sortie = Sigmoid(z) = 1 / (1 + e^(-{z_out:.2f})) = {output_activation:.2f}")
return model, weights1, biases1, weights2, biases2
# Fonction pour générer des poids aléatoires
def generate_random_weights(hidden_units=3):
# Générer des poids aléatoires pour la première couche
weights1 = np.random.normal(0, 1, (2, hidden_units))
biases1 = np.random.normal(0, 1, hidden_units)
# Générer des poids aléatoires pour la couche de sortie
weights2 = np.random.normal(0, 1, (hidden_units, 1))
biases2 = np.random.normal(0, 1, 1)
return weights1, biases1, weights2, biases2
Cellule 8 (Code) - Interface interactive pour le réseau
# Créer des widgets interactifs pour le réseau
x1_net_slider = widgets.FloatSlider(value=0.5, min=0.0, max=1.0, step=0.05, description='Entrée x₁:')
x2_net_slider = widgets.FloatSlider(value=0.5, min=0.0, max=1.0, step=0.05, description='Entrée x₂:')
hidden_units_slider = widgets.IntSlider(value=3, min=1, max=5, description='Neurones cachés:')
activation_net_dropdown = widgets.Dropdown(
options=['relu', 'sigmoid', 'tanh', 'linear'],
value='relu',
description='Activation:'
)
random_button = widgets.Button(description="Poids aléatoires")
# Variables pour stocker les poids courants
current_weights1, current_biases1, current_weights2, current_biases2 = generate_random_weights()
# Fonction pour visualiser le réseau
def update_network_visualization(x1, x2, hidden_units, activation):
global current_weights1, current_biases1, current_weights2, current_biases2
# Ajuster les dimensions des poids si nécessaire
if current_weights1.shape[1] != hidden_units:
current_weights1, current_biases1, current_weights2, current_biases2 = generate_random_weights(hidden_units)
# Visualiser le réseau
inputs = np.array([x1, x2])
_, w1, b1, w2, b2 = visualize_network(
inputs, current_weights1, current_biases1, current_weights2, current_biases2,
hidden_units, activation
)
# Mettre à jour les poids courants
current_weights1, current_biases1 = w1, b1
current_weights2, current_biases2 = w2, b2
# Fonction pour générer de nouveaux poids aléatoires
def regenerate_weights(b):
global current_weights1, current_biases1, current_weights2, current_biases2
current_weights1, current_biases1, current_weights2, current_biases2 = generate_random_weights(
hidden_units_slider.value
)
# Mettre à jour la visualisation
update_network_visualization(
x1_net_slider.value, x2_net_slider.value,
hidden_units_slider.value, activation_net_dropdown.value
)
# Associer la fonction au bouton
random_button.on_click(regenerate_weights)
# Interface interactive pour le réseau
network_output = widgets.interactive_output(
update_network_visualization,
{'x1': x1_net_slider, 'x2': x2_net_slider,
'hidden_units': hidden_units_slider, 'activation': activation_net_dropdown}
)
# Afficher les widgets pour le réseau
print("\nExplorez le comportement d'un réseau simple:")
display(widgets.VBox([
widgets.HBox([x1_net_slider, x2_net_slider]),
widgets.HBox([hidden_units_slider, activation_net_dropdown]),
random_button
]))
display(network_output)
Cellule 9 (Markdown) - Visualisation de l'entraînement
## Visualisation de l'entraînement
Dans cette dernière partie, nous allons observer l'évolution des poids pendant l'entraînement d'un réseau de neurones sur un problème classique : le problème XOR.
Le problème XOR (OU exclusif) consiste à prédire la sortie de la fonction logique XOR :
- (0,0) → 0
- (0,1) → 1
- (1,0) → 1
- (1,1) → 0
Ce problème n'est pas linéairement séparable, ce qui signifie qu'il ne peut pas être résolu par un seul neurone.
Cellule 10 (Code) - Génération de données XOR
# Générer des données XOR
def generate_xor_data(n_samples=100):
X = np.random.rand(n_samples, 2)
y = np.logical_xor(X[:, 0] > 0.5, X[:, 1] > 0.5).astype(np.float32)
return X, y
# Afficher quelques exemples de données XOR
X_sample, y_sample = generate_xor_data(20)
plt.figure(figsize=(6, 6))
plt.scatter(X_sample[:, 0], X_sample[:, 1], c=y_sample, cmap='coolwarm', s=100)
plt.xlabel('x₁')
plt.ylabel('x₂')
plt.title('Problème XOR')
plt.grid(True, alpha=0.3)
plt.show()
print("Exemples de données XOR:")
for i in range(5):
x1, x2 = X_sample[i]
y = y_sample[i]
print(f"x1={x1:.2f}, x2={x2:.2f} → y={y:.0f}")
Cellule 11 (Code) - Création et entraînement du modèle XOR
# Créer un modèle pour résoudre XOR
learning_rate = 0.1
hidden_units = 4
epochs = 20
# Générer des données
X_train, y_train = generate_xor_data(200)
# Créer un modèle
model = Sequential([
Dense(hidden_units, activation='relu', input_shape=(2,)),
Dense(1, activation='sigmoid')
])
# Compiler avec un optimiseur personnalisé
optimizer = tf.keras.optimizers.SGD(learning_rate=learning_rate)
model.compile(optimizer=optimizer, loss='binary_crossentropy', metrics=['accuracy'])
# Entraîner le modèle
history = model.fit(
X_train, y_train,
epochs=epochs,
batch_size=32,
verbose=1
)
# Afficher les résultats d'entraînement
plt.figure(figsize=(12, 5))
# Graphique de précision
plt.subplot(1, 2, 1)
plt.plot(history.history['accuracy'], '-o')
plt.title('Précision pendant l\'entraînement')
plt.xlabel('Époque')
plt.ylabel('Précision')
plt.grid(True, alpha=0.3)
# Graphique de perte
plt.subplot(1, 2, 2)
plt.plot(history.history['loss'], '-o')
plt.title('Perte pendant l\'entraînement')
plt.xlabel('Époque')
plt.ylabel('Perte')
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Je vais compléter le fichier anatomie-reseau.md à partir de la cellule 12, en continuant le document là où il a été interrompu.
Cellule 12 (Code) - Visualisation de la frontière de décision
# Visualiser la frontière de décision finale
h = 0.01
x_min, x_max = 0, 1
y_min, y_max = 0, 1
xx, yy = np.meshgrid(np.arange(x_min, x_max, h), np.arange(y_min, y_max, h))
grid_points = np.c_[xx.ravel(), yy.ravel()]
# Convertir les points en format approprié pour le modèle
grid_pred = model.predict(grid_points)
grid_pred = grid_pred.reshape(xx.shape)
# Tracer la frontière de décision
plt.figure(figsize=(8, 6))
plt.contourf(xx, yy, grid_pred, alpha=0.8, cmap=plt.cm.RdBu)
# Tracer les données d'entraînement
plt.scatter(X_train[:, 0], X_train[:, 1], c=y_train, cmap=plt.cm.RdBu, edgecolors='k')
plt.xlabel('x1')
plt.ylabel('x2')
plt.title('Frontière de décision pour le problème XOR')
plt.colorbar()
plt.show()
# Évaluer les performances finales
train_loss, train_acc = model.evaluate(X_train, y_train, verbose=0)
print(f"Précision finale sur l'ensemble d'entraînement: {train_acc*100:.2f}%")
Cellule 13 (Markdown) - Exploration interactive avancée
## Exploration interactive avancée
Maintenant que nous avons exploré les bases des réseaux de neurones, exploitons davantage l'interactivité pour comprendre comment ils apprennent et se comportent.
Utilisez les widgets interactifs ci-dessous pour explorer différentes architectures et configurations du réseau sur le problème XOR. Observez comment les changements affectent la frontière de décision et les performances.
Cellule 14 (Code) - Interface interactive avancée
# Créer des widgets interactifs pour l'exploration avancée
num_hidden_slider = widgets.IntSlider(value=4, min=2, max=10, step=1, description='Neurones cachés:')
learning_rate_slider = widgets.FloatLogSlider(value=0.1, base=10, min=-3, max=0, step=0.1, description='Learning rate:')
epochs_slider = widgets.IntSlider(value=100, min=10, max=500, step=10, description='Époques:')
activation_dropdown = widgets.Dropdown(
options=['relu', 'tanh', 'sigmoid'],
value='relu',
description='Activation:'
)
# Fonction pour créer et entraîner le modèle avec les paramètres spécifiés
def create_and_train_model(hidden_units, learning_rate, epochs, activation):
# Créer un modèle
model = Sequential([
Dense(hidden_units, activation=activation, input_shape=(2,)),
Dense(1, activation='sigmoid')
])
# Compiler le modèle
optimizer = tf.keras.optimizers.SGD(learning_rate=learning_rate)
model.compile(optimizer=optimizer, loss='binary_crossentropy', metrics=['accuracy'])
# Créer des données
X, y = generate_xor_data(200)
# Afficher les données
plt.figure(figsize=(12, 5))
plt.subplot(1, 2, 1)
plt.scatter(X[:, 0], X[:, 1], c=y, cmap='coolwarm')
plt.title('Données XOR')
plt.xlabel('x1')
plt.ylabel('x2')
# Entraîner le modèle
history = model.fit(
X, y,
epochs=epochs,
batch_size=32,
verbose=0
)
# Afficher l'historique d'entraînement
plt.subplot(1, 2, 2)
plt.plot(history.history['loss'])
plt.title('Perte pendant l\'entraînement')
plt.xlabel('Époque')
plt.ylabel('Perte')
plt.tight_layout()
plt.show()
# Visualiser la frontière de décision
h = 0.01
x_min, x_max = 0, 1
y_min, y_max = 0, 1
xx, yy = np.meshgrid(np.arange(x_min, x_max, h), np.arange(y_min, y_max, h))
grid_points = np.c_[xx.ravel(), yy.ravel()]
# Obtenir les prédictions
grid_pred = model.predict(grid_points, verbose=0)
grid_pred = grid_pred.reshape(xx.shape)
# Tracer la frontière de décision
plt.figure(figsize=(8, 6))
plt.contourf(xx, yy, grid_pred, alpha=0.8, cmap=plt.cm.RdBu)
# Tracer les données
plt.scatter(X[:, 0], X[:, 1], c=y, cmap=plt.cm.RdBu, edgecolors='k')
plt.xlabel('x1')
plt.ylabel('x2')
plt.title(f'Frontière de décision (Neurones: {hidden_units}, LR: {learning_rate:.4f}, Activation: {activation})')
plt.colorbar()
plt.show()
# Évaluer le modèle
loss, acc = model.evaluate(X, y, verbose=0)
print(f"Architecture: {hidden_units} neurones cachés, learning rate: {learning_rate:.4f}, activation: {activation}")
print(f"Précision: {acc*100:.2f}%")
print(f"Perte: {loss:.4f}")
# Afficher les poids du réseau pour comprendre ce qu'il a appris
weights1, biases1 = model.layers[0].get_weights()
weights2, biases2 = model.layers[1].get_weights()
print("\nPoids de la couche cachée:")
for i in range(hidden_units):
print(f"Neurone {i+1}: {weights1[:, i]} (biais: {biases1[i]:.4f})")
print("\nPoids de la couche de sortie:")
print(f"{weights2.flatten()} (biais: {biases2[0]:.4f})")
# Interface interactive
interactive_output = widgets.interactive_output(
create_and_train_model,
{'hidden_units': num_hidden_slider,
'learning_rate': learning_rate_slider,
'epochs': epochs_slider,
'activation': activation_dropdown}
)
# Afficher les widgets
print("Explorez différentes architectures et configurations:")
display(widgets.VBox([
widgets.HBox([num_hidden_slider, activation_dropdown]),
widgets.HBox([learning_rate_slider, epochs_slider])
]))
display(interactive_output)
Cellule 15 (Markdown) - Interpréter les résultats
## Interpréter les résultats
Maintenant que vous avez exploré différentes configurations de réseaux de neurones, prenons un moment pour analyser et comprendre les résultats :
### Observations clés
1. **Nombre de neurones cachés** :
- Trop peu de neurones (2-3) limitent la capacité du réseau à apprendre la fonction XOR
- Un nombre approprié (4-6) permet généralement une bonne séparation
- Trop de neurones peuvent parfois mener à du surapprentissage (la frontière devient trop complexe)
2. **Taux d'apprentissage (Learning Rate)** :
- Trop faible (< 0.01) : apprentissage très lent, peut ne pas converger dans le nombre d'époques donné
- Approprié (0.01 - 0.1) : bonne convergence avec une frontière stable
- Trop élevé (> 0.5) : instabilité, oscillations, voire divergence
3. **Fonction d'activation** :
- ReLU : rapide, peut parfois créer des frontières plus angulaires
- Tanh : frontières plus lisses, parfois meilleure pour ce problème spécifique
- Sigmoid : peut être plus lente à converger pour des problèmes comme XOR
4. **Nombre d'époques** :
- Insuffisant : modèle sous-entraîné, frontière imprécise
- Suffisant : bonne frontière de décision
- Excessif : risque de surapprentissage, mais moins problématique pour ce cas simple
### Comment le réseau apprend-il le XOR ?
Le problème XOR est intéressant car il n'est pas linéairement séparable. En d'autres termes, on ne peut pas tracer une seule ligne droite pour séparer les classes.
Un réseau avec une couche cachée résout ce problème en :
1. Créant des "lignes de séparation" avec chaque neurone de la couche cachée
2. Combinant ces lignes pour former des régions complexes
3. Ajustant les poids pour positionner ces lignes de manière optimale
C'est une parfaite illustration de pourquoi nous avons besoin de réseaux multicouches pour résoudre des problèmes non linéaires.
Cellule 16 (Code) - Visualisation des neurones cachés
# Fonction pour visualiser la contribution de chaque neurone caché
def visualize_hidden_neurons(hidden_units=4, activation='relu'):
# Créer un modèle
model = Sequential([
Dense(hidden_units, activation=activation, input_shape=(2,)),
Dense(1, activation='sigmoid')
])
# Compiler le modèle
optimizer = tf.keras.optimizers.SGD(learning_rate=0.1)
model.compile(optimizer=optimizer, loss='binary_crossentropy', metrics=['accuracy'])
# Créer des données
X, y = generate_xor_data(200)
# Entraîner le modèle
model.fit(X, y, epochs=100, batch_size=32, verbose=0)
# Obtenir les poids
weights1, biases1 = model.layers[0].get_weights()
weights2, biases2 = model.layers[1].get_weights()
# Créer une grille de points pour visualisation
h = 0.01
x_min, x_max = 0, 1
y_min, y_max = 0, 1
xx, yy = np.meshgrid(np.arange(x_min, x_max, h), np.arange(y_min, y_max, h))
grid_points = np.c_[xx.ravel(), yy.ravel()]
# Créer un modèle intermédiaire pour obtenir les activations de la couche cachée
intermediate_model = tf.keras.Model(inputs=model.input, outputs=model.layers[0].output)
hidden_activations = intermediate_model.predict(grid_points, verbose=0)
# Visualiser la contribution de chaque neurone caché
fig, axes = plt.subplots(2, hidden_units, figsize=(4*hidden_units, 8))
# Pour chaque neurone caché
for i in range(hidden_units):
# Activation du neurone
neuron_activation = hidden_activations[:, i].reshape(xx.shape)
# La ligne de décision du neurone (où l'activation est proche de 0)
if activation == 'tanh':
decision_boundary = np.zeros_like(neuron_activation)
elif activation == 'relu':
decision_boundary = np.zeros_like(neuron_activation)
else: # sigmoid
decision_boundary = np.ones_like(neuron_activation) * 0.5
# Visualiser l'activation du neurone
im = axes[0, i].contourf(xx, yy, neuron_activation, cmap='viridis')
axes[0, i].set_title(f'Neurone {i+1}\nw=[{weights1[0, i]:.2f}, {weights1[1, i]:.2f}]\nb={biases1[i]:.2f}')
axes[0, i].set_xlabel('x1')
axes[0, i].set_ylabel('x2')
plt.colorbar(im, ax=axes[0, i])
# Visualiser la ligne de décision
axes[1, i].contour(xx, yy, neuron_activation, levels=[0] if activation in ['tanh', 'relu'] else [0.5],
colors='r', linewidths=2)
axes[1, i].scatter(X[:, 0], X[:, 1], c=y, cmap='coolwarm', edgecolors='k')
axes[1, i].set_title(f'Ligne de décision\nContribution finale: {"+" if weights2[i, 0] > 0 else "-"}{abs(weights2[i, 0]):.2f}')
axes[1, i].set_xlabel('x1')
axes[1, i].set_ylabel('x2')
plt.tight_layout()
plt.show()
# Afficher la frontière de décision finale
hidden_output = np.dot(hidden_activations, weights2) + biases2
final_pred = 1 / (1 + np.exp(-hidden_output)) # sigmoid
final_pred = final_pred.reshape(xx.shape)
plt.figure(figsize=(8, 6))
plt.contourf(xx, yy, final_pred, alpha=0.8, cmap=plt.cm.RdBu)
plt.scatter(X[:, 0], X[:, 1], c=y, cmap=plt.cm.RdBu, edgecolors='k')
plt.xlabel('x1')
plt.ylabel('x2')
plt.title('Frontière de décision finale (combinaison des neurones cachés)')
plt.colorbar()
plt.show()
# Expliquer comment les neurones se combinent
print("Comment les neurones cachés se combinent pour résoudre le problème XOR:")
print("-" * 80)
print("1. Chaque neurone caché crée une 'ligne de décision' dans l'espace d'entrée")
print("2. Le signe du poids de sortie détermine si le neurone contribue positivement ou négativement")
print("3. La combinaison de ces lignes forme la frontière de décision complexe finale")
print("-" * 80)
print("\nPoids de la couche de sortie:")
for i in range(hidden_units):
print(f"Neurone {i+1} → Sortie: {'positif' if weights2[i, 0] > 0 else 'négatif'} ({weights2[i, 0]:.4f})")
print(f"Biais de sortie: {biases2[0]:.4f}")
# Créer des widgets pour l'exploration
hidden_units_viz = widgets.IntSlider(value=4, min=2, max=8, step=1, description='Neurones:')
activation_viz = widgets.Dropdown(
options=['relu', 'tanh', 'sigmoid'],
value='relu',
description='Activation:'
)
# Interface interactive
viz_output = widgets.interactive_output(
visualize_hidden_neurons,
{'hidden_units': hidden_units_viz, 'activation': activation_viz}
)
# Afficher les widgets
print("Explorez comment chaque neurone caché contribue à la solution:")
display(widgets.HBox([hidden_units_viz, activation_viz]))
display(viz_output)
Cellule 17 (Markdown) - Conclusion
## Conclusion : L'anatomie d'un réseau de neurones
### Ce que nous avons exploré
Dans ce notebook, nous avons disséqué le fonctionnement interne d'un réseau de neurones en explorant :
1. **Le neurone individuel**
- Comment les entrées sont pondérées et combinées
- L'effet du biais sur le seuil d'activation
- L'impact des différentes fonctions d'activation
2. **La structure d'un réseau**
- Comment les neurones s'organisent en couches
- Comment l'information se propage à travers le réseau
- Comment les couches interagissent pour créer des représentations complexes
3. **Le processus d'apprentissage**
- Comment un réseau s'entraîne par descente de gradient
- Comment les poids s'ajustent pour minimiser l'erreur
- Comment le réseau apprend progressivement à résoudre des problèmes complexes
4. **La résolution de problèmes non linéaires**
- Comment un problème comme XOR nécessite plusieurs neurones
- Comment chaque neurone caché contribue à la solution finale
- Comment les frontières de décision complexes émergent de la combinaison de neurones simples
### Applications pratiques
Ces connaissances fondamentales vous permettront de :
- **Concevoir** des architectures appropriées pour différents problèmes
- **Diagnostiquer** les problèmes dans vos modèles (sous-apprentissage, sur-apprentissage)
- **Optimiser** les performances de vos réseaux
- **Expliquer** le fonctionnement interne des modèles de Deep Learning
### Prochaines étapes
Dans les modules suivants, nous approfondirons ces concepts en explorant :
- Les réseaux de neurones convolutifs (CNN) pour la vision par ordinateur
- Les réseaux récurrents (RNN) pour le traitement de séquences
- Les techniques avancées d'entraînement et d'optimisation
- L'application pratique de ces connaissances dans des projets réels
Maintenant que vous avez une compréhension solide de l'anatomie d'un réseau de neurones, vous êtes prêt à aborder des architectures plus complexes et spécialisées !
Cellule 18 (Code) - Schéma conceptuel à compléter
# Fonction pour générer un schéma conceptuel à compléter
def create_conceptual_diagram():
# Créer la figure
plt.figure(figsize=(12, 10))
# Définir les positions des composants
input_layer_x = 0.1
hidden_layer1_x = 0.3
hidden_layer2_x = 0.5
output_layer_x = 0.7
prediction_x = 0.9
input_layer_y = [0.2, 0.5, 0.8]
hidden_layer1_y = [0.2, 0.5, 0.8]
hidden_layer2_y = [0.3, 0.7]
output_layer_y = [0.5]
# Dessiner les couches
plt.text(0.02, 0.5, "1. Couche d'entrée", fontsize=12, ha='left', va='center')
for y in input_layer_y:
circle = plt.Circle((input_layer_x, y), 0.05, fill=True, color='lightblue', alpha=0.7)
plt.gca().add_patch(circle)
plt.text(hidden_layer1_x-0.08, 0.08, "2. Première couche cachée", fontsize=12, ha='center', va='center')
for y in hidden_layer1_y:
circle = plt.Circle((hidden_layer1_x, y), 0.05, fill=True, color='lightgreen', alpha=0.7)
plt.gca().add_patch(circle)
plt.text(hidden_layer2_x-0.08, 0.08, "3. Deuxième couche cachée", fontsize=12, ha='center', va='center')
for y in hidden_layer2_y:
circle = plt.Circle((hidden_layer2_x, y), 0.05, fill=True, color='lightsalmon', alpha=0.7)
plt.gca().add_patch(circle)
plt.text(output_layer_x-0.02, 0.08, "4. Couche de sortie", fontsize=12, ha='center', va='center')
for y in output_layer_y:
circle = plt.Circle((output_layer_x, y), 0.05, fill=True, color='plum', alpha=0.7)
plt.gca().add_patch(circle)
# Dessiner le processus d'apprentissage
plt.text(prediction_x-0.02, 0.08, "5. Prédiction", fontsize=12, ha='center', va='center')
rect = plt.Rectangle((prediction_x-0.06, 0.45), 0.12, 0.1, fill=True, color='lightgrey', alpha=0.7)
plt.gca().add_patch(rect)
plt.text(prediction_x, 0.5, "ŷ", fontsize=14, ha='center', va='center')
# Erreur et données réelles
plt.text(prediction_x-0.02, 0.35, "6. Calcul de l'erreur", fontsize=12, ha='center', va='center')
rect = plt.Rectangle((prediction_x-0.06, 0.25), 0.12, 0.1, fill=True, color='lightcoral', alpha=0.7)
plt.gca().add_patch(rect)
plt.text(prediction_x, 0.3, "Loss", fontsize=14, ha='center', va='center')
plt.text(prediction_x-0.02, 0.15, "7. Données réelles", fontsize=12, ha='center', va='center')
rect = plt.Rectangle((prediction_x-0.06, 0.15), 0.12, 0.1, fill=True, color='lightblue', alpha=0.7)
plt.gca().add_patch(rect)
plt.text(prediction_x, 0.2, "y", fontsize=14, ha='center', va='center')
# Connexions entre les couches
for y1 in input_layer_y:
for y2 in hidden_layer1_y:
plt.plot([input_layer_x, hidden_layer1_x], [y1, y2], 'k-', alpha=0.3)
for y1 in hidden_layer1_y:
for y2 in hidden_layer2_y:
plt.plot([hidden_layer1_x, hidden_layer2_x], [y1, y2], 'k-', alpha=0.3)
for y1 in hidden_layer2_y:
for y2 in output_layer_y:
plt.plot([hidden_layer2_x, output_layer_x], [y1, y2], 'k-', alpha=0.3)
# Connexion sortie -> prédiction
plt.plot([output_layer_x, prediction_x], [output_layer_y[0], 0.5], 'k-', alpha=0.3)
# Flux d'erreur
plt.plot([prediction_x, prediction_x], [0.45, 0.35], 'r--', alpha=0.7)
plt.arrow(prediction_x, 0.2, 0, 0.05, head_width=0.01, head_length=0.01, fc='blue', ec='blue')
# Rétropropagation
plt.arrow(prediction_x-0.1, 0.3, -0.1, 0, head_width=0.01, head_length=0.01, fc='red', ec='red', linestyle='dashed')
plt.text(prediction_x-0.15, 0.33, "Rétropropagation", fontsize=10, ha='center', va='center', color='red')
# Propagation avant
plt.arrow(hidden_layer2_x+0.1, 0.5, 0.1, 0, head_width=0.01, head_length=0.01, fc='green', ec='green')
plt.text(hidden_layer2_x+0.15, 0.53, "Propagation avant", fontsize=10, ha='center', va='center', color='green')
# Finalisation du schéma
plt.xlim(0, 1)
plt.ylim(0, 1)
plt.title("Schéma conceptuel d'un réseau de neurones", fontsize=16)
plt.axis('off')
plt.tight_layout()
plt.show()
print("Complétez le schéma conceptuel en identifiant les éléments numérotés:")
print("1. ________________________________")
print("2. ________________________________")
print("3. ________________________________")
print("4. ________________________________")
print("5. ________________________________")
print("6. ________________________________")
print("7. ________________________________")
# Afficher le schéma conceptuel
create_conceptual_diagram()
Cellule 19 (Markdown) - Exercice final
## Exercice final : Synthèse des connaissances
Pour consolider votre compréhension des réseaux de neurones, complétez les informations suivantes :
### Structure d'un réseau de neurones pour la reconnaissance de chiffres manuscrits (MNIST)
| Couche | Nombre de neurones | Fonction d'activation recommandée |
|--------|-------------------|----------------------------------|
| Couche d'entrée | _______ | _______ |
| Première couche cachée | _______ | _______ |
| Deuxième couche cachée (facultative) | _______ | _______ |
| Couche de sortie | _______ | _______ |
### Processus d'apprentissage
Décrivez brièvement les étapes du processus d'apprentissage d'un réseau de neurones :
1. _________________________________________________________________
2. _________________________________________________________________
3. _________________________________________________________________
4. _________________________________________________________________
### Réflexion personnelle
Comment expliqueriez-vous maintenant le fonctionnement d'un réseau de neurones à un camarade qui n'a jamais étudié ce sujet ?
_________________________________________________________________
_________________________________________________________________
_________________________________________________________________
### Auto-évaluation
Sur une échelle de 1 à 5, évaluez votre niveau de compréhension actuel des éléments suivants :
- Structure d'un neurone : ___/5
- Fonctions d'activation : ___/5
- Architecture d'un réseau : ___/5
- Processus d'apprentissage : ___/5
Ce contenu complète le document sur l'anatomie d'un réseau de neurones, en ajoutant les cellules 12 à 19 qui manquaient dans la version originale.