lab
Neural Network
Español

Construya y entrene redes neuronales desde cero

Visión general

Se han contado muchos conocimientos teóricos antes, ahora es el momento de comenzar la práctica. Intentemos construir una red neuronal desde cero y entrenarla para encadenar todo el proceso.


Para ser más intuitivos y fáciles de entender, seguimos los siguientes principios:

  1. No utilice bibliotecas de terceros para simplificar la lógica;
  2. Sin optimización del rendimiento: evite la introducción de conceptos y técnicas adicionales, aumentando la complejidad;

Conjunto de datos

Primero, necesitamos un conjunto de datos. Para facilitar la visualización, usamos una función binaria como función objetivo y luego generamos el conjunto de datos muestreando en él.


Nota: En proyectos de ingeniería reales, la función objetivo es desconocida, pero podemos tomar muestras de ella.

Función objetiva

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

El código se muestra a continuación:

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

Generar conjunto de datos

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 ]

El conjunto de datos generado es: [[-2.0, -2.0, 0.0], [-2.0, -1.6, 0.0], ...]

Imagen del conjunto de datos

Construye una red neuronal

Función de activación

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

Neurona

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)

La expresión de la neurona es:

sigmoid(wx+b)\text{sigmoid}(\mathbf w \mathbf x + b)
  • w\mathbf w: vector, correspondiente a la matriz de pesos en el código
  • bb: corresponde al sesgo en el código

Nota: Los parámetros de la neurona se inicializan aleatoriamente. Sin embargo, para garantizar experimentos reproducibles, se establece una semilla aleatoria (seed(0))

Redes neuronales

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 ] # devuelve la salida de la última neurona return inputs[0]

Construya una red neuronal de la siguiente manera:

net = MyNet(2, [4])

En este punto, tenemos una red neuronal (net), que puede llamar a su función de red neuronal:

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

Obtenga el valor de la función 0.55 ..., la red neuronal en este momento es una red no capacitada.

Imagen inicial de la función de red neuronal

Entrena la red neuronal

Función de pérdida

Primero defina una función de pérdida:

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

Calcular el gradiente

El cálculo del gradiente es complicado, especialmente para redes neuronales profundas. Back Propagation Algorithm es un algoritmo diseñado específicamente para calcular el gradiente de una red neuronal.


Debido a su complejidad, no se describirá aquí. Los interesados pueden consultar el siguiente código detallado. Además, el marco actual de aprendizaje profundo tiene la función de calcular automáticamente el gradiente.


Defina la función derivada:

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

Encuentre la derivada parcial (parte de los datos se almacena en caché en la función de avance para facilitar la derivada):

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)]
  • Las derivadas parciales se almacenan en d_weights y d_bias respectivamente
  • La función zero_grad se usa para borrar el gradiente, incluida cada derivada parcial
  • La función backward se utiliza para calcular la derivada parcial y almacenar su valor de forma acumulativa

Actualizar parámetros

Utilice el método de descenso de gradiente para actualizar los parámetros:

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)

Realizar entrenamiento

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

Entrenamiento 2000 pasos:

train(2000, learning_rate=10)

Nota: Aquí se utiliza una tasa de aprendizaje relativamente grande, que está relacionada con la situación del proyecto. La tasa de aprendizaje en proyectos reales suele ser muy pequeña

Imagen de la función de red neuronal después del entrenamiento
log y\text{log y}
La curva de pérdidas

Inferencia

Después del entrenamiento, el modelo se puede utilizar para la inferencia:

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

Consulte el código completo: nn_from_scratch.py

Resumen

Los pasos de esta práctica son los siguientes:

  1. Construya una función objetivo virtual: o(x,y)o(x, y);
  2. Muestreo de o(x,y)o(x, y) para obtener el conjunto de datos, es decir, la función del conjunto de datos: d(x,y)d(x, y)
  3. Construyó una red neuronal completamente conectada con una capa oculta, es decir, función de red neuronal: f(x,y)f(x, y)
  4. Utilice el método de descenso de gradiente para entrenar la red neuronal de modo que f(x,y)f(x, y) se aproxime a d(x,y)d(x, y)

La parte más complicada es encontrar el gradiente, que utiliza el algoritmo de retropropagación. En proyectos reales, el uso de marcos de aprendizaje profundo convencionales para el desarrollo puede ahorrar el código para gradientes y reducir el umbral.


En los experimentos de clasificación 3D del laboratorio, el segundo conjunto de datos es muy similar al de esta práctica, por lo que puede ingresar y operarlo.