Introdução a Deep Learning - Redes Neurais e TensorFlow

“Acredito que o desenvolvimento pleno da inteligência artificial poderia significar o fim da raça humana.”
- Stephen Hawking.

Você alguma vez já imaginou máquinas que são capazes de pensar? Já imaginou como é possível um computador aprender sobre o mundo? Esses conceitos não foram introduzidos recentemente, pelo contrário, eles já estão aí há um bom tempo. Em 1989, um projeto chamado Cyc, tinha como objetivo ensinar para um computador sobre o mundo, através de regras de instruções. As regras eram inseridas por uma equipe de supervisores humanos. Entretanto, Cyc falhou para entender alguns padrões, isso se deve a pouca quantidade de dados na época e não pode evoluir por conta própria.

Quando ficou claro que estaríamos longe de construir algo incrivelmente complexo utilizando redes neurais, os financiamentos ligados a isso desapareceram. Esse período ficou conhecido como o Inverno da Inteligência Artificial. Por volta de 1991, novas estruturas de redes neurais e melhores técnicas de treino foram construídas, isso ajudou a renovar os interesses na área.

1. O cérebro humano

Vamos pensar no funcionamento do nosso cérebro, especificamente em como os neurônios trocam informações entre si.

Neurônio é uma célula nervosa composta por um corpo celular contendo o núcleo e diversas ramificações que são chamadas de dendritos e uma longa extensão chamada axônio. Os neurônios recebem impulsos elétricos por meio das sinapses, que são conectadas nos dendritos de outros neurônios.

Troca de informações entre neurônios
Olhar para a arquitetura do cérebro como uma inspiração para construir uma máquina inteligente é o caminho mais óbvio para alcançar tal resultado. Essa foi a ideia principal para criar redes neurais artificiais.

Deep Learning não tenta criar uma cópia do cérebro humano, essa tarefa é responsabilidade da Neurociência computacional. Entretanto, usa o conceito de compartilhamento de informação através de uma rede neural para resolver problemas complexos. Devemos lembrar que o cérebro humano é o sistema mais inteligente que existe, a eficiência energética do cérebro é de aproximadamente 10–16 joules (J) por operação por segundo, enquanto que a eficiência energética do melhor computador atualmente é de cerca de 10–6 joules (J) por operação por segundo.

A maior dificuldade em aplicações de Inteligência Artificial utilizada no mundo real é a variação de dados que podemos observar. Por exemplo, suponha que temos que criar uma aplicação para identificar a cor de um carro, durante o dia é possível perceber que a cor do carro é vermelho, porém, durante a noite, esse pixel se assemelha muito com a cor preta.

Deep Learning tenta resolver essa questão quebrando a aplicação em diversas etapas mais simples. Isso permite que o computador construa conceitos complexos a partir de conceitos mais elementares.

2. Redes Neurais

Entrando no campo da computação, a implementação de redes neurais segue esse mesmo conceito de compartilhamento de informações que os neurônios utilizam.

Uma rede neural é um processador paralelamente distribuído constituído de unidades de processamento simples. Essas unidades armazenam conhecimentos experimentais. O processo para realizar o aprendizado é por meio de um algoritmo de Machine Learning, cuja função é modificar os pesos da rede para alcançar um objetivo final.

2.1. Perceptron

O Perceptron é uma das arquiteturas mais simples de redes neurais usada para a classificação de padrões que são separados linearmente. Cada entrada está conectada a um peso sináptico e um bias(x = 1). Hoje não é muito utilizado, já que existem outras arquiteturas mais eficientes, mas é uma excelente forma de entendermos o funcionamento matemático por trás de uma rede neural.

Representação do Perceptron

Então, como um Perceptron é treinado? No exemplo acima, o Perceptron possui as entradas(x) com os pesos associados(w). A saída é determinada pelo seguinte par de equações:

É realizado uma soma ponderada da multiplicação dos pesos sinápticos pelo valor de entrada.

Logo em seguida, a função de ativação é aplicada, determinando o valor de saída.

O valor de cada neurônio de saída é linear, por conta disso, a arquitetura de um Perceptron é incapaz de aprender padrões complexos. Se precisarmos resolver problemas como o XOR(veremos mais a frente), teremos que montar uma rede neural mais robusta.

2.2. Multi-Layer Perceptron

Pense em MLP como um Perceptron, porém mais desenvolvido e com maior número de neurônios.

MLPs são constituídos por três importantes propriedades:

Input Layer: Entrada dos dados. Fornecem os elementos iniciais da rede.

Hidden Layer: Responsável pelo processamento dos dados. São chamados assim, pois não tem contato com o mundo externo. A função dos neurônios ocultos é intervir entre a camada de entrada e saída. Adicionando-se uma ou mais camadas de neurônios ocultos, tornamos a rede capaz de extrair estatísticas mais elaboradas.

Output Layer: Valor final.

Esses mesmos conceitos se aplicam ao Perceptron, porém sem o as camadas escondidas.

Os nós de entrada da rede fornecem informações como entrada para a segunda camada(camada oculta). Os sinais de saída dos neurônios ocultos são utilizados como entradas para a terceira camada, e assim por diante.

O número de neurônios de entrada e saída dependem do tipo de tarefa que está sendo realizada. Para as camadas ocultas, é possível criar quantas você desejar. Uma prática comum é declarar todas as camadas ocultas com o mesmo número de neurônios. Entretanto, encontrar o número perfeito de camadas ocultas é realmente um desafio.

MLPs são comumente usadas para problemas de classificação, com cada neurônio de saída representando uma classe binária diferente.

Exemplo de classificação com rede neural

Note que o fluxo de sinal segue em uma única direção, portanto, essa arquitetura é um exemplo de feedforward neural network.

Um fato importante é que existe uma diferença entre Redes Neurais Profundas e MLPs. Redes Neurais Profundas possuem duas ou mais camadas escondidas.

3. Interpretação do conhecimento por uma rede neural

Uma função importante que a rede neural deve desempenhar é de reconhecer os padrões de um modelo do ambiente no qual ela está trabalhando. Este conhecimento do mundo é representado pelo o que é e o que deixou de ser.

Na maioria dos casos, lidamos com aprendizagem supervisionada, no qual uma entrada é associada a uma saída. Um conjunto de pares de entrada e saída, com cada par associado, é mencionado como um conjunto de dados de treinamento. Para essa ideia ficar mais clara, considere o reconhecimento de dígitos: MNIST. Neste desafio, a entrada consiste em pixels, com cada imagem representando um número de 0 a 10. A rede neural deve receber esses pixels e apresentar uma saída correspondente ao dígito. Os dados de treino são uma grande variedade de dígitos que são representados em uma situação do mundo real.

Classificação de dígitos com redes neurais

A questão da interpretação do conhecimento em uma rede neural está ligada a sua arquitetura. Atualmente, não existe uma explicação em como encontrar a arquitetura certa que deva interagir com o ambiente. A única maneira para encontrar a combinação perfeita é através de tentativas.

4. Como treinar uma rede neural?

Grande parte das pesquisas em redes neurais estão focadas em sua habilidade de aprender a partir do seu ambiente e de melhorar seus resultados. Uma rede neural aprende sobre a tarefa que está lidando através de um processo interativo de ajustes de pesos sinápticos.

De acordo com Mendel e McClaren, definimos o aprendizado de uma rede neural como:

Aprendizagem é um processo pelo qual os parâmetros livres de uma rede neural são adaptados através de um processo de estimulação pelo ambiente no qual a rede está inserida. O tipo de aprendizagem é determinado pela maneira pela qual a modificação dos parâmetros ocorre.

Imagine uma rede neural como uma criança que está aprendendo a andar. Durante o começo, a criança terá muita dificuldade para se equilibrar e manter-se em pé, mas com treinamento, cada vez mais essa tarefa deixará de ser complexa e ficará automática com o passar do tempo.

Essa é a ideia principal de uma rede neural, equilibrar seus pesos sinápticos e chegar ao valor final mais próximo possível do valor real.

4.1. Função de custo

Sempre que vamos treinar uma rede neural, é importante escolher uma função de custo que se adeque ao modelo. A função de custo nos diz como está a taxa de erro da rede neural.

Em grande parte dos problemas envolvendo redes neurais, é comum o usar cross-entopy. Além dessa, essas são algumas das funções de custos mais utilizadas:

  • MSE;
  • MAE;
  • Hinge;

4.2. Treinamento

Quando começamos do zero nossa rede neural, os pesos sinápticos são inicializados aleatoriamente. Durante o processo de treino, começamos com um desempenho ruim e queremos terminar com uma alta acurácia. Nossa função de custo no começo irá apresentar valores altos, que significa que estamos com um número alto de erros e no final, essa quantidade de erros irá diminuir ao passo que treinamos nossa rede.

Para diminuir o valor da função de custo, introduzimos os algoritmos de otimização. Existe uma variedade de algoritmos otimizadores, acredito que você já ouviu falar de Gradient Descent. Entretanto, vamos focar apenas em Stochastic Gradient Descent — Excelente algoritmo para melhorar os erros em nossa rede, baseia-se em pegar uma instância aleatória nos dados de treino a cada interação e calcular o gradiente baseado apenas nessas instâncias. Isso torna o algoritmo muito rápido, já que manipula pouca quantidade de dados em cada interação e é uma ótima escolha para tratar grandes quantidades dados.

Trajetória de diferentes algoritmos de otimização

4.3. Backpropagation

O algoritmo de propagação de volta permite que as informações de erro da função sejam propagadas de volta por toda rede, fazendo os pesos sinápticos serem atualizados de acordo com a contribuição com o erro. Isso possibilita o cálculo do gradiente.

Treinamento em uma rede neural — Diminuição da função de custo

Entretanto, o funcionamento do algoritmo de propagação de volta é considerado por muitos pesquisadores como uma caixa preta.

5. Função de ativação

Basicamente, a função de ativação é um neurônio adicionado no final da rede usado para determinar a saída.

Em geral, a função de ativação mapeia os valores resultantes no intervalo desejado, como entre 0 a 1 ou -1 a 1 etc. (dependendo da escolha da função de ativação).
Diferentes tipos de função de ativação

5.1 Qual função de ativação usar?

Na maioria dos casos, é comum ver o uso da função de ativação ReLu, mas não fique limitado apenas nela. Quando você conhece o problema que está lidando, escolha uma função de ativação que possua as características apropriadas ao seu modelo. Por exemplo, é comum usar a função de ativação sigmoid para problemas de classificação e para determinar a probabilidade de uma certa classe, é frequente o uso da função softmax.

Uma curiosidade sobre como a função de ativação sigmoid ficou popular, se baseia no fato de que algumas pesquisas científicas mostram que os neurônios biológicos utilizam essa função de ativação, entretanto, funções de ativação como ReLu mostram um desempenho em velocidade melhor em redes neurais profundas.

6. Problema XOR

Para ficar claro o uso de redes neurais, vamos realizar uma aplicação em um problema que por muito tempo não foi solucionado usando Deep Learning. A função XOR é uma operação usando dois valores binários(0 e 1).

Tabela — função XOR

O objetivo é construir uma rede neural capaz de retornar o valor de saída correspondente em binário. Vamos realizar essa tarefa usando o TensorFlow.

Show me the code!

Vamos começar importando as bibliotecas necessárias.

import numpy as np
import tensorflow as tf

Precisamos passar a tabela contendo os valores de entrada e saída para o código.

binary_i = np.array([[0,0], [0,1], [1,0], [1,1]])
binary_o = np.array([[0], [1], [1], [0]])

Temos que criar dois tensores, que serão alimentados durante o treino com os valores de entrada e saída.

X = tf.placeholder(tf.float32, shape=(None, 2), name="X")
y = tf.placeholder(tf.float32, shape=(None, 1), name="y")

Podemos agora definir a estrutura da nossa rede neural.

n_inputs = 2
n_hidden1 = 3
n_outputs = 1
hidden1 = tf.layers.dense(X, n_hidden1, name="hidden1-new",
activation=tf.nn.sigmoid)
logits = tf.layers.dense(hidden1, n_outputs, name="outputs-new")

Nossa rede vai possuir a seguinte disposição:

Definimos a função sigmoide como ativação.

Acredito que o treinamento da rede neural é um dos passos mais importantes, portanto, vamos definir qual algoritmo será usado.

eta = 0.1
n_epochs = 500
cost = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits=logits, labels=y))
#train = tf.train.AdamOptimizer(eta).minimize(cost)
optimizer = tf.train.AdamOptimizer(eta)
training_op = optimizer.minimize(cost)

Perceba que, para a função de custo, definimos a MSE e para treinamento, estabelecemos o algoritmo Adam Optimization.

Perfeito! Temos tudo pronto para o nosso modelo. Agora podemos realizar o treinamento da rede.

pred = tf.nn.sigmoid(logits)
init = tf.global_variables_initializer()
with tf.Session() as sess:
sess.run(init)
for i in range(n_epochs):
sess.run(training_op, feed_dict={X: binary_i, y: binary_o})
print("Treino completo!")

prediction = sess.run(pred, feed_dict={X: binary_i})
print("Porcentagem: ")
print(prediction)
print("Arredondando")
print(np.round(prediction))

O mais incrível dessa aplicação é que em poucas linhas de código, definimos os dados que iríamos trabalhar, a estrutura da rede neural e o algoritmo de treinamento. Tudo isso graças ao TensorFlow.

7. Conclusão

Essa abordagem teórica e prática foi apenas uma introdução sobre Deep Learning. É possível realizar tarefas mais complexas utilizando esses conceitos. Existem diversos outros campos dentro de Deep Learning para trabalhar com atividades específicas.

Deep Learning está sendo uma promessa para o futuro e é um dos campos de Inteligência Artificial capaz de realizar performances em problemas do mundo real.

8. Referências

Deep Learning - Ian Goodfellow and Yoshua Bengio and Aaron Courville

Hands-On Machine Learning with Scikit-Learn & TensorFlow - Aurélien Géron

Redes Neurais - Simon Haykin