Escrito por Xavier García Andrade en Planeta Chatbot. 

El Deep Learning es uno de los campos más prometedores dentro de la inteligencia artificial, después de los éxitos cosechados en áreas desde la visión por computador hasta el procesado de lenguaje natural. En este artículo, aprenderás a diseñar una red neuronal sin previo conocimiento sobre esta disciplina.

Requisitos:

  • Álgebra Lineal
  • Cálculo Básico
  • Programación

¿Qué es una red neuronal?

Una red neuronal (NN por sus siglas en inglés) se puede definir como un modelo de computación que descubre y aprende patrones en un conjunto de datos. Este modelo está conformado por diversas capas: Una capa de entrada, L capas ocultas y una capa de salida.

Red Neuronal con una única capa oculta.

Cada capa está formada por neuronas. Cada neurona presenta dos conjuntos de parámetros, pesos y sesgos, cuya función quedará más clara después de discutir nuestra implementación.

Cuándo los datos de entrada llegan a una neurona, se calcula una función lineal usando los pesos y los sesgos como parámetros. Es decir:

Dónde X, W y b son los datos de entrada, pesos y sesgos respectivamente. Si una NN sólamente realizase esta operación, sería análoga a una regresión lineal. Sin embargo, queremos que nuestra red aprenda patrones no lineales, por lo que es necesario aplicar una nueva función a Z. Esto se conoce como función de activación:

Este proceso se repite en cada neurona a través de la red, hasta llegar a la capa de salida.

Para comparar la salida de nuestra NN con los datos que queremos predecir Y, es necesario introducir una función de pérdida. Un ejemplo de función de pérdida (para ilustrar su propósito) es el error cuadrático medio (MSE):

Dónde y_sombrero es la salida de nuestra NN. La función de pérdida es una medida de cuánto difiere nuestra predicción de la realidad. El objetivo de la red neuronal es aproximar las predicciones a los datos Y al máximo mediante la minimización de la función de pérdida y el aprendizaje de los parámetros óptimos.

Los pesos y sesgos son aprendidos mediante el algoritmo Backpropagation (retropropagación). En matemáticas, minimizar una función significa calcular derivadas (gradientes) de la función. Si la función es una función compuesta, es posible utilizar la regla de la cadena para calcular las derivadas con respecto a los distintos parámetros.

Después de calcular los gradientes con respecto a los parámetros, los podemos actualizar usando el método del gradiente (Gradient Descent), un algoritmo de optimización iterativo para encontrar el mínimo de una función. Intuitivamente, el método del gradiente se puede entender con la siguiente visualizacion:


Método del gradiente encontrando el mínimo tras cuatro iteraciones.

Este proceso es repetido sistemáticamente hasta que nuestra red neuronal alcanza una precisión significativa. A partir de este punto, nos referiremos al cálculo de la salida de la red como Forward Step (“paso hacia delante”) y al cálculo de los gradientes como Backward Step (“paso hacia atrás”) . Para consolidar nuestros conocimientos de redes neuronales, vamos a construir una red desde cero usando el lenguaje de programación Julia, centrándonos en un ejemplo concreto.

Ejemplo:

Nuestro objetivo será enseñarle a la NN a realizar una clasificación binaria. Es decir, dados unos datos de entrada X, queremos que prediga unos datos (formado por ceros y unos). Para ello, construiremos una red neuronal con L capas ocultas, usando las funciones ReLu y sigmoide como funciones de activación.

En Julia, podemos definir una implementación vectorizada de ReLu y la función sigmoide:

function sigmoid(X)      sigma = 1 ./(1 .+ exp.(.-X))      return sigma , X   end  function relu(X)      rel = max.(0,X)      return rel , X  end

Enunciado este problema, explicaremos como implementar los pasos Forward y Backwards de forma que finalmente tendremos una red neuronal completamente funcional.

Inicialización de Parámetros:

Primero debemos iniciar los parámetros de la red neuronal. Es necesario que los pesos sean iniciados aleatoriamente para facilitar la ruptura de simetría. Ruptura de simetría es un concepto de las matemáticas que surge en los problemas de optimización. Recordando que nuestro objetivo final es optimizar una función, utilizando pesos diferentes tenemos más opciones de llegar al mínimo.

function init_param(layer_dimensions)        param = Dict()            for l=2:length(layer_dimensions)            param[string("W_" , string(l-1))] = rand(layer_dimensions[l] ,  				layer_dimensions[l-1])*0.1          param[string("b_" , string(l-1))] = zeros(layer_dimensions[l] , 1)            end        return param    end

Paso Forward:

El siguiente paso es calcular el paso Forward para las L capas de la red. Dividiremos el proceso en varias funciones auxiliares para entender mejor las diferentes operaciones. La parte lineal del proceso puede ser calculada mediante la fórmula siguiente:

function forward_linear(A , w , b)        Z = w*A .+ b      cache = (A , w , b)        return Z,cache    end

Es importante enfatizar que cada etapa será almacenada en una variable “cache” para después acelerar el paso Backwards.

Después de calcular la función lineal aplicaremos ReLu o la función sigmoide a la salida:

function calculate_activation_forward(A_pre , W , b , function_type)        if (function_type == "sigmoid")            Z , linear_step_cache = forward_linear(A_pre , W , b)          A , activation_step_cache = sigmoid(Z)        elseif (function_type == "relu")            Z , linear_step_cache = forward_linear(A_pre , W , b)          A , activation_step_cache = relu(Z)        end        cache = (linear_step_cache , activation_step_cache)      return A , cache    end

Para obtener la salida final de nuestro modelo, es necesario iterar sobre cada capa, de forma que la salida de una capa es la entrada de la siguiente.

function model_forward_step(X , params)        all_caches = []      A = X      L = length(params)/2        for l=1:L-1          A_pre = A          A , cache = calculate_activation_forward(A_pre , params[string("W_" , string(Int(l)))] , params[string("b_" , string(Int(l)))] , "relu")          push!(all_caches , cache)      end       A_l , cache = calculate_activation_forward(A , params[string("W_" , string(Int(L)))] , params[string("b_" , string(Int(L)))] , "sigmoid")      push!(all_caches , cache)          return A_l , all_caches     end

Una vez tenemos el resultado de la última capa, podemos calcular la función de pérdida, la cual determinará el problema de optimización que queremos resolver. Para este ejemplo, utilizaremos la entropía cruzada binaria:


Nuestro algoritmo de aprendizaje estará guiado por la minimización de la función de pérdida. Los parámetros se actualizarán de forma que J es minimizada.

function cost_function(AL , Y)            cost = -mean(Y.*log.(AL) + (1 .- Y).*log.(1 .- AL))        return cost     end

Paso Backward:

El algoritmo Backpropagation es habitualmente la parte más confusa a la hora de entrar una red neuronal. Lo dividiremos en varias funciones distintas para lograr una mayor comprensión de lo ocurre en realidad.

Recordando que la parte lineal para cada capa se puede calcular como:

Dada la derivada con respecto a Z (en adelante dZ), queremos calcular las derivadas con respecto a los parámetros y a la parte de activación: dW, db y dA:

Fórmulas para BackpropagationLos lectores duchos en cálculo pueden tratar de deducir estas fórmulas por su cuenta mediante el uso de la regla de la cadena. Utilizando estas fórmulas con los “caches” del paso Forward:

La derivada dZ se calcula de la siguiente forma:


function backward_linear_step(dZ , cache)        A_prev , W , b = cache        m = size(A_prev)[2]            dW = dZ * (A_prev')/m      db = sum(dZ , dims = 2)/m      dA_prev = (W')* dZ      return dW , db , dA_prev     end

Esto requiere computar la derivada con respecto a la función de activación g(Z). Escribiremos dos funciones en Julia para calcular versiones vectorizadas de ReLu y la función sigmoide:

function backward_relu(dA , cache_activation)      return dA.*(cache_activation.>0)  end     function backward_sigmoid(dA , cache_activation)      return dA.*(sigmoid(cache_activation)[1].*(1 .- sigmoid(cache_activation)[1]))  end

Combinando las derivadas de la parte de activación y la lineal en una función:

function backward_activation_step(dA , cache , activation)        linear_cache , cache_activation = cache      if (activation == "relu")            dZ = backward_relu(dA , cache_activation)          dW , db , dA_prev = backward_linear_step(dZ , linear_cache)        elseif (activation == "sigmoid")            dZ = backward_sigmoid(dA , cache_activation)          dW , db , dA_prev = backward_linear_step(dZ , linear_cache)        end         return dW , db , dA_prev    end

El último paso es calcular la derivada de la función de pérdida con respecto a la salida de la última capa. En este caso, es la derivada de la entropía cruzada binaria con respecto a la función sigmoide:


En cada iteración, debemos calcular los gradientes de cada parámetro en cada capa, empezando por la capa de salida. Los gradientes serán guardados en un diccionario que será usado para actualizar los parámetros.

function model_backwards_step(A_l , Y , caches)        grads = Dict()        L = length(caches)        m = size(A_l)[2]        Y = reshape(Y , size(A_l))      dA_l = (-(Y./A_l) .+ ((1 .- Y)./( 1 .- A_l)))      current_cache = caches[L]      grads[string("dW_" , string(L))] , grads[string("db_" , string(L))] , grads[string("dA_" , string(L-1))] = backward_activation_step(dA_l , current_cache , "sigmoid")      for l=reverse(0:L-2)          current_cache = caches[l+1]          grads[string("dW_" , string(l+1))] , grads[string("db_" , string(l+1))] , grads[string("dA_" , string(l))] = backward_activation_step(grads[string("dA_" , string(l+1))] , current_cache , "relu")        end         return grads     end

Los parámetros se actualizan usando el método del gradiente:

Donde alfa es la tasa de aprendizaje (learning rate), un hiperparámetro que debe ser seleccionado después de experimentar con distintos valores.

function update_param(parameters , grads , learning_rate)        L = Int(length(parameters)/2)        for l=0:(L-1)            parameters[string("W_" , string(l+1))] -= learning_rate.*grads[string("dW_" , string(l+1))]          parameters[string("b_",string(l+1))] -= learning_rate.*grads[string("db_",string(l+1))]        end         return parameters    end

Conclusión:

Juntando los distintos pasos podemos escribir la función train_nn para calcular los pasos Forward y Backward durante un número de iteraciones seleccionado, actualizando los parámetros en cada una. El número de iteraciones es otro hiperparámetro que podemos elegir de forma empírica.

function train_nn(layers_dimensions , X , Y , learning_rate , n_iter)        params = init_param(layers_dimensions)      costs = []      iters = []      accuracy = []      for i=1:n_iter          A_l , caches  = model_forward_step(X , params)          cost = cost_function(A_l , Y)          acc = check_accuracy(A_l , Y)          grads  = model_backwards_step(A_l , Y , caches)          params = update_param(params , grads , learning_rate)          println("Iteration ->" , i)          println("Cost ->" , cost)          println("Accuracy -> " , acc)          push!(iters , i)          push!(costs , cost)          push!(accuracy , acc)                end       plt = plot(iters , costs ,title =  "Cost Function vs Number of Iterations" , lab ="J")      xaxis!("N_Iterations")      yaxis!("J")      plt_2 = plot(iters , accuracy ,title =  "Accuracy vs Number of Iterations" , lab ="Acc" , color = :green)      xaxis!("N_Iterations")      yaxis!("Accuracy")      plot(plt , plt_2 , layout = (2,1))      savefig("cost_plot_rand.pdf")      return params , costs     end

Resolviendo nuestro problema de clasificación binaria, podemos visualizar la función de pérdida y la precisión con respecto al número de iteraciones para asegurarnos que nuestro algoritmo de aprendizaje está funcionando correctamente.

El código se puede encontrar en https://github.com/XabierGA/DNN_Julia. En próximos artículos, utilizaremos este modelo para tareas de clasificación de imágenes.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *