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.
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.