Redes Neurais com TensorFlow: primeiros passos

Tiago M. Leite
Ensina.AI
Published in
7 min readAug 16, 2018

Em meu último artigo foi apresentada uma fundamentação teórica sobre como funciona o modelo computacional de um neurônio e como ele pode ser combinado para formar uma Rede Neural Perceptron Multicamadas (MLP), bem como funciona o algoritmo backpropagation, utilizado durante o treinamento de tal rede. Você pode consultar o texto completo no seguinte link:

Hoje vamos construir uma rede MLP para identificação em imagens de dígitos manuscritos. Para isso, utilizaremos a linguagem Python (versão 3.5.2) e a biblioteca TensorFlow (versão 1.10.0).

O MNIST é um dataset muito utilizado em projetos iniciais de machine learning, em especial no âmbito de classificação de imagens. É composto de 70.000 pequenas imagens, de tamanho 28x28 pixels, em escala de cinza, contendo cada uma delas a figura de um dígito (de 0 a 9) desenhado. Seguem alguns exemplos:

TensorFlow é uma poderosa biblioteca de código aberto criada por pesquisadores do Google, voltada ao desenvolvimento de modelos de aprendizado de máquina, especialmente em deep learning. A instalação pode ser realizada através do site oficial: https://www.tensorflow.org/install/.

Implementaremos inicialmente uma rede neural com somente uma camada, com dez neurônios, cada um representando um dos dez possíveis dígitos. A entrada da rede será um vetor de tamanho 784, obtido a partir da conversão da imagem, originalmente uma matriz de tamanho 28x28, para um vetor de 784 posições. A figura a seguir ilustra a arquitetura da rede.

O trecho inicial de código a seguir realiza o import do TensorFlow e o download do MNIST, cujas imagens são divididas em conjunto de treinamento (55 mil), validação (5 mil) e teste (10 mil).

import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data

mnist = input_data.read_data_sets("MNIST_data", one_hot=True)

A seguir definiremos alguma variáveis placeholders que serão utilizadas; são assim chamadas pois seus reais valores serão informados mais tarde no momento do treinamento da rede. Assim, x representa um conjunto com um certo número, a ser posteriormente definido, de imagens linearizadas, cada uma de tamanho 784, como já vimos. Por sua vez, y indica a classe a que pertence cada imagem, aqui na forma de um vetor one-hot encoded, isto é, um vetor com valor 1 na posição relativa à classe a que pertence a amostra da imagem, e zeros nas demais posições. Esses são os formatos em que as imagens e seus rótulos são obtidos através do download que realizamos no trecho anterior.

x = tf.placeholder(dtype=tf.float32, shape=[None, 784])
y = tf.placeholder(dtype=tf.float32, shape=[None, 10])

Agora definimos as variáveis referentes aos pesos e biases da nossa rede. Como temos apenas uma camada de neurônios, todos os pesos podem ser representados por uma única matriz W com 784 linhas e 10 colunas, com cada coluna representando um neurônio; note que cada uma das linhas relaciona-se a cada pixel da imagem que serve dado de entrada à rede. Os pesos são iniciados com valores aleatórios seguindo uma distribuição normal e os biases com valores todos iguais (0.1).

w = tf.Variable(tf.random_normal(shape=[784, 10], stddev=0.1))
b = tf.Variable(tf.constant(0.1, shape=[10]))

As operações que cada neurônio realiza, multiplicando seus pesos pelos valores de entrada, podem ser representadas pela multiplicação de matrizes a seguir. Na prática, em vez de empregarmos uma única imagem de entrada por vez, utilizamos um pequeno lote (mini-batch), por isso a matriz X apresenta aqui 100 linhas, cada uma delas representando uma das 100 imagens do lote.

A matriz b representa os biases de cada neurônio; repare que ela deve apresentar apenas uma linha e, como seu formato não bate com o do produto X.W, o que ocorre aqui é a repetição das suas linhas para se ajustar ao tamanho. Escrevendo isso em código, temos:

z = tf.matmul(x, w) + b

Ou seja, atribuímos à variável z o resultado da multiplicação. Resta agora aplicarmos a função de ativação a esse resultado, para termos a saída final da rede. Softmax é uma típica função de ativação utilizada na camada final da rede pois normaliza as saídas dos neurônios no intervalo [0, 1], cuja somatória vale 1, permitindo interpretá-las como probabilidades.

y_ = tf.nn.softmax(z)

A arquitetura da rede está pronta. Precisamos agora estabelecer como a rede será treinada. Começaremos por definir a função de erro (ou custo, perda), a ser utilizada para medir o quanto nossa rede está errando ao fazer suas classificações. Que função utilizar? Existem várias que funcionam bem, mas a que tem sido mais utilizada para problemas de classificação é a cross-entropy. Sendo y a saída esperada e ŷ a obtida pela rede, ambas de tamanho N, temos:

O termo ε é um valor constante pequeno para evitar a ocorrência de log(0).

error = tf.reduce_sum(-(y*tf.log(y_ + 0.00001)))

Para calcularmos os acertos das previsões realizadas, primeiro comparamos os índices do elemento de maior valor do vetor de saída da rede e do vetor de saída esperada, o que retorna uma lista de valores booleanos, que são então convertidos para valores em ponto flutuante. Por exemplo, [False, True, True, False] se torna [0, 1, 1, 0]. Reduzimos esse vetor à sua média, o que nos fornece a taxa de acertos para o lote ou conjunto de imagens usado.

prediction = tf.equal(tf.argmax(y, axis=1), tf.argmax(y_, axis=1))
acc = tf.reduce_mean(tf.cast(prediction, tf.float32))

Para que ocorra a atualização dos pesos da rede no sentido de minimizar a função de erro, o TensorFlow fornece diversos otimizadores. São eles que calculam os gradientes da função de erro em relação a todos os parâmetros da rede (pesos e biases), fornecendo os valores utilizados para atualizá-los. Um dos mais simples otimizadores é o GradientDescentOptimizer, que é simplesmente a aplicação direta do algoritmo backpropagation, enquanto outros otimizadores, como AdagradOptimizer, AdadeltaOptimizer e AdamOptimizer, são suas variações, mais refinadas, que acrescentam outros termos à fórmula de atualização dos pesos, como taxa de aprendizado variável e momento dos gradientes (cujos detalhes não serão abordados neste texto), com intenção de acelerar a convergência e fugir de mínimos locais. No caso deste exemplo, todos terão aproximadamente o mesmo desempenho no que diz respeito à taxa de acertos no conjunto de teste. O processo do cálculo dos gradientes e atualização dos parâmetros ocorre através do método minimize do nosso otimizador.

optimizer = tf.train.GradientDescentOptimizer(learning_rate=0.005)
train_step = optimizer.minimize(error)

Neste instante terminamos de definir todas as variáveis e as operações a serem realizadas, que são internamente configuradas no TensorFlow na forma de um grafo. Entretanto, até agora ainda não executamos de fato nenhuma operação. Para tanto, criamos uma sessão, que é o ambiente onde as operações serão realizadas, e então realizamos a inicialização de todas as variáveis utilizadas em nosso modelo:

with tf.Session() as sess:
sess.run(tf.global_variables_initializer())

Agora criamos um loop no qual ocorrerão as várias etapas do treinamento da rede. Em cada uma, coletamos um novo lote de 100 imagens aleatórias do conjunto de treinamento, bem como seus rótulos, em x_batch e y_batch, respectivamente; então rodamos um passo de treinamento na seção criada, fornecendo os valores aos placeholders, que havíamos criado anteriormente, através do dicionário feed_dict.

    for k in range(10000):
x_batch, y_batch = mnist.train.next_batch(100)
step, e = sess.run([train_step, error],
feed_dict={x: x_batch, y: y_batch})
if k % 100 == 0:
print("Step", k, 'Error:', e)

Em cada passo do treinamento, os gradientes são calculados com base numa média dos gradientes das amostras contidas no lote sendo utilizado, num processo denominado Gradiente Descendente Estocástico. Isso faz com que o caminho percorrido pelos pesos e biases ao longo da superfície da função de erro seja mais suave e estável em relação ao que ocorreria se fosse utilizada apenas uma imagem em cada passo; além disso, utilizar lotes de imagens favorece a eficiência computacional ao se utilizarem GPUs no treinamento.

Por fim, calculamos a acurácia da rede, passando as todas as imagens e rótulos do conjunto de teste, através do mencionado dicionário.

accuracy = sess.run(acc, feed_dict={x: mnist.test.images,
y: mnist.test.labels})
print("Test accuracy:", accuracy)

Tal valor deverá estar em torno de 92%, o que pode ser considerado razoável dada a simplicidade do nosso modelo. Podemos criar uma rede mais profunda, com 2 camadas intermediárias, com 200 e 100 neurônios, além da camada de saída (com 10 neurônios), conforme a imagem e o código a seguir:

w1 = tf.Variable(tf.random_normal(shape=[784, 200], stddev=0.1))
b1 = tf.Variable(tf.constant(0.1, shape=[200]))

w2 = tf.Variable(tf.random_normal(shape=[200, 100], stddev=0.1))
b2 = tf.Variable(tf.constant(0.1, shape=[100]))

w3 = tf.Variable(tf.random_normal(shape=[100, 10], stddev=0.1))
b3 = tf.Variable(tf.constant(0.1, shape=[10]))

# model:
z1 = tf.matmul(x, w1) + b1
y1 = tf.nn.relu(z1)

z2 = tf.matmul(y1, w2) + b2
y2 = tf.nn.relu(z2)

z3 = tf.matmul(y2, w3) + b3
y_ = tf.nn.softmax(z3)

Note que agora foi utilizada a função de ativação relu nas camadas intermediárias, sendo a softmax empregada apenas na última. Com esse modelo já é possível alcançar acurácia de cerca de 98% no conjunto de teste. O código final pode ser encontrado aqui: https://github.com/TiagoLeite/MediumCodes/blob/master/mnist/main.py

A partir desse ponto, adicionar mais camadas, ou mais neurônios por camada, não fará muita diferença na taxa de acertos de rede, que permanecerá em torno daquele valor. Note que, ao transformarmos uma imagem de duas dimensões em um vetor de apenas uma dimensão, estamos jogando fora informações de natureza espacial, destruindo relações entre pixels vizinhos, prejudicando o entendimento de padrões relacionados a formas, o que dessa forma compromete a inteligência do nosso modelo. As Redes Convolucionais são modelos deep learning que tentam resolver esse problema, sendo o estado-da-arte para classificação de imagens, mas isso é assunto para outro episódio…

--

--

Tiago M. Leite
Ensina.AI

Computer Science Student | Deep Learning Enthusiast