Pour le second post de l'année 2020, une implémentation d'un algo assez simple de Machine Learning, le Perceptron
import numpy as np
commençons par vérifier quel est la taille d'une matrice numpy quand on charge typiquement un fichier csv ou son équivalent.
Ca nous aidera à élaborer nos tests.
Voici le contenu du fichier "test.csv"
var1, var2, var3
1,2,3
4,5,6
7,8,9
10,11,12
13,14,15
on va vérifier qu'on arrive à le charger de telle sorte que le premier individu a pour valeur [1,2,3], le second [4,5,6], et le 3e [7,8,9].
on verra quelle forme a l'objet numpy, et quels entrées devra traiter notre perceptron
from numpy import genfromtxt
my_data = genfromtxt('test.csv', delimiter=',')
print(my_data.shape)
print(my_data)
oups, nous avons oublié d'ignorer l'en-tête. Cependant le csv est bien chargé et on sait quelle forme "doivent" avoir les données en entrée.
my_data = genfromtxt('test.csv', delimiter=',',skip_header=True)
print(my_data.shape)
print(my_data)
une petite création à la main pour s'en convaincre encore plus.
my_data2 = np.array(range(15)).reshape(5,3)
print(my_data2.shape)
print(my_data2)
print(my_data)
On va s'essayer à l'élaboration du perception en TDD.
En premier lieu, nous allons vérifier que nous pouvons éxécuter des tests unitaires dans notre notebook
import unittest
class Test(unittest.TestCase):
def test_true_is_not_false(self):
#devrait échouer
self.assertEquals(True, False)
unittest.main(argv=[''], verbosity=2, exit=False)
Le test échoue comme on l'attend
class Test(unittest.TestCase):
def test_true_is_true(self):
#devrait réussir
self.assertEquals(True, True)
unittest.main(argv=[''], verbosity=2, exit=False)
Et ici, le test réussit comme on s'y attend. Je préfère utiliser une librairie de test unitaires plutôt que des assert bruts, car il y a un affichage des données comparées, ce qui rend un peu plus explicite les causes d'erreur et nous évite quelques print à tout va.
Nous allons commencer nos par la fonction de transfert, ou la somme pondérées des entrées.
pour les 2 entrées
1, 2
4, 5
les poids: [0.1, 0.2], et un biais de 0.5, nous attendons les sorties suivantes : 1 et 1.9
class TestPerceptron(unittest.TestCase):
def test_somme_pondérée(self):
#devrait échouer
poids = [0.1, 0.2]
biais=[0.5]
p = Perceptron(poids=poids, biais=biais)
self.assertEquals(p.poids, poids)
self.assertEquals(p.biais, biais)
unittest.main(argv=[''], verbosity=2, exit=False)
Il faut créer la classe Perceptron
class Perceptron:
def __init__(self, poids, biais):
self.poids = poids
self.biais = biais
class TestPerceptron(unittest.TestCase):
def test_somme_pondérée(self):
#devrait réussir
poids = [0.1, 0.2]
biais = [0.5]
p = Perceptron(poids=poids, biais=biais)
self.assertEquals(p.poids, poids)
self.assertEquals(p.biais, biais)
unittest.main(argv=[''], verbosity=2, exit=False)
Le perceptron est bien initialisé, passons donc au test de la somme pondérée, et renommons le test précédent
class TestPerceptron(unittest.TestCase):
def test_initialisation(self):
#devrait réussir
poids = [0.1, 0.2]
biais = [0.5]
p = Perceptron(poids=poids, biais=biais)
self.assertEquals(p.poids, poids)
self.assertEquals(p.biais, biais)
def test_somme_pondérée(self):
#devrait réussir
poids = [0.1, 0.2]
biais = [0.5]
p = Perceptron(poids=poids, biais=biais)
x = np.array([1,2,4,5]).reshape(2,2)
y = p.transfer(x)
y_expected = np.array([1,1.9])
self.assertEquals(y, y_expected)
unittest.main(argv=[''], verbosity=2, exit=False)
En effet il faut créer la méthode transfer dans la classe Perceptron. Bon alors autant je pratique TDD autant que je peux, surtout en Java au travail, mais je vais prendre quelques raccourcis pour ne pas surcharger le notebook.
Et recopier le code précédent dans une nouvelle cellule, etc. pour faire du baby step et l'illustrer c'est juste chiant.
En tout cas j'ai plus envie de me concentrer sur mon avancée, du coup je vais faire un compromis entre explicitation des étapes détaillée, avancée et pénibilité
class Perceptron:
def __init__(self, poids, biais):
self.poids = np.array(poids)
self.biais = biais
def transfer(self, x):
return np.dot(self.poids, x)+self.biais
class TestPerceptron(unittest.TestCase):
def test_initialisation(self):
#devrait réussir
poids = [0.1, 0.2]
biais = [0.5]
p = Perceptron(poids=poids, biais=biais)
self.assertEquals(p.poids.all(), np.array(poids).all())
self.assertEquals(p.biais, biais)
def test_somme_pondérée(self):
#devrait réussir
poids = [0.1, 0.2]
biais = [0.5]
p = Perceptron(poids=poids, biais=biais)
x = np.array([1,2,4,5]).reshape(2,2)
y = p.transfer(x)
y_expected = np.array([1,1.9])
self.assertEquals(y.all(), y_expected.all())
unittest.main(argv=[''], verbosity=2, exit=False)
devoir rajouter .all() à chaque numpy.array est juste beaucoup trop pénible, nous allons donc créer une méthode helper pour ne pas avoir à l'écrire dans nos tests:
class TestPerceptron(unittest.TestCase):
def test_initialisation(self):
#devrait réussir
poids = [0.1, 0.2]
biais = [0.5]
p = Perceptron(poids=poids, biais=biais)
self.assertNumpyArraysEqual(p.poids, np.array(poids))
self.assertEquals(p.biais, biais)
def test_somme_pondérée(self):
#devrait réussir
poids = [0.1, 0.2]
biais = [0.5]
p = Perceptron(poids=poids, biais=biais)
x = np.array([1,2,4,5]).reshape(2,2)
y = p.transfer(x)
y_expected = np.array([1,1.9])
self.assertNumpyArraysEqual(y, y_expected)
def assertNumpyArraysEqual(self, actual, expected):
self.assertEquals(actual.all(), expected.all())
unittest.main(argv=[''], verbosity=2, exit=False)
En tout cas, nous avons pu vérifier que la somme pondérée était correcte, testons maintenant la fonction d'activation.
Nous allons implémenter une fonction d'activation "unit step", voir wikipédia pour différentes fonctions d'activation.
Et pour notre premier test, nous allons tester une entrée de 1 inférieur à un seuil de 1.5, et vérifier que la sortie est bien 0.
class TestPerceptron(unittest.TestCase):
def test_initialisation(self):
#devrait réussir
poids = [0.1, 0.2]
biais = [0.5]
p = Perceptron(poids=poids, biais=biais)
self.assertNumpyArraysEqual(p.poids, np.array(poids))
self.assertEquals(p.biais, biais)
def test_somme_pondérée(self):
#devrait réussir
poids = [0.1, 0.2]
biais = [0.5]
p = Perceptron(poids=poids, biais=biais)
x = np.array([1,2,4,5]).reshape(2,2)
y = p.transfer(x)
y_expected = np.array([1,1.9])
self.assertNumpyArraysEqual(y, y_expected)
def test_fonction_activation_inférieure_à_seuil(self):
p = Perceptron(activation_seuil=1.5)
y = p.activation(1)
self.assertEquals(y, 0)
def assertNumpyArraysEqual(self, actual, expected):
self.assertEquals(actual.all(), expected.all())
unittest.main(argv=[''], verbosity=2, exit=False)
Alors ici la sortie nous signale qu'il n'y a pas de constructeur ayant pour paramètre "activation_seuil" pour notre classe Perceptron.
On remarque aussi, même si ce n'est pas dans les logs, qu'il n'y a pas de méthode "activation".
Corrigeons ces 2 erreurs
class Perceptron:
def __init__(self, poids, biais, activation_seuil=0):
self.poids = np.array(poids)
self.biais = biais
self.activation_seuil = activation_seuil
def transfer(self, x):
return np.dot(self.poids, x)+self.biais
def activation(self, x):
return None;
class TestPerceptron(unittest.TestCase):
def test_initialisation(self):
#devrait réussir
poids = [0.1, 0.2]
biais = [0.5]
p = Perceptron(poids=poids, biais=biais)
self.assertNumpyArraysEqual(p.poids, np.array(poids))
self.assertEquals(p.biais, biais)
def test_somme_pondérée(self):
#devrait réussir
poids = [0.1, 0.2]
biais = [0.5]
p = Perceptron(poids=poids, biais=biais)
x = np.array([1,2,4,5]).reshape(2,2)
y = p.transfer(x)
y_expected = np.array([1,1.9])
self.assertNumpyArraysEqual(y, y_expected)
def test_fonction_activation_inférieure_à_seuil(self):
p = Perceptron(activation_seuil=1.5)
y = p.activation(1)
self.assertEquals(y, 0)
def assertNumpyArraysEqual(self, actual, expected):
self.assertEquals(actual.all(), expected.all())
unittest.main(argv=[''], verbosity=2, exit=False)
OK, on a juste envie de tester l'activation, veut-on mettre des paramètre bidons pour les poids et le biais ?
Doit-on séparer l'activation dans sa propre fonction ? Initialement c'était la solution que je préférais, "separation of concerns", et ça me paraissait un peu plus "clean", aussi ça permettrait d'injecter une fonction d'activation de notre choix, mais peut-être un poil overengineered à ce stade.
Le fait de pouvoir injecter différentes fonctions d'activation est attrayant, mais ce cas ne se présente pas encore.
C'est peut-être sale et inapproprié, j'acceuille vos remarques et suggestions d'amélioration avec plaisir, mais du coup pour le moment je vais partir sur le fait de mettre None
pour les poids et biais dans le cadre du test de l'activation, et refacto pour injecter une fonction d'activation quand je chercherais à en implémenter plusieurs sur le même Perceptron, si jamais ce cas se présente.
Après tout je travaille toujours à plein temps, il est 5h30, je vais chercher à publier ce notebook en post de blog d'ici ce soir max, comme ça a été le cas pour knn hier.
class TestPerceptron(unittest.TestCase):
def test_initialisation(self):
#devrait réussir
poids = [0.1, 0.2]
biais = [0.5]
p = Perceptron(poids=poids, biais=biais)
self.assertNumpyArraysEqual(p.poids, np.array(poids))
self.assertEquals(p.biais, biais)
def test_somme_pondérée(self):
#devrait réussir
poids = [0.1, 0.2]
biais = [0.5]
p = Perceptron(poids=poids, biais=biais)
x = np.array([1,2,4,5]).reshape(2,2)
y = p.transfer(x)
y_expected = np.array([1,1.9])
self.assertNumpyArraysEqual(y, y_expected)
def test_fonction_activation_inférieure_à_seuil(self):
p = Perceptron(poids=None,biais=None,activation_seuil=1.5)
y = p.activation(1)
self.assertEquals(y, 0)
def assertNumpyArraysEqual(self, actual, expected):
self.assertEquals(actual.all(), expected.all())
unittest.main(argv=[''], verbosity=2, exit=False)
AssertionError: None != 0
-> en effet nous avons créer la méthode activation mais nous ne l'avons pas implémenté. Corrigeons donc cela
class Perceptron:
def __init__(self, poids, biais, activation_seuil=0):
self.poids = np.array(poids)
self.biais = biais
self.activation_seuil = activation_seuil
def transfer(self, x):
return np.dot(self.poids, x)+self.biais
def activation(self, x):
return 1 if x > self.activation_seuil else 0;
class TestPerceptron(unittest.TestCase):
def test_initialisation(self):
#devrait réussir
poids = [0.1, 0.2]
biais = [0.5]
p = Perceptron(poids=poids, biais=biais)
self.assertNumpyArraysEqual(p.poids, np.array(poids))
self.assertEquals(p.biais, biais)
def test_somme_pondérée(self):
#devrait réussir
poids = [0.1, 0.2]
biais = [0.5]
p = Perceptron(poids=poids, biais=biais)
x = np.array([1,2,4,5]).reshape(2,2)
y = p.transfer(x)
y_expected = np.array([1,1.9])
self.assertNumpyArraysEqual(y, y_expected)
def test_fonction_activation_inférieure_à_seuil(self):
p = Perceptron(poids=None,biais=None,activation_seuil=1.5)
y = p.activation(1)
self.assertEquals(y, 0)
def assertNumpyArraysEqual(self, actual, expected):
self.assertEquals(actual.all(), expected.all())
unittest.main(argv=[''], verbosity=2, exit=False)
Super, les tests passent. Maintenant vérifions le cas où l'entrée est supérieure à un seuil. Nous allons prendre le cas 1.9
class TestPerceptron(unittest.TestCase):
def test_initialisation(self):
#devrait réussir
poids = [0.1, 0.2]
biais = [0.5]
p = Perceptron(poids=poids, biais=biais)
self.assertNumpyArraysEqual(p.poids, np.array(poids))
self.assertEquals(p.biais, biais)
def test_somme_pondérée(self):
#devrait réussir
poids = [0.1, 0.2]
biais = [0.5]
p = Perceptron(poids=poids, biais=biais)
x = np.array([1,2,4,5]).reshape(2,2)
y = p.transfer(x)
y_expected = np.array([1,1.9])
self.assertNumpyArraysEqual(y, y_expected)
def test_fonction_activation_inférieure_à_seuil(self):
p = Perceptron(poids=None,biais=None,activation_seuil=1.5)
y = p.activation(1)
self.assertEquals(y, 0)
def test_fonction_activation_supérieure_à_seuil(self):
p = Perceptron(poids=None,biais=None,activation_seuil=1.5)
y = p.activation(1.9)
self.assertEquals(y, 1)
def assertNumpyArraysEqual(self, actual, expected):
self.assertEquals(actual.all(), expected.all())
unittest.main(argv=[''], verbosity=2, exit=False)
Super, ça marche aussi !
Nous n'avons pas choisi 1 et 1.9 au hasard, ces valeurs correspondent à la sortie de la fonction de transfert (la somme pondérée implémentée dans la méthode "transfer"), ainsi nous allons pouvoir vérifier que l'entrée de la fonction d'activation reliée à la sortie de la fonction de transfert donne de bons résultats.
Nous allons appeler "forward" la méthode qui fait passer l'entrée par la fonction de transfert ET la fonction d'activation
class TestPerceptron(unittest.TestCase):
def test_initialisation(self):
#devrait réussir
poids = [0.1, 0.2]
biais = [0.5]
p = Perceptron(poids=poids, biais=biais)
self.assertNumpyArraysEqual(p.poids, np.array(poids))
self.assertEquals(p.biais, biais)
def test_somme_pondérée(self):
poids = [0.1, 0.2]
biais = [0.5]
p = Perceptron(poids=poids, biais=biais)
x = np.array([1,2,4,5]).reshape(2,2)
y = p.transfer(x)
y_expected = np.array([1,1.9])
self.assertNumpyArraysEqual(y, y_expected)
def test_fonction_activation_inférieure_à_seuil(self):
p = Perceptron(poids=None,biais=None,activation_seuil=1.5)
y = p.activation(1)
self.assertEquals(y, 0)
def test_fonction_activation_supérieure_à_seuil(self):
p = Perceptron(poids=None,biais=None,activation_seuil=1.5)
y = p.activation(1.9)
self.assertEquals(y, 1)
def test_fonction_transfert_plus_fonction_activation(self):
poids = [0.1, 0.2]
biais = [0.5]
p = Perceptron(poids=poids, biais=biais, activation_seuil=1.5)
x = np.array([1,2,4,5]).reshape(2,2)
y = p.forward(x)
y_expected = np.array([0,1])
self.assertNumpyArraysEqual(y, y_expected)
def assertNumpyArraysEqual(self, actual, expected):
self.assertEquals(actual.all(), expected.all())
unittest.main(argv=[''], verbosity=2, exit=False)
En effet les tests échouent car la méthode "forward" n'existe pas. Créons-la
class Perceptron:
def __init__(self, poids, biais, activation_seuil=0):
self.poids = np.array(poids)
self.biais = biais
self.activation_seuil = activation_seuil
def transfer(self, x):
return np.dot(self.poids, x)+self.biais
def activation(self, x):
return 1 if x > self.activation_seuil else 0;
def forward(self, x):
return self.activation(self.transfer(x))
class TestPerceptron(unittest.TestCase):
def test_initialisation(self):
#devrait réussir
poids = [0.1, 0.2]
biais = [0.5]
p = Perceptron(poids=poids, biais=biais)
self.assertNumpyArraysEqual(p.poids, np.array(poids))
self.assertEquals(p.biais, biais)
def test_somme_pondérée(self):
poids = [0.1, 0.2]
biais = [0.5]
p = Perceptron(poids=poids, biais=biais)
x = np.array([1,2,4,5]).reshape(2,2)
y = p.transfer(x)
y_expected = np.array([1,1.9])
self.assertNumpyArraysEqual(y, y_expected)
def test_fonction_activation_inférieure_à_seuil(self):
p = Perceptron(poids=None,biais=None,activation_seuil=1.5)
y = p.activation(1)
self.assertEquals(y, 0)
def test_fonction_activation_supérieure_à_seuil(self):
p = Perceptron(poids=None,biais=None,activation_seuil=1.5)
y = p.activation(1.9)
self.assertEquals(y, 1)
def test_fonction_transfert_plus_fonction_activation(self):
poids = [0.1, 0.2]
biais = [0.5]
p = Perceptron(poids=poids, biais=biais, activation_seuil=1.5)
x = np.array([1,2,4,5]).reshape(2,2)
y = p.forward(x)
y_expected = np.array([0,1])
self.assertNumpyArraysEqual(y, y_expected)
def assertNumpyArraysEqual(self, actual, expected):
self.assertEquals(actual.all(), expected.all())
unittest.main(argv=[''], verbosity=2, exit=False)
Oups, notre méthode activation n'accepte pas en entrée les numpy.array. Que faire ?
Nous pouvons modifier la méthode activation, ou bien dans "forward", utiliser par exemple une boucle ou une lambda pour l'appliquer élément par élément.
il y a aussi une méthode numpy.heaviside qui implémente la fonction "unit step".
Nous allons quand même chercher à implémenter la plus efficace. Aussi nous essaierons les 3, p-e pas tout de suite, mais à un moment lors de notre élaboration de cette classe.
Commençons par la solution:
def step(x):
return 1 * (x > 0)
décrite sur cette page stackoverflow, puisqu'elle marcherait à la fois sur les numpy array, ainsi que sur les scalaires. Pratique pour garder les tests existants, mais en vrai, cette méthode étant utilisée uniquement dans la classe Perceptron, elle ne devrait travailer que sur des numpy array.
La praticité de cette solution ne devrait pas guider nos choix d'implem finaux, on devrait modifier nos tests in fine. Mais allons-y étape par étape:
class Perceptron:
def __init__(self, poids, biais, activation_seuil=0):
self.poids = np.array(poids)
self.biais = biais
self.activation_seuil = activation_seuil
def transfer(self, x):
return np.dot(self.poids, x)+self.biais
def activation(self, x):
return 1 * (x > 0)
def forward(self, x):
return self.activation(self.transfer(x))
class TestPerceptron(unittest.TestCase):
def test_initialisation(self):
#devrait réussir
poids = [0.1, 0.2]
biais = [0.5]
p = Perceptron(poids=poids, biais=biais)
self.assertNumpyArraysEqual(p.poids, np.array(poids))
self.assertEquals(p.biais, biais)
def test_somme_pondérée(self):
poids = [0.1, 0.2]
biais = [0.5]
p = Perceptron(poids=poids, biais=biais)
x = np.array([1,2,4,5]).reshape(2,2)
y = p.transfer(x)
y_expected = np.array([1,1.9])
self.assertNumpyArraysEqual(y, y_expected)
def test_fonction_activation_inférieure_à_seuil(self):
p = Perceptron(poids=None,biais=None,activation_seuil=1.5)
y = p.activation(1)
self.assertEquals(y, 0)
def test_fonction_activation_supérieure_à_seuil(self):
p = Perceptron(poids=None,biais=None,activation_seuil=1.5)
y = p.activation(1.9)
self.assertEquals(y, 1)
def test_fonction_transfert_plus_fonction_activation(self):
poids = [0.1, 0.2]
biais = [0.5]
p = Perceptron(poids=poids, biais=biais, activation_seuil=1.5)
x = np.array([1,2,4,5]).reshape(2,2)
y = p.forward(x)
y_expected = np.array([0,1])
self.assertNumpyArraysEqual(y, y_expected)
def assertNumpyArraysEqual(self, actual, expected):
self.assertEquals(actual.all(), expected.all())
unittest.main(argv=[''], verbosity=2, exit=False)
ah, l'un de nos tests précédents échoue. En y regardant de plus près, nous avons été un peu distrait et avons copié-collé sans réfléchir suffisamment la solution de stackoverflow et nous avons oublié de prendre en compte le seuil d'activation.
C'était une bonne chose d'y aller étape par étape, garder nos cas de tests exister et constater cette erreur rapidement. Ce genre d'erreur d'inattention arrive constamment, peut-être un peu plus que les autres pour l'auteur de ce post ... ou peut-être pas. En tout cas, ça arrive TOUT LE TEMPS, et c'est bien d'avoir de bonnes méthodes pour le voir vite et ne pas perdre de temps et d'énergie dessus.
Petite illustration en pratique de la force du TDD. Là c'est un exemple simple, mais sur des projets plus importants, ou au travail par exemple, ce genre d'erreur peut être constaté très tard, voir en production, ça va vous faire péter un cable, ça peut semer la zizanie dans l'équipe, le manque de confiance : par rapport au code, par rapport à un ou des membres de l'équipe.
Ce genre d'erreur amène frustration, fatigue, toxicité dans l'équipe alors qu'on a des méthodes et pratiques pas trop compliquées pour l'éviter.
En pratique les gens préfèreront ignorer ces méthodes, se blâmer mutuellement, écraser l'autre, prendre le dessus sur lui ou elle, se poser en leader/tyran "on va faire comme je dis" ... bref ^^.
Au passage, le jugement, le blâme, l'empathie, la patience, et une certaine idée du progrès rationnel et de l'agilité feront probablement l'objet d'un ou plusieurs posts. Mais ce sont des gros sujets, dont je n'ai pas encore une grande maitrise pratique, mais passons.
class Perceptron:
def __init__(self, poids, biais, activation_seuil=0):
self.poids = np.array(poids)
self.biais = biais
self.activation_seuil = activation_seuil
def transfer(self, x):
return np.dot(self.poids, x)+self.biais
def activation(self, x):
return 1 * (x >= self.activation_seuil)
def forward(self, x):
return self.activation(self.transfer(x))
class TestPerceptron(unittest.TestCase):
def test_initialisation(self):
#devrait réussir
poids = [0.1, 0.2]
biais = [0.5]
p = Perceptron(poids=poids, biais=biais)
self.assertNumpyArraysEqual(p.poids, np.array(poids))
self.assertEquals(p.biais, biais)
def test_somme_pondérée(self):
poids = [0.1, 0.2]
biais = [0.5]
p = Perceptron(poids=poids, biais=biais)
x = np.array([1,2,4,5]).reshape(2,2)
y = p.transfer(x)
y_expected = np.array([1,1.9])
self.assertNumpyArraysEqual(y, y_expected)
def test_fonction_activation_inférieure_à_seuil(self):
p = Perceptron(poids=None,biais=None,activation_seuil=1.5)
y = p.activation(1)
self.assertEquals(y, 0)
def test_fonction_activation_supérieure_à_seuil(self):
p = Perceptron(poids=None,biais=None,activation_seuil=1.5)
y = p.activation(1.9)
self.assertEquals(y, 1)
def test_fonction_transfert_plus_fonction_activation(self):
poids = [0.1, 0.2]
biais = [0.5]
p = Perceptron(poids=poids, biais=biais, activation_seuil=1.5)
x = np.array([1,2,4,5]).reshape(2,2)
y = p.forward(x)
y_expected = np.array([0,1])
self.assertNumpyArraysEqual(y, y_expected)
def assertNumpyArraysEqual(self, actual, expected):
self.assertEquals(actual.all(), expected.all())
unittest.main(argv=[''], verbosity=2, exit=False)
ok les tests passent: la phase "forward" est prête. On va pouvoir passer à la phase "backward", ou la mise à jour des poids pour minimiser l'erreur.
on accompagnera cela d'un fonction de visualisation de la frontière de décision, et vérifier à la fois que la somme des coûts/erreurs diminue, et globalement que l'apprentissage se déroule bien
Nous allons commencer par des tests unitaires sur le calcul de $\Delta$W = $\alpha$ (y-ŷ) x_i .
(y-ŷ) peut prendre les valeur 0, 1, -1. Nous allons utiliser un test paramétré pour effectuer le test sur les différentes valeurs
!pip install parameterized
ce message s'affiche chez moi car j'ai deja installé la librairie, mais ce n'était pas le cas initialement. Importons maintenant la librairie et écrivons les nouveaux tests
from parameterized import parameterized
class TestPerceptron(unittest.TestCase):
def assertNumpyArraysEqual(self, actual, expected):
self.assertEquals(actual.all(), expected.all())
def test_initialisation(self):
#devrait réussir
poids = [0.1, 0.2]
biais = [0.5]
p = Perceptron(poids=poids, biais=biais)
self.assertNumpyArraysEqual(p.poids, np.array(poids))
self.assertEquals(p.biais, biais)
def test_somme_pondérée(self):
poids = [0.1, 0.2]
biais = [0.5]
p = Perceptron(poids=poids, biais=biais)
x = np.array([1,2,4,5]).reshape(2,2)
y = p.transfer(x)
y_expected = np.array([1,1.9])
self.assertNumpyArraysEqual(y, y_expected)
def test_fonction_activation_inférieure_à_seuil(self):
p = Perceptron(poids=None,biais=None,activation_seuil=1.5)
y = p.activation(1)
self.assertEquals(y, 0)
def test_fonction_activation_supérieure_à_seuil(self):
p = Perceptron(poids=None,biais=None,activation_seuil=1.5)
y = p.activation(1.9)
self.assertEquals(y, 1)
def test_fonction_transfert_plus_fonction_activation(self):
poids = [0.1, 0.2]
biais = [0.5]
p = Perceptron(poids=poids, biais=biais, activation_seuil=1.5)
x = np.array([1,2,4,5]).reshape(2,2)
y = p.forward(x)
y_expected = np.array([0,1])
self.assertNumpyArraysEqual(y, y_expected)
@parameterized.expand([
[0,0, [1,2]],
[0,1, [-0.5,-1]],
[1,0, [0.5,1]],
[1,1, [1,2]]
])
def test_calcul_gradient_poids(self, y, y_hat, expected_gradient):
expected_gradient = np.array(expected_gradient)
alpha = 0.5
x = np.array([1,2])
p = Perceptron(poids=None,biais=None,activation_seuil=None)
actual_gradient = p.compute_gradient(alpha=alpha, y=y, y_hat=y_hat, x=x)
self.assertNumpyArraysEqual(actual_gradient, expected_gradient)
unittest.main(argv=[''], verbosity=2, exit=False)
Oui alors là je ne savais pas quelle était la meilleure façon de procéder et je n'en ai pas trouvé une qui me satisfasse (+1 pour l'utilisation de ce temps obscur dont j'ai oublié le nom).
le calcul du gradient me semble spécifique à la classe Perceptron, mais je vois aussi des raisons que ça ne soit pas le cas. Et l'écriture du test tend à vouloir en faire une fonction à part ...
Du coup cette verrue de code, que je corrigerais (ou pas) plus tard.
class Perceptron:
def __init__(self, poids, biais, activation_seuil=0):
self.poids = np.array(poids)
self.biais = biais
self.activation_seuil = activation_seuil
def transfer(self, x):
return np.dot(self.poids, x)+self.biais
def activation(self, x):
return 1 * (x >= self.activation_seuil)
def forward(self, x):
return self.activation(self.transfer(x))
def compute_gradient(self, alpha, y, y_hat, x):
return alpha * (y-y_hat) * x
class TestPerceptron(unittest.TestCase):
def assertNumpyArraysEqual(self, actual, expected):
self.assertEquals(actual.all(), expected.all())
def test_initialisation(self):
#devrait réussir
poids = [0.1, 0.2]
biais = [0.5]
p = Perceptron(poids=poids, biais=biais)
self.assertNumpyArraysEqual(p.poids, np.array(poids))
self.assertEquals(p.biais, biais)
def test_somme_pondérée(self):
poids = [0.1, 0.2]
biais = [0.5]
p = Perceptron(poids=poids, biais=biais)
x = np.array([1,2,4,5]).reshape(2,2)
y = p.transfer(x)
y_expected = np.array([1,1.9])
self.assertNumpyArraysEqual(y, y_expected)
def test_fonction_activation_inférieure_à_seuil(self):
p = Perceptron(poids=None,biais=None,activation_seuil=1.5)
y = p.activation(1)
self.assertEquals(y, 0)
def test_fonction_activation_supérieure_à_seuil(self):
p = Perceptron(poids=None,biais=None,activation_seuil=1.5)
y = p.activation(1.9)
self.assertEquals(y, 1)
def test_fonction_transfert_plus_fonction_activation(self):
poids = [0.1, 0.2]
biais = [0.5]
p = Perceptron(poids=poids, biais=biais, activation_seuil=1.5)
x = np.array([1,2,4,5]).reshape(2,2)
y = p.forward(x)
y_expected = np.array([0,1])
self.assertNumpyArraysEqual(y, y_expected)
@parameterized.expand([
[0,0, [1,2]],
[0,1, [-0.5,-1]],
[1,0, [0.5,1]],
[1,1, [1,2]]
])
def test_calcul_gradient_poids(self, y, y_hat, expected_gradient):
expected_gradient = np.array(expected_gradient)
alpha = 0.5
x = np.array([1,2])
p = Perceptron(poids=None,biais=None,activation_seuil=None)
actual_gradient = p.compute_gradient(alpha=alpha, y=y, y_hat=y_hat, x=x)
self.assertNumpyArraysEqual(actual_gradient, expected_gradient)
unittest.main(argv=[''], verbosity=2, exit=False)
ok, il y a une erreur pour les cas 0 et 3, qui correspondent à y = ŷ. Cependant la lib de test n'affiche pas à quoi correspond "actual_gradient". Affichons-le pour voir
class TestPerceptron(unittest.TestCase):
def assertNumpyArraysEqual(self, actual, expected):
self.assertEquals(actual.all(), expected.all())
def test_initialisation(self):
#devrait réussir
poids = [0.1, 0.2]
biais = [0.5]
p = Perceptron(poids=poids, biais=biais)
self.assertNumpyArraysEqual(p.poids, np.array(poids))
self.assertEquals(p.biais, biais)
def test_somme_pondérée(self):
poids = [0.1, 0.2]
biais = [0.5]
p = Perceptron(poids=poids, biais=biais)
x = np.array([1,2,4,5]).reshape(2,2)
y = p.transfer(x)
y_expected = np.array([1,1.9])
self.assertNumpyArraysEqual(y, y_expected)
def test_fonction_activation_inférieure_à_seuil(self):
p = Perceptron(poids=None,biais=None,activation_seuil=1.5)
y = p.activation(1)
self.assertEquals(y, 0)
def test_fonction_activation_supérieure_à_seuil(self):
p = Perceptron(poids=None,biais=None,activation_seuil=1.5)
y = p.activation(1.9)
self.assertEquals(y, 1)
def test_fonction_transfert_plus_fonction_activation(self):
poids = [0.1, 0.2]
biais = [0.5]
p = Perceptron(poids=poids, biais=biais, activation_seuil=1.5)
x = np.array([1,2,4,5]).reshape(2,2)
y = p.forward(x)
y_expected = np.array([0,1])
self.assertNumpyArraysEqual(y, y_expected)
@parameterized.expand([
[0,0, [1,2]],
[0,1, [-0.5,-1]],
[1,0, [0.5,1]],
[1,1, [1,2]]
])
def test_calcul_gradient_poids(self, y, y_hat, expected_gradient):
expected_gradient = np.array(expected_gradient)
alpha = 0.5
x = np.array([1,2])
p = Perceptron(poids=None,biais=None,activation_seuil=None)
actual_gradient = p.compute_gradient(alpha=alpha, y=y, y_hat=y_hat, x=x)
print(actual_gradient)
self.assertNumpyArraysEqual(actual_gradient, expected_gradient)
unittest.main(argv=[''], verbosity=2, exit=False)
D'accord, le gradient calculé vaut 0 quand y = ŷ. Ce qui est logique, si on ne fait pas d'erreur, on ne corrige pas les poids.
Notre test était mal écrit. Corrigeons cela
class TestPerceptron(unittest.TestCase):
def assertNumpyArraysEqual(self, actual, expected):
self.assertEquals(actual.all(), expected.all())
def test_initialisation(self):
#devrait réussir
poids = [0.1, 0.2]
biais = [0.5]
p = Perceptron(poids=poids, biais=biais)
self.assertNumpyArraysEqual(p.poids, np.array(poids))
self.assertEquals(p.biais, biais)
def test_somme_pondérée(self):
poids = [0.1, 0.2]
biais = [0.5]
p = Perceptron(poids=poids, biais=biais)
x = np.array([1,2,4,5]).reshape(2,2)
y = p.transfer(x)
y_expected = np.array([1,1.9])
self.assertNumpyArraysEqual(y, y_expected)
def test_fonction_activation_inférieure_à_seuil(self):
p = Perceptron(poids=None,biais=None,activation_seuil=1.5)
y = p.activation(1)
self.assertEquals(y, 0)
def test_fonction_activation_supérieure_à_seuil(self):
p = Perceptron(poids=None,biais=None,activation_seuil=1.5)
y = p.activation(1.9)
self.assertEquals(y, 1)
def test_fonction_transfert_plus_fonction_activation(self):
poids = [0.1, 0.2]
biais = [0.5]
p = Perceptron(poids=poids, biais=biais, activation_seuil=1.5)
x = np.array([1,2,4,5]).reshape(2,2)
y = p.forward(x)
y_expected = np.array([0,1])
self.assertNumpyArraysEqual(y, y_expected)
@parameterized.expand([
[0,0, [0,0]],
[0,1, [-0.5,-1]],
[1,0, [0.5,1]],
[1,1, [0,0]]
])
def test_calcul_gradient_poids(self, y, y_hat, expected_gradient):
expected_gradient = np.array(expected_gradient)
alpha = 0.5
x = np.array([1,2])
p = Perceptron(poids=None,biais=None,activation_seuil=None)
actual_gradient = p.compute_gradient(alpha=alpha, y=y, y_hat=y_hat, x=x)
self.assertNumpyArraysEqual(actual_gradient, expected_gradient)
unittest.main(argv=[''], verbosity=2, exit=False)
Super, le gradient des poids est désormais bien calculé, du moins respecte la spec implémentée dans les tests. Il nous faut maintenant calculer le gradient du biais pour pouvoir le mettre à jour
On rappelle la formule de mise à jour des paramètres du Perceptron:
$params_{t+1} = params_{t} - \alpha \frac{\nabla L}{\nabla params}$ où L est la fonction de coût associé à la décision du Perceptron et $\frac{\nabla L}{\nabla params}$ est le gradient du coût par rapport aux paramètres que l'on souhaite mettre à jour
Plus de détails sur les gradients etc. dans un prochain post.
$$ \begin{eqnarray} W_{t+1} &=& W_{t} - \alpha \frac{\nabla L}{\nabla W} \\ &=& W_{t} - \alpha (- (y-ŷ)X) \\ &=& W_{t} + \alpha (y-ŷ)X \\ \end{eqnarray} $$Et pour les biais:
$$ \begin{eqnarray} b_{t+1} &=& b_{t} - \alpha \frac{\nabla L}{\nabla b} \\ &=& b_{t} - \alpha (y-ŷ) \\ \end{eqnarray} $$Ecrivons donc le code pour calculer le gradient des biais. Oui déso la fin est un peu moins TDD
class Perceptron:
def __init__(self, poids, biais, activation_seuil=0):
self.poids = np.array(poids)
self.biais = biais
self.activation_seuil = activation_seuil
def transfer(self, x):
return np.dot(self.poids, x)+self.biais
def activation(self, x):
return 1 * (x >= self.activation_seuil)
def forward(self, x):
return self.activation(self.transfer(x))
def compute_gradient(self, alpha, y, y_hat, x):
return alpha * (y-y_hat) * x
def compute_gradient_biases(self, alpha, y, y_hat):
return alpha * (y-y_hat)
Il nous faut maintenant une méthode pour mettre à jour les poids et les biais, appelons-la "backward":
class Perceptron:
def __init__(self, poids, biais, activation_seuil=0):
self.poids = np.array(poids)
self.biais = biais
self.activation_seuil = activation_seuil
def transfer(self, x):
return np.dot(self.poids, x)+self.biais
def activation(self, x):
return 1 * (x >= self.activation_seuil)
def forward(self, x):
return self.activation(self.transfer(x))
def compute_gradient(self, alpha, y, y_hat, x):
return alpha * (y-y_hat) * x
def compute_gradient_biais(self, alpha, y, y_hat):
return alpha * (y-y_hat)
def backward(self):
gradient_poids = self.compute_gradient(self.alpha, self.y_train, self.y_hat_train, self.x_train)
gradient_biais = self.compute_gradient_biais(self.alpha, self.y_train, self.y_hat_train)
self.poids += gradient_poids
self.biais -= gradient_biais
Et oui, techniquement $\alpha$, le taux d'apprentissage, ne fait pas partie du gradient. Un erreur issue d'un copier-coller d'une vidéo pour démarrer cet article :/
Corrigé dans une future mise à jour.
Maintenant il nous faudrait une méthode pour enchaîner passe-avant et passe-arrière, appelons-là "step"
class Perceptron:
def __init__(self, poids, biais, activation_seuil=0):
self.poids = np.array(poids)
self.biais = biais
self.activation_seuil = activation_seuil
def transfer(self, x):
return np.dot(self.poids, x)+self.biais
def activation(self, x):
return 1 * (x >= self.activation_seuil)
def forward(self, x):
return self.activation(self.transfer(x))
def compute_gradient(self, alpha, y, y_hat, x):
return alpha * (y-y_hat) * x
def compute_gradient_biais(self, alpha, y, y_hat):
return alpha * (y-y_hat)
def backward(self):
gradient_poids = self.compute_gradient(self.alpha, self.y_train, self.y_hat_train, self.x_train)
gradient_biais = self.compute_gradient_biais(self.alpha, self.y_train, self.y_hat_train)
self.poids += gradient_poids
self.biais -= gradient_biais
def step(self,x_train,y_train):
self.x_train = x_train
self.y_train = y_train
self.y_hat_train = self.forward(x_train)
self.backward()
On est prêt à essayer notre algo sur des données jouet. Nous allons définitivement quitter TDD pour valider visuellement et intuitivement le fonctionnement de l'algo.
Pour allons utilser matplotlib pour afficher tout ce qu'il faut afficher, et créer une fonction helper pour nous aider à afficher la frontière de décision.
Le code d'affichage de la fonction de décision est, pour le coup, purement et simplement recopiée depuis le cours de Deep Learning de Jerry Kurata sur Pluralsight
from sklearn.datasets import make_blobs
import matplotlib.pyplot as plt
make_blobs est une fonction qui va nous permettre de générer un dataset jouet
def plot_data(pl,X,y):
pl.plot(X[y==0,0],X[y==0,1], 'ob', alpha=0.5)
pl.plot(X[y==1,0],X[y==1,1], 'xr', alpha=0.5)
pl.legend(['0','1'])
return pl
def plot_decision_boundary(model, X,y):
amin, bmin = X.min(axis=0) - 0.1
amax, bmax = X.max(axis=0) + 0.1
hticks = np.linspace(amin,amax,101)
vticks = np.linspace(bmin,bmax,101)
aa,bb = np.meshgrid(hticks, vticks)
ab = np.c_[aa.ravel(), bb.ravel()]
print("ab.shape ",ab.shape)
c = model.predict(ab)
Z = c.reshape(aa.shape)
plt.figure(figsize=(12,8))
plt.contourf(aa,bb,Z,cmap='bwr', alpha=0.2)
plot_data(plt,X,y)
x, y = make_blobs(centers=2, n_samples=6, random_state=42)
plot_data(plt,x,y)
Très bien, cherchons donc à afficher la frontière de décision avec les paramètre initiaux, et histoire de vérifier que tout fonctionne et pouvoir corriger.
p = Perceptron(poids=[0,0], biais=0.5, activation_seuil=0)
plot_decision_boundary(p,x,y)
En effet, ce code est utilisé pour afficher des frontières de décision pour des modèles keras et scikit-learn, qui ont une méthode predict, prenant en paramètre un array numpy, puisqu'il sagit d'effectuer une prédiction sur une grille de points pour afficher une frontière, implémentons-la donc:
class Perceptron:
def __init__(self, poids, biais, activation_seuil=0):
self.poids = np.array(poids)
self.biais = biais
self.activation_seuil = activation_seuil
def transfer(self, x):
return np.dot(self.poids, x)+self.biais
def activation(self, x):
return 1 * (x >= self.activation_seuil)
def forward(self, x):
return self.activation(self.transfer(x))
def compute_gradient(self, alpha, y, y_hat, x):
return alpha * (y-y_hat) * x
def compute_gradient_biais(self, alpha, y, y_hat):
return alpha * (y-y_hat)
def backward(self):
gradient_poids = self.compute_gradient(self.alpha, self.y_train, self.y_hat_train, self.x_train)
gradient_biais = self.compute_gradient_biais(self.alpha, self.y_train, self.y_hat_train)
self.poids += gradient_poids
self.biais -= gradient_biais
def step(self,x_train,y_train):
self.x_train = x_train
self.y_train = y_train
self.y_hat_train = self.forward(x_train)
self.backward()
def predict(self, x):
self.forward(x)
p = Perceptron(poids=[0,0], biais=0.5, activation_seuil=0)
plot_decision_boundary(p,x,y)
Il va nous falloir jouer avec predict, transfer et activation pour arriver à effectuer le calcul sur l'array numpy directement
class Perceptron:
def __init__(self, poids, biais, activation_seuil=0):
self.poids = np.array(poids)
self.biais = biais
self.activation_seuil = activation_seuil
def transfer(self, x):
return np.dot(self.poids,x.T)+self.biais
def activation(self, x):
return 1 * (x >= self.activation_seuil)
def forward(self, x):
return self.activation(self.transfer(x))
def compute_gradient(self, alpha, y, y_hat, x):
return alpha * (y-y_hat) * x
def compute_gradient_biais(self, alpha, y, y_hat):
return alpha * (y-y_hat)
def backward(self):
gradient_poids = self.compute_gradient(self.alpha, self.y_train, self.y_hat_train, self.x_train)
gradient_biais = self.compute_gradient_biais(self.alpha, self.y_train, self.y_hat_train)
self.poids += gradient_poids
self.biais -= gradient_biais
def step(self,x_train,y_train):
self.x_train = x_train
self.y_train = y_train
self.y_hat_train = self.forward(x_train)
self.backward()
def predict(self, x):
return self.forward(x)
p = Perceptron(poids=[1,1], biais=0, activation_seuil=0)
plot_decision_boundary(p,x,y)
dans la méthode transfer, nous sommes passés de return np.dot(self.poids, x)+self.biais
à return np.dot(self.poids, x.T)+self.biais
.
Utiliser la transposée de x au lieu de x semble faire le boulot pour faire matcher les dimensions.
La frontière semble bonne, en effet des poids $w_1$ et $w_2$ à 1 et un biais à 0 nous donnent: $$ w_1 x_1+w_2 x_2+b=0\\ x_1+x_2=0 x_2=-x_1 $$
ce qui correspond visuellement à l'équation de la droite définie par la frontière de décision
Essayons maintenant d'entraîner le modèle, et modifions légèrement le code de Perceptron pour afficher la données exemple considérée, la classe prédite et la vraie classe de l'exemple, voir si l'entraînement se passe bien.
class Perceptron:
def __init__(self, poids, biais, activation_seuil=0):
self.poids = np.array(poids)
self.biais = biais
self.activation_seuil = activation_seuil
def transfer(self, x):
return np.dot(self.poids,x.T)+self.biais
def activation(self, x):
return 1 * (x >= self.activation_seuil)
def forward(self, x):
return self.activation(self.transfer(x))
def compute_gradient(self, alpha, y, y_hat, x):
return alpha * (y-y_hat) * x
def compute_gradient_biais(self, alpha, y, y_hat):
return alpha * (y-y_hat)
def backward(self):
gradient_poids = self.compute_gradient(self.alpha, self.y_train, self.y_hat_train, self.x_train)
gradient_biais = self.compute_gradient_biais(self.alpha, self.y_train, self.y_hat_train)
self.poids += gradient_poids
self.biais -= gradient_biais
def step(self,x_train,y_train):
self.x_train = x_train
self.y_train = y_train
self.y_hat_train = self.forward(x_train)
print("(x_train, y_train):",x_train, y_train," predicted:",self.y_hat_train)
self.backward()
def predict(self, x):
return self.forward(x)
p = Perceptron(poids=[1,1], biais=0, activation_seuil=0)
for i in range(1):
x_train = x[i%len(x)]
y_train = y[i%len(x)]
p.step(x_train,y_train)
#plot_decision_boundary(p,x,y)
plot_decision_boundary(p,x,y)
Oups, nous avons commis une erreur en quelque part dans la rédaction de ce notebook et avons soit oublié, soit supprimé alpha, le taux d'apprentissage. Corrigeons cela
class Perceptron:
def __init__(self, poids, biais, activation_seuil=0, alpha=0.05):
self.poids = np.array(poids)
self.biais = biais
self.activation_seuil = activation_seuil
self.alpha=alpha
def transfer(self, x):
return np.dot(self.poids,x.T)+self.biais
def activation(self, x):
return 1 * (x >= self.activation_seuil)
def forward(self, x):
return self.activation(self.transfer(x))
def compute_gradient(self, alpha, y, y_hat, x):
return alpha * (y-y_hat) * x
def compute_gradient_biais(self, alpha, y, y_hat):
return alpha * (y-y_hat)
def backward(self):
gradient_poids = self.compute_gradient(self.alpha, self.y_train, self.y_hat_train, self.x_train)
gradient_biais = self.compute_gradient_biais(self.alpha, self.y_train, self.y_hat_train)
self.poids += gradient_poids
self.biais -= gradient_biais
def step(self,x_train,y_train):
self.x_train = x_train
self.y_train = y_train
self.y_hat_train = self.forward(x_train)
print("(x_train, y_train):",x_train, y_train," predicted:",self.y_hat_train)
self.backward()
def predict(self, x):
return self.forward(x)
p = Perceptron(poids=[1,1], biais=0, activation_seuil=0)
for i in range(1):
x_train = x[i%len(x)]
y_train = y[i%len(x)]
p.step(x_train,y_train)
#plot_decision_boundary(p,x,y)
plot_decision_boundary(p,x,y)
Castons, castons
class Perceptron:
def __init__(self, poids, biais, activation_seuil=0, alpha=0.05):
self.poids = np.array(poids)
self.biais = biais
self.activation_seuil = activation_seuil
self.alpha=alpha
def transfer(self, x):
return np.dot(self.poids,x.T)+self.biais
def activation(self, x):
return 1 * (x >= self.activation_seuil)
def forward(self, x):
return self.activation(self.transfer(x))
def compute_gradient(self, alpha, y, y_hat, x):
return alpha * (y-y_hat) * x
def compute_gradient_biais(self, alpha, y, y_hat):
return alpha * (y-y_hat)
def backward(self):
gradient_poids = self.compute_gradient(self.alpha, self.y_train, self.y_hat_train, self.x_train)
gradient_biais = self.compute_gradient_biais(self.alpha, self.y_train, self.y_hat_train)
self.poids += gradient_poids.astype('float64')
self.biais -= gradient_biais.astype('float64')
def step(self,x_train,y_train):
self.x_train = x_train
self.y_train = y_train
self.y_hat_train = self.forward(x_train)
print("(x_train, y_train):",x_train, y_train," predicted:",self.y_hat_train)
self.backward()
def predict(self, x):
return self.forward(x)
p = Perceptron(poids=[1,1], biais=0, activation_seuil=0)
for i in range(1):
x_train = x[i%len(x)]
y_train = y[i%len(x)]
p.step(x_train,y_train)
#plot_decision_boundary(p,x,y)
plot_decision_boundary(p,x,y)
Ah, c'est manifestement le poids défini dans le constructeur qui pose problème
class Perceptron:
def __init__(self, poids, biais, activation_seuil=0, alpha=0.05):
self.poids = np.array(poids).astype('float64')
self.biais = biais
self.activation_seuil = activation_seuil
self.alpha=alpha
def transfer(self, x):
return np.dot(self.poids,x.T)+self.biais
def activation(self, x):
return 1 * (x >= self.activation_seuil)
def forward(self, x):
return self.activation(self.transfer(x))
def compute_gradient(self, alpha, y, y_hat, x):
return alpha * (y-y_hat) * x
def compute_gradient_biais(self, alpha, y, y_hat):
return alpha * (y-y_hat)
def backward(self):
gradient_poids = self.compute_gradient(self.alpha, self.y_train, self.y_hat_train, self.x_train)
gradient_biais = self.compute_gradient_biais(self.alpha, self.y_train, self.y_hat_train)
self.poids += gradient_poids.astype('float64')
self.biais -= gradient_biais.astype('float64')
def step(self,x_train,y_train):
self.x_train = x_train
self.y_train = y_train
self.y_hat_train = self.forward(x_train)
print("(x_train, y_train):",x_train, y_train," predicted:",self.y_hat_train)
self.backward()
def predict(self, x):
return self.forward(x)
p = Perceptron(poids=[1,1], biais=0, activation_seuil=0)
plot_decision_boundary(p,x,y)
for i in range(1):
x_train = x[i%len(x)]
y_train = y[i%len(x)]
p.step(x_train,y_train)
plot_decision_boundary(p,x,y)
Ah top, ça corrige le problème, et on constate que le premier exemple, mal classé, entraîne une modification des poids et de la frontière de décision, super !
Continuons exemple après exemple, jusqu'à ce que toutes les données soient bien classées
p = Perceptron(poids=[1,1], biais=0, activation_seuil=0)
plot_decision_boundary(p,x,y)
for i in range(2):
x_train = x[i%len(x)]
y_train = y[i%len(x)]
p.step(x_train,y_train)
plot_decision_boundary(p,x,y)
Ici, le 2e point évalué était bien classé, du coup le frontière de décision ne bouge pas. Après 2 exemples, c'est la même qu'après un seul exemple. Bon on va arrêter d'afficher la frontière de décision initiale
p = Perceptron(poids=[1,1], biais=0, activation_seuil=0)
for i in range(3):
x_train = x[i%len(x)]
y_train = y[i%len(x)]
p.step(x_train,y_train)
plot_decision_boundary(p,x,y)
Le 3e exemple est bien classé, la frontière ne bouge pas.
p = Perceptron(poids=[1,1], biais=0, activation_seuil=0)
for i in range(4):
x_train = x[i%len(x)]
y_train = y[i%len(x)]
p.step(x_train,y_train)
plot_decision_boundary(p,x,y)
Idem pour le 4e exemple
p = Perceptron(poids=[1,1], biais=0, activation_seuil=0)
for i in range(5):
x_train = x[i%len(x)]
y_train = y[i%len(x)]
p.step(x_train,y_train)
plot_decision_boundary(p,x,y)
Ah, enfin un peu d'activité ! Le 5e exemple est mal classé et provoque une correction de la frontière de décision.
A partir de cet instant, tous les exemples sont bien classés, la frontière de décision ne bougera plus.
Cependant, comme on peut le constater visuellement, ce n'est pas la meilleure frontière de décision, elle est un peu trop proche des points bleus et on se doute qu'en rajoutant plus de points, ou en cherchant à classer de nouveaux points ne faisant pas partie des données initiales, elle a de grandes chances de commettre des erreurs.
On préfèrerait une frontière de décision la plus éloignée possible des points des différentes classes. Ce point sera adressé lorsque nous aborderons un prochain algo de classification à savoir, SVM !
Bon j'espère que cet article vous a paru au moins un poil intéressant. Il n'est pas aussi bon que ce que je me serais attendu initialement mais je débute, un peu de bienveillance, et à ce stade, la qualité passe par la quantité et le plus mauvais des articles est celui que l'on ne publie pas.
A très bientôt pour un prochain algo ou tout autre insight sur le machine learning ou autre