Plusieurs neurones, pourquoi ?

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

Notre neurone sait tracer une direction. Notre règle, elle, lui demande un virage.

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_rate dé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 ?