lab
Neural Network
Français

Construisez et entraînez des réseaux de neurones à partir de zéro

Aperçu

Beaucoup de connaissances théoriques ont été enseignées auparavant, il est maintenant temps de commencer la pratique. Essayons de créer un réseau de neurones à partir de zéro et de l'entraîner pour enchaîner l'ensemble du processus.


Afin d'être plus intuitif et plus facile à comprendre, nous suivons les principes suivants:

  1. N'utilisez pas de bibliothèques tierces pour simplifier la logique;
  2. Pas d'optimisation des performances: évitez d'introduire des concepts et techniques supplémentaires, ce qui augmente la complexité ;

Base de données

Tout d'abord, nous avons besoin d'un ensemble de données. Pour faciliter la visualisation, nous utilisons une fonction binaire comme fonction objectif, puis générons l'ensemble de données en échantillonnant dessus.


Remarque: Dans les projets d'ingénierie réels, la fonction objectif est inconnue, mais nous pouvons l'échantillonner.

Fonction objectif

o(x,y)={1x2+y2<10autremento(x, y) = \begin{cases} 1 & x^2 + y^2 < 1 \\ 0 & \text{autrement}\end{cases}

Le code s'affiche comme ci-dessous:

def o(x, y): return 1.0 if x*x + y*y < 1 else 0.0

Générer un ensemble de données

sample_density = 10 xs = [ [-2.0 + 4 * x/sample_density, -2.0 + 4 * y/sample_density] for x in range(sample_density+1) for y in range(sample_density+1) ] dataset = [ (x, y, o(x, y)) for x, y in xs ]

L'ensemble de données généré est: [[-2.0, -2,0, 0.0], [-2.0, -1.6, 0.0], ...]

Image de l'ensemble de données

Construire un réseau de neurones

Fonction d'activation

import math def sigmoid(x): return 1 / (1 + math.exp(-x))

Neurone

from random import seed, random seed(0) class Neuron: def __init__(self, num_inputs): self.weights = [random()-0.5 for _ in range(num_inputs)] self.bias = 0.0 def forward(self, inputs): # z = wx + b z = sum([ i * w for i, w in zip(inputs, self.weights) ]) + self.bias return sigmoid(z)

L'expression neuronale est:

sigmoid(wx+b)\text{sigmoid}(\mathbf w \mathbf x + b)
  • w\mathbf w: vecteur, correspondant au tableau des poids dans le code
  • bb: correspond au biais dans le code

Remarque: les paramètres du neurone sont initialisés de manière aléatoire. Cependant, afin d'assurer des expériences reproductibles, une graine aléatoire est définie(seed(0))

Les réseaux de neurones

class MyNet: def __init__(self, num_inputs, hidden_shapes): layer_shapes = hidden_shapes + [1] input_shapes = [num_inputs] + hidden_shapes self.layers = [ [ Neuron(pre_layer_size) for _ in range(layer_size) ] for layer_size, pre_layer_size in zip(layer_shapes, input_shapes) ] def forward(self, inputs): for layer in self.layers: inputs = [ neuron.forward(inputs) for neuron in layer ] # return the output of the last neuron return inputs[0]

Construisez un réseau de neurones comme suit:

net = MyNet(2, [4])

À ce stade, nous avons un réseau de neurones (net), qui peut appeler sa fonction de réseau de neurones:

print(net.forward([0, 0]))

Obtenez la valeur de fonction 0,55..., le réseau de neurones à ce moment est un réseau non formé.

Image initiale de la fonction de réseau neuronal

Former le réseau de neurones

Fonction de perte

Définissez d'abord une fonction de perte:

def square_loss(predict, target): return (predict-target)**2

Calculer le gradient

Le calcul du gradient est compliqué, en particulier pour les réseaux de neurones profonds. Back Propagation Algorithm est un algorithme spécialement conçu pour calculer le gradient d'un réseau de neurones.


En raison de sa complexité, il ne sera pas décrit ici. Les personnes intéressées peuvent se référer au code détaillé suivant. De plus, le cadre d'apprentissage profond actuel a pour fonction de calculer automatiquement le gradient.


Définissez la fonction dérivée:

def sigmoid_derivative(x): _output = sigmoid(x) return _output * (1 - _output) def square_loss_derivative(predict, target): return 2 * (predict-target)

Trouvez la dérivée partielle (une partie des données est mise en cache dans la fonction forward pour faciliter la dérivée):

class Neuron: ... def forward(self, inputs): self.inputs_cache = inputs # z = wx + b self.z_cache = sum([ i * w for i, w in zip(inputs, self.weights) ]) + self.bias return sigmoid(self.z_cache) def zero_grad(self): self.d_weights = [0.0 for w in self.weights] self.d_bias = 0.0 def backward(self, d_a): d_loss_z = d_a * sigmoid_derivative(self.z_cache) self.d_bias += d_loss_z for i in range(len(self.inputs_cache)): self.d_weights[i] += d_loss_z * self.inputs_cache[i] return [d_loss_z * w for w in self.weights] class MyNet: ... def zero_grad(self): for layer in self.layers: for neuron in layer: neuron.zero_grad() def backward(self, d_loss): d_as = [d_loss] for layer in reversed(self.layers): da_list = [ neuron.backward(d_a) for neuron, d_a in zip(layer, d_as) ] d_as = [sum(da) for da in zip(*da_list)]
  • Les dérivées partielles sont stockées dans d_weights et d_bias respectivement
  • La fonction zero_grad est utilisée pour effacer le gradient, y compris chaque dérivée partielle
  • La fonction backward est utilisée pour calculer la dérivée partielle et stocker sa valeur cumulativement

Mettre à jour les paramètres

Utilisez la méthode de descente de gradient pour mettre à jour les paramètres:

class Neuron: ... def update_params(self, learning_rate): self.bias -= learning_rate * self.d_bias for i in range(len(self.weights)): self.weights[i] -= learning_rate * self.d_weights[i] class MyNet: ... def update_params(self, learning_rate): for layer in self.layers: for neuron in layer: neuron.update_params(learning_rate)

Effectuer la formation

def one_step(learning_rate): net.zero_grad() loss = 0.0 num_samples = len(dataset) for x, y, z in dataset: predict = net.forward([x, y]) loss += square_loss(predict, z) net.backward(square_loss_derivative(predict, z) / num_samples) net.update_params(learning_rate) return loss / num_samples def train(epoch, learning_rate): for i in range(epoch): loss = one_step(learning_rate) if i == 0 or (i+1) % 100 == 0: print(f"{i+1} {loss:.4f}")

Formation 2000 étapes:

train(2000, learning_rate=10)

Remarque: Un taux d'apprentissage relativement élevé est utilisé ici, qui est lié à la situation du projet. Le taux d'apprentissage dans les projets réels est généralement très faible

Image de la fonction de réseau neuronal après l'entraînement
log y\text{log y}
La courbe de perte

Inférence

Après la formation, le modèle peut être utilisé pour l'inférence :

def inference(x, y): return net.forward([x, y]) print(inference(1, 2))

Veuillez vous référer au code complet:nn_from_scratch.py

Sommaire

Les étapes de cette pratique sont les suivantes:

  1. Construire une fonction objectif virtuelle: o(x,y)o(x, y);
  2. Échantillonnage sur o(x,y)o(x, y) pour obtenir l'ensemble de données, c'est-à-dire la fonction de l'ensemble de données: d(x,y)d(x, y)
  3. Construit un réseau de neurones entièrement connecté avec une couche cachée, c'est-à-dire une fonction de réseau de neurones: f(x,y)f(x, y)
  4. Utilisez la méthode de descente de gradient pour entraîner le réseau de neurones afin que f(x,y)f(x, y) se rapproche de d(x,y)d(x, y)

La partie la plus compliquée est de trouver le gradient, qui utilise l'algorithme de rétro-propagation. Dans les projets réels, l'utilisation de cadres d'apprentissage en profondeur traditionnels pour le développement peut économiser le code pour les gradients et abaisser le seuil.


Dans les expériences de classification 3D du laboratoire, le deuxième ensemble de données est très similaire à celui de cette pratique, vous pouvez donc y entrer et l'utiliser.