Tom
Marx
>_

Introduction aux réseaux de neurones - Partie 2 : Mise en pratique

Maintenant que vous comprenez le fonctionnement d'un neurone, nous allons coder une classe python capable de résoudre des problèmes.

15 minutes

Introduction

Dans cette deuxième partie, nous allons coder un neurone en se basant sur la théorie vue dans l'article précédent. Le code en example sera en Python, mais vous pouvez très bien le faire dans votre langage favori. Pour cette partie vous aurez donc besoin d'une compréhension minimale de la programmation. Tous les fichiers sont disponibles en téléchargement à la fin de l'article.
Nous allons résoudre un problème simple : on nous donne la masse et le diamètre d'un fruit, et l'on doit déterminer si ce fruit est une pomme ou un citron.
Nous disposons du dataset suivante :

On défini le type 0 pour les pommes et 1 pour les citrons. Le dataset est composé de 100 examples de fruits.

La classe neurone

Pour commencer, listons les différentes propriétés d'un objet neurone :

Le neurone est construit à partir d'un nombre d'entrées. Au lancement du programme, les poids sont des valeurs aléatoires comprises entre -1 et 1. Notons que cette classe n'est pas spécifique à notre problème, vous pourrez donc la réutiliser pour d'autres projets. Voilà ce que ça donne en Python :

# Fichier "Neuron.py"
class Neuron:

    def __init__(self, inputs_n):
        self.learning_rate = 0.01
        self.weights = []
        self.bias = 0
        self.init_weights(inputs_n)

    # Fonction d'initialisation des poids, on crée le tableau de dimension inputs_n et on le rempli de valeurs aléatoires entre -1 et 1
    def init_weights(self, inputs_n):
        for i in range(0, inputs_n):
            self.weights.append(random.uniform(-1, 1))

La fonction de prédiction

Maintenant que toutes nos propriétés sont définies est initialisées, on peut créer une méthode 'guess', qui prend en entrée un tableau de valeur, les inputs, et qui retourne la prédiction. Pour réaliser cette fonction on a besoin de définir notre fonction d'activation. L'algorithme que nous faisons est un classificateur linéaire, ses valeurs de sorties sont soit pomme soit citron. Notre fonction d'activation doit donc renvoyer soit 1, soit 0. Pour cela, nous allons utiliser une fonction qui renvoi 1 si `x >= 0`, 0 si `x < 0` :

# Fonction d'activation, x: float
def activate(self, x):
    if x >= 0:
        return 1
    else:
        return 0

On peut ensuite définir notre méthode 'guess'. Pour rappel, voici le procédé d'activation d'un neurone :

# Fonction de prédiction, inputs: float[], retourne la prédiction, 0 (pomme) ou 1 (citron)
def guess(self, inputs):
    sum = 0
    for i in range(0, len(inputs)):
        sum += self.weights[i] * inputs[i]
    sum += self.bias
    return self.activate(sum)

L'entrainement

Notre neurone est désormais capable de faire un prédiction, mais ses poids sont définis de manière aléatoirs. Notre fonction 'train' prend en entrée les inputs, et la sortie correspondante. Voici un rappel du déroulement de l'entrainement :

# Fonction d'entrainement, input: float[], output: 0 ou 1, retourne l'erreur
def train(self, inputs, output):
    # On réalise une prédiction
    guess = self.guess(inputs)
    # On calcul l'écart entre notre prédiction et la réponse attendue
    error = output - guess
    # On corrige chacun des poids
    for i in range(0, len(self.weights)):
        self.weights[i] += error * inputs[i] * self.learning_rate
    self.bias += error * self.learning_rate
    return error

Application à notre problème

Nous avons maintenant une classe neurone complètement fonctionnelle, capable de réaliser une prédiction et de s'entrainer. On veut maintenant créer un neurone à 2 entrées, la masse et le diamètre du fruit. Ensuite il faudra l'entrainer avec notre dataset. En plus, on calculera l'erreur moyenne que commet le neurone pour chaque entrainement, afin de suivre l'évolution des résultats.
Vous devrez parser le dataset qui est au format JSON. Beaucoup de langagues ont des outils pour le faire. Il se présente sous la forme :

[
  {
    "masse": 130,
    "diametre": 5,
    "type": 0
  },
  {
    "masse": 99,
    "diametre": 6,
    "type": 1
  },
  ...
]

On va répéter l'entrainement 1000 fois, afin de trouver un résultat satisfaisant. Voici le code :

# Fichier "main.py"
from Neuron import Neuron
import math
import json

# On défini une fonction qui calcul la valeur moyenne d'un tableau de float
def array_average(array):
    sum = 0
    for i in array:
        sum += i
    return sum / len(array)

# On instantie notre neurone avec 2 entrées, masse et diamètre
neuron = Neuron(2)

# On charge en mémoire le dataset sous forme de tableau
dataset = json.loads(open('dataset.json').read())

# Ce tableau va contenir l'erreur moyenne de chaque entrainement (1000 au total)
average_errors = []

# On répète 1000 fois l'entrainement
for x in range(1, 1000):
    # On enregistre toutes les erreurs de cet entrainement afin de calculer l'erreur moyenne sur cet entrainement
    errors = []
    # Pour toutes les entrées du dataset on fait apprendre le neurone et on enregistre son erreur dans le tableau errors
    for fruit in dataset:
        inputs = [fruit['masse'], fruit['diametre']]
        errors.append(math.fabs(neuron.train(inputs, fruit['type'])))

    average_errors.append(array_average(errors))
    print('Entrainement n°', x, 'erreur moyenne :', array_average(errors))

print('Entrainement terminé')

Si on exécute notre programme, voici le résultat :

Entrainement n° 1 erreur moyenne : 0.77
Entrainement n° 2 erreur moyenne : 0.81
Entrainement n° 3 erreur moyenne : 0.81
Entrainement n° 20 erreur moyenne : 0.77
Entrainement n° 21 erreur moyenne : 0.74
Entrainement n° 265 erreur moyenne : 0.63
Entrainement n° 266 erreur moyenne : 0.62
Entrainement n° 689 erreur moyenne : 0.38
Entrainement n° 690 erreur moyenne : 0.29
Entrainement n° 731 erreur moyenne : 0.09
Entrainement n° 732 erreur moyenne : 0.0
Entrainement n° 999 erreur moyenne : 0.0
Entrainement terminé

Et voilà ! Notre neurone est capable de différencier parfaitement les deux fruits après 732 répétitions de l'entrainement. Si on lui demande de prédire la nature d'un fruit qui n'est pas dans le dataset, il y arrivera sans soucis. Ci-dessous le graphe de la progession du neurone :

Progression de notre neurone

On voit que la courbe est loin d'être lisse, mais globalement le réseau progresse. Maintenant, nous allons reproduire l'expérience sans le bias :

Progression de notre neurone sans le bias

On voit que le neurone est incapable de descendre en dessous d'une erreur de 0,6. Le bias a donc bien une importance cruciale. Faisons un autre essai, cette fois-ci avec un learning rate à 1 (ce qui équivaut à l'absence de celui-ci) :

Progression de notre neurone sans learning rate

Là aussi, le neurone bloque aux alentours de 0,6. Ces deux valeurs sont donc indispensables pour un perceptron. Vous pouvez essayer de faire varier les différents paramètres de votre neurone pour en voir l'influence sur le résultat. Vous pouvez également essayer de donner un learning rate différent pour le bias, vous verrez que pour ce dataset en particulier, il est possible d'optimiser l'apprentissage.

La limite du neurone seul

Il faut bien l'avouer, notre problème aurait pu être résolu de manière beaucoup plus simple, en définissant des seuils pour la masse et le diamètre par exemple.
En effet, le machine learning est surtout utile pour résoudre des problèmes très complexes à modéliser mathématiquement. Demandez à un humain de reconnaître un arbre sur une image : il y arrivera sans aucune difficultée. Maintenant écrivez un algorithme capable de reconnaître un arbre. Il est impossible de déterminer une formule mathématique qui reconnais à coup sûr un motif qui dépend de l'angle de vue, de l'éclairage, qui subit des variations... C'est pour ce genre d'usage que le machine learning est très fort.
Nous avons pour le moment vu le fonctionnement d'un réseau composé d'un seul neurone. Ce genre de réseau est appelé perceptron. Ils ne peuvent résoudre que des problèmes de classification linéaire : si l'on représente nos fruits sur un plan avec deux axes, masse et diamètre, on est capable de tracer une droite qui sépare les deux fruits :

Représentation graphique du dataset

Le bias est lié à l'ordonnée à l'origine de la droite, les poids au coefficient diecteur. Un neurone seul sera incapable de classifier ce genre de dataset :

Progression de notre neurone sans learning rate

Dans ce cas, il est impossible de séparer les classes avec une seule droite, il faudra donc plus d'un neurone. Pour résumer, un réseau de neurones et un approximateur de fonction. Plus sa taille est grande, plus il pourra trouver des fonctions complexes.
Un réseau de neurones prend des entrées et des sorties numériques, et trouve la relation mathématique qui les lient.

Conclusion

Nous avons crée un neurone capable de s'entrainer à résoudre des problèmes très simples. L'entrainement permet de trouver les meilleurs poids, mais cela prend un certain nombre d'entrainements avant de pouvoir obtenir un résultat satisfaisant. Il est très important d'avoir un retour sur la progression de l'algorithme, en calculant l'erreur moyenne par exemple. Le fonctionnement d'un neurone peut être représenté graphiquement, par le fait de trouver une droite capable de séparer les différentes classes du dataset.