Um mergulho profundo na matemática por trás das redes neurais

Mistérios das Redes Neurais Parte I

Davi Candido
9 min readJan 7, 2019

De maneira geral, eu prefiro ler e aprender sobre tecnologia em inglês pelo simples fato de ter muito mais documentação de todos os assuntos e também de boa parte da terminologia utilizada ser nesse idioma. Porém o fato de ainda ter pouca coisa sobre redes neurais e inteligência artificial em português me incomoda um pouco. Durante um dia de estudos eu me deparei com um conteúdo muito bom e explicado de maneira bem “legível”, por assim dizer, e perguntei para o autor se poderia fazer uma tradução do mesmo para o nosso idioma. O conteúdo deste post é uma tradução do texto de Piotr Skalski, Deep Dive into Math Behind Deep Networks. Caso você também queira ver os códigos e análises feitas para a produção do artigo, basta acessar o repositório dele no Github. Divirta-se.

Nos dias de hoje, tendo à nossa disposição muitas bibliotecas especializadas e frameworks como Keras, TensorFlow ou PyTorch, nós não precisamos nos preocupar constantemente com o tamanho das nossas matrizes de pesos ou lembrar das fórmulas da derivada da função de ativação que optamos usar. Frequentemente, tudo que precisamos para criar uma rede neural, mesmo uma com uma estrutura complexa, são alguns imports e algumas linhas de código. Isto nos economiza horas de procura por bugs e erros e agiliza nosso trabalho. Entretanto, saber o que acontece por dentro da rede neural nos ajuda bastante com tarefas como escolha de arquitetura, ajuste de hiper-parâmetros e otimização.

Introdução

Para entender um pouco mais sobre como redes neurais funcionam, eu (o autor do post original) decidi gastar algum tempo neste verão e dar uma olhada na matemática que ocorre por baixo dos panos. Eu também decidi escrever um artigo, um pouco pra mim — pra organizar a informação recentemente aprendida, um pouco para os outros — para ajudá-los a entender esses difíceis conceitos. Eu tentarei ser o mais gentil possível com aqueles que se sentem menos confortáveis com álgebra e cálculo diferencial, mas como o título sugere, será um artigo com muita matemática.

Figura 1. Visualização do conjunto de treino.

Como exemplo, nós resolveremos o problema de classificação binária do conjunto apresentado na Figura 1. Pontos pertencentes a duas classes distintas formam círculos — este arranjo é inconveniente para muitos algoritmos de machine learning, mas uma pequena rede neural deveria funcionar bem. Para abordar este problema, usaremos uma rede neural com a estrutura mostrada na Figura 2. — cinco camadas totalmente conectadas, com diferentes números de unidades. Para as camadas escondidas usaremos ReLU como função de ativação, e Sigmoid como camada de saída. Isto é uma arquitetura relativamente simples, mas suficientemente complicada para ser um exemplo útil para as nossas deliberações.

Figura 2. Arquitetura da rede neural

Solução com Keras

Primeiro, vou mostrar uma solução usando umas mais populares bibliotecas de machine learning — KERAS.

E é isso. Como mencionado na introdução, alguns imports e algumas linhas de código são suficientes para criar e treinar um modelo que então consegue classificar as entradas do nosso conjunto de teste e com quase 100% de acurácia. Nossa tarefa se resume em providenciar os hiper-parâmetros (número de camadas, número de neurônios por camada, função de ativação e/ou número de épocas) de acordo com a arquitetura selecionada. Agora vamos olhar o que aconteceu nos bastidores. Ah… e uma visualização legal criada durante o processo de aprendizado, que espero que impeça você de pegar no sono.

Figura 3. Visualização das áreas qualificadas para cada classe durante o processo de treinamento

O que são as redes neurais?

Vamos começar respondendo esta questão chave: O que é uma rede neural? É um método, inspirado na biologia, de construir programas de computador que são capazes de aprender e independentemente encontrar conexões em dados. Como a Figura 2. mostra, redes são uma coleção de ‘neurônios’ arranjados em camadas, conectados de uma maneira que permite comunicação entre si.

Neurônio

Cada neurônio recebe um conjunto de valores x (numerados de 1 a n) como entrada e computa um valor previsto de ŷ (y-hat). O vetor x contém os valores de características em um dos m exemplos do conjunto de treino. Cada unidade tem o seu próprio conjunto de parâmetros, geralmente referido como w (vetor coluna com os pesos) e b (bias, ou viés), que mudam durante o processo de aprendizado. Em cada iteração, o neurônio calcula a média ponderada dos valores do vetor x, baseada no valor atual do vetor w e adiciona o bias. Por fim, o resultado deste cálculo é passado por uma função de ativação não-linear g. Vamos tratar um pouco das funções de ativação mais populares na parte seguinte do artigo.

Figura 4. Neurônio

Camada

Agora vamos considerar como os cálculos são feitas na camada inteira da rede neural. Utilizaremos nosso conhecimento sobre o que está acontecendo dentro de uma única unidade e vetorizar através da camada inteira para combinar esses cálculos na matriz de equações. Para unificar a notação, as equações serão escritas para a camada escolhida [l]. A propósito, subscrito i marca o índice do neurônio nesta camada.

Figura 5. Camada [l]

Mais uma observação importante: quando escrevemos os equações para uma única unidade, usamos x e ŷ (y-hat), o que eram respectivamente o vetor coluna de características e o valor previsto. Quando mudamos para a notação geral da camada, usamo o vetor a — mostrando a ativação da camada correspondente. O vetor x é então a ativação para a camada 0 — a camada de entrada. Cada neurônio na camada efetua um cálculo parecido de acordo com a seguintes equações:

Para clarificar um pouco, vamos escrever as equações para a camada 2 como exemplo:

Como você pode ver, para cada uma das camadas nós realizamos um número de operações similares. Usar um for-loop não é muito eficiente para este propósito, então usaremos vetorização. Primeiramente, ao empilhar horizontalmente vetores de pesos w (transpostos) nós temos a matriz W. Similarmente, empilharemos os viéses (bias) de cada neurônio na camada criando o vetor vertical b. Agora não há nada nos impedindo de criar uma matriz única com as equações que nos permitem realizar os cálculos para todos os neurônios da camada de uma única vez. Vamos escrever as dimensões das matrizes e vetores que usamos.

Vetorização com múltiplos exemplos

A equação que escrevemos até agora envolve somente um exemplo. Durante o processo de aprendizado de uma rede neural, você geralmente trabalha com um grande conjunto de dados, chegando até milhões de registros. O próximo passo então será a vetorização através de múltiplos exemplos. Vamos assumir então que nosso conjunto de dados tem m registros com nx características cada. Primeiramente, vamos colocar junto os vetores verticais x, a e z de cada camada criando as matrizes X, A e Z, respectivamente. Então reescrevemos a equação apresentada anteriormente, levando em conta as novas matrizes criadas.

O que é uma função de ativação e porquê precisamos dela?

Funções de ativação são um dos elementos chave de uma rede neural. Sem elas, nossa rede neural se tornaria uma combinação de funções lineares, sendo apenas uma função linear por si só. Nosso modelo teria uma expansividade limitada, não maior que uma regressão logística. O elemento de não-linearidade nos permite uma maior flexibilidade e a criação de funções complexas durante o processo de aprendizado. A função de ativação também tem um impacto significativo na velocidade de aprendizado, o que é um dos principais critérios para sua seleção. A Figura 6 mostra algumas das funções de ativação mais utilizadas. Atualmente, a mais popular para camadas escondidas é provavelmente a ReLU. Às vezes utiliza-se Sigmoid, especialmente na camada de saída, quando estamos lidando com classificação binária e queremos que os valores retornados do modelo estejam entre 0 e 1.

Figura 6. Diagramas das funções de ativação mais comuns e suas derivadas.

Função de perda

A fonte básica de informação sobre o progresso do processo de aprendizado é a função de perda (loss function), ou função de custo (cost function). De um modo geral, a função de perda é desenhada para mostrar o quão longe estamos da solução ‘ideal’. No nosso caso utilizamos entropia cruzada binária (binary cross-entropy), mas dependendo do problema que estamos lidando funções diferente podem ser aplicadas. A função usada por nós é descrita pela seguinte fórmula, e a mudança do seu valor durante o processo de aprendizado é visualizada na Figura 7. Ela mostra como em cada iteração o valor da função de perda decresce e a acurácia aumenta.

Figura 7. Variação dos valores de acurácia e perda durante o processo de aprendizado

Como as redes neurais aprendem?

O processo de aprendizado é a mudança dos valores nos parâmetros W e b onde a função de perda é minimizada. Para alcançar este objetivo, iremos precisar de ajuda do Cálculo e usar o método do gradiente para encontrar o mínimo de uma função. Em cada iteração nós calcularemos os valores das derivadas parciais da função de perda em relação cada parâmetro da nossa rede neural. Para aqueles menos familiares com este tipo de cálculo, só falarei que as derivadas tem a fantástica habilidade de descrever o declive de uma função. Graças a isso, sabemos como manipular variáveis para mover-se para baixo no gráfico. Com o objetivo de criar uma intuição em como o método do gradiente funciona (e impedir você de dormir novamente), preparei uma pequena visualização. Você pode ver como em cada época sucessiva nós estamos indo em direção ao mínimo. Em nossa rede neural funciona do mesmo jeito — o gradiente calculado em cada iteração nos mostra a direção em que devemos nos mover. A principal diferença é que em nossa rede de exemplo, temos muito mais parâmetros para manipular. Exatamente… Como calcular essas derivadas complexas?ç

Figura 8. Método do gradiente em ação

Retro propagação

Retro propagação (backpropagation) é um algoritmo que nos permite calcular um gradiente muito complicado, como o que nós precisamos. Os parâmetros da rede neural são ajustados de acordo com a seguinte fórmula.

Nas equações acima, α representa a taxa de aprendizado — um hiper parâmetro que permite você de controlar o valor de ajuste realizado. Escolher a taxa de aprendizado é crucial — se setarmos muito baixo, a rede neural aprenderá muito devagar, se setarmos um valor muito alto não alcançaremos o mínimo. dW e db são calculados utilizando a regra da cadeira, derivadas parciais da função de perda em relação a W e b. O tamanho de dW e db são os mesmos de W e b, respectivamente. A Figura 9 mostra a sequência de operações dentro de uma rede neural. Podemos ver claramente como a propagação para a frente e para trás trabalham juntas para otimizar a função de perda.

Figura 9. Forward e Backward Propagation

Conclusão

Como mencionei antes, este artigo é uma tradução de um conteúdo que julguei muito bom de se ter em mãos pra iniciar na matemática por trás das redes neurais. Ele me ajudou bastante a entender um pouco melhor esses conceitos que estava aprendendo na especialização de Deep Learning do Coursera (o que eu recomendo bastante para quem está afim de aprender mais sobre o assunto).

Até a próxima!

--

--

Davi Candido

Oceanographer and Programmer / Photographer and Diver / Music and Sunsets / Coffee and Code / https://www.linkedin.com/in/davivc/