Recherche de site Web

Comment coder un réseau neuronal avec rétropropagation en Python (à partir de zéro)


L'algorithme de rétropropagation est utilisé dans le réseau neuronal artificiel classique à rétroaction.

C’est la technique encore utilisée pour former de grands réseaux de deep learning.

Dans ce tutoriel, vous découvrirez comment implémenter l'algorithme de rétropropagation pour un réseau de neurones à partir de zéro avec Python.

Après avoir terminé ce tutoriel, vous saurez :

  • Comment propager une entrée pour calculer une sortie.
  • Comment rétropropager une erreur et entraîner un réseau.
  • Comment appliquer l'algorithme de rétropropagation à un problème de modélisation prédictive réel.

Démarrez votre projet avec mon nouveau livre Machine Learning Algorithms From Scratch, comprenant des tutoriels pas à pas et les fichiers code source Python pour tous les exemples.

Commençons.

  • Mise à jour novembre/2016 : correction d'un bug dans la fonction activate(). Merci Alex!
  • Mise à jour de janvier 2017 : corrige des problèmes avec Python 3.
  • Mise à jour janvier 2017 : mise à jour d'un petit bug dans update_weights(). Merci Tomasz!
  • Mise à jour d'avril 2018 : ajout d'un lien direct vers l'ensemble de données CSV.
  • Mise à jour d'août 2018 : testée et mise à jour pour fonctionner avec Python 3.6.
  • Mise à jour de septembre 2019 : mise à jour du fichier Wheat-seeds.csv pour résoudre les problèmes de formatage.
  • Mise à jour d'octobre 2021 : inversez le signe d'erreur pour être cohérent avec les autres publications.

Description

Cette section fournit une brève introduction à l'algorithme de rétropropagation et à l'ensemble de données Wheat Seeds que nous utiliserons dans ce didacticiel.

Algorithme de rétropropagation

L'algorithme de rétropropagation est une méthode d'apprentissage supervisé pour les réseaux à réaction multicouche du domaine des réseaux de neurones artificiels.

Les réseaux de neurones feed-forward s’inspirent du traitement de l’information d’une ou plusieurs cellules neuronales, appelées neurones. Un neurone accepte les signaux d’entrée via ses dendrites, qui transmettent le signal électrique au corps cellulaire. L’axone transmet le signal aux synapses, qui sont les connexions de l’axone d’une cellule aux dendrites d’autres cellules.

Le principe de l'approche de rétropropagation est de modéliser une fonction donnée en modifiant les pondérations internes des signaux d'entrée pour produire un signal de sortie attendu. Le système est formé à l’aide d’une méthode d’apprentissage supervisé, dans laquelle l’erreur entre la sortie du système et une sortie attendue connue est présentée au système et utilisée pour modifier son état interne.

Techniquement, l'algorithme de rétropropagation est une méthode permettant d'entraîner les poids dans un réseau neuronal multicouche à action directe. En tant que tel, il nécessite de définir une structure de réseau composée d’une ou plusieurs couches, une couche étant entièrement connectée à la couche suivante. Une structure de réseau standard comprend une couche d’entrée, une couche cachée et une couche de sortie.

La rétropropagation peut être utilisée à la fois pour des problèmes de classification et de régression, mais nous nous concentrerons sur la classification dans ce didacticiel.

Dans les problèmes de classification, les meilleurs résultats sont obtenus lorsque le réseau possède un neurone dans la couche de sortie pour chaque valeur de classe. Par exemple, un problème de classification à 2 classes ou binaire avec les valeurs de classe A et B. Ces résultats attendus devraient être transformés en vecteurs binaires avec une colonne pour chaque valeur de classe. Tels que [1, 0] et [0, 1] pour A et B respectivement. C'est ce qu'on appelle un encodage à chaud.

Ensemble de données sur les graines de blé

L'ensemble de données sur les graines implique la prédiction des espèces à partir de mesures de graines de différentes variétés de blé.

Il y a 201 enregistrements et 7 variables d'entrée numériques. Il s'agit d'un problème de classification avec 3 classes de sortie. L'échelle de chaque valeur d'entrée numérique varie, une certaine normalisation des données peut donc être nécessaire pour une utilisation avec des algorithmes qui pondèrent les entrées comme l'algorithme de rétropropagation.

Vous trouverez ci-dessous un échantillon des 5 premières lignes de l'ensemble de données.

15.26,14.84,0.871,5.763,3.312,2.221,5.22,1
14.88,14.57,0.8811,5.554,3.333,1.018,4.956,1
14.29,14.09,0.905,5.291,3.337,2.699,4.825,1
13.84,13.94,0.8955,5.324,3.379,2.259,4.805,1
16.14,14.99,0.9034,5.658,3.562,1.355,5.175,1

En utilisant l'algorithme Zero Rule qui prédit la valeur de classe la plus courante, la précision de base du problème est de 28,095 %.

Vous pouvez en savoir plus et télécharger l’ensemble de données Seeds à partir du référentiel UCI Machine Learning.

Téléchargez l'ensemble de données Seeds et placez-le dans votre répertoire de travail actuel avec le nom de fichier seeds_dataset.csv.

L'ensemble de données est au format séparé par des tabulations, vous devez donc le convertir au format CSV à l'aide d'un éditeur de texte ou d'un tableur.

Mettre à jour, télécharger directement le jeu de données au format CSV :

  • Télécharger l'ensemble de données sur les graines de blé

Tutoriel

Ce tutoriel se décompose en 6 parties :

  1. Initialisez le réseau.
  2. Propagation avant.
  3. Erreur de propagation arrière.
  4. Réseau ferroviaire.
  5. Prédire.
  6. Étude de cas sur l'ensemble de données sur les semences.

Ces étapes fourniront la base dont vous avez besoin pour implémenter l'algorithme de rétropropagation à partir de zéro et l'appliquer à vos propres problèmes de modélisation prédictive.

1. Initialiser le réseau

Commençons par quelque chose de simple, la création d’un nouveau réseau prêt à se former.

Chaque neurone possède un ensemble de poids qui doivent être maintenus. Un poids pour chaque connexion d'entrée et un poids supplémentaire pour le biais. Nous devrons stocker des propriétés supplémentaires pour un neurone pendant l'entraînement, nous utiliserons donc un dictionnaire pour représenter chaque neurone et stockerons les propriétés par des noms tels que « poids » pour les poids.

Un réseau est organisé en couches. La couche d'entrée n'est en réalité qu'une ligne de notre ensemble de données de formation. La première vraie couche est la couche cachée. Vient ensuite la couche de sortie qui possède un neurone pour chaque valeur de classe.

Nous organiserons les couches sous forme de tableaux de dictionnaires et traiterons l’ensemble du réseau comme un tableau de couches.

Il est recommandé d'initialiser les pondérations du réseau sur de petits nombres aléatoires. Dans ce cas, utiliserons-nous des nombres aléatoires compris entre 0 et 1.

Vous trouverez ci-dessous une fonction nommée initialize_network() qui crée un nouveau réseau neuronal prêt pour l'entraînement. Il accepte trois paramètres, le nombre d'entrées, le nombre de neurones à avoir dans la couche cachée et le nombre de sorties.

Vous pouvez voir que pour la couche cachée, nous créons des neurones n_hidden et chaque neurone de la couche cachée a n_inputs + 1 poids, un pour chaque colonne d'entrée dans un ensemble de données et un supplémentaire un pour le biais.

Vous pouvez également voir que la couche de sortie qui se connecte à la couche cachée possède des neurones n_outputs, chacun avec des poids n_hidden + 1. Cela signifie que chaque neurone de la couche de sortie se connecte à (a un poids pour) chaque neurone de la couche cachée.

# Initialize a network
def initialize_network(n_inputs, n_hidden, n_outputs):
	network = list()
	hidden_layer = [{'weights':[random() for i in range(n_inputs + 1)]} for i in range(n_hidden)]
	network.append(hidden_layer)
	output_layer = [{'weights':[random() for i in range(n_hidden + 1)]} for i in range(n_outputs)]
	network.append(output_layer)
	return network

Testons cette fonction. Vous trouverez ci-dessous un exemple complet qui crée un petit réseau.

from random import seed
from random import random

# Initialize a network
def initialize_network(n_inputs, n_hidden, n_outputs):
	network = list()
	hidden_layer = [{'weights':[random() for i in range(n_inputs + 1)]} for i in range(n_hidden)]
	network.append(hidden_layer)
	output_layer = [{'weights':[random() for i in range(n_hidden + 1)]} for i in range(n_outputs)]
	network.append(output_layer)
	return network

seed(1)
network = initialize_network(2, 1, 2)
for layer in network:
	print(layer)

En exécutant l’exemple, vous pouvez voir que le code imprime chaque couche une par une. Vous pouvez voir que la couche cachée a un neurone avec 2 poids d'entrée plus le biais. La couche de sortie comporte 2 neurones, chacun avec 1 poids plus le biais.

[{'weights': [0.13436424411240122, 0.8474337369372327, 0.763774618976614]}]
[{'weights': [0.2550690257394217, 0.49543508709194095]}, {'weights': [0.4494910647887381, 0.651592972722763]}]

Maintenant que nous savons comment créer et initialiser un réseau, voyons comment l’utiliser pour calculer une sortie.

2. Propagation avant

Nous pouvons calculer une sortie d'un réseau neuronal en propageant un signal d'entrée à travers chaque couche jusqu'à ce que la couche de sortie génère ses valeurs.

Nous appelons cela la propagation vers l’avant.

C'est la technique dont nous aurons besoin pour générer des prédictions pendant l'entraînement qui devra être corrigée, et c'est la méthode dont nous aurons besoin une fois le réseau entraîné pour faire des prédictions sur de nouvelles données.

Nous pouvons décomposer la propagation vers l’avant en trois parties :

  1. Activation des neurones.
  2. Transfert de neurones.
  3. Propagation vers l'avant.

2.1. Activation des neurones

La première étape consiste à calculer l’activation d’un neurone à partir d’une entrée.

L'entrée peut être une ligne de notre ensemble de données d'entraînement, comme dans le cas de la couche cachée. Il peut également s'agir des sorties de chaque neurone de la couche cachée, dans le cas de la couche de sortie.

L'activation des neurones est calculée comme la somme pondérée des entrées. Un peu comme la régression linéaire.

activation = sum(weight_i * input_i) + bias

weight est un poids de réseau, input est une entrée, i est l'indice d'un poids ou d'une entrée et biais est un poids spécial qui n'a aucune entrée avec laquelle multiplier (ou vous pouvez considérer l'entrée comme étant toujours 1,0).

Vous trouverez ci-dessous une implémentation de ceci dans une fonction nommée activate(). Vous pouvez voir que la fonction suppose que le biais est le dernier poids de la liste des poids. Cela aide ici et plus tard à rendre le code plus facile à lire.

# Calculate neuron activation for an input
def activate(weights, inputs):
	activation = weights[-1]
	for i in range(len(weights)-1):
		activation += weights[i] * inputs[i]
	return activation

Voyons maintenant comment utiliser l’activation des neurones.

2.2. Transfert de neurones

Une fois qu’un neurone est activé, nous devons transférer l’activation pour voir quelle est réellement la sortie du neurone.

Différentes fonctions de transfert peuvent être utilisées. Il est traditionnel d'utiliser la fonction d'activation sigmoïde, mais vous pouvez également utiliser la fonction tanh (tangente hyperbolique) pour transférer les sorties. Plus récemment, la fonction de transfert par redresseur est devenue populaire auprès des grands réseaux d’apprentissage profond.

La fonction d’activation sigmoïde ressemble à une forme en S, on l’appelle aussi fonction logistique. Il peut prendre n'importe quelle valeur d'entrée et produire un nombre compris entre 0 et 1 sur une courbe en S. C'est aussi une fonction dont nous pouvons facilement calculer la dérivée (pente) dont nous aurons besoin plus tard lors de la rétropropagation de l'erreur.

Nous pouvons transférer une fonction d'activation en utilisant la fonction sigmoïde comme suit :

output = 1 / (1 + e^(-activation))

e est la base des logarithmes naturels (nombre d’Euler).

Vous trouverez ci-dessous une fonction nommée transfer() qui implémente l'équation sigmoïde.

# Transfer neuron activation
def transfer(activation):
	return 1.0 / (1.0 + exp(-activation))

Maintenant que nous avons les pièces, voyons comment elles sont utilisées.

2.3. Propagation vers l'avant

La propagation directe d’une entrée est simple.

Nous travaillons sur chaque couche de notre réseau en calculant les sorties pour chaque neurone. Toutes les sorties d’une couche deviennent des entrées pour les neurones de la couche suivante.

Vous trouverez ci-dessous une fonction nommée forward_propagate() qui implémente la propagation directe d'une ligne de données de notre ensemble de données avec notre réseau neuronal.

Vous pouvez voir que la valeur de sortie d'un neurone est stockée dans le neurone sous le nom de « sortie ». Vous pouvez également voir que nous collectons les sorties d'une couche dans un tableau nommé new_inputs qui devient le tableau inputs et est utilisé comme entrées pour la couche suivante.

La fonction renvoie les sorties de la dernière couche également appelée couche de sortie.

# Forward propagate input to a network output
def forward_propagate(network, row):
	inputs = row
	for layer in network:
		new_inputs = []
		for neuron in layer:
			activation = activate(neuron['weights'], inputs)
			neuron['output'] = transfer(activation)
			new_inputs.append(neuron['output'])
		inputs = new_inputs
	return inputs

Rassemblons tous ces éléments et testons la propagation vers l'avant de notre réseau.

Nous définissons notre réseau en ligne avec un neurone caché qui attend 2 valeurs d'entrée et une couche de sortie avec deux neurones.

from math import exp

# Calculate neuron activation for an input
def activate(weights, inputs):
	activation = weights[-1]
	for i in range(len(weights)-1):
		activation += weights[i] * inputs[i]
	return activation

# Transfer neuron activation
def transfer(activation):
	return 1.0 / (1.0 + exp(-activation))

# Forward propagate input to a network output
def forward_propagate(network, row):
	inputs = row
	for layer in network:
		new_inputs = []
		for neuron in layer:
			activation = activate(neuron['weights'], inputs)
			neuron['output'] = transfer(activation)
			new_inputs.append(neuron['output'])
		inputs = new_inputs
	return inputs

# test forward propagation
network = [[{'weights': [0.13436424411240122, 0.8474337369372327, 0.763774618976614]}],
		[{'weights': [0.2550690257394217, 0.49543508709194095]}, {'weights': [0.4494910647887381, 0.651592972722763]}]]
row = [1, 0, None]
output = forward_propagate(network, row)
print(output)

L'exécution de l'exemple propage le modèle d'entrée [1, 0] et produit une valeur de sortie qui est imprimée. Étant donné que la couche de sortie comporte deux neurones, nous obtenons une liste de deux nombres en sortie.

Les valeurs de sortie réelles n’ont aucun sens pour l’instant, mais nous commencerons ensuite à apprendre comment rendre les poids dans les neurones plus utiles.

[0.6629970129852887, 0.7253160725279748]

3. Erreur de propagation arrière

L'algorithme de rétropropagation doit son nom à la manière dont les poids sont entraînés.

L'erreur est calculée entre les sorties attendues et les sorties propagées vers l'avant depuis le réseau. Ces erreurs sont ensuite propagées vers l'arrière à travers le réseau, de la couche de sortie à la couche cachée, attribuant la responsabilité de l'erreur et mettant à jour les pondérations au fur et à mesure.

Le calcul de l'erreur de rétropropagation est ancré dans le calcul, mais nous resterons de haut niveau dans cette section et nous concentrerons sur ce qui est calculé et comment plutôt que sur pourquoi les calculs prennent cette forme particulière.

Cette partie est divisée en deux sections.

  1. Dérivé de transfert.
  2. Rétropropagation des erreurs.

3.1. Dérivé de transfert

Étant donné la valeur de sortie d’un neurone, nous devons calculer sa pente.

Nous utilisons la fonction de transfert sigmoïde dont la dérivée peut être calculée comme suit :

derivative = output * (1.0 - output)

Vous trouverez ci-dessous une fonction nommée transfer_derivative() qui implémente cette équation.

# Calculate the derivative of an neuron output
def transfer_derivative(output):
	return output * (1.0 - output)

Voyons maintenant comment cela peut être utilisé.

3.2. Rétropropagation des erreurs

La première étape consiste à calculer l'erreur pour chaque neurone de sortie, cela nous donnera notre signal d'erreur (entrée) à propager vers l'arrière à travers le réseau.

L'erreur pour un neurone donné peut être calculée comme suit :

error = (output - expected) * transfer_derivative(output)

expected est la valeur de sortie attendue pour le neurone, output est la valeur de sortie du neurone et transfer_derivative() calcule la pente du neurone. valeur de sortie, comme indiqué ci-dessus.

Ce calcul d'erreur est utilisé pour les neurones de la couche de sortie. La valeur attendue est la valeur de classe elle-même. Dans la couche cachée, les choses sont un peu plus compliquées.

Le signal d'erreur pour un neurone dans la couche cachée est calculé comme l'erreur pondérée de chaque neurone dans la couche de sortie. Pensez à l'erreur qui remonte le long des poids de la couche de sortie jusqu'aux neurones de la couche cachée.

Le signal d'erreur rétropropagé est accumulé, puis utilisé pour déterminer l'erreur du neurone dans la couche cachée, comme suit :

error = (weight_k * error_j) * transfer_derivative(output)

error_j est le signal d'erreur provenant du jème neurone dans la couche de sortie, weight_k est le poids qui relie le kth neurone au neurone actuel et la sortie est la sortie du neurone actuel.

Vous trouverez ci-dessous une fonction nommée backward_propagate_error() qui implémente cette procédure.

Vous pouvez voir que le signal d’erreur calculé pour chaque neurone est stocké sous le nom « delta ». Vous pouvez voir que les couches du réseau sont itérées dans l’ordre inverse, en commençant par la sortie et en remontant. Cela garantit que les neurones de la couche de sortie ont des valeurs « delta » calculées en premier que les neurones de la couche cachée peuvent utiliser lors de l'itération suivante. J'ai choisi le nom « delta » pour refléter le changement que l'erreur implique sur le neurone (par exemple le delta de poids).

Vous pouvez voir que le signal d'erreur pour les neurones de la couche cachée est accumulé à partir des neurones de la couche de sortie où le numéro de neurone caché j est également l'indice du poids du neurone dans la couche de sortie neurone ['poids'][j].

# Backpropagate error and store in neurons
def backward_propagate_error(network, expected):
	for i in reversed(range(len(network))):
		layer = network[i]
		errors = list()
		if i != len(network)-1:
			for j in range(len(layer)):
				error = 0.0
				for neuron in network[i + 1]:
					error += (neuron['weights'][j] * neuron['delta'])
				errors.append(error)
		else:
			for j in range(len(layer)):
				neuron = layer[j]
				errors.append(neuron['output'] - expected[j])
		for j in range(len(layer)):
			neuron = layer[j]
			neuron['delta'] = errors[j] * transfer_derivative(neuron['output'])

Rassemblons toutes les pièces et voyons comment cela fonctionne.

Nous définissons un réseau neuronal fixe avec des valeurs de sortie et rétropropagons un modèle de sortie attendu. L’exemple complet est répertorié ci-dessous.

# Calculate the derivative of an neuron output
def transfer_derivative(output):
	return output * (1.0 - output)

# Backpropagate error and store in neurons
def backward_propagate_error(network, expected):
	for i in reversed(range(len(network))):
		layer = network[i]
		errors = list()
		if i != len(network)-1:
			for j in range(len(layer)):
				error = 0.0
				for neuron in network[i + 1]:
					error += (neuron['weights'][j] * neuron['delta'])
				errors.append(error)
		else:
			for j in range(len(layer)):
				neuron = layer[j]
				errors.append(neuron['output'] - expected[j])
		for j in range(len(layer)):
			neuron = layer[j]
			neuron['delta'] = errors[j] * transfer_derivative(neuron['output'])

# test backpropagation of error
network = [[{'output': 0.7105668883115941, 'weights': [0.13436424411240122, 0.8474337369372327, 0.763774618976614]}],
		[{'output': 0.6213859615555266, 'weights': [0.2550690257394217, 0.49543508709194095]}, {'output': 0.6573693455986976, 'weights': [0.4494910647887381, 0.651592972722763]}]]
expected = [0, 1]
backward_propagate_error(network, expected)
for layer in network:
	print(layer)

L’exécution de l’exemple imprime le réseau une fois la rétropropagation de l’erreur terminée. Vous pouvez voir que les valeurs d'erreur sont calculées et stockées dans les neurones pour la couche de sortie et la couche cachée.

[{'output': 0.7105668883115941, 'weights': [0.13436424411240122, 0.8474337369372327, 0.763774618976614], 'delta': 0.0005348048046610517}]
[{'output': 0.6213859615555266, 'weights': [0.2550690257394217, 0.49543508709194095], 'delta': 0.14619064683582808}, {'output': 0.6573693455986976, 'weights': [0.4494910647887381, 0.651592972722763], 'delta': -0.0771723774346327}]

Utilisons maintenant la rétropropagation des erreurs pour entraîner le réseau.

4. Réseau ferroviaire

Le réseau est formé par descente de gradient stochastique.

Cela implique plusieurs itérations d'exposition d'un ensemble de données d'entraînement au réseau et, pour chaque ligne de données, la propagation des entrées, la rétropropagation de l'erreur et la mise à jour des pondérations du réseau.

Cette partie se décompose en deux sections :

  1. Mettre à jour les poids.
  2. Réseau ferroviaire.

4.1. Mettre à jour les poids

Une fois les erreurs calculées pour chaque neurone du réseau via la méthode de rétro-propagation ci-dessus, elles peuvent être utilisées pour mettre à jour les poids.

Les pondérations du réseau sont mises à jour comme suit :

weight = weight - learning_rate * error * input

weight est un poids donné, learning_rate est un paramètre que vous devez spécifier, error est l'erreur calculée par la procédure de rétropropagation pour le neurone et input est la valeur d'entrée qui a provoqué l'erreur.

La même procédure peut être utilisée pour mettre à jour la pondération du biais, sauf qu'il n'y a pas de terme d'entrée ou que l'entrée est la valeur fixe de 1,0.

Le taux d'apprentissage contrôle dans quelle mesure modifier le poids pour corriger l'erreur. Par exemple, une valeur de 0,1 mettra à jour le poids de 10 % du montant qu'il pourrait éventuellement être mis à jour. Des taux d'apprentissage faibles sont préférés, car ils entraînent un apprentissage plus lent sur un grand nombre d'itérations de formation. Cela augmente la probabilité que le réseau trouve un bon ensemble de pondérations dans toutes les couches plutôt que l'ensemble de pondérations le plus rapide qui minimise l'erreur (appelé convergence prématurée).

Vous trouverez ci-dessous une fonction nommée update_weights() qui met à jour les poids d'un réseau en fonction d'une ligne de données d'entrée, d'un taux d'apprentissage et suppose qu'une propagation vers l'avant et vers l'arrière a déjà été effectuée.

N'oubliez pas que l'entrée de la couche de sortie est une collection de sorties de la couche cachée.

# Update network weights with error
def update_weights(network, row, l_rate):
	for i in range(len(network)):
		inputs = row[:-1]
		if i != 0:
			inputs = [neuron['output'] for neuron in network[i - 1]]
		for neuron in network[i]:
			for j in range(len(inputs)):
				neuron['weights'][j] -= l_rate * neuron['delta'] * inputs[j]
			neuron['weights'][-1] -= l_rate * neuron['delta']

Maintenant que nous savons comment mettre à jour les pondérations du réseau, voyons comment nous pouvons le faire à plusieurs reprises.

4.2. Réseau ferroviaire

Comme mentionné, le réseau est mis à jour par descente de gradient stochastique.

Cela implique d'abord une boucle pendant un nombre fixe d'époques et, au sein de chaque époque, la mise à jour du réseau pour chaque ligne de l'ensemble de données d'entraînement.

Étant donné que des mises à jour sont effectuées pour chaque modèle de formation, ce type d'apprentissage est appelé apprentissage en ligne. Si des erreurs se sont accumulées au cours d’une époque avant la mise à jour des poids, on parle alors d’apprentissage par lots ou de descente de gradient par lots.

Vous trouverez ci-dessous une fonction qui implémente la formation d'un réseau neuronal déjà initialisé avec un ensemble de données de formation donné, un taux d'apprentissage, un nombre fixe d'époques et un nombre attendu de valeurs de sortie.

Le nombre attendu de valeurs de sortie est utilisé pour transformer les valeurs de classe dans les données d'entraînement en un seul codage à chaud. Il s'agit d'un vecteur binaire avec une colonne pour chaque valeur de classe afin de correspondre à la sortie du réseau. Ceci est nécessaire pour calculer l’erreur de la couche de sortie.

Vous pouvez également voir que l'erreur de somme quadratique entre la sortie attendue et la sortie réseau est accumulée à chaque époque et imprimée. Ceci est utile pour créer une trace de la mesure dans laquelle le réseau apprend et s’améliore à chaque époque.

# Train a network for a fixed number of epochs
def train_network(network, train, l_rate, n_epoch, n_outputs):
	for epoch in range(n_epoch):
		sum_error = 0
		for row in train:
			outputs = forward_propagate(network, row)
			expected = [0 for i in range(n_outputs)]
			expected[row[-1]] = 1
			sum_error += sum([(expected[i]-outputs[i])**2 for i in range(len(expected))])
			backward_propagate_error(network, expected)
			update_weights(network, row, l_rate)
		print('>epoch=%d, lrate=%.3f, error=%.3f' % (epoch, l_rate, sum_error))

Nous disposons désormais de tous les éléments pour former le réseau. Nous pouvons rassembler un exemple qui inclut tout ce que nous avons vu jusqu'à présent, y compris l'initialisation du réseau et former un réseau sur un petit ensemble de données.

Vous trouverez ci-dessous un petit ensemble de données artificielles que nous pouvons utiliser pour tester l'entraînement de notre réseau neuronal.

X1			X2			Y
2.7810836		2.550537003		0
1.465489372		2.362125076		0
3.396561688		4.400293529		0
1.38807019		1.850220317		0
3.06407232		3.005305973		0
7.627531214		2.759262235		1
5.332441248		2.088626775		1
6.922596716		1.77106367		1
8.675418651		-0.242068655		1
7.673756466		3.508563011		1

Vous trouverez ci-dessous l'exemple complet. Nous utiliserons 2 neurones dans la couche cachée. C'est un problème de classification binaire (2 classes) donc il y aura deux neurones dans la couche de sortie. Le réseau sera formé pendant 20 époques avec un taux d'apprentissage de 0,5, ce qui est élevé car nous nous entraînons pour si peu d'itérations.

from math import exp
from random import seed
from random import random

# Initialize a network
def initialize_network(n_inputs, n_hidden, n_outputs):
	network = list()
	hidden_layer = [{'weights':[random() for i in range(n_inputs + 1)]} for i in range(n_hidden)]
	network.append(hidden_layer)
	output_layer = [{'weights':[random() for i in range(n_hidden + 1)]} for i in range(n_outputs)]
	network.append(output_layer)
	return network

# Calculate neuron activation for an input
def activate(weights, inputs):
	activation = weights[-1]
	for i in range(len(weights)-1):
		activation += weights[i] * inputs[i]
	return activation

# Transfer neuron activation
def transfer(activation):
	return 1.0 / (1.0 + exp(-activation))

# Forward propagate input to a network output
def forward_propagate(network, row):
	inputs = row
	for layer in network:
		new_inputs = []
		for neuron in layer:
			activation = activate(neuron['weights'], inputs)
			neuron['output'] = transfer(activation)
			new_inputs.append(neuron['output'])
		inputs = new_inputs
	return inputs

# Calculate the derivative of an neuron output
def transfer_derivative(output):
	return output * (1.0 - output)

# Backpropagate error and store in neurons
def backward_propagate_error(network, expected):
	for i in reversed(range(len(network))):
		layer = network[i]
		errors = list()
		if i != len(network)-1:
			for j in range(len(layer)):
				error = 0.0
				for neuron in network[i + 1]:
					error += (neuron['weights'][j] * neuron['delta'])
				errors.append(error)
		else:
			for j in range(len(layer)):
				neuron = layer[j]
				errors.append(neuron['output'] - expected[j])
		for j in range(len(layer)):
			neuron = layer[j]
			neuron['delta'] = errors[j] * transfer_derivative(neuron['output'])

# Update network weights with error
def update_weights(network, row, l_rate):
	for i in range(len(network)):
		inputs = row[:-1]
		if i != 0:
			inputs = [neuron['output'] for neuron in network[i - 1]]
		for neuron in network[i]:
			for j in range(len(inputs)):
				neuron['weights'][j] -= l_rate * neuron['delta'] * inputs[j]
			neuron['weights'][-1] -= l_rate * neuron['delta']

# Train a network for a fixed number of epochs
def train_network(network, train, l_rate, n_epoch, n_outputs):
	for epoch in range(n_epoch):
		sum_error = 0
		for row in train:
			outputs = forward_propagate(network, row)
			expected = [0 for i in range(n_outputs)]
			expected[row[-1]] = 1
			sum_error += sum([(expected[i]-outputs[i])**2 for i in range(len(expected))])
			backward_propagate_error(network, expected)
			update_weights(network, row, l_rate)
		print('>epoch=%d, lrate=%.3f, error=%.3f' % (epoch, l_rate, sum_error))

# Test training backprop algorithm
seed(1)
dataset = [[2.7810836,2.550537003,0],
	[1.465489372,2.362125076,0],
	[3.396561688,4.400293529,0],
	[1.38807019,1.850220317,0],
	[3.06407232,3.005305973,0],
	[7.627531214,2.759262235,1],
	[5.332441248,2.088626775,1],
	[6.922596716,1.77106367,1],
	[8.675418651,-0.242068655,1],
	[7.673756466,3.508563011,1]]
n_inputs = len(dataset[0]) - 1
n_outputs = len(set([row[-1] for row in dataset]))
network = initialize_network(n_inputs, 2, n_outputs)
train_network(network, dataset, 0.5, 20, n_outputs)
for layer in network:
	print(layer)

L’exécution de l’exemple imprime d’abord l’erreur de somme carrée à chaque époque d’entraînement. Nous pouvons constater une tendance à la diminution de cette erreur à chaque époque.

Une fois formé, le réseau est imprimé, affichant les poids appris. Le réseau contient également des valeurs de sortie et delta qui peuvent être ignorées. Nous pourrions mettre à jour notre fonction de formation pour supprimer ces données si nous le souhaitions.

>epoch=0, lrate=0.500, error=6.350
>epoch=1, lrate=0.500, error=5.531
>epoch=2, lrate=0.500, error=5.221
>epoch=3, lrate=0.500, error=4.951
>epoch=4, lrate=0.500, error=4.519
>epoch=5, lrate=0.500, error=4.173
>epoch=6, lrate=0.500, error=3.835
>epoch=7, lrate=0.500, error=3.506
>epoch=8, lrate=0.500, error=3.192
>epoch=9, lrate=0.500, error=2.898
>epoch=10, lrate=0.500, error=2.626
>epoch=11, lrate=0.500, error=2.377
>epoch=12, lrate=0.500, error=2.153
>epoch=13, lrate=0.500, error=1.953
>epoch=14, lrate=0.500, error=1.774
>epoch=15, lrate=0.500, error=1.614
>epoch=16, lrate=0.500, error=1.472
>epoch=17, lrate=0.500, error=1.346
>epoch=18, lrate=0.500, error=1.233
>epoch=19, lrate=0.500, error=1.132
[{'weights': [-1.4688375095432327, 1.850887325439514, 1.0858178629550297], 'output': 0.029980305604426185, 'delta': 0.0059546604162323625}, {'weights': [0.37711098142462157, -0.0625909894552989, 0.2765123702642716], 'output': 0.9456229000211323, 'delta': -0.0026279652850863837}]
[{'weights': [2.515394649397849, -0.3391927502445985, -0.9671565426390275], 'output': 0.23648794202357587, 'delta': 0.04270059278364587}, {'weights': [-2.5584149848484263, 1.0036422106209202, 0.42383086467582715], 'output': 0.7790535202438367, 'delta': -0.03803132596437354}]

Une fois qu’un réseau est formé, nous devons l’utiliser pour faire des prédictions.

5. Prédire

Faire des prédictions avec un réseau neuronal entraîné est assez simple.

Nous avons déjà vu comment propager un modèle d'entrée pour obtenir une sortie. C’est tout ce que nous devons faire pour faire une prédiction. Nous pouvons utiliser les valeurs de sortie elles-mêmes directement comme probabilité qu'un modèle appartienne à chaque classe de sortie.

Il peut être plus utile de transformer cette sortie en une prédiction de classe précise. Nous pouvons le faire en sélectionnant la valeur de classe avec la plus grande probabilité. Ceci est également appelé fonction arg max.

Vous trouverez ci-dessous une fonction nommée predict() qui implémente cette procédure. Il renvoie l'index de la sortie réseau qui a la plus grande probabilité. Cela suppose que les valeurs de classe ont été converties en entiers commençant à 0.

# Make a prediction with a network
def predict(network, row):
	outputs = forward_propagate(network, row)
	return outputs.index(max(outputs))

Nous pouvons assembler cela avec notre code ci-dessus pour une entrée à propagation directe et avec notre petit ensemble de données artificiel pour tester la réalisation de prédictions avec un réseau déjà formé. L’exemple code en dur un réseau formé à partir de l’étape précédente.

L’exemple complet est répertorié ci-dessous.

from math import exp

# Calculate neuron activation for an input
def activate(weights, inputs):
	activation = weights[-1]
	for i in range(len(weights)-1):
		activation += weights[i] * inputs[i]
	return activation

# Transfer neuron activation
def transfer(activation):
	return 1.0 / (1.0 + exp(-activation))

# Forward propagate input to a network output
def forward_propagate(network, row):
	inputs = row
	for layer in network:
		new_inputs = []
		for neuron in layer:
			activation = activate(neuron['weights'], inputs)
			neuron['output'] = transfer(activation)
			new_inputs.append(neuron['output'])
		inputs = new_inputs
	return inputs

# Make a prediction with a network
def predict(network, row):
	outputs = forward_propagate(network, row)
	return outputs.index(max(outputs))

# Test making predictions with the network
dataset = [[2.7810836,2.550537003,0],
	[1.465489372,2.362125076,0],
	[3.396561688,4.400293529,0],
	[1.38807019,1.850220317,0],
	[3.06407232,3.005305973,0],
	[7.627531214,2.759262235,1],
	[5.332441248,2.088626775,1],
	[6.922596716,1.77106367,1],
	[8.675418651,-0.242068655,1],
	[7.673756466,3.508563011,1]]
network = [[{'weights': [-1.482313569067226, 1.8308790073202204, 1.078381922048799]}, {'weights': [0.23244990332399884, 0.3621998343835864, 0.40289821191094327]}],
	[{'weights': [2.5001872433501404, 0.7887233511355132, -1.1026649757805829]}, {'weights': [-2.429350576245497, 0.8357651039198697, 1.0699217181280656]}]]
for row in dataset:
	prediction = predict(network, row)
	print('Expected=%d, Got=%d' % (row[-1], prediction))

L'exécution de l'exemple imprime le résultat attendu pour chaque enregistrement de l'ensemble de données d'entraînement, suivi de la prédiction précise effectuée par le réseau.

Cela montre que le réseau atteint une précision de 100 % sur ce petit ensemble de données.

Expected=0, Got=0
Expected=0, Got=0
Expected=0, Got=0
Expected=0, Got=0
Expected=0, Got=0
Expected=1, Got=1
Expected=1, Got=1
Expected=1, Got=1
Expected=1, Got=1
Expected=1, Got=1

Nous sommes maintenant prêts à appliquer notre algorithme de rétropropagation à un ensemble de données du monde réel.

6. Ensemble de données sur les graines de blé

Cette section applique l'algorithme de rétropropagation à l'ensemble de données sur les graines de blé.

La première étape consiste à charger l'ensemble de données et à convertir les données chargées en nombres que nous pouvons utiliser dans notre réseau neuronal. Pour cela, nous utiliserons la fonction d'assistance load_csv() pour charger le fichier, str_column_to_float() pour convertir les numéros de chaîne en flottants et str_column_to_int() pour convertir la colonne de classe en valeurs entières.

Les valeurs d'entrée varient en échelle et doivent être normalisées dans la plage de 0 et 1. Il est généralement recommandé de normaliser les valeurs d'entrée dans la plage de la fonction de transfert choisie, dans ce cas, la fonction sigmoïde qui génère des valeurs entre 0 et 1. . Les fonctions d'assistance dataset_minmax() et normalize_dataset() ont été utilisées pour normaliser les valeurs d'entrée.

Nous évaluerons l'algorithme en utilisant une validation croisée k fois avec 5 fois. Cela signifie que 201/5=40,2 ou 40 enregistrements seront dans chaque pli. Nous utiliserons les fonctions d'assistance evaluate_algorithm() pour évaluer l'algorithme avec validation croisée et accuracy_metric() pour calculer l'exactitude des prédictions.

Une nouvelle fonction nommée back_propagation() a été développée pour gérer l'application de l'algorithme de rétropropagation, en initialisant d'abord un réseau, en l'entraînant sur l'ensemble de données d'entraînement, puis en utilisant le réseau entraîné pour faire des prédictions sur un ensemble de données de test.

L’exemple complet est répertorié ci-dessous.

# Backprop on the Seeds Dataset
from random import seed
from random import randrange
from random import random
from csv import reader
from math import exp

# Load a CSV file
def load_csv(filename):
	dataset = list()
	with open(filename, 'r') as file:
		csv_reader = reader(file)
		for row in csv_reader:
			if not row:
				continue
			dataset.append(row)
	return dataset

# Convert string column to float
def str_column_to_float(dataset, column):
	for row in dataset:
		row[column] = float(row[column].strip())

# Convert string column to integer
def str_column_to_int(dataset, column):
	class_values = [row[column] for row in dataset]
	unique = set(class_values)
	lookup = dict()
	for i, value in enumerate(unique):
		lookup[value] = i
	for row in dataset:
		row[column] = lookup[row[column]]
	return lookup

# Find the min and max values for each column
def dataset_minmax(dataset):
	minmax = list()
	stats = [[min(column), max(column)] for column in zip(*dataset)]
	return stats

# Rescale dataset columns to the range 0-1
def normalize_dataset(dataset, minmax):
	for row in dataset:
		for i in range(len(row)-1):
			row[i] = (row[i] - minmax[i][0]) / (minmax[i][1] - minmax[i][0])

# Split a dataset into k folds
def cross_validation_split(dataset, n_folds):
	dataset_split = list()
	dataset_copy = list(dataset)
	fold_size = int(len(dataset) / n_folds)
	for i in range(n_folds):
		fold = list()
		while len(fold) < fold_size:
			index = randrange(len(dataset_copy))
			fold.append(dataset_copy.pop(index))
		dataset_split.append(fold)
	return dataset_split

# Calculate accuracy percentage
def accuracy_metric(actual, predicted):
	correct = 0
	for i in range(len(actual)):
		if actual[i] == predicted[i]:
			correct += 1
	return correct / float(len(actual)) * 100.0

# Evaluate an algorithm using a cross validation split
def evaluate_algorithm(dataset, algorithm, n_folds, *args):
	folds = cross_validation_split(dataset, n_folds)
	scores = list()
	for fold in folds:
		train_set = list(folds)
		train_set.remove(fold)
		train_set = sum(train_set, [])
		test_set = list()
		for row in fold:
			row_copy = list(row)
			test_set.append(row_copy)
			row_copy[-1] = None
		predicted = algorithm(train_set, test_set, *args)
		actual = [row[-1] for row in fold]
		accuracy = accuracy_metric(actual, predicted)
		scores.append(accuracy)
	return scores

# Calculate neuron activation for an input
def activate(weights, inputs):
	activation = weights[-1]
	for i in range(len(weights)-1):
		activation += weights[i] * inputs[i]
	return activation

# Transfer neuron activation
def transfer(activation):
	return 1.0 / (1.0 + exp(-activation))

# Forward propagate input to a network output
def forward_propagate(network, row):
	inputs = row
	for layer in network:
		new_inputs = []
		for neuron in layer:
			activation = activate(neuron['weights'], inputs)
			neuron['output'] = transfer(activation)
			new_inputs.append(neuron['output'])
		inputs = new_inputs
	return inputs

# Calculate the derivative of an neuron output
def transfer_derivative(output):
	return output * (1.0 - output)

# Backpropagate error and store in neurons
def backward_propagate_error(network, expected):
	for i in reversed(range(len(network))):
		layer = network[i]
		errors = list()
		if i != len(network)-1:
			for j in range(len(layer)):
				error = 0.0
				for neuron in network[i + 1]:
					error += (neuron['weights'][j] * neuron['delta'])
				errors.append(error)
		else:
			for j in range(len(layer)):
				neuron = layer[j]
				errors.append(neuron['output'] - expected[j])
		for j in range(len(layer)):
			neuron = layer[j]
			neuron['delta'] = errors[j] * transfer_derivative(neuron['output'])

# Update network weights with error
def update_weights(network, row, l_rate):
	for i in range(len(network)):
		inputs = row[:-1]
		if i != 0:
			inputs = [neuron['output'] for neuron in network[i - 1]]
		for neuron in network[i]:
			for j in range(len(inputs)):
				neuron['weights'][j] -= l_rate * neuron['delta'] * inputs[j]
			neuron['weights'][-1] -= l_rate * neuron['delta']

# Train a network for a fixed number of epochs
def train_network(network, train, l_rate, n_epoch, n_outputs):
	for epoch in range(n_epoch):
		for row in train:
			outputs = forward_propagate(network, row)
			expected = [0 for i in range(n_outputs)]
			expected[row[-1]] = 1
			backward_propagate_error(network, expected)
			update_weights(network, row, l_rate)

# Initialize a network
def initialize_network(n_inputs, n_hidden, n_outputs):
	network = list()
	hidden_layer = [{'weights':[random() for i in range(n_inputs + 1)]} for i in range(n_hidden)]
	network.append(hidden_layer)
	output_layer = [{'weights':[random() for i in range(n_hidden + 1)]} for i in range(n_outputs)]
	network.append(output_layer)
	return network

# Make a prediction with a network
def predict(network, row):
	outputs = forward_propagate(network, row)
	return outputs.index(max(outputs))

# Backpropagation Algorithm With Stochastic Gradient Descent
def back_propagation(train, test, l_rate, n_epoch, n_hidden):
	n_inputs = len(train[0]) - 1
	n_outputs = len(set([row[-1] for row in train]))
	network = initialize_network(n_inputs, n_hidden, n_outputs)
	train_network(network, train, l_rate, n_epoch, n_outputs)
	predictions = list()
	for row in test:
		prediction = predict(network, row)
		predictions.append(prediction)
	return(predictions)

# Test Backprop on Seeds dataset
seed(1)
# load and prepare data
filename = 'seeds_dataset.csv'
dataset = load_csv(filename)
for i in range(len(dataset[0])-1):
	str_column_to_float(dataset, i)
# convert class column to integers
str_column_to_int(dataset, len(dataset[0])-1)
# normalize input variables
minmax = dataset_minmax(dataset)
normalize_dataset(dataset, minmax)
# evaluate algorithm
n_folds = 5
l_rate = 0.3
n_epoch = 500
n_hidden = 5
scores = evaluate_algorithm(dataset, back_propagation, n_folds, l_rate, n_epoch, n_hidden)
print('Scores: %s' % scores)
print('Mean Accuracy: %.3f%%' % (sum(scores)/float(len(scores))))

Un réseau avec 5 neurones dans la couche cachée et 3 neurones dans la couche de sortie a été construit. Le réseau a été formé pendant 500 époques avec un taux d'apprentissage de 0,3. Ces paramètres ont été trouvés après quelques essais et erreurs, mais vous pourrez peut-être faire beaucoup mieux.

L’exécution de l’exemple imprime la précision moyenne de la classification sur chaque pli ainsi que les performances moyennes sur tous les plis.

Vous pouvez voir que la rétropropagation et la configuration choisie ont atteint une précision de classification moyenne d'environ 93 %, ce qui est considérablement meilleure que l'algorithme Zero Rule qui a obtenu une précision légèrement supérieure à 28 %.

Scores: [92.85714285714286, 92.85714285714286, 97.61904761904762, 92.85714285714286, 90.47619047619048]
Mean Accuracy: 93.333%

Rallonges

Cette section répertorie les extensions du didacticiel que vous souhaiterez peut-être explorer.

  • Régler les paramètres de l'algorithme. Essayez des réseaux plus grands ou plus petits, formés plus ou moins longtemps. Voyez si vous pouvez obtenir de meilleures performances sur l’ensemble de données Seeds.
  • Méthodes supplémentaires. Expérimentez avec différentes techniques d'initialisation de poids (telles que de petits nombres aléatoires) et différentes fonctions de transfert (telles que tanh).
  • Plus de calques. Ajoutez la prise en charge d'un plus grand nombre de couches cachées, entraînées de la même manière que la couche cachée utilisée dans ce didacticiel.
  • Régression. Modifiez le réseau pour qu'il n'y ait qu'un seul neurone dans la couche de sortie et qu'une valeur réelle soit prédite. Choisissez un ensemble de données de régression sur lequel vous entraîner. Une fonction de transfert linéaire pourrait être utilisée pour les neurones de la couche de sortie, ou les valeurs de sortie de l'ensemble de données choisi pourraient être mises à l'échelle à des valeurs comprises entre 0 et 1.
  • Descente de dégradé par lots. Modifiez la procédure de formation de la descente de gradient en ligne à la descente de gradient par lots et mettez à jour les poids uniquement à la fin de chaque époque.

Avez-vous essayé l'une de ces extensions ?
Partagez vos expériences dans les commentaires ci-dessous.

Revoir

Dans ce didacticiel, vous avez découvert comment implémenter l'algorithme de rétropropagation à partir de zéro.

Concrètement, vous avez appris :

  • Comment transmettre et propager une entrée pour calculer une sortie réseau.
  • Comment propager l'erreur et mettre à jour les pondérations du réseau.
  • Comment appliquer l'algorithme de rétropropagation à un ensemble de données du monde réel.

Avez-vous des questions ?
Posez vos questions dans les commentaires ci-dessous et je ferai de mon mieux pour y répondre.

Articles connexes