Implementación práctica
En la primera parte hemos visto cómo se deriva Back Propagation de una manera para minimizar la función de costo. En este artículo veremos el aspecto de la implementación y algunas de las mejores prácticas para evitar errores comunes.
Todavía estamos en el modo simple, donde la entrada se maneja de una en una.
Clase de capa
Considere la posibilidad de una red neuronal conectada como en la figura a continuación.
Cada capa se modelará con un objeto de Capa que contiene los pesos, los valores de activación (salida de la capa), el gradiente dZ (no representado en la imagen), el error acumulativo delta (𝚫), así como la función de activación f (x) y su derivada f ‘(x) . La razón para almacenar el intermediario es evitar calcularlos cada vez que se necesitan.
Consejo: Es mejor organizar el código en pocas clases y evitar agrupar todo en arreglos, ya que es muy fácil para perderse.
Tenga en cuenta que la capa de entrada no estará representada por un objeto de Capa, ya que solo consta de un vector.
clase Capa: def __ init __ (self, dim , id, act, act_prime, isoutputLayer = False ): self.weight = 2 * np.random.random (dim) - 1 self.delta = Ninguno self.A = Ninguno self.activation = act self.activation_prime = act_prime self.isoutputLayer = isoutputLayer self.id = id
The constructor of la clase de capa, toma como parámetros:
- dim: dimensiones de la matriz de peso,
- id: entero como id de la capa, [19659019] act, act_prime: la función de activación y su derivada,
- isoutputlayer: True si esta capa es la salida, False de lo contrario.
Inicializa los pesos aleatoriamente a números entre -1 y +1, y establece los diferentes variables que se utilizarán dentro del objeto.
El objeto de capa tiene tres métodos:
- adelante, para calcular la salida de la capa.
- hacia atrás, para propagar el error entre el objetivo y la salida de nuevo al trabajo de newtwork. 19659019] actualizar, para actualizar los pesos de acuerdo con un descenso de gradiente.
def forward (self, x): z = np.dot (x, self.weight) self.A = self.activation (z) self.dZ = self.activation_prime (z);
La función de avance, calcula y devuelve la salida de la capa, tomando la entrada x y calcula y Almacena la salida A = activación (WX). También calcula y almacena dZ, que es la derivada de la salida en relación con la entrada.
Las funciones hacia atrás toman dos parámetros, el objetivo y y el marcador derecho que es la capa (𝓁-1) asumiendo que el actual es 𝓁. [19659003] Calcula el error acumulativo delta que se está propagando desde la salida hacia el inicio de la red.
IMPORTANTE : un error común es pensar que Back Propagation es algún tipo de bucle invertido En el que la salida se inyecta de nuevo en la red. Así que en lugar de usar dZ = self.activation_prime (z); algunos usos self.activation_prime (A) .
Esto es incorrecto, simplemente porque lo que estamos tratando de hacer es averiguar cómo la salida A variaría con respecto a la entrada z. Esto significa calcular la derivada ∂a / ∂z = ∂g (z) / ∂z = g ‘(z) de acuerdo con la regla de la cadena.
Este error puede ser debido al hecho de que en el caso de la función de activación sigmoide a = (z) la derivada 𝜎 ‘(z) = 𝜎 (z) * (1-𝜎 (z)) = a * (1-a). Lo que da la ilusión de que la salida se inyecta en la red, mientras que la verdad es que estamos computando 𝜎 ‘(z).
def Back Propagation hacia atrás (self, y, rightLayer): if self.isoutputLayer: error = self.A - y self.delta = np.atleast_2d (error * self.dZ) else : self.delta = np.atleast_2d ( rightLayer.delta.dot (rightLayer.weight.T) * self.dZ) return self.delta
Lo que la función Back Propagation hacia atrás lo que hace es calcular y devolver el delta, según la fórmula:
Finalmente, la función de actualización usa el descenso de gradiente para actualizar los pesos de la capa actual.
def update (self, learning_rate, left_a): a = np.atleast_2d (left_a) d = np.atleast_2d (self.delta) ad = aTdot (d) self.weight - = learning_rate * ad
NeuralNetwor k clase
Como es de suponer que las capas forman una red, la clase NeuralNetwork se usa para organizar y coordinar las capas.
Su constructor toma la configuración de las capas, que es una matriz cuya longitud determina el número de capas. La red y cada elemento definen el número de nodos en la capa correspondiente.
Por ejemplo, [2, 4, 5, ] significa que la red tiene 4 capas con la capa de entrada que tiene 2 nodos, las siguientes capas ocultas tienen 4 y 5 nodos respectivamente y la capa de salida tiene 1 nodo. El segundo parámetro es el tipo de función de activación que se utilizará para todas las capas.
La función de ajuste es donde ocurre toda la capacitación. Comienza seleccionando una muestra de entrada, calcula el avance sobre todas las capas, luego calcula el error entre la salida de la red y el valor objetivo y propaga este error a la red llamando a la función Back Propagation hacia atrás de cada capa en orden inverso, comenzando por el último hasta el primero.
Finalmente, se llama a la función de actualización para que cada capa actualice los pesos.
Estos pasos se repiten varias veces determinadas por la época del parámetro.
Después de que el entrenamiento es completa, se puede llamar a la función de predicción para probar la entrada. La función de predicción es simplemente un avance de toda la red.
class NeuralNetwork: def __ init __ (self, layersDim ,ctivation = & # 039; tanh & # 039; ): if activación == & # 039; sigmoide & # 039; : self.activation = sigmo ] self.activation_prime = sigmoid_prime elif [1 0] ] activación == & # 039; tanh & # 039; : self.activation = tanh self.activation_prime = tanh_prime elif activación == y # 039; relu & # 039; : self.activation = relu self.activation_prime = relu_prime self.layers = [1945903214] i en ] rango (1, len (layersDim) - 1): dim = (layersDim [i - 1] + 1, layersDim [i] + 1) self.layers.append (Layer (dim, i, self. activación , self.activation_prime)) dim = (layersDim [i] + 1, layersDim [i + 1]) self.layers.append (Layer (dim, len (layersDim) - 1, self.activation, self. activation_prime, True ))
# tain the network def fit (self, X, y, learning_rate = 0.1, epochs = 10000): # Agregar columna de unidades a X # Esto es para agregar la unidad de polarización a la capa de entrada unos = np.atleast_2d (np.ones (X.shape [0])) X = np.concatenate (unos. T, X), eje = 1) para k en rango (épocas): i = np.random.randint (X.shape [0]) a = X [i] # computar la alimentación hacia adelante para l en el rango de (len (self.layers)): a = self.layers [l] .forward (a) # calcula Back Propagation delta = self.layers [-1] .backward (y [i] None ) for l in range (len (self.layers) - 2, -1, -1): delta = self.layers [l] .backward (delta, self.layers [l+1]) # actualizar pesos a = X [i] para capa en self.layers: layer.update (learning_rate, a) a = layer.A
# predict input def def (self, x): a = np.concatenate ((np.ones (1) .T, np.array (x)) axis = 0) para l en el rango de (0, len (self.layers)): a = self.layers [l] .forward (a) return a
Running The Network
Para ejecutar la red tomamos como ejemplo, la aproximación de la función Xor.
Probamos la configuración de varias redes, utilizando diferentes tasas de aprendizaje e iteraciones de época.
Los resultados se enumeran a continuación:
Resultado con tanh [0 0] [-0.00011187] [0 1] [ 0.98090146] [1 0] [1 1] [1 1]]
Resultado con sigmoide [0 0] [ 0.01958287] [0 1] [ 0.05132127] [1 0] [ 0.97699611]] [ 0.97699611] [ 0.97699611]] ] [0 0] [ 0.] [0 1] [ 1.] [1 0] [ 1.] [1 1] [ 4.23272528e-16]
Es aconsejable que pruebe una configuración diferente y vea por usted mismo cuál uno da los mejores y más estables resultados.
El código fuente
El código completo se puede descargar aquí .
Conclusión
La propagación de vuelta puede ser confusa y difícil de implementar. Puede que tengas la ilusión de que lo entiendes a través de la teoría, pero la verdad es que, al implementarlo, es fácil caer en muchas trampas. Debe ser paciente y persistente, ya que Back Propagation es una piedra angular de las Redes Neuronales.