lab
Neural Network
Italiano

Costruisci e addestra reti neurali da zero

Panoramica

Molte conoscenze teoriche sono state raccontate prima, ora è il momento di iniziare la pratica. Proviamo a costruire una rete neurale da zero e ad addestrarla a mettere insieme l'intero processo.


Per essere più intuitivi e di facile comprensione, seguiamo i seguenti principi:

  1. Non utilizzare librerie di terze parti per semplificare la logica;
  2. Nessuna ottimizzazione delle prestazioni: evita di introdurre concetti e tecniche aggiuntivi, aumentando la complessità;

set di dati

Innanzitutto, abbiamo bisogno di un set di dati. Per facilitare la visualizzazione, utilizziamo una funzione binaria come funzione obiettivo, quindi generiamo il set di dati campionandolo.


Nota: nei progetti di ingegneria reali, la funzione obiettivo è sconosciuta, ma possiamo campionarla.

Funzione obiettivo

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

Il codice mostra come di seguito:

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

Genera set di dati

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 ]

Il set di dati generato è: [[-2.0, -2.0, 0.0], [-2.0, -1.6, 0.0], ...]

Immagine del set di dati

Costruisci una rete neurale

Funzione di attivazione

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'espressione del neurone è:

sigmoid(wx+b)\text{sigmoid}(\mathbf w \mathbf x + b)
  • w\mathbf w: vettore, corrispondente all'array dei pesi nel codice
  • bb: corrisponde alla distorsione nel codice

Nota: i parametri nel neurone vengono inizializzati casualmente. Tuttavia, per garantire la riproducibilità degli esperimenti, viene impostato un seme casuale(seed(0))

Reti neurali

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 ] # restituisce l'output dell'ultimo neurone return inputs[0]

Costruisci una rete neurale come segue:

net = MyNet(2, [4])

A questo punto, abbiamo una rete neurale (rete), che può chiamare la sua funzione di rete neurale:

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

Ottieni il valore della funzione 0,55..., la rete neurale in questo momento è una rete non addestrata.

Immagine della funzione di rete neurale iniziale

Allena la rete neurale

Funzione di perdita

Definire prima una funzione di perdita:

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

Calcola il gradiente

Il calcolo del gradiente è complicato, soprattutto per le reti neurali profonde. Back Propagation Algorithm è un algoritmo progettato specificamente per calcolare il gradiente di una rete neurale.


A causa della sua complessità, non verrà qui descritto. Gli interessati possono fare riferimento al seguente codice dettagliato. Inoltre, l'attuale framework di deep learning ha la funzione di calcolare automaticamente il gradiente.


Definire la funzione derivata:

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

Trova la derivata parziale (parte dei dati è memorizzata nella cache nella funzione forward per facilitare la derivata):

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)]
  • Le derivate parziali sono memorizzate rispettivamente in d_weights e d_bias
  • La funzione zero_grad viene utilizzata per cancellare il gradiente, inclusa ogni derivata parziale
  • La funzione indietro viene utilizzata per calcolare la derivata parziale e memorizzare il suo valore cumulativamente

Aggiorna parametri

Usa il metodo di discesa del gradiente per aggiornare i parametri:

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)

Esegui l'allenamento

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}")

Formazione 2000 fasi:

train(2000, learning_rate=10)

Nota: qui viene utilizzato un tasso di apprendimento relativamente elevato, che è correlato alla situazione del progetto. Il tasso di apprendimento nei progetti reali è solitamente molto basso

Immagine della funzione di rete neurale dopo l'allenamento
log y\text{log y}
La curva delle perdite

Inferenza

Dopo l'allenamento, il modello può essere utilizzato per l'inferenza:

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

Fare riferimento al codice completo:nn_from_scratch.py

Riepilogo

I passaggi di questa pratica sono i seguenti:

  1. Costruire una funzione obiettivo virtuale: o(x,y)o(x, y);
  2. Campionamento su o(x,y)o(x, y) per ottenere il set di dati, ovvero la funzione del set di dati: d(x,y)d(x, y)
  3. Costruito una rete neurale completamente connessa con un livello nascosto, ovvero una funzione di rete neurale: f(x,y)f(x, y)
  4. Utilizzare il metodo di discesa del gradiente per addestrare la rete neurale in modo che f(x,y)f(x, y) si avvicini a d(x,y)d(x, y)

La parte più complicata è trovare il gradiente, che utilizza l'algoritmo di retropropagazione. Nei progetti reali, l'utilizzo di framework di deep learning tradizionali per lo sviluppo può salvare il codice per i gradienti e abbassare la soglia.


Negli esperimenti di classificazione 3D del laboratorio, il secondo set di dati è molto simile a quello di questa pratica, quindi puoi entrare e utilizzarlo.