Plusieurs neurones, pourquoi ?#
Dans la leçon précédente, nous avons construit notre premier neurone artificiel.
Température et humidité entrent dans notre modèle. Quelques poids, un biais et une sigmoïde plus tard, nous obtenons une sortie comprise entre 0 et 1.
Nous lui avons même appris à corriger ses paramètres.
Et ça marche.
Sur nos cinq observations, les résultats se trouvent tous du bon côté de 0.5.
Et si nous poussions notre neurone à ses limites ?#
Sommes-nous en mesure de lui fournir une règle d’arrosage qui reste parfaitement logique pour nous, mais qui soit à la limite pour lui ?
Une contrainte supplémentaire assez simple : arroseriez-vous lorsqu’il fait très chaud en plein soleil, le sol lézardé et crevassé ?
Dans notre scénario expérimental, il est préférable d’attendre que la température redescende.
Nous aurions donc quelque chose comme :
| Température | Humidité | Arrosage |
|---|---|---|
| 22 °C | 40 % | non |
| 28 °C | 40 % | oui |
| 36 °C | 40 % | non |
Même humidité.
La température augmente.
Et pourtant nous demandons successivement à notre neurone :
non → oui → non
Peut-il apprendre cette règle ?
Ne sortons pas notre serpent de la poche tout de suite…
Essayons.
Nous reprenons le neurone que nous avions construit précédemment et changeons uniquement ses données d’entraînement :
training_data = [
([22, 40], 0),
([28, 40], 1),
([36, 40], 0),
]
Après dix itérations :
Initial loss: 1.4647299641116458
Iteration: 10 Loss: 1.0883803995070394
=== Predictions after training ===
[22, 40] target = 0 output = 0.224012789428278
[28, 40] target = 1 output = 0.7134254811096002
[36, 40] target = 0 output = 0.9777902202427355
Les deux premiers cas vont dans le bon sens.
Le troisième, beaucoup moins.
À 36 °C, alors que nous demandons 0, notre neurone répond presque 1.
Peut-être a-t-il simplement besoin de davantage de temps.
Passons à 1000 itérations.
Initial loss: 1.4647299641116458
Iteration: 100 Loss: 1.087317171813268
Iteration: 500 Loss: 1.0764500144047928
Iteration: 1000 Loss: 1.0558732819682475
=== Predictions after training ===
[22, 40] target = 0 output = 0.1732503426654294
[28, 40] target = 1 output = 0.8173583777871752
[36, 40] target = 0 output = 0.9962427608620434
Non.
Il insiste même.
Plus la température augmente, plus sa sortie augmente.
Pourquoi ?
Avec une humidité toujours égale à 40, son calcul revient finalement à quelque chose de cette forme :
score = température × poids + constante
Si le poids est positif, le score monte avec la température.
S’il est négatif, il descend.
Mais notre neurone ne peut pas lui demander :
descends → monte → redescends
Nous venons de rencontrer une limite de notre neurone unique.
Et si nous partagions le travail ?#
Essayons une autre approche.
Un neurone pourrait répondre à :
« Fait-il assez chaud ? »
Et un autre :
« Ne fait-il pas trop chaud ? »
À 22 °C, le premier devrait répondre non.
À 28 °C, les deux devraient répondre oui.
À 36 °C, le second devrait répondre non.
Pour vérifier cette idée, commençons par leur donner nous-mêmes leur rôle :
def neuron_min_temperature(temperature):
score = temperature - 25
return sigmoid(score)
def neuron_max_temperature(temperature):
score = 32 - temperature
return sigmoid(score)
Résultat :
22 min = 0.04742587317756678 max = 0.9999546021312976
28 min = 0.9525741268224334 max = 0.9820137900379085
36 min = 0.999983298578152 max = 0.01798620996209156
À 22 °C, le premier bloque.
À 28 °C, les deux sont fortement activés.
À 36 °C, c’est le second qui bloque.
Essayons de combiner leurs réponses :
def watering_decision(minimum, maximum):
return minimum * maximum
Pour chaque température, nous calculons les réponses de nos deux neurones puis leur décision commune :
minimum = neuron_min_temperature(temperature)
maximum = neuron_max_temperature(temperature)
decision = watering_decision(
minimum,
maximum
)
Résultat :
22 min = 0.04742587317756678 max = 0.9999546021312976 watering = 0.04742372014400317
28 min = 0.9525741268224334 max = 0.9820137900379085 watering = 0.935440928572949
36 min = 0.999983298578152 max = 0.01798620996209156 watering = 0.017985909566811537
Avec 0.5 comme seuil :
22 °C → non
28 °C → oui
36 °C → non
Ça marche.
Sauf que nous avons un peu triché.
Nous avons nous-mêmes écrit :
score = temperature - 25
et :
score = 32 - temperature
Nos deux neurones n’ont donc rien appris.
Nous leur avons soufflé la réponse.
Laissons-les apprendre#
C’est là que nous retrouvons ce que nous avions vu précédemment : laisser notre neurone apprendre ses paramètres plutôt que les choisir nous-mêmes.
Commençons avec celui qui doit reconnaître « assez chaud ».
Nous lui donnons :
training_data = [
(22, 0),
(28, 1),
(36, 1),
]
Et surtout :
weight = 0.0
bias = 0.0
Cette fois, pas de 25 caché dans notre formule.
Après 1000 itérations :
22 target = 0 output = 0.5224750647105219
28 target = 1 output = 0.583933496214407
36 target = 1 output = 0.6617084149920676
Il va dans la bonne direction, mais 22 °C reste juste au-dessus de 0.5.
Continuons.
Après 10000 itérations :
Weight: 0.2444448774407787
Bias : -5.9646538878523465
22 target = 0 output = 0.357354127926033
28 target = 1 output = 0.7067813299744308
36 target = 1 output = 0.9445570592542989
Cette fois :
non → oui → oui
Notre premier neurone a appris son rôle.
Son point de bascule se situe autour de 24.4 °C.
Nous ne lui avons pas donné cette température : elle résulte des paramètres qu’il a appris.
Passons au second :
training_data = [
(22, 1),
(28, 1),
(36, 0),
]
Nous voulons maintenant :
oui → oui → non
Après 10000 itérations, quelque chose nous chiffonne :
Iteration: 3000 Loss: 0.49476354205827655
Iteration: 5000 Loss: 0.8418379912683794
Iteration: 7000 Loss: 0.31766551719763914
Iteration: 8000 Loss: 0.6487793462315034
Iteration: 10000 Loss: 0.43142388969196016
Ça descend.
Ça remonte.
Ça redescend.
Notre apprentissage fait un peu le yoyo.
Nous utilisons alors :
learning_rate = 0.01
Réduisons-le :
learning_rate = 0.001
Les oscillations disparaissent.
En revanche, après 10000 itérations :
22 target = 1 output = 0.6295212385672573
28 target = 1 output = 0.585818945176154
36 target = 0 output = 0.5255027722192142
C’est plus stable, mais plus lent.
À 36 °C, nous sommes encore du mauvais côté.
Poussons à 100000 itérations.
Iteration: 10000 Loss: 0.5849536224578121
Iteration: 50000 Loss: 0.30110131056896317
Iteration: 100000 Loss: 0.1871744257344013
Weight: -0.21687684001056218
Bias : 6.880062347894262
22 target = 1 output = 0.891752839001062
28 target = 1 output = 0.6915788232571953
36 target = 0 output = 0.28343139488534946
Cette fois :
oui → oui → non
Le second neurone a appris son rôle lui aussi.
Son point de bascule se situe autour de 31.7 °C.
Souvenons-nous de notre triptyque :
- le poids détermine comment une entrée influence le neurone : dans quel sens et avec quelle force ;
- le biais décale le point à partir duquel le neurone commence à s’activer ;
- le
learning_ratedétermine la taille des corrections effectuées pendant l’apprentissage.
En raccourci : le poids incline, le biais décale, le learning_rate règle la taille des pas.
Trop grands, les pas peuvent faire osciller l’apprentissage. Trop petits, il avance… mais prend son temps.
Nous venons justement de le voir.
Réunissons nos deux neurones#
Nous avons maintenant :
neurone 1 : "assez chaud ?"
22 → non
28 → oui
36 → oui
neurone 2 : "pas trop chaud ?"
22 → oui
28 → oui
36 → non
Réutilisons notre multiplication :
watering = minimum * maximum
Résultat :
22 min = 0.3608567127375629 max = 0.891752839001062 watering = 0.3217949980563124
28 min = 0.7049013197602678 max = 0.6915788232571953 watering = 0.48749482523224996
36 min = 0.9423475981684872 max = 0.28343139488534946 watering = 0.2670908942157531
Ah.
À 28 °C, chacun de nos neurones répond pourtant oui :
0.705 > 0.5
0.692 > 0.5
Mais :
0.705 × 0.692 ≈ 0.487
Nous repassons sous 0.5.
non → non → non
Avoir plusieurs neurones ne suffit donc pas.
Encore faut-il savoir quoi faire de leurs réponses.
Et si nous laissions un neurone apprendre cela aussi ?
Un neurone qui écoute les autres#
Ajoutons un troisième neurone.
Cette fois, ses entrées ne seront ni une température ni une humidité.
Elles seront les sorties de nos deux premiers neurones :
neurone 1 ──┐
température ──── ├──> neurone de sortie ──> arrosage
neurone 2 ──┘
Ses données d’entraînement deviennent :
[0.3608567127375629, 0.891752839001062] → 0
[0.7049013197602678, 0.6915788232571953] → 1
[0.9423475981684872, 0.28343139488534946] → 0
Nous initialisons encore ses poids et son biais à zéro.
Résultat avant apprentissage :
Loss: 0.75
22 → 0.5
28 → 0.5
36 → 0.5
Normal.
Avec des poids à zéro et un biais à zéro, son score vaut zéro.
Et sigmoid(0) = 0.5.
Laissons-le apprendre.
Après 100000 itérations :
22 → 0.3627140055495805
28 → 0.382212386991098
36 → 0.32137259020941683
Toujours :
non → non → non
Mais quelque chose a changé.
La valeur la plus élevée est maintenant celle de 28 °C.
Notre neurone commence donc à distinguer le cas qui nous intéresse.
Continuons.
Après 500000 itérations :
Weight min: 5.740437152115647
Weight max: 5.7586645184847685
Bias : -7.962631163158695
Et surtout :
22 min = 0.3608567127375629 max = 0.891752839001062 target = 0 output = 0.3195478557091965
28 min = 0.7049013197602678 max = 0.6915788232571953 target = 1 output = 0.5165891570076458
36 min = 0.9423475981684872 max = 0.28343139488534946 target = 0 output = 0.28476279992382514
Nous y sommes.
22 °C → non
28 °C → oui
36 °C → non
Ce qu’un neurone seul n’arrivait pas à apprendre, plusieurs neurones viennent de le faire ensemble.
Bon.
Il aura tout de même fallu 500000 itérations à notre neurone de sortie pour franchir enfin 0.5 au bon endroit.
Cela peut sembler énorme à l’échelle de notre petit programme Python. À l’échelle de l’entraînement des modèles d’IA modernes, c’est pourtant minuscule.
Notre Arduino UNO Q n’est pas en train d’entraîner ChatGPT. :)
Mais ces 500000 petits pas nous donnent déjà un aperçu très concret de ce que signifie apprendre en répétant encore et encore de petites corrections.
Le programme qui nous a permis d’y arriver#
Puisque ce petit réseau a fini par faire ce que nous lui demandions, gardons le programme complet.
# ==========================================
# Lesson 06
# Two neurons learn their roles
# then an output neuron learns to combine them
# ==========================================
import math
training_data_min = [
(22, 0),
(28, 1),
(36, 1),
]
training_data_max = [
(22, 1),
(28, 1),
(36, 0),
]
min_weight = 0.0
min_bias = 0.0
max_weight = 0.0
max_bias = 0.0
step = 0.001
learning_rate = 0.001
def sigmoid(x):
return 1 / (1 + math.exp(-x))
def neuron(temperature, weight, bias):
score = temperature * weight + bias
return sigmoid(score)
def compute_total_loss(training_data, weight, bias):
total = 0
for temperature, target in training_data:
output = neuron(
temperature,
weight,
bias
)
error = target - output
total += error ** 2
return total
def train_neuron(training_data, weight, bias, iterations):
for iteration in range(iterations):
original_weight = weight
loss_before = compute_total_loss(
training_data,
original_weight - step,
bias
)
loss_after = compute_total_loss(
training_data,
original_weight + step,
bias
)
gradient_weight = (
loss_after - loss_before
) / (2 * step)
original_bias = bias
loss_before = compute_total_loss(
training_data,
weight,
original_bias - step
)
loss_after = compute_total_loss(
training_data,
weight,
original_bias + step
)
gradient_bias = (
loss_after - loss_before
) / (2 * step)
weight -= learning_rate * gradient_weight
bias -= learning_rate * gradient_bias
return weight, bias
print("=== Training minimum temperature neuron ===")
min_weight, min_bias = train_neuron(
training_data_min,
min_weight,
min_bias,
100000
)
print("Weight:", min_weight)
print("Bias :", min_bias)
print()
print("=== Training maximum temperature neuron ===")
max_weight, max_bias = train_neuron(
training_data_max,
max_weight,
max_bias,
100000
)
print("Weight:", max_weight)
print("Bias :", max_bias)
# ==========================================
# Build training data for the output neuron
# ==========================================
output_training_data = []
for temperature, target in [
(22, 0),
(28, 1),
(36, 0),
]:
minimum = neuron(
temperature,
min_weight,
min_bias
)
maximum = neuron(
temperature,
max_weight,
max_bias
)
output_training_data.append(
([minimum, maximum], target)
)
# ==========================================
# Output neuron
# ==========================================
output_weight_min = 0.0
output_weight_max = 0.0
output_bias = 0.0
def output_neuron(minimum, maximum):
score = (
minimum * output_weight_min
+ maximum * output_weight_max
+ output_bias
)
return sigmoid(score)
def compute_output_loss():
total = 0
for features, target in output_training_data:
minimum = features[0]
maximum = features[1]
output = output_neuron(
minimum,
maximum
)
error = target - output
total += error ** 2
return total
def train_output_neuron():
global output_weight_min
global output_weight_max
global output_bias
original_weight_min = output_weight_min
output_weight_min = original_weight_min - step
loss_before = compute_output_loss()
output_weight_min = original_weight_min + step
loss_after = compute_output_loss()
gradient_weight_min = (
loss_after - loss_before
) / (2 * step)
output_weight_min = original_weight_min
original_weight_max = output_weight_max
output_weight_max = original_weight_max - step
loss_before = compute_output_loss()
output_weight_max = original_weight_max + step
loss_after = compute_output_loss()
gradient_weight_max = (
loss_after - loss_before
) / (2 * step)
output_weight_max = original_weight_max
original_bias = output_bias
output_bias = original_bias - step
loss_before = compute_output_loss()
output_bias = original_bias + step
loss_after = compute_output_loss()
gradient_bias = (
loss_after - loss_before
) / (2 * step)
output_bias = original_bias
output_weight_min -= (
learning_rate * gradient_weight_min
)
output_weight_max -= (
learning_rate * gradient_weight_max
)
output_bias -= (
learning_rate * gradient_bias
)
print()
print("=== Output neuron training data ===")
for features, target in output_training_data:
print(
features,
"target =", target
)
print()
print("=== Training output neuron ===")
print("Initial loss:", compute_output_loss())
for iteration in range(500000):
train_output_neuron()
if (iteration + 1) % 50000 == 0:
print(
"Iteration:",
iteration + 1,
"Loss:",
compute_output_loss()
)
print()
print("=== Output neuron parameters ===")
print("Weight min:", output_weight_min)
print("Weight max:", output_weight_max)
print("Bias :", output_bias)
print()
print("=== Final predictions ===")
for temperature, target in [
(22, 0),
(28, 1),
(36, 0),
]:
minimum = neuron(
temperature,
min_weight,
min_bias
)
maximum = neuron(
temperature,
max_weight,
max_bias
)
output = output_neuron(
minimum,
maximum
)
print(
temperature,
"min =", minimum,
"max =", maximum,
"target =", target,
"output =", output
)
Avec notre expérience :
22 min = 0.3608567127375629 max = 0.891752839001062 target = 0 output = 0.3195478557091965
28 min = 0.7049013197602678 max = 0.6915788232571953 target = 1 output = 0.5165891570076458
36 min = 0.9423475981684872 max = 0.28343139488534946 target = 0 output = 0.28476279992382514
Voilà pour notre premier réseau.
Mais avant d’aller plus loin, regardons le chemin parcouru.
Nous étions partis avec une question toute simple :
Pourquoi aurions-nous besoin de plusieurs neurones ?
Cette fois, nous avons de quoi y répondre.
Alors, pourquoi plusieurs neurones ?#
Nous avons commencé avec un seul neurone.
Tant que la décision évoluait dans une seule direction, il s’en sortait plutôt bien.
Mais nous lui avons demandé :
non → oui → non
Et là, il s’est retrouvé coincé.
Nous avons alors partagé le problème entre deux neurones : l’un pour reconnaître « assez chaud », l’autre « pas trop chaud ».
Ça fonctionne lorsque nous leur donnons nous-mêmes leurs rôles.
Nous les avons donc laissés apprendre.
Le premier a appris sa frontière.
Le second aussi — non sans nous rappeler qu’un learning_rate trop important peut faire valser l’apprentissage.
Restait à réunir leurs réponses.
Notre première idée, les multiplier, nous a donné :
0.705 × 0.692 ≈ 0.487
Raté de peu.
Alors plutôt que de décider nous-mêmes comment les combiner, nous avons confié ce travail à un troisième neurone.
Et après apprentissage :
22 °C → non
28 °C → oui
36 °C → non
Nous avons notre réponse.
Un neurone peut apprendre une décision simple.
Plusieurs neurones peuvent se répartir le problème, puis apprendre à combiner ce qu’ils ont compris.
Sans vraiment nous en rendre compte, nous venons de construire notre premier petit réseau de neurones.
Il reste pourtant quelque chose que nous avons fait à sa place.
Nous avons décidé du rôle des deux premiers neurones et nous les avons entraînés séparément.
Un réseau pourrait-il apprendre tout cela ensemble, sans que nous lui indiquions le rôle de chaque neurone ?