com Scikit-Learn
Nesse tutorial vamos trabalhar alguns conceitos básicos sobre Redes Neurais. Por ser um tutorial mais básico vou utilizar o próprio scikit-learn. Um estudo mais aprofundado das redes neurais será colocado juntamente com o material do Tensorflow.
As redes neurais vem de um desejo de criar máquinas que de alguma forma "imitem" o comportamento humano. Melhor ainda, "imitar" um comportamento que ocorre de forma natural no cérebro humano. Nós humanos conseguimos realizar tarefas que para nós são simples e imediatas, mas que ao passa-las para uma máquina não se tornam tão simples. O desenvolvimento das redes neurais parte do princípio de construir um mecanismo que possa de alguma forma executar as tarefas realizadas pelo cérebro humano. Para entender como as redes neurais funcionam, é preciso entender como funciona o sistema nervoso humano.
A imagem a seguir mostra o principal componente do sistema nervoso humano: os neurônios. É através deles que o cérebro transmite e processa a gama de informações que capturamos. A estrutura complexa do cérebro faz tarefas extramamente complexas parecerem triviais.
Para entender um pouco de como o neorônio, veja o vídeo a seguir:
In [2]:
from IPython.display import YouTubeVideo, Image
YouTubeVideo('r8D16C6-D5M')
Out[2]:
"As RNAs são sistemas computacionais distribuídos compostos de unidades de processamento simples, densamente interconectadas. Essas unidades, conhecidas como neurônios artificiais, computam funções matemáticas. As unidades são dispostas em uma ou mais camadas e interligadas por um grade número de conexões, geralmente unidirecionais. Na maioria das arquiteturas, essas conexões, que simulam as sinapses biológicas, possuem pesos associados, que ponderam a entrada recebida por cada neurônio da rede. Os pesos podem assumir valores positivos ou negativos, dependendo se comportamento da conexão é excitatório ou inibitório, respectivamente. Os pesos têm seus valores ajustados em um processo de aprendizado e codificam o conhecimento adquirido pela rede".
A imagem a seguir mostra a arquitetura de um neurônio artificial:
O neurônio com $d$ terminais (que simula os dendritos) recebe como entrada um objeto $x$ com $d$ atributos. Esse objeto é representado pelo vetor $x = [x_1, x_2, ..., x_d]^t$. Cada terminal do neurônio tem um peso $w$ associado. Estes pesos podem ser representados também por um vetor $w = [w_1, w_2, ..., w_d]$. A entrada total do neurônio é representada pela equação:
$u = \sum_{j=1}^{d}{x_j w_j}$
A saída do neurônio é determinada pela aplicação de um função de ativação ($f_a$) à entrada total $u$:
Várias funções de ativações aparecem na literatura. A imagem a seguir mostra três dessas funções: (a) linear, (b) limiar e (c) sigmoidal:
A função linear identidade implica retornar como saída o valor de $u$. Na função limiar, o valor do limiar define quando o resultado da função será igual a 1 ou 0. Quando a soma das entradas recebidas ultrapassa o limiar estabelecido, o neurônio torna-se ativo. Quanto maior o valor do limiar, maior tem que ser o valor da entrada total para que o valor de saída do neurônio seja igual a 1. Na função sigmoidal, diferentes inclinações podem ser utilizadas.
Em uma rede neural, os neurônios podem está dispostos em mais de uma camada. Em uma arquitetura de várias camadas a saída de um neurônio é entrada para outro neurônio. A imagem a seguir mostra esse tipo de arquitetura:
Alguns modelos de redes neurais permite a retroalimentação (ou feedback). Nesse tipo de redes é permitido que um neurônio receba como entrada as saídas geradas por camadas posteriores ou pela própria camada. Desta forma, podemos classificar as redes neurais em RNA feedfoward (sem retroalimentação) ou redes recorrentes (com retroalimentação).
A rede perceptron é a forma mais simples de configuração de uma rede neural artificial. A arquitetura da rede se aproxima daquela que foi apresentada no problema de regressão linear.
A imagem a seguir mostra a arquitetura da rede perceptron.
Observe que a rede é composta por um conjunto de sinais de entrada ($x_{train} = [x_1, x_2, ..., x_n]$). Cada sinal é poderado por um peso w, dado por $weights = [w_1, w_2, ..., w_3]$ e somado por um limiar de ativação ($\theta$). Sendo assim, o neurônio é representado pela seguinte operação:
$u = \sum_{i=1}^{n}{w_i*x_i} + bias$
O valor inicial do $bias$ é dado por $-\theta$. Neste exemplo, $\theta = 1$.
O valor de $u$ é entrada para uma função de ativação ($g$) gerando o sinal de saída $y=g(u)$.
Nesse exemplo, a função de ativação é dada por:
$g(u) = 1$, se $u >= 0$
$g(u) = 0$, se $u < 0$
Ou seja, estamos utilizando uma função limiar.
Modelo criado. A próxima etapa é definir como nosso modelo será treinado. Ou seja, como um modelo deste tipo aprende.
Este problema é uma tarefa de classificação. Cada instância vai ser classificada como 0 ou 1 de acordo com a classe que pertence. Sendo assim, o primeiro passo é comparar a saída com a classificação da base de treinamento. Para isso foi calculado o erro da seguinte forma:
$mse = \sum_{i = 1}^{N}{(y_i - output_i)^2}$
onde $y_i$ é o valor real e $output_i$ o valor encontrado pelo modelo.
O objetivo do treinamento é reduzir esse erro. Em outras palavras, estamos interessados em encontrar valores para os pesos e bias que minimizem este erro.
Um outro passo do treinamento é a atualização dos valores dos pesos e do limiar. Esses parâmetros são atualizados segundo fórmula descrita no livro do Ivan Nunes.
$w_{i}^{atual} = w_{i}^{anterior} + \eta (d^{(k)} - y).x_{i}^{(k)}$
$\theta_{i}^{atual} = \theta_{i}^{anterior} + \eta (d^{(k)} - y).(-1)$
onde:
$d^{(k)}$ é o valor desejado e $y$, o valor de saída produzido pela perceptron. Essa diferença é representada pelo que chamamos do erro que é minimizado. $\eta$ é uma constante que define a taxa de aprendizagem da rede. O valor da taxa de apendizada define a magnitude dos ajustes dos pesos. Valores altos fazem com que as variações sejam grandes, enquanto taxas pequenas implicam em poucas variações nos pesos. Essa magnitude influencia a velocidade de convergência.
O algoritmo a seguir mostra o processo de treinamento da rede perceptron.
Uma introdução mais detalhada de como funciona a rede perceptron pode ser encontrada no link a seguir:
In [5]:
YouTubeVideo('pkAKtL9FvFI')
Out[5]:
A rede perceptron é utilizada em problemas que são ditos linearmente separáveis. Entende-se por esse tipo de problema aqueles que são compostos por dados que podem ser separados por uma função linear. Para isso, vamos criar um conjunto de dados que possuem tal característica. Como o propósito é só mostrar o funcionamento da rede, vamos criar um conjunto de dados sem nenhum próposito específico.
Os dados de entrada são constituídos de várias instâncias contendo duas variáveis cada ($x_1$ e $x_2$) e cada instância é classificada em 0 ou 1. Sendo assim, a tarefa da rede é aprender um modelo que seja capaz de separar estas duas classes. O código a seguir cria os dados e os exibem em um gráfico.
In [1]:
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
#Criando os dados de entrada (x = features e y = classes)
x_train = np.array([[2., 2.],[1., 3.],[2., 3.],[5., 3.],[7., 3.],[2., 4.],[3., 4.],[6., 4.],
[1., 5.],[2., .5],[5., 5.],[4., 6.],[6., 6.],[5., 7.]],dtype="float32")
y_train = np.array([0., 0., 0., 1., 1., 0., 0., 1., 0., 0., 1., 1., 1., 1.], dtype="float32")
#Mostrando o Gráfico
A = x_train[:, 0]
B = x_train[:, 1]
colormap = np.array(['r', 'k'])
# Plot the original data
plt.scatter(A, B, c=colormap[[0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1]], s=40)
plt.ylim([0,8]) # Limit the y axis size
plt.show()
In [2]:
from sklearn.linear_model import perceptron
net = perceptron.Perceptron(n_iter=100, eta0=0.1, random_state=0, verbose=True)
X = x_train
y = y_train.T
net.fit(X,y)
Out[2]:
Esse modelo treinado possui os seguintes valores para pesos e bias:
In [3]:
# Output the coefficints
print("Coefficient 0 " + str(net.coef_[0,0]))
print("Coefficient 1 " + str(net.coef_[0,1]))
print("Bias " + str(net.intercept_))
In [4]:
# Plot the original data
plt.scatter(A, B, c=colormap[[0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1]], s=40)
# Calc the hyperplane (decision boundary)
ymin, ymax = plt.ylim()
w = net.coef_[0]
a = -w[0] / w[1]
xx = np.linspace(ymin, ymax)
yy = a * xx - (net.intercept_[0]) / w[1]
# Plot the hyperplane
plt.plot(xx,yy, 'k-')
plt.ylim([0,8]) # Limit the y axis size
Out[4]:
"Uma limitação das redes neurais de uma comada, como as redes perceptron e adaline, é que elas conseguem classificar apenas objetos que são linearmente separáveis".
Supondo que tenhamos uma base de dados com dois atributos. Se plotarmos estes dados em um plano cartesiano, eles vão ser linearmente separáveis se existe uma reta que separa os objetos de uma classe dos objetos de outra classe.
Se em vez de dois atributos a base possui $d$ atributos, o espaço de soluções será $d$-dimensional. Neste caso, os objetos são linearmente separáveis se houver um hiperplano que separe os dados das duas classes.
Uma rede perceptron multicamadas (Multilayer Perceptron - MLP) é caracterizada pela presença de pelo menos uma camada intermediária (escondida ou hidden layer) de neurônios, situada entre a camada de entrada e a respectiva camada neural de saída. Sendo assim, as MLP possuem pelo menos duas camadas de nurônios, o quais estarão distribuídos entre as camadas intermediárias e a camada de saída.
A figura a seguir ilustra este modelo.
Para mostrar este modelo vamos utilizar o exemplo disponível em neste link com a base do MNIST para treinar o modelo criado.
Antes de começar a entrar em detalhes da rede, vamos baixar a base do MNIST que será utilizada. O MNIST é um dataset de dígitos escritos a mão. A tarefa consiste em dada uma imagem que representa um dígito escrito à mão classifica-la de acordo com o dígito que foi escrito. Detalhes da base podem ser encontrados neste link. Por ser uma base bastante utilizada, a API do tensorflow já possui a base em um pacote do framework.
Cada imagem do dataset possui o tamanho de 28x28 e representa um dígito escrito à mão. A imagem a seguir ilustra uma instância da base:
As imagens vão ser transformadas em um vetor de 784 posições ($28*28$). A entrada da rede são vários vetores deste tipo. Cada vetor vai representar uma imagem. A saída da rede é definida por um vetor de 10 posições, onde cada posição representa uma possível classe do dígito (a base do MNIST trabalha com dígitos de 0 a 9).
Se considerarmos que a base de treinamento possui 55000 imagens, as imagens a seguir representam a entrada e saída da rede, respectivamente:
A diferença desta representação para o modelo que será implementado aqui é que o nosso modelo será alimentado por batch.
Explicações dadas, vamos para o modelo que será implementado.
Jessica Yung em seu tutorial Explaining TensorFlow code for a Multilayer Perceptron faz uma imagem bem representativa do modelo que será implementado:
O tutorial do link foi implementado no Tensoflow. Irei executa-lo utilizando o scikit-learn.
Uma questão importante no entendimento (e, consequentemente, na implementação) de qualquer modelo de rede neural é entender as dimensões dos dados ao passar por cada camada. A imagem anterior deixa isso bem claro. Por isso, vamos analisar camada por camada para que possamos entender como essas dimensões são modificadas. Na imagem, h1 e h2 são a quantidade de neurônios nas camadas intermediárias. A quantidade de neurônios de uma camada é que indica a dimensão da saída daquela camada. Outra informação importante é o tamanho do batch (já explicado anteriormente).
Com o batch igual a 100, a rede está recebendo como entrada uma matriz de 100x784, onde 784 é quantidade de pixel de cada imagem. Sendo assim, cada linha dessa matriz representa uma imagem da base de treinamento. Isso é passado para a primeira camada, onde será aplicada a seguinte operação $xW_1 + b_1$ onde, $W_1$ são os pesos de entrada e $b_1$, o bias. A imagem a seguir detalha esta operação juntamente com suas dimensões:
A saída da primeira camada é uma matriz 100x256, ou seja, 100 que representa a quantidade de instâncias que foram passadas na entrada e 256, a quantidade de neurônios. Ou seja, cada neurônio processou cada imagem e deu como resultado uma representação própria da entrada poderada pela operação definida. Ao resultado será aplicada uma função de ativação do tipo RELU (acesse o tutorial da Jessica Yung para ver detalhes do funcionamento deste tipo de função).
A entrada da segunda rede é uma matriz 100x256 (saída da camada anterior). As operações e dimensões da segunda camada são detalhadas na imagem a seguir:
Assim, como na primeira camada, a saída é uma matriz 100x256 que será aplicada uma função de atividação do tipo RELU. A camada de saída recebe os dados da segunda e gera como saída uma vetor que represente as 10 classes. Nesse caso, a saída será de 100x10, por conta do batch. Em outras palavras, estamos gerando um vetor que pesa cada possível classe para cada uma das 100 instâncias passadas como entrada. A imagem ilustra as operações e dimensões da camada de saída.
À saída da rede é aplicada a função Softmax que transforma os valores dos vetores em probabilidades. A posição que possuir o maior valor de probabilidade representa a classe à qual o dígito pertence.
Uma rápida explicação de como funciona a softmax pode ser encontrada neste vídeo.
In [110]:
# Carregando a base. Se a base não existir a pasta "dataset/MNIST" será criada e a base salva nesta pasta.
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets("dataset/MNIST", one_hot=True)
In [114]:
from sklearn.neural_network import MLPClassifier
mlp = MLPClassifier(
hidden_layer_sizes=(256,256),
activation='relu',
batch_size=100,
verbose=True,
max_iter=30,
learning_rate_init=0.001,
alpha=0.1
)
In [115]:
mlp.fit(mnist.train.images, mnist.train.labels)
Out[115]:
In [57]:
print(mlp.score(mnist.train.images, mnist.train.labels))
print(mlp.score(mnist.test.images, mnist.test.labels))
Vamos utilizar alguns dados mais complexos. Para isso vamos ver como se comporta a rede neural perceptron na base de dados: http://archive.ics.uci.edu/ml/datasets/Breast+Cancer+Wisconsin+%28Diagnostic%29
In [83]:
import pandas
data = pandas.read_csv('http://archive.ics.uci.edu/ml/machine-learning-databases/breast-cancer-wisconsin/breast-cancer-wisconsin.data',
sep=",",
header=None,
names=["ClumpThickness","CellSize","CellShape","MarginalAdhesion","SingleEpithelialCellSize",
"BareNuclei","BlandChromatin","NormalNucleoli","Mitoses","Class"])
data = data.replace('?',0)
data.head()
Out[83]:
In [91]:
from sklearn.utils import column_or_1d
X = data[["ClumpThickness","CellSize","CellShape","MarginalAdhesion","SingleEpithelialCellSize",
"BareNuclei","BlandChromatin","NormalNucleoli","Mitoses"]]
y = data[['Class']]
y = column_or_1d(y, warn=False)
In [95]:
mlp = MLPClassifier(hidden_layer_sizes=(256,256), activation='relu', batch_size=100, verbose=True, max_iter=100, learning_rate_init=0.001)
In [96]:
mlp.fit(X, y)
Out[96]:
In [97]:
mlp.score(X, y)
Out[97]:
In [100]:
# Importa o método de valiação cruzada
from sklearn.model_selection import cross_val_score
# Aplica a validação cruzada (5 folds) no modelo KNN (k=3) criado anteriomente
scores = cross_val_score(mlp, X, y, cv=5, scoring='accuracy')
print(scores.mean())