Como construir uma Rede Neural do zero com Javascript

Original article was published by Danilo Silva on Deep Learning on Medium


Como construir uma Rede Neural do zero com Javascript

O objetivo deste post é dar uma visão geral do que são e como funcionam as redes neurais artificiais e apresentar uma aplicação do zero (sem utilização de qualquer framework) em linguagem Javascript de uma rede neural bastante simples que será capaz de aprender a função XOR (ou exclusivo).

Não existe nenhuma pretensão de apresentar um código que se equipare em termos de usabilidade e performance aos diversos frameworks já existentes, como por exemplo tensorflow.js, ml5.js, etc. Queremos apenas demonstrar que é possível construir uma rede neural do zero com Javascript, e o mais importante, ao construir nossa própria rede neural, aprendermos como estas redes funcionam em seus detalhes, por “debaixo dos panos”.

O que é uma Rede Neural Artificial?

Uma Rede Neural Artificial é um software com capacidade de aprender e tem seu funcionamento inspirado na forma como os neurônios biológicos operam no cérebro.

Os neurônios biológicos são conectados entre si através de sinapses recebendo cargas elétricas como entrada e “disparando” cargas elétricas como saída para outros neurônios.

Os neurônios artificiais podem ser pensados como algo que armazena um valor numérico entre 0 e 1. Eles recebem como entrada as saídas provenientes de neurônios da camada anterior e alimentam os neurônios da próxima camada com sua saída.

As diferentes camadas da Rede Neural se conectam através de pesos que podem ser pensados como sendo as sinapses no modelo biológico.

Uma Rede Neural também pode ser pensada como sendo uma complexa cadeia de funções que recebe uma entrada e produz uma saída.

Suponha que tenhamos 3 funções, f(1), f(2) e f(3), então temos que nossa rede é f(3)(f(2)(f(1)(x))). Sendo cada função neste caso uma camada da rede, f(1) a primeira camada, f(2) a segunda e assim por diante.

O propósito das Redes Neurais Artificiais é aprender os parâmetros (pesos e viés) desta complexa cadeia de funções, através de aprendizado supervisionado. Ou seja, dada uma função f(x) = y, que nos permite criar um classificador que mapeia uma entrada x para uma saída y, desejamos aproximar uma função f*(x; θ) = y, onde θ são os parâmetros a serem aprendidos, ou seja, f*(x; w, b), onde w = pesos e b = viés.

Figura 1: Representação de uma Rede Neural

Na figura acima temos a representação de uma Rede Neural com 3 entradas uma camada “escondida” com 2 neurônios e a sua saída. Note que a saída de cada neurônio de uma camada se conecta à todos os neurônios da próxima camada através dos pesos.

Como uma Rede Neural aprende?

O aprendizado da Rede Neural se dá da seguinte maneira:

Primeiramente, precisamos de um conjunto de dados para treinar a nossa rede e para cada um dos exemplos deste conjunto precisamos saber qual o valor de saída desejado, nosso label.

Começamos atribuindo pesos aleatórios com valores entre 0 e 1 entre cada uma das entradas da rede e cada neurônio da camada seguinte. No caso do exemplo dado na imagem acima temos uma matriz de pesos de 3 linhas por 2 colunas.

Nós então aplicamos o produto escalar da matriz de pesos pelo vetor de entradas e aplicamos uma função não linear ao resultado deste produto, em nosso caso, a função sigmóide, que faz com que nossa saída fique “espremida” entre 0 e 1, quanto mais negativo o número mais próximo de 0, quanto maior o número, mais próximo de 1.

Figura 2: Função Sigmóide

O processo se repete até que cheguemos à ultima camada, ou seja, até a saída.

Nós então calculamos o erro, ou seja, o quão longe a saída produzida (y’) está da saída desejada (y), subtraindo a saída produzida da saída desejada (y- y’) .

Após o cálculo do erro nós aplicamos o algoritmo de backpropagation para atualização dos nossos pesos.

O algoritmo de backpropagation funciona da seguinte maneira:

Após calcularmos o erro, nós calculamos o gradiente descendente.

O gradiente descendente é obtido da seguinte maneira: Primeiro obtemos o valor da derivada da função que produziu a saída, neste caso a derivada da função sigmóide e então multiplicamos o erro por esta derivada, o resultado desta multiplicação representa o quanto precisamos ajustar nossos pesos, repetimos então este processo para cada uma das camadas anteriores da rede, de “trás para frente” dai o nome backpropagation (algo como “propagação para trás”).

O objetivo é repetirmos todo este processo até que consigamos diminuir o valor do erro o máximo possível, e desta forma, obtermos um modelo (valores dos pesos e demais parâmetros) que conseguirá prever com grande confiabilidade valores de entrada que não faziam parte do conjunto de treinamento.

O Código

Abaixo apresentamos o código fonte de uma rede neural em Javascript, o objetivo desta rede é aprender a função XOR (ou exclusivo), a saída deve ser 1 se uma e somente uma das duas entradas for igual a 1.

[0,0] = 0

[0,1] = 1

[1,0] = 1

[1,1] = 0

Figura 3: Valores da saída após o treinamento da rede

Na linha 46 definimos nossa matriz de entrada que será utilizada para o treinamento da rede.

Na linha 52 definimos nossos labels, ou seja, quais os valores esperados como saída para cada um dos exemplos de nossa matriz de entrada.

Na linha 53 nós geramos os pesos iniciais que conectam a camada de entrada à camada escondida, neste caso, uma matriz de 2 linhas por 16 colunas cada, isto significa que nossa camada escondida possui 16 neurônios.

Na linha 54 nós geramos os pesos iniciais que conectam a camada escondida à saída da rede.

Na linha 55 definimos a camada de saída com valor inicial nulo (para que possamos referencia-la após o loop de treinamento).

Na linha 57 definimos um loop de 10000 iterações (epochs), ou seja, nossa rede será treinada com o conjunto de entradas 10000 vezes.

Na linha 58 calculamos a saída da camada de entrada.

Na linha 59 calculamos a saída da camada escondida.

Na linha 60 calculamos o erro, ou seja, o quão longe a saída calculada ficou da saída esperada (labels).

Na linha 61 calculamos a derivada da saída da camada escondida.

Na linha 62 calculamos a derivada da saída da camada de entrada.

Na linha 63 calculamos o valor a ser ajustado para os pesos da camada escondida.

Na linha 64 calculamos o valor a ser ajustado para os pesos da camada de entrada.

Na linha 65 ajustamos os pesos da camada escondida.

Na linha 66 ajustamos os pesos da camada de entrada.

Na linha 69 mostramos o valor da matriz de saída após o treinamento.

O código pode ser obtido em: https://github.com/danilorenatosilva/rede-neural-javascript