De l'apprentissage à la prédiction

Retour à notre modèle d’arrosage#

Dans les deux premières leçons, nous avons construit progressivement un modèle capable d’apprendre à partir de données.

Nous avons ensuite étudié la descente de gradient avec un seul poids w.

Revenons maintenant à notre problème initial : décider s’il faut arroser une plante en fonction de la température et de l’humidité.

Cette fois, notre modèle possède plusieurs paramètres.

Il doit apprendre :

  • un poids pour la température ;
  • un poids pour l’humidité ;
  • un biais.

Nous disposons toujours de quelques observations simples :

Température Humidité Arrosage
22 70 0
24 65 0
27 42 1
29 35 1
25 55 0

La valeur 0 signifie « ne pas arroser ».

La valeur 1 signifie « arroser ».

Notre objectif est maintenant de laisser la descente de gradient apprendre les trois paramètres du modèle.

Notre modèle#

Le modèle calcule d’abord un score :

score = température × poids_temperature
        + humidité × poids_humidite
        + biais

Puis nous transformons ce score en décision :

si score > 0
    arroser
sinon
    ne pas arroser

Pour commencer, nous faisons volontairement simple.

Nous n’utilisons encore aucune autre fonction. Le score peut donc prendre n’importe quelle valeur.

Le problème est de savoir si cette approche suffit.

Le programme complet#

Avant de regarder Python, résumons ce que le programme doit faire.

Pseudo-code
charger les données d’apprentissage
initialiser les poids et le biais à zéro
choisir un taux d’apprentissage
répéter pour chaque époque
    mettre les gradients et la perte à zéro
    pour chaque observation
        calculer la prédiction à partir de la température, de l’humidité et des paramètres
        calculer l’erreur
        ajouter la perte
        calculer la contribution de l’observation aux gradients
    calculer les valeurs moyennes de la perte et des gradients
    modifier les poids et le biais dans la direction indiquée par les gradients
    afficher les résultats de l’époque
afficher les paramètres appris
tester le modèle sur les observations
pour chaque observation
    calculer son score
    si le score est positif, décider « arroser »
    sinon, décider « ne pas arroser »

Nous retrouvons exactement le mécanisme étudié dans la leçon précédente, mais avec plusieurs paramètres.

Voici maintenant le programme Python complet :

donnees = [
    (22, 70, 0),
    (24, 65, 0),
    (27, 42, 1),
    (29, 35, 1),
    (25, 55, 0),
]

poids_temperature = 0.0
poids_humidite = 0.0
biais = 0.0

taux_apprentissage = 0.001
nombre_epochs = 100


def prediction(temperature, humidite):
    return (
        temperature * poids_temperature
        + humidite * poids_humidite
        + biais
    )


for epoch in range(nombre_epochs):
    gradient_temperature = 0.0
    gradient_humidite = 0.0
    gradient_biais = 0.0
    perte = 0.0

    for temperature, humidite, label in donnees:
        resultat = prediction(temperature, humidite)
        erreur = resultat - label

        perte += erreur ** 2

        gradient_temperature += 2 * temperature * erreur
        gradient_humidite += 2 * humidite * erreur
        gradient_biais += 2 * erreur

    gradient_temperature /= len(donnees)
    gradient_humidite /= len(donnees)
    gradient_biais /= len(donnees)
    perte /= len(donnees)

    poids_temperature -= taux_apprentissage * gradient_temperature
    poids_humidite -= taux_apprentissage * gradient_humidite
    biais -= taux_apprentissage * gradient_biais

    print(
        "Epoch :", epoch + 1,
        "| perte :", round(perte, 6),
        "| grad_T :", round(gradient_temperature, 6),
        "| grad_H :", round(gradient_humidite, 6),
        "| grad_biais :", round(gradient_biais, 6)
    )

print("\n=== Modèle final ===")
print("Poids température :", poids_temperature)
print("Poids humidité :", poids_humidite)
print("Biais :", biais)

print("\n=== Test ===")

for temperature, humidite, label in donnees:
    score = prediction(temperature, humidite)

    if score > 0:
        decision = "arroser"
    else:
        decision = "ne pas arroser"

    print(
        "température =", temperature,
        "| humidité =", humidite,
        "| score =", round(score, 6),
        "| attendu =", label,
        "| décision =", decision
    )

Cette fois, nous n’avons plus un seul poids w.

Nous avons donc trois paramètres que la descente de gradient doit ajuster.

Une première tentative#

Commençons avec :

taux_apprentissage = 0.001
nombre_epochs = 100

Le résultat n’est pas très encourageant.

Dès les premières époques, la perte augmente fortement :

Epoch : 1 | perte : 0.4 | grad_T : -22.4 | grad_H : -30.8 | grad_biais : -0.8
Epoch : 2 | perte : 3.979453 | grad_T : 88.41312 | grad_H : 215.14896 | grad_biais : 3.62896
Epoch : 3 | perte : 145.603543 | ...
Epoch : 4 | perte : 5662.132883 | ...

Les valeurs continuent ensuite à grossir.

Le modèle diverge.

Le taux d’apprentissage est pourtant le même principe que dans la leçon précédente.

Mais nos données ne sont plus du tout dans la même échelle.

La température est autour de 20 ou 30.

L’humidité est autour de 40 à 70.

Le poids associé à l’humidité reçoit donc des corrections beaucoup plus importantes que lorsque nous travaillions avec une simple valeur x comprise entre 1 et 4.

Notre premier réflexe est donc de réduire le taux d’apprentissage.

Essayons plus petit#

Passons à :

taux_apprentissage = 0.00001

Avec 100 époques, l’apprentissage devient stable.

La perte diminue :

Epoch : 1 | perte : 0.4
Epoch : 10 | perte : 0.323331
Epoch : 100 | perte : 0.249006

Cette fois, le modèle ne diverge plus.

Mais le résultat final n’est toujours pas satisfaisant.

Les paramètres appris sont :

Poids température : 0.008899723076964385
Poids humidité : 0.0016567612636759176
Biais : 0.00026465299479816375

Les scores restent tous proches les uns des autres.

Notre modèle décide alors :

arroser
arroser
arroser
arroser
arroser

Nous avons donc stabilisé l’apprentissage, mais nous n’avons pas réellement appris notre règle de décision.

Trouvons un compromis#

Augmentons légèrement le taux d’apprentissage :

taux_apprentissage = 0.0001

Après 100 époques :

Epoch : 1 | perte : 0.4
Epoch : 10 | perte : 0.253249
Epoch : 100 | perte : 0.066838

Cette fois, le résultat est nettement meilleur.

Le modèle apprend :

Poids température : 0.04433169880296688
Poids humidité : -0.01417226800761924
Biais : 0.0012139153511863618

Les décisions deviennent :

température = 22 | humidité = 70 | score = -0.0155 | attendu = 0 | décision = ne pas arroser
température = 24 | humidité = 65 | score = 0.144... | attendu = 0 | décision = arroser
température = 27 | humidité = 42 | score = 0.602... | attendu = 1 | décision = arroser
température = 29 | humidité = 35 | score = 0.790... | attendu = 1 | décision = arroser
température = 25 | humidité = 55 | score = 0.33... | attendu = 0 | décision = arroser

Nous obtenons donc 3 bonnes décisions sur 5.

Ce n’est pas catastrophique.

Mais ce n’est pas non plus le modèle que nous cherchons.

La tentation d’attendre#

Peut-être que le problème vient simplement du nombre d’époques.

Après tout, nous n’avons utilisé que 100 époques.

Passons donc à :

taux_apprentissage = 0.0001
nombre_epochs = 10000

Mais nous ne voulons pas afficher 10 000 lignes dans le terminal.

Il suffit de modifier l’affichage du programme.

Nous remplaçons :

print(
    "Epoch :", epoch + 1,
    "| perte :", round(perte, 6),
    "| grad_T :", round(gradient_temperature, 6),
    "| grad_H :", round(gradient_humidite, 6),
    "| grad_biais :", round(gradient_biais, 6)
)

par :

if epoch < 10 or (epoch + 1) % 1000 == 0:
    print(
        "Epoch :", epoch + 1,
        "| perte :", round(perte, 6),
        "| grad_T :", round(gradient_temperature, 6),
        "| grad_H :", round(gradient_humidite, 6),
        "| grad_biais :", round(gradient_biais, 6)
    )

Nous ne changeons pas l’apprentissage.

Nous changeons uniquement ce qui est affiché.

epoch commence à 0.

Nous utilisons donc epoch + 1 pour afficher un compteur commençant à 1.

L’opérateur modulo % donne le reste d’une division.

Ainsi :

2000 % 1000

vaut 0.

Nous pouvons donc afficher les dix premières époques, puis seulement les époques qui sont des multiples de 1 000.

Le résultat devient beaucoup plus lisible :

Epoch : 1 | perte : 0.4
Epoch : 2 | perte : ...
...
Epoch : 10 | perte : ...
Epoch : 1000 | perte : 0.042614
Epoch : 2000 | perte : 0.042614
...
Epoch : 10000 | perte : 0.04261

Nous pouvons maintenant laisser tourner les 10 000 époques sans transformer notre terminal en mur de texte.

Et surtout, le résultat nous surprend.

Après 10 000 époques, la perte reste pratiquement la même :

0.04261

Les paramètres finaux sont :

Poids température : 0.06243206569256493
Poids humidité : -0.02229944125735614
Biais : 0.003899875682740519

Et les décisions restent identiques.

Le modèle ne tend donc pas vers une solution parfaite simplement parce que nous le laissons travailler plus longtemps.

Nous pourrions laisser tourner ce programme dix fois plus longtemps, cela ne résoudrait pas le problème.

Il faut donc regarder ailleurs.

Regardons l’échelle de nos données#

Nous avons jusqu’ici utilisé directement les valeurs :

température : 22 à 29
humidité    : 35 à 70

Regardons maintenant ce que cela signifie pour les gradients.

Dans notre formule :

gradient_temperature += 2 × température × erreur
gradient_humidite += 2 × humidité × erreur

les valeurs de température et d’humidité sont directement utilisées dans le calcul.

Le gradient dépend donc fortement de leur échelle.

Nous pouvons ramener les données dans un intervalle plus petit.

Essayons de diviser les deux mesures par 100.

Normalisons les données#

La normalisation consiste à transformer les valeurs d’entrée pour les ramener dans une échelle comparable et plus petite.

Dans notre cas, nous divisons simplement la température et l’humidité par 100 afin de les rapprocher de l’intervalle 0 à 1.

Les données deviennent :

donnees = [
    (22 / 100, 70 / 100, 0),
    (24 / 100, 65 / 100, 0),
    (27 / 100, 42 / 100, 1),
    (29 / 100, 35 / 100, 1),
    (25 / 100, 55 / 100, 0),
]

La température et l’humidité sont maintenant comprises approximativement entre 0 et 1.

Le reste du programme ne change pas.

Nous pouvons garder :

taux_apprentissage = 0.0001
nombre_epochs = 10000

Cette fois, le comportement est différent.

La perte diminue progressivement :

Epoch : 1 | perte : 0.4
Epoch : 10 | perte : 0.399295
Epoch : 100 | perte : 0.392428
Epoch : 1000 | perte : 0.339117
Epoch : 2000 | perte : 0.303096
...
Epoch : 10000 | perte : 0.245249

La normalisation change donc bien l’échelle des calculs.

Mais avec le même taux d’apprentissage, l’apprentissage devient maintenant beaucoup plus lent.

Nous avons changé l’échelle des entrées.

Il faut donc adapter le taux d’apprentissage à cette nouvelle échelle.

Adaptons le taux d’apprentissage à cette nouvelle échelle#

Passons maintenant à :

taux_apprentissage = 0.001
nombre_epochs = 10000

La perte descend davantage :

Epoch : 1 | perte : 0.4
Epoch : 10 | perte : 0.393092
Epoch : 100 | perte : 0.33948
Epoch : 1000 | perte : 0.245259
Epoch : 2000 | perte : 0.232627
...
Epoch : 10000 | perte : 0.158705

Les paramètres finaux sont :

Poids température : 0.3567913070729241
Poids humidité : -0.710991373229769
Biais : 0.6721755384720498

La perte est maintenant plus faible.

Pourtant, notre règle de décision n’est toujours pas idéale.

Le modèle produit encore :

arroser
arroser
arroser
arroser
arroser

Nous venons donc de découvrir quelque chose d’important.

La descente de gradient fonctionne bien.

Elle ajuste bien les paramètres.

La perte diminue.

Mais cela ne suffit pas à garantir que la sortie de notre modèle soit directement interprétable comme une décision.

Ce que nous venons d’apprendre#

Nous avons commencé avec un problème concret.

Notre modèle devait décider s’il fallait arroser.

Nous lui avons donné deux caractéristiques :

  • la température ;
  • l’humidité.

La descente de gradient a réussi à apprendre trois paramètres :

  • poids_temperature ;
  • poids_humidite ;
  • biais.

Nous avons aussi rencontré deux problèmes très concrets.

D’abord, l’échelle des données influence fortement les gradients.

Une même valeur de taux_apprentissage ne se comporte pas nécessairement de la même manière selon l’échelle des données.

Ensuite, diminuer la perte ne signifie pas automatiquement que la sortie du modèle correspond à une décision facilement exploitable.

Notre modèle sait calculer un score.

Mais ce score peut être négatif, positif, très petit ou beaucoup plus grand.

Le modèle calcule un score#

Notre modèle produit actuellement :

score = température × poids_temperature
        + humidité × poids_humidite
        + biais

Ce score est parfaitement utilisable pour comparer deux observations.

Une observation avec un score plus élevé est davantage orientée vers la classe 1.

Une observation avec un score plus faible est davantage orientée vers la classe 0.

Mais nous avons utilisé :

if score > 0:
    decision = "arroser"
else:
    decision = "ne pas arroser"

Nous avons donc ajouté nous-mêmes une frontière artificielle à 0.

Le modèle produit un nombre continu.

Nous le transformons ensuite brutalement en deux choix.

Il nous manque donc une étape entre les deux.

De l’apprentissage à la prédiction#

Nous avons maintenant :

température
      +
humidité
   paramètres
      score
   décision

Le problème se situe entre le score et la décision.

Nous aimerions pouvoir interpréter la sortie du modèle comme une valeur comprise entre 0 et 1.

Une valeur proche de 0 pourrait représenter une faible probabilité d’arroser.

Une valeur proche de 1 pourrait représenter une forte probabilité d’arroser.

Cela nous permettrait de conserver une information progressive au lieu de passer directement d’un score arbitraire à une décision binaire.

Comment transformer notre score en une sortie comprise entre 0 et 1 ?

C’est précisément ce que nous allons regarder ensuite.

La réponse nous conduira vers une fonction très simple en apparence, mais particulièrement importante en apprentissage automatique : la sigmoïde.