Introdução ao Perceptron multicamadas passo a passo

Olá meus queridos sete leitores!

Para você que vem estudando Machine Learning ou se deparou com esse tópico na faculdade já deve ter encontrado em seu caminho um modelo chamado perceptron multicamadas do inglês Multilayer perceptron.

Para aqueles que querem se  aprofundar nos estudos de redes neurais artificiais e nas aplicações desses modelos matemáticos fascinantes é de extrema importância entender a topologia dessa rede e um pouco algoritmo utilizado para a otimização.

No post de hoje iremos fazer um tutorial simples desse modelo e do algoritmo gradiente descendente que pode ser usado para treiná-lo.  Aqui irei focar na simplicidade e mostrar a forma mais básica desse modelo. É importante lembrar que existe uma enorme quantidade cálculos envolvidas no processo, porem são cálculos simples que podem ser aprendido se forem estudados com calma de forma separada. Convido a você dar uma olhada nesse post como um ponta pé inicial para se aprofundar em deep learning. A ideia de MLP vai nos da uma generalização, mesmo que simples nesse caso, do principal problema que estamos interessados em solucionar

Antes de entrar de cabeça no post é interessante que você saiba como funciona o modelo Perceptron. Eu fiz um post sobre o assunto:  Introdução ao perceptron passo a passo.

Para você que já entendeu como funciona o perceptron deve ter notado também sua limitação quando aplicado a problemas de classificação. Ele apenas resolve problemas em que os dados são linearmente separáveis. Ou seja, situações em que é possível traçar uma “reta” entre os dois conjuntos.

Problemas reais, entretanto, na maioria das vezes são mais complexos.

Hoje iremos focar no  tipo de problema representado à direita.

Para isso vamos usar um conjunto de dados fictício representado no gráfico abaixo:

Figura_2

Repare que nessa situação é impossível traçar uma reta que divide os conjuntos em duas partes. Logo, não é possível resolver o problema utilizando um simples Perceptron.

Observação!! : A escala dos dados aqui estão entre 1 e 3. Ná prática é necessário reescalar seus dados pois o MLP é sensível a escala dos dados e isso pode gerar uma série de inconsistência numéricas, irei discorrer sobre isso em post futuros, mas para esse exemplo irei deixar dessa forma para facilitar um pouco mais os cálculos.

O modelo Perceptron multicamadas

Multilayer Perceptron, apesar do nome, não se refere a um perceptron com múltiplas camadas. Tecnicamente ele seria composto de vários perceptrons, mas só tecnicamente, pois pela definição o Perceptron é um tipo especial de rede neural que apenas realiza classificação binária. 

No nosso caso é importante saber que graças a essa camada oculta (hidden layer) podemos captar novos padrões . É possível aprender funções polinomiais e não lineares em geral.

Segundo o teorema de George Cybenko um multilayer perceptron com uma função de ativação sigmoide é um aproximador universal para qualquer função de base real. Mas não irei entrar em detalhes sobre isso. O que nos interessa por hora é aprender o básico de como esse modelo é otmizado.

Definindo o modelo e os parâmetros

Para resolver esse problemas utilizaremos a seguinte a arquitetura:

Figura_3

Na figura acima temos o layer de input com os dois inputs x_{1} e x_{2} mais o bias b .

Uma observação sobre o bias é que ele sempre levara o input 1, o que importa de fato são os pesos b_{1} e b_{2}. Logo depois temos os pesos da primeira camada que vai de w_{1} ate w_{4} .

Uma vez que multiplicamos os pesos e adicionamos os biases aplicamos a função sigmóide e o mesmo processo é feito nos outputs da primeira camada até gerar os outputs da rede.

Organizamos o conjunto de treino em uma tabela que pode ser visualizada abaixo:

table_1

Repare que na coluna b temos apenas o número 1. Isso é um truque metodológico para deixar a ideia de bias mais intuitiva.

Vamos agora definir nossos pesos iniciais de forma aleatória.

w_{1} = -0.22

w_{2} = -0.07

w_{3} = -0.09

w_{4} = 0.39

w_{5} = 1.32

w_{6} = 0.66

Definimos os biases:

b_{1} = 0.87

b_{2} = 1.26

b_{3} = -0.8

Agora definimos os parâmetros da rede e a função de ativação:

Aqui definimos o alpha, a taxa de aprendizado, esse parâmetro regula a velocidade com que a rede irá aprender. Usei um alpha menor pois vi que com valores maiores a rede pode simplesmente passar do ponto ótimo.

\alpha = 0.01

A seguir à função de ativação sigmóide que irá gerar o output de cada neurônio. A função sigmóide é interessante pois tende a 1 quando x tende ao infinito e tende a 0 quando x tende ao infinito negativo. Ela nos dá um discriminante em 0.5  acima disso posso classificar como positivo e abaixo como negativo por exemplo.

o_{j} = \frac{1}{1+e^{n_{j}}}

O n_{j} aqui se refere ao resultado da multiplicação dos inputs pelos pesos de cada neurônio, antes de aplicar uma ativação dado um neurônio  j qualquer o seu valor será dado por:

n_{j} = \sum_{i=1}^{n}{w_{i}x_{i}}

Aqueles da área de estatística podem estranhar a fórmula acima pois falta b acrescentado ao resultado. Porém para esse tutorial irei tratar o bias como um input, lembre-se da coluna contendo apenas o número 1.

Dessa forma multiplicamos todos os inputs e adicionamos ao somatório.

Nossa função de perda(Loss Function.) será o erro quadrático médio.

n_{j} = \frac{1}{2N}\sum_{i=1}^{n}(\hat{y_{i}} - y_{i} )^2

Aqui o erro E é dado pela média da soma dos quadrados das diferenças dos outputs da rede \hat{y_{i}} em relação a label original y_{i} . O número  2 no denominador vai nos ajudar na hora de calcular a derivada do erro.

Temos que minimizar esse erro e assim achar o melhor conjunto de parâmetros w_{i} e b_{i} que nos dará o modelo ótimo para esse tipo de problema.

Antes de partir para o treino vamos tentar entender como é gerado o output final do MLP.

Entendendo como a informação é processada

Figura_4

Na figura abaixo iremos calcular o output do neurônio vermelho. Aqui e são os outputs dos dois neurônios das camadas ocultas respectivamente, depois de aplicar a função sigmoide.

Na figura temos o vetor de input que será é representado por:

\begin{bmatrix} x_{1} & x_{2} & b \end{bmatrix}

E repare que temos o vetor de peso, as arestas em vermelho que é representado por:

\begin{bmatrix} w_{1}\\ w_{2} \\ b_{1} \end{bmatrix}

Multiplicaremos cada input pelo seu respectivo peso e somaremos o resultado. Isso nada mais é do que o produto entre dois vetores.

Vamos substituir o vetor de input pela terceira linha do conjunto de treino(Pois a primeira tem apenas 1 e pode ficar confuso) e o vetor de pesos pelos seus respectivos pesos e realizar a multiplicação.

n_{1} = \begin{bmatrix} 2 & 3 & 1 \end{bmatrix} * \begin{bmatrix} -0.22\\ -0.09\\ 0.87 \end{bmatrix} = 2*(-0.22) + 3*(-0.09)+1*0.87 = 0.16

Esse é o resultado da multiplicação do input pelos pesos correspondente da camada, após isso vamos aplicar a função sigmoide e dessa forma gerar o output do neurônio em vermelho.

o_{1} = \frac{1}{1+e^{(-0.16)}} = \frac{1}{1.851} = 0.53991

No estado atual esse seria o output do neurônio vermelho.

Fazemos o mesmo procedimento para obter o output do outro neurônio.

n_{2} = \begin{bmatrix} 2 & 3 & 1 \end{bmatrix} * \begin{bmatrix} -0.07\\ 0.39\\ 1.26 \end{bmatrix} = 2*(-0.07) + 3*(0.39)+1*1.26 = 2.29

depois basta aplicar a sigmoid:

0_{2} = \frac{1}{1+e^{(-2.29)}} = \frac{1}{1.10} = 0.90805

Dessa forma obtemos o output da primeira camada. Veja no desenho abaixo.

Figura_5

Olhe com atenção a mecânica dos cálculos, pois assim ficará mais fácil entender como é feito o treino da rede.

Vamos calcular o output final da rede. Repare que temos os pesos w_{1} e w_{2}  mais o bias que leva como input o número 1.

n_{3} = \sum_{i=1}^{n}w_{i}x_{i}

Agora basta substituir os valores:

0_{3} = 0.53991w_{5} + 0.90805w_{6} + b_{2} = 0.53991*1.32 + 0.90805*0.66 + 1*(-0.8) = 0.51198

Aplicamos a função sigmóide para gerar o output final da rede:

\hat{y} = \frac{1}{1+e^{(-0.51198)}} = \frac{1}{1.59931} = 0.62527

Esse é o output da rede para a terceira linha do nosso conjunto de treino. Se está certo ou errado depende da forma que decidir o discriminate.

É comum usarmos uma regra do tipo:  Acima de 0.5 é igual a 1 e abaixo de 0.5 é igual a 0. Por essa regra a predição estaria errada e teríamos que otimizar a rede.

Treinando o Perceptron Multicamadas

A primeira coisa a fazer agora é calcular os outputs para todos os exemplos do conjunto de treino abaixo.

table_1

Como foi demonstrado acima a forma que o MLP processa a informação irei me abster de realizar os cálculos e já podemos demonstrar o resultado de cada output na coluna net_out.

table_2

Com isso feito podemos medir o erro quadrado médio:

E = \frac{1}{2N} \sum_{i=0}^{N}{(\hat{y_{i}} - y_{i} )^2}

Logo basta somar os quadrados das diferenças dos outputs da rede em relação às labels e dividir pela quantidade de exemplos vezes 2. Lembrando que multiplicamos por 2 apenas para facilitar o cálculo da derivada.

Dê uma olhada no cálculo abaixo:

 E = \frac{1} { 2*4 } *[ (0.64277 - 0)^2 + (0.64371 - 1)^2+(0.62527 - 0)^2+(0.62521 - 1)^2] =

 = \frac{1} { 8 } * ( 0.41314 + 0.12693 + 0.39095 + 0.14047 ) = 0.13393

Aqui tivemos um erro quadrado médio de 0.13393.

Agora que sabemos o erro podemos iniciar a propagação do erro para as camadas e atualizar cada peso conforme a regra abaixo.

w_{new} = w_{old} - \alpha \frac{\partial E}{\partial w_{old}}

Onde  w_{new} será o valor do peso após a atualização,  w_{old} o valor atual do peso, \alpha a taxa de aprendizado que definimos,aqui é 0.01, e \frac{\partial E}{\partial w_{old}} é o gradiente do erro em relação ao peso que iremos atualizar.

Mas antes voltemos ao esquema da rede.

Figura_6

Repare que para atualizar um peso dessa rede teremos que achar o gradiente do erro em relação ao peso que iremos utualizar.

Iremos realizar a primeira atualização da rede passo a passo de forma bem simples.

A primeira coisa que temos que fazer é achar a derivada do erro total E em relação ao output y . Basta definir a derivada da função de perda.

Logo:

\frac{d}{dy}E = \frac{1}{N}\sum_{i=0}^{N}{2*(\hat{y} - y)*1 - 0} = \frac{1}{N}\sum_{i=0}^{n}{(\hat{y} - y)}

A conclusão que chegamos aqui é que a derivada de E em relação a  \hat{y} é nada mais do que erro médio.

Antes de ir para o próximo passo observe o desenho a abaixo:

Figura_7

Acrescentei no final a letra E para dar uma ideia de como o erro se relaciona com o output da rede e para mostrar de forma esquematizada a derivada do erro em relação ao output.

Agora iremos achar a derivada do output da rede em relação a n_{3} . Lembre-se que após a multiplicação pelos pesos é aplicado a função sigmóide ao resultado:

\hat{y} = \frac{1}{1+e^{-n_{3}}}

n_{3} = w_{5}* o_{1} + w_{6}*o_{2}+b*b_{3}

Não irei entrar no mérito de derivar a função sigmóide aqui. Dado que o post já está grande demais. Porém você pode dar uma olhada neste excelente post do portal Towards Data Science que mostra de maneira detalhada o processo de derivação dessa função:

https://towardsdatascience.com/derivative-of-the-sigmoid-function-536880cf918e

Assumimos que a derivada do ouput\hat{y} em relação ao resultado da camada anterior n_{j} é dada pela parcial:

\frac{\partial \hat{y}}{\partial n_{j}} = \hat{y}(1 - \hat{y})

Onde y é a função sigmóide aplicada ao resultado da multiplicação da camada anterior pelos pesos subsequentes. Repare que é uma derivada parcial pois irei deixar as outras variáveis constantes.

Figura_8

Repare que \hat{y} é o output da rede. No caso é a função sigmóide aplicada ao resultado da multiplicação dos outputs da camada subsequente que são o_{1} eo_{2}  pelos pesos w_{5} e w_{6} e adicionado do bias. Entenda bem isso pois quando formos propagar o erro para camada de input iremos derivar em relação a esses dois outputs também.

O próximo passo é a parcial do output n_{3} em relação ao um dos pesos.

vamos atualizar w_{5} agora:

n_{3} = o_{1}w_{5} + o_{2}w_{6} + b_{3}

\frac{\partial n_{3}}{\partial w_{3}} = 1*o_{1}*w_{5}^{(1-1)} + o_{2}*0 + b*0 = 0_{1}

Observe a figura abaixo:

Figura_9

Aqui concluímos que a derivada parcial do resultado on_{3} em relação ao peso w_{5}  é o próprio input o_{1} que multiplica esse peso.

Porém o que interessa aqui é saber a derivada parcial do erro em relação a esse peso. Para isso basta utilizar a regra da cadeia.

Ou seja:

\frac{\partial E}{\partial w_{5}} = \frac{dE}{d \hat{y}} * \frac{\partial \hat{y}}{\partial \hat{y}}*\frac{\partial n_{3}}{\partial w_{5}}

Uma observação importante: Irei utilizar a média dos valores dos gradientes dos outputs para atualizar o peso. Na fórmula acima temos a regra para atualizar com base em um único output, estocástica, porém iremos atualizar de forma direta, utilizando todo o conjunto de treino.

Vamos começar vendo a derivada do erro atual E em relação ao output da rede \hat{y} :

\frac{dE}{dy} = \frac{1}{N} \sum_{i=0}^{N}(\hat{y_{i}} - y_{i}) =

 = \frac{1}{4}*[(0.64277 - 0) + (0.64371 - 1)+(0.62527 - 0)+(0.62521 - 1)] =

 = \frac{0.53696}{4} = 0.13424

O próximo passo é calcular o \hat{y} o gradiente do output em relação a n_{3}.

É importante lembrar aqui que como estamos trabalhando com um batch o que teremos na verdade é a média dos gradientes de cada erro em relação ao peso em questão.

Vamos dar uma olhada nisso mais de perto. Como vimos a derivada da sigmoid é dada por:

\frac{\partial \hat{y}}{\partial n_{3}} = \hat{y}(1 - \hat{y})

O gradiente para o primeiro input seria:

\frac{\partial \hat{y}}{\partial n_{3}} = 0.64277(1 - 0.64277) = 0.22962

Mas o que queremos é a média de todas as derivadas de um dado Batch. No nosso caso todo o conjunto de treino.

Logo teremos:

\frac{1}{N} \sum{\frac{\partial \hat{y}}{\partial n_{3}}} = \frac{1}{4}*(0.22962 + 0.22935 + 0.23431 + 0.23432) = \frac{0.9276}{4} = 0.2319

Agora basta achar a derivada do resultado n_{3} em relação ao peso w_{5} :

\frac{\partial n_{3}}{\partial w_{5}} = 1*o_{1} + 0*0_{2} + 0*b_{3} = o_{1}

Como queremos a média de todos outputs do batch basta somar todas as saídas de o_{1}:

Logo teremos:

\frac{1}{N} \sum{ \frac{\partial n_{3}}{\partial w_{5}}} = \frac{1}{4}( 0.63645 + 0.61538 + 0.53991 + 0.58419) = \frac{2.37593}{4} = 0.59397

Agora pela regra da cadeia temos que:

\frac{\partial E}{\partial w_{5}} = \frac{dE}{d \hat{y}} * \frac{\partial \hat{y}}{ \partial n_{3}}* \frac{ \partial n_{3}}{ \partial w_{5}} = 0.13424 * 0.2319 * 0.59397 = 0.01848

Com isso temos o gradiente do erro em relação ao peso w_{5} . Como temos um erro positivo é correto pensar que temos que diminuir o valor do peso para que tenhamos um erro menor.

Vamos antes lembrar alguns parâmetros da rede.

O primeiro é a taxa de aprendizado que foi definida em 0.01. O segundo é o peso w_{5} antigo que foi inicializado como 1.32.

logo temos:

\alpha = 0.01

w_{5} = 1.32

E agora basta aplicar a regra de atualização:

w_{new} = w_{old} - \alpha \frac{\partial E}{\partial w_{old}} = 1.32 - 0.01*0.01848 = 1.31981

Com isso concluímos a atualização do primeiro peso da rede.

Temos agora:

w_{5} = 1.31981

Vamos fazer o mesmo procedimento para, abaixo temos o valor inicial do peso:

w_{6} = 0.66

Lembrando que a derivada parcial de n_{3} em relação a w_{6} é dada por:

\frac{\partial n_{3}}{\partial w_{6}} = 0*o_{1} + 1*o+{1}*w_{6}^{1 - 1} + 0*b_{3} = o_{2}

Dae basta pegar a média dos gradientes de n_{3} de em relação a w_{6} :

\frac{1}{N} \sum{\frac{\partial n_{3}}{\partial w_{6}}} = \frac{1}{4} (0.8292 + 0.87761 + 0.90805 + 0.81906) = \frac{3.43392}{4} = 0.85848

Aplicamos a regra da cadeia:

\frac{\partial E}{\partial w_{6}} = \frac{dE}{d \hat{y}}*\frac{\partial \hat{y}}{\partial n_{3}}*\frac{\partial n_{3}}{\partial w_{6}} = 0.13424*0.2319*0.85848 = 0.026725

w_{new} = w_{old} - \alpha \frac{\partial E}{\partial w_{old}} = 0.66 - 0.01*0.026725 = 0.65972

w_{6} = 0.65972

Se aplicarmos o mesmo procedimento ao bias b_{3}  teremos:

\frac{\partial n_{3}}{\partial b_{3}} = 0*o_{1} + 0*w_{6} + 1*1*b_{3}^{1 - 1} = 1

Aqui é importante lembrar que o input do bias é sempre 1. por esse motivo a derivada do bias é igual a 1. depois basta tirar a média de todas as derivadas dos inputs que compõem o conjunto de treino.

\frac{1}{N} \sum{\frac{\partial n_{3}}{\partial b_3}} = \frac{1}{4}(1+1+1+1) = \frac{4}{4} = 1

É interessante notar que a derivada do bias é 1 justamente por esse ser o coeficiente linear da equação que compõe o output.

E finalmente calculamos a derivada do erro em relação ao bias:

\frac{\partial E}{\partial w_{6}} = \frac{d E}{d \hat{y}}*\frac{\partial \hat{y}}{\partial n_{3}} * \frac{\partial n_{3}}{\partial b_{3}} = 0.13424*0.2319*1 = 0.03110

Com isso em mãos podemos atualizar o bias dessa camada:

b_{new} = b_{old} - \alpha \frac{\partial E}{\partial b_{old}} = -0.8 - 0.01*0.03110 = -0.8003

b_{3} = -0.8003

Até aqui já atualizamos a última camada da rede.

Porém agora atualizaremos os pesos da primeira camada. partiremos do peso w_{1}.

Antes precisamos entender como o erro varia em relação a esse peso.

Se olhar na figura anterior podera ver que derivamos o erro ate em relação a w_{5} porem agora encaixaremos mais uma peça na nossa figura.

Para calcular a variação do erro em relação a w_{1} é necessário obter as seguintes derivadas.

\frac{\partial E}{\partial w_{1}} = \frac{d E}{d \hat{y}}*\frac{\partial \hat{y}}{\partial n_{3}} *\frac{\partial n_{3}}{\partial 0_{1}} * \frac{\partial 0_{1}}{\partial n_{1}}*\frac{\partial n_{1}}{\partial w_{1}}

Repare que não derivamos o erro mais em relação a w_{5} pois nessa etapa ele é considerado como uma constante e o o_{1} que será considerado como uma variável.

Vamos começar o processo. Como vimos antes já possuímos os gradientes do erro até em relação a n_{3} calculados.

Logo vamos adicioná las a fórmula:

\frac{\partial E}{\partial w_{1}} = 0.13424*0.2319*\frac{\partial n_{3}}{\partial o_{1}}*\frac{\partial o_{1}}{\partial n_{1}}*\frac{\partial n_{1}}{\partial w_{1}}

Lembrando que os valores se referem a média das derivadas do batch todo. Vamos calcular a próxima parte dessa derivada:

Figura_10

Como vimos:

n_{3} = w_{5}*o_{1} + w_{6}*o_{2} + b_{3}*1

Logo:

\frac{\partial n_{3}}{\partial o_{1}} = 1*w_{5}*o_{1}^{(1-1)} + w_{6}*0 + b_{3}*0 = w_{5}

A diferença dessa derivada em relação a que derivamos na outra vez é que aqui derivamos em relação a o_{1} e não em relação a w_{5}.

Como sabemos que inicial é igual a 1.32 basta multiplicar por 4 e dividir por 4. É bem óbvio que o valor será o mesmo, porém é interessante ilustrar.

\frac{\partial E}{\partial w_{1}} = 0.13424*0.2319*1.32*\frac{\partial o_{1}}{\partial n_{1}} * \frac{\partial n_{1}}{\partial w_{1}}

Agora vamos à próxima derivada.

Figura_11

Sabemos que:

o_{1} = \frac{1}{1+e^{-n_{1}}}

Logo:

\frac{\partial o_1}{\partial n_{1}} = o_{1}*(1 - o_{1})

Agora pegamos a média dos gradientes para o batch:

\frac{1}{4} \sum{\frac{\partial o_{1}}{\partial n_{1}}} = \frac{1}{4}[\sum{o_{ij}*(1-o_{ij})}] =  = \frac{1}{4}[[0.63645<em>( 1 - 0.63645 ) + 0.61538 * (1 - 0.61538 ) + 0.53991</em>( 1 - 0.53991) + 0.58419*( 1 - 0.58419 )]] \frac{1}{4}\sum{\frac{\partial o_{1}}{\partial n_{1}}} = \frac{1}{4}(0.23138 + 0.23669 + 0.24841 + 0.24291) = \frac{0.95939}{4} = 0.23985

Agora basta encaixar esse resultado na equação:

\frac{\partial E}{\partial w_{1}} = 0.13424*0.2419*1.32*0.23985*\frac{\partial n_{1}}{\partial w_{1}}

O próximo passo é achar o gradiente de n_{1} em relação a w_{1}:

Figura_12
n_{1} = w_{1}*x_{1} + w_{2}*x_{2} + b_{2}*1

logo:

\frac{\partial n_{1}}{\partial w_{1}} = x_{1}*w_{1} + x_{2}*w_{2} + 1*b_{1} = 1*x_{1} * w_{1}^{(1 -1)} + x_{2}*0 + 1 * 0 = x_{1}

Calculamos a média de todos os inputs para x_{1}:

\frac{1}{N} \sum{x_{1i}} = \frac{1}{4}(1+1+2+2) = \frac{6}{4} = 1.5

Encaixamos isso na equação para obter o resultado final, a variação do erro em relação ao peso w_{1}.

\frac{\partial E}{\partial w_{1}} = 0.13424 * 0.2419 * 1.32 * 0.23985 * 1.5 = 0.01541

Com isso em mãos pode-se aplicar a regra de atualização. (Delta rule.) e atualizar o peso w_{1}

w_{new} = w_{old} - \alpha \frac{\partial E}{\partial w_{old}} = 0.22 - 0.01*0.01541 = -0.22014

Irei aplicar o mesmo procedimento ao peso w_{3} e o bias b_{1}. Por hora irei pular os cálculos. Mas a ideia é a mesma, a única coisa que muda é que será calculado a deriva em relação a aquele peso ou bias.

Como já temos a derivada parcial até o neurônio n_{1} basta apenas calcular a derivada em relação a w_{3} que como já vimos é o próprio input  x_{2}.

Temos que:

\frac{\partial E}{\partial w_{3}} = 0.13424*0.2419*1.32*0.23985*1.75 = 0.017989

Fazemos a atualização:

w_{new} = w_{old} - \alpha \frac{\partial E}{\partial w_{old}} = -0.09-0.01*0.01798 = -0.09018

w_{3} = -0.09018

Vamos atualizar o bias b_{1} agora:

\frac{\partial E}{\partial b_{1}} = 0.13424*0.2435*1.32*0.23985*1 = 0.01035

b_{new} = b_{old} - \alpha \frac{\partial E}{\partial w_{old}} = 0.87*0.01035 = 0.8699

b_{1} = 0.8699

Com isso feito. Atualizamos os pesos que geram o output de primeiro neurônio da primeira camada oculta o_{1}.

Ainda falta fazer a atualização dos pesos w_{2} , w_{4} e o bias b_{2} que geram o output o_{2}. Nesse momento iremos ter que achar a derivada de n_{3} em relação a o_{2}.

Veja a figura abaixo:

Figura_13

As linhas pretas indicam as derivadas que já possuímos e as vermelhas indicam as que devemos calcular agora.

Vamos iniciar calculando a derivada em relação a w_{2}:

Voltando a equação da cadeia com os gradientes que já possuímos vemos que:

\frac{\partial E}{\partial w_{2}} = 0.13424*0.2435*\frac{\partial n_{3}}{\partial o_{2}}*\frac{\partial o_{2}}{\partial n_{2}}*\frac{\partial n_{2}}{\partial w_{2}}

Vamos calcular a próxima derivada:

\frac{\partial n_{3}}{\partial o_{2}} = o_{1}*w_{5} + o_{2}*w_{6}+1*b_{3} = 0*1+o_{2}^{(1-1)}*w_{6}+0 = w_6

Calculamos a média de para todos os elementos do batch que no caso é igual ao próprio w_{6}.

\frac{1}{N} \sum{w_{i}} = 0.66

E encaixamos isso na nossa equação:

\frac{\partial E}{\partial w_{2}} = o.13424*0.2435*0.66*\frac{\partial o_{2}}{\partial n_{2}}*\frac{\partial n_{2}}{\partial w_{2}}

Agora é hora de achar a próxima derivada da equação.

sabemos que:

o_2 = \frac{1}{1+e^{-n_{2}}}

logo:

\frac{\partial o_{2}}{\partial n_{2}} = o_{2}*(1-o_{2})

Agora basta pegar a média dos gradientes para o neurônio o_{2}:

\frac{1}{N} \sum{\frac{\partial o_{2i}}{\partial n_{2i}}} = \frac{1}{4}(0.14163 + 0.10741+ 0.0835 + 0.1482) = \frac{ 0.48074}{4} = 0.12018

Encaixamos mais esse resultado em nosso equação:

\frac{\partial E}{\partial w_{2}} = 0.13424*0.2435*0.66*0.12018*\frac{\partial n_{2}}{\partial w_{2}}

Agora iremos derivar a última parte. E como já vimos:

n_{2} = x_{1}*w_{2} + x_{2}*w_{4} + 1*b_{2}

\frac{\partial n_{2}}{\partial w_{2}} = 1*x_{1} * w_{2}^{(1-1)}+0+0 = x_{1}

Agora basta adicionar a média dos quatro inputs em nossa equação:

\frac{1}{N}\sum{\frac{\partial n_{2}}{\partial w_{2}}} = \frac{1}{4}(1+1+2+2) = \frac{4}{4} = 1

\frac{\partial E}{\partial w_{2}} = 0.13424 * 0.2435 * 0.66 * 0.12018 * 1 = 0.00258

Com isso em mão podemos atualizar o peso w_{2}:

w_{new} = w_{old} - \alpha \frac{\partial E}{\partial w_{old}} = -0.07 - 0.01*0.00258 = -0.07002

w_{2} = -0.07002

Como já sabemos que o processo é o mesmo para o peso w_{4} e o bias b_{2} irei já colocar o valor atualizado de cada um:

w_{4} = 0.38994

b_{2} = 1.25996

E com isso temos todos os pesos e biases atualizados e a primeira iteração terminada:

w_{1} = -0.22014

 w_{2} = -0.07002

 w_{3} = -0.09018

w_{4} = 0.38994

w_{5} = 1.31981

w_{6} = 0.65972

b_{1} = 0.8699

b_{2} = 1.25996

b_{3} = -0.8003

Agora vamos recalcular os outputs da rede e comparar os erros quadráticos médios:

Primeiro pegamos a tabela contendo todos os novos outputs da rede:

table_3
E = \frac{1}{2*4} * [ (0.64259 - 0)^2 + (0.64352 - 1)^2+(0.62505 - 0)^2+(0.62501 - 1)^2] =

 = \frac{1}{8} * ( 0.41291 + 0.12708 + 0.39069 + 0.14062 ) = 0.133910

E=0.13390

O erro depois da atualização ficou em: 0.13390 houve pequena redução em relação a 1.3393.

Com isso encerramos a primeira época. Caso continuemos com o treino iremos repetir esse processo até minimizar o erro ao valor mínimo.

Repare que houve uma diminuição em todos os valores da rede. Isso ocorreu pois por hora todos os valores de outputs ainda estão acima de 0.5.

Para você que se interessou pelo post vale a pena tentar reproduzir o procedimento em uma linguagem de programação e analisar a convergência da rede.

Gostaria também de lembrar ao leitor que para fazer esse post foi necessário uma grande quantidade de cálculos manuais e então infelizmente existe uma probabilidade de erro e inconsistências e caso veja alguma coisa errada pode está entrando em contato com autor pois assim estará colaborando para o desenvolvimento de material de qualidade para a comunidade.

Até o próximo post e bons estudos.

Introdução a Tensorboard

Ola para você que passou por aqui. Bem vindo meu blog e nesse post estarei dando início a série aprendendo Tensorflow.

Meu intuito com estes posts não é fazer uma série organizada para aprender Tensorflow. Mas sim discorrer sobre alguns temas interessantes e ajudar o leitor com dificuldades comuns.

Muito bem hoje iremos falar sobre Tensorboard. Caso você ainda não tenha uma boa base em Tensorflow aconselho a leitura destes meus dois postes Multilayer perceptron com TensorFlowUma introdução ao TensorFlow e suas operações básicas ,eles não são muito detalhados mas dão uma boa ideia de como o TF funciona.

O Tensorflow é constituído basicamente de Nodes e Arestas. Nas Arestas fluem os escalares, vetores, matrizes ou tensores enquanto que nos nodes são feitas as operações.

Quando você cria seu modelo em Tensorflow você basicamente cria um grafo por onde serão realizadas operações a cada passo da execução, essa feita dentro da Seção que irá executar o grafo.

A ideia é simples porem na hora da prática as coisas não são bem assim, é muito confuso lidar com todas as operações e nodes de um grafo ou vários grafos, isso já é complicado em modelos isolados como uma CNN ou um MLP que são muito utilizados. Agora imagine em um sistema maior com vários modelos combinados ?

Apesar de a versão 2.0, que ate a data deste post esta em beta, estar quase sendo lançada e nessa versão haver uma integração muito maior com o Keras e outras funcionalidades. Mesmo assim vale a pena entender melhor como funciona o TensorFlow, pois muitos modelos e ferramentas ainda utilizam as versão antiga e entender um pouco mais o que acontece por baixo dos panos pode ser um diferencial e tanto.

Mas sem mais delongas e vamos ao que interessa!

Antes de mais nada é importante que você fique ciente que a versão que sera usada aqui é o Tensorflow 1.13.1 que é ultima versão estável ate o momento deste post.

Hoje iremos desenvolver um simples Multilayer Perceptron e fazer os procedimentos para treina-lo. Apos isso iremos analisar o grafo e algumas métricas básicas na Tensorboard.

Então. vá lá e abra seu editor que vamos começar!

A primeira coisa a fazer é importar os módulos que serão utilizados:

import numpy as np #numpy para operações com arrays
from sklearn.datasets import load_iris # irei usar o dataset iris pré processado do sklearn
from sklearn.model_selection import train_test_split # para dividir o dataset em treino e teste
import tensorflow as tf # e tensorflow para construirmos nosso grafo&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;lt;span 				data-mce-type="bookmark" 				id="mce_SELREST_start" 				data-mce-style="overflow:hidden;line-height:0" 				style="overflow:hidden;line-height:0" 			&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;#65279;&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;lt;/span&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;gt;

Repare que não importei a Tensorboard de maneira explicita. Ela já algo que vem integrado com o Tensorflow. Justamente para nos ajudar a lidar com toda a confusão gerada pela combinação dos tensores e operações.

A próxima linha de código:


#função para "resetar" o grafo atual
tf.reset_default_graph()

A função tf.reset_default_graph() é usada para reiniciar o grafo atual, pois quando você monta e compila um grafo em Tensorflow a estrutura permanece na memória. E se você for executar o código outra vez ele ira construir um novo grafo.
Essa função nos permite então “limpar” a memoria para executar um novo grafo sem complicações e inconsistências. Extremamente útil quando se estar montando e testando seu modelo de maneira interativa e atualmente já existe outras soluções para nos ajudar com isso, mas não aconselho a deixa-la no código em produção.

Agora vamos instanciar o objeto load_digits ele serve para gerenciar os dados relacionados ao dataset digits, uma verção reduzida do MNIST , como as variáveis independentes, as dependentes, nomes das classes, colunas e etc.


#Aqui istancio o dataset para leitura
#isso na verdade é uma instáncia do objeto sklearn.utils.Bunch
digits_data = load_digits()

Armazeno as variáveis dependentes na variável x_data:


#Aqui armazeno os dados na variável x_data
x_data = digits_data.data

Aqui irei armazenar a variáveis dependentes na variável y_data:


#Aqui irei armazenar as variaveis dependentes na variavel y_data
y_data = digits_data.target

Agora antes de ir para o próximo passo gostaria de fazer uma observação, principalmente para quem esta começando na área.

Se você dar um print na variável y_data verá que se trata de uma np.array com uma unica dimensão.

In [7]: print(y_data)
	[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
	 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
	 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2
	 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
	 2 2]

No exemplo de hoje iremos utilizar o formato one_hot_vector que nada mais é do que uma matriz A_{ij} onde A_{j} é o número de linhas e A_{i} o número de classes e cada vetor A_{i} tem o número 1 no índice da classe e 0 nos outros indices.

Para fazer essa transformação vamos criar a função abaixo:


#Aqui irei criar uma função para transformar o y_data em onehotvector
def make_one_hot_vector(y_data):

    new_data = np.zeros((len(y_data),len(np.unique(y_data))))

    for i,value in enumerate(y_data):
        new_data[i][value] = 1

    return new_data

#Aqui aplicamos a função na nosa y_data
y_data = make_one_hot_vector(y_data)

Se você dar um print na y_data verá como é formato one_hot_vector e verá também que é muito intuitivo.


In [13]: print(y_data[:3])
	[[1. 0. 0.]
	 [1. 0. 0.]
	 [1. 0. 0.]]

Separamos os dados em treino e teste, nesse caso 20% deles irão ficar para teste.


#Agora iremos separar o dataset em conjunto de treino e teste
x_train,x_test,y_train,y_test = train_test_split(x_data,y_data)

Agora vamos reescalar os dados. Mas antes gostaria de fazer uma observação: Aqui irei pegar os valores máximos do conjunto de treino para reescalar os dados para o intervalo entre 0 e 1 porem isso não garante que os dados do conjunto de teste serão reescalado para entre 0 e 1 pois o valor máximo do conjunto de teste pode ser maior do que valor máximo do conjunto de treino. Saber reescalar os dados é uma etapa muito importante do processo de desenvolvimento e treino de um modelo.

Mas para esse exemplo irei deixar dessa forma!

#iremo reescalar os dados

#essa função é para evitar incosistência númerica.
# pois algumas colunas tem somente 0 ae o valor máximo fica sendo 0
#divisão por 0 gera "valores infinitos" nan e causa incosistencia numérica
f = lambda x: 1 if x == 0 else x

#pego os valores maximos de cada coluna em x_train para reescalar os valores de x_data
#aplicando a função lambda para tratar o problema dos máximos serem 0.
max_values = [f(x.max()) for x in x_train.T]

#Agora irei normalizar as nossas features para ficar entre 0 e 1
#Preste atenção ao normalizar seus dados
x_train = np.array([x/max_values for x in x_train])
x_test = np.array([x/max_values for x in x_test])

Agora vamos começar a montar nosso código que irá gerar o grafo. Se não entender algum passo peço que tenha calma,  que as coisas vão se encaixar mais a frente.

A primeira coisa que irei fazer é definir os diretórios onde serão salvos os eventos de treino e teste.

train_logs_path = './summaries/train/exp_1'
test_logs_path = './summaries/test/exp_1'

Agora defino os parâmetros básicos da rede.

learning_rate = 0.1# influencia na quantidade de iterações necessárias para o modelo aprender.
Epochs = 10000 # quantidade de iterações em todo o dataset
batch_size = 50 # quantas linhas serão inseridas no modelo por vez

A primeiro coisa que precisamos fazer é definir as arestas por onde iremos inserir as variáveis independente es as variáveis independentes


#Esse serão os  tensores de input do grafo
#O número de linhas é desconhecido e o número de colunas igual a quantidade de features
x = tf.placeholder(tf.float32,[None,len(x_data[0])],name='input')

#O número de linhas é desconhecido e o número de coluna é igual a quantidade de classes
y = tf.placeholder(tf.float32,[None,len(y_data[0])],name='train_labels')

Agora iremos fazer um Perceptron multicamadas contendo uma camada escondida com uma função sigmoid e um output com função sigmoid. Um modelo simples e eficiente.


def mlp_model(x):

    #com o tf.layers construir os modelo fica bem parecido com keras
    w1 = tf.layers.dense(x,100,tf.nn.sigmoid,name='input_layer')
    with tf.name_scope('histograms'): #aqui nos criamos o nome de scopo histograms
        # adicionamos o output das ativações da primeira camada ao histograma
        #interessante para visualizar a distribuição do output do primeiro layer
        tf.summary.histogram('w1',w1)

    logits = tf.layers.dense(w1,len(y_data[0]),activation=tf.nn.sigmoid,name='logits')

    with tf.name_scope('histograms'):
        tf.summary.histogram('logits',logits)#mesmo procedimento com o segundo layer

    return logits

Repare que utilizei a função tf.summary.histogram nos outputs gerados de cada camada, isso quer dizer que toda vez que algum output for gerado por aquele node ele será gravado no summary de operações, e o tf.name_scope é para sinalizar que isso deve ser mostrado na área ou quadrante histograms. Ira ficar um pouco mais claro quando fazermos a visualização.

Basicamente é isso que a Tensorboard faz, ela pega os resultados gerados por cada node ou do node que eu apontar e organiza e salva em um formato que possa ser estruturado e apresentado de maneira mais humana.

Cada iteração que ser feita pelo grafo e alterar os valores dentro do grafo será mostrado na Tensorboard, tanto para escalares, vetores, matrizes ou tensores.

Com nossa função que constrói o grafo do modelo pronta. podemos definir o node de output do modelo.

logits = mlp_model(x)

A variável logit armazeno o ultimo node do grafo, se eu chama-la ela vai desencadear todas as operações que compõe o modelo perceptron multicamadas.

Agora iremos definir as operações para treino do modelo e assim achar os valores certos para os pesos de modo a obter a melhor predição possível.


#Aqui iremos definir a operação de treino e
with tf.name_scope('loss_function'):
    cost = tf.reduce_mean(tf.losses.softmax_cross_entropy(y,logits))
    tf.summary.scalar('loss',cost)# repare que agora armazeno um scalar ao histogram

Iremos definir o procedimento para pegar informações da acurácia, tanto do conjunto de teste quanto no de treino. Repare que agora utilizei a função tf.name_scope para organizar todas as operações em um só lugar na hora de mostrar o grafo.


#Aqui iremos definir a operação de treino e
with tf.name_scope('loss_function'):
    cost = tf.reduce_mean(tf.losses.softmax_cross_entropy(y,logits))
    tf.summary.scalar('loss',cost)# repare que agora armazeno um scalar ao histogram

Agora iremos definir a operação de otimização que será utilizada, para isso iremos utilizar o clássico gradiente descendente estocástico que funciona muito bem para problemas relativamente simples.


#Aqui definimos nosso otimizador para fazer atualização dos pesos e convergencia
optimizer = tf.train.GradientDescentOptimizer(learning_rate).minimize(cost)

Uma vez que tenhamos informações de todas as operações que precisamos, nos iremos junta-las a um só tf.summary dessa forma as coisas poderão ser organizadas internamente e mostrada de forma que possamos entender.


#Juntamos todos os súmarios em unica operação
merged_summary_op = tf.summary.merge_all()

Inicializamos as variáveis globais, no caso a inicialização dos pesos e biases  de forma aleatória e coisas do tipo.


#Isso é utilizado para inicializar todas as variáveis globais
init = tf.global_variables_initializer()

Com isso feito podemos partir para o procedimento de treino do algoritmo.


#Agora vamos dar inicio ao treino da rede
with tf.Session() as sess:

    sess.run(init)

    global_step = 0 #global step é interessante para contar quantas vezes foi passado um               		          #batch pelo grafo

    #operação para escrever os logs no tensorflow
    # toda vez que é chamda salva o atual estado do grafo no sumario
    train_writer = tf.summary.FileWriter(train_logs_path,graph=sess.graph)
    test_writer = tf.summary.FileWriter(test_logs_path,graph=sess.graph)

    #essá variável me da a quantidade de batchs mais um para não sobrar dados de treino
    total_batch = round(len(x_train)/batch_size)+1)
    #Aqui vamos iterar em cada epoch
    for epoch in range(epochs):

        #vamos iterar por todos os batchs em cada epoca
        #é comun embaralhar o dataset apos cada epoca mas aqui não irei fazer isso
        for i in range(total_batch):
            global_step += 1

	    #Aqui faço uma regra para salvar a accuracia no summario
            if global_step % total_batch == 0:
                acc,summary = sess.run([accuracy,merged_summary_op],
                                                          feed_dict={x:x_test,y:y_test})
                test_writer.add_summary(summary, global_step)

            else:
	        batch_x = x_train[i*batch_size:(i*batch_size) + batch_size]
                batch_y = y_train[i*batch_size:(i*batch_size) + batch_size]

            if len(batch_x) != 0: #verifico o tamanho do batch para não vir array vazia

                #Aqui execultamos as operações de treino e accuracia no dataset de treino
                _,c,summary,acc  = sess.run([optimizer,cost,
                                                                        merged_summary_op,accuracy],
                                                                        feed_dict={x:batch_x,y:batch_y})

                #salvamos os outputs gerados no summario
                train_writer.add_summary(summary, global_step)

                print(c) #printa a loss, eu gosto para ver o treino. rsrs

Caso você não tenha criado o diretório summaries não se preocupe ele cria automaticamente dentro do diretório que você esta executando o código.

Com isso já temos o código completo. Para nossa introdução a Tensorboard. Caso você prefira segue o código completo abaixo.

import numpy as np
from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split
import tensorflow as tf

tf.reset_default_graph()

#Aqui istancio o dataset para leitura
digits_data = load_digits()

#Aqui armazeno os dados na variável x_data
x_data = digits_data.data

#Aqui irei armazenar as variaveis dependentes na variavel y_data
###lembrar de explicar o onehotvector
y_data = digits_data.target

#Aqui irei criar uma função para transformar o y_data em onehotvector
def make_one_hot_vector(y_data):

    new_data = np.zeros((len(y_data),len(np.unique(y_data))))

    for i,value in enumerate(y_data):
        new_data[i][value] = 1

    return new_data

y_data = make_one_hot_vector(y_data)

#Agora iremos separar o dataset em conjunto de treino e teste
x_train,x_test,y_train,y_test = train_test_split(x_data,y_data,test_size=0.5)

#iremo reescalar os dados

#essa função é para evitar incosistencia númerica.
# pois algumas colunas tem somente 0 ae o valor máximo fica sendo 0
#divisão por 0 gera "valores infinitos" nan e causa incosistencia numérica
f = lambda x: 1 if x == 0 else x

#pego os valores maximos de cada coluna em x_train para reescalar os valores de x_data

max_values = [f(x.max()) for x in x_train.T]

#Agora irei normalizar as nossas features para ficar entre 0 e 1
#Preste atenção ao normalizar seus dados
x_train = np.array([x/max_values for x in x_train])
x_test = np.array([x/max_values for x in x_test])

train_logs_path = './summarie/train/exp_1'
test_logs_path = './summarie/test/exp_1'

learning_rate = 0.01
epochs = 2000
batch_size = 50

#Esses serão os
x = tf.placeholder(tf.float32,[None,len(x_data[0])],name='input')
y = tf.placeholder(tf.float32,[None,len(y_data[0])],name='train_labels')

def mlp_model(x):

    w1 = tf.layers.dense(x,100,tf.nn.sigmoid,name='input_layer')
    with tf.name_scope('histograms'):
        tf.summary.histogram('w1',w1)

    logits = tf.layers.dense(w1,len(y_data[0]),activation=tf.nn.sigmoid,name='logits')

    with tf.name_scope('histograms'):
        tf.summary.histogram('logits',logits)

    return logits

logits = mlp_model(x)

#Aqui iremos definir a operação de treino e adicionamos ela ao nosso sumário
cost = tf.reduce_mean(tf.losses.softmax_cross_entropy(y,logits))
tf.summary.scalar('loss',cost)

with tf.name_scope('metrics'):
    correct_prediction = tf.equal(tf.argmax(y, 1), tf.argmax(logits, 1))
    accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
    tf.summary.scalar('accuracy', accuracy)

#Juntamos todos os súmarios em unica operação
merged_summary_op = tf.summary.merge_all()

#Aqui definimos nosso otimizador para fazer atualização dos pesos e convergencia
optimizer = tf.train.GradientDescentOptimizer(learning_rate).minimize(cost)

#Isso é utilizado para inicializar todas as variáveis globais
init = tf.global_variables_initializer()

#Agora vamos dar inicio ao treino da rede
with tf.Session() as sess:

    sess.run(init)

    global_step = 0

    #operação para escriver os logs no tensorflow
    train_writer = tf.summary.FileWriter(train_logs_path,graph=sess.graph)
    test_writer = tf.summary.FileWriter(test_logs_path,graph=sess.graph)

    total_batch = round(len(x_train)/batch_size)+1
    #Aqui vamos iterar em cada epoch
    for epoch in range(epochs):

        for i in range(total_batch):
            global_step += 1

            if global_step % total_batch == 0:
                acc,summary = sess.run([accuracy,merged_summary_op],feed_dict={x:x_test,y:y_test})
                test_writer.add_summary(summary, global_step)

            else:

                batch_x = x_train[i*batch_size:(i*batch_size) + batch_size]
                batch_y = y_train[i*batch_size:(i*batch_size) + batch_size]

                if len(batch_x) != 0:
                    _,c,summary,acc = sess.run([optimizer,
                                               cost,merged_summary_op,accuracy],
                                               feed_dict={x:batch_x,y:batch_y})

                    train_writer.add_summary(summary, global_step)

                    print(c)

Quando você executar o código, perceba que foi criada uma pasta summaries, caso não exista, caso contrário repare que dentro da pasta estão sendo salvo os arquivos de treino e teste dos respectivos experimentos.

Agora para ativar a Tensorboard podemos chama-la pelo cmd, para quem utiliza anaconda basta utilizar o anaconda prompt de dentro do diretório que você executou o código.

foto_1

Apos executar o código você verá o endereço do servidor que ela faz na sua maquina.

foto_2

Agora basta abrir a pagina e você verá uma tela bem parecida com a tela abaixo:

foto_3.png

Repare nas opções na parte superior, vamos discorrer um pouco sobre cada uma:

Scalars: Aqui fica armazenado todos os escalares que coletamos em nosso processo de treino da rede, repare que temos a loss e a accuracy do treino e do teste. Eu sei que parece estranho ele ter pegado a loss do test sendo que não executamos a função de otimização para ele, mas existe uma explicação não tão intuitiva para isso e tentarei explicar em futuros posts essas questões de operações internas.

foto_4

Graphs: Aqui temos os grafo de nosso modelo, repare que temos a operação placeholder de nome input o tensor que entra por esse node vai tanto para o layer de input do modelo como para operação de treino gradient pois ele é utilizado tanto para gerar os outputs do modelo quanto para fazer o calculo do gradiente.

foto_5

Irei discorrer apenas um pouco nesse post sobre como interpretar os grafos, irei discutir mais sobre isso em post futuros.

Uma coisa que podemos fazer para visualizar melhor nosso modelo é clicar em cima de algum namespace e tira-lo do grafo principal, no caso você pode clicar em metrics com o botão direito do mouse e selecionar a opção remove from main graph faça a mesma coisa com gradients, o placeholder Mean lá no final e train_labels que esta abreviado como train_lab…

o grafo agora ficara da seguinte forma:

foto_6.png

Você pode dar um click duplo em um namespace para ver as operações que tensores que ocorrem dentro dele. Irei clicar no namespace input_layer para vermos como os tensores e operações interagem ali dentro.

foto_7.png

Veja que dentro do Namespace input_layer temos um tensor contendo os vetores de input do dataset, esses logo em seguida são multiplicados pelos pesos ou Kernel, isso acontece na operação MatMul e logo em seguida isso gera um tensor de output que é adicionado de uma Bias na operação BiasAdd e em seguidas aplicamos a função logística sigmoid no operação Sigmoid.

Se você já tem uma base de como funciona uma rede será bem fácil compreender como essas operações funcionam. Caso contrário te aconselho a dar uma estudada no assunto um livro em português que eu pessoalmente achei bem interessante é Ciência de Dados e Aprendizado de Máquina ele é bem detalhado e com códigos bem esplicados ideal para que quer já começar colocando a mão na massa.

Histogram: Nessa aba podemos ver a distribuição dos outputs de um determinado layer ou da própria rede é interessante para analisar o comportamento do treino e achar problemas em alguns layers.
Falarei mais sobre histograms am posts futuros.

foto_8.png

E isso finaliza introdução básica a Tensorboard.

E caso Tenha visto algum erro, inconsistência ou algo de difícil entendimento não exite em deixar nos comentários pois estará ajudando a melhorar a qualidade do post para os próximos leitores.

Espero que tenha ajudado vocês e ate o próximo post.

Introdução ao PCA

Olá pessoas, faz bastante tempo que não escrevo e hoje resolvi trazer um conceito antigo, mas mesmo assim bastante utilizado. Que é o Principal Component Analysis ( Análise da componente principal).

Essa técnica é bastante utilizada para reduzir as dimensões do conjunto de dados sem perder tanta informação no processo. E quando bem utilizada pode nos dar um excelente desempenho tanto em ganhos de acurácia quanto em custos computacionais.

A ideia é reduzir a quantidade de dimensões que um conjunto de dados possui. Por exemplo: imagine um conjunto de dados com 1000000 colunas, uma quantidade enorme de colunas assim pode inviabilizar a manipulação e análise desses dados de forma viável. Claro que atualmente temos os grandes computadores que podem sim nos permitir lidar com uma quantidade absurda de dados de forma direta, mas essa nem sempre é a melhor opção quando precisamos usar as informações e aplicar transformações em ambiente de produção.

Uma forma de lidar com isso pode ser reduzindo as dimensões desse conjunto de dados de modo a conseguir obter um com menos dimensões, porém com quase a mesma quantidade de informação. Dessa forma podemos lidar melhor com ele.

Uma forma de lidar com isso pode ser reduzindo as dimensões desse conjunto de dados de modo a conseguir obter um com menos dimensões, porém com quase a mesma quantidade de informação. Dessa forma podemos manejá-lo melhor.

Vamos começar com o conjunto de dados abaixo:

tabela_1

O primeiro passo a fazer é zerar a média das duas colunas. Como fazemos isso?. Basta subtrair a média da coluna de todos os elementos que compõem a coluna

Vamos definir a média de x como \bar{x} e de y como \bar{y} e depois vamos subtrair esses valores de cada elemento da respectiva coluna.

Acredito que se você esta lendo esse post com certeza sabe como calcular uma média. Então vamos defini-las como:

\bar{x} = 1.81 \bar{y} = 1.91

isso feito. Vamos subtrair esses valores de cada elemento de sua respectiva coluna.

tabela_2

Esse tipo de normalização é conhecida como “média zero”. esse tipo de normalização é exigido por alguns algoritmos. Mas não falemos disso agora.

Ó próximo passo é calcular a covariância dessa duas variáveis, para isso utilizaremos a formula da covariância aplicada uma amostra:

cov(X,Y) = \frac{\sum _{i=0}^{n}(X_{i} - \bar{X})*(Y_{i} - \bar{Y})}{n - 1}

A covariância nos mostra o quanto as dispersões de duas variáveis estão relacionadas. Antes de eu falar mais sobre esse índice vamos plotar o gráfico dessas variáveis. Assim as coisas ficarão mais claras.

Figure_1

Repare que á medida que a variável x cresce a variável y também cresce, isso nos diz que ambas possuem um covariância positiva.

Vamos ver se isso é verdade, por praticidade já irei colocar o valor final da covariância que é:

cov(x,y) = 0.6154444

Fica claro que eles crescem em proporção parecida.

Porem para aplicar o PCA precisamos passar isso para a forma matricial, repare que temos duas colunas. Vamos permutar as colunas da seguinte forma:

cov(x,y) = \begin{bmatrix} cov(x,x)& cov(x,y) \\ cov(y,x)& cov(y,y) \end{bmatrix}

Isso quer dizer que iremos calcular a covariância para cada combinação de colunas possível considerando a ordem. Pode não fazer muito sentido agora mas já irá entender o porque dessa operação.

Logo teremos a seguinte matriz:

cov(x,y) = \begin{bmatrix} 0.61655556& 0.61544444 \\ 0.61544444&0.71655556 \end{bmatrix}

Essa é a chamada matrix de covariância, na diagonal da esquerda para a direita temos a covariância da variável com ela mesma.(o desvio padrão.). enquanto na outra temos a covariância das duas variáveis.

A próxima etapa desse processo é calcular os autovalores e autovetores dessa matriz. Porem antes vamos dar uma relembrada básica nesses dois conceitos.

Autovetores e autovalores

A ideia

Vamos partir de um exemplo simples.

Suponha o seguinte vetor \begin{bmatrix} 3\\2 \end{bmatrix} podemos considerar o primeiro elemento como x e o segundo como y e podemos plotar ele no plano. Supomos que parta da origem.

Figure_2

Agora considere a matriz \begin{bmatrix} 2 & 3 \\ 2 & 1 \end{bmatrix} . iremos multiplicar essa matriz por aquele vetor, logo:

\begin{bmatrix} 2 & 3 \\ 2 & 1 \end{bmatrix} * \begin{bmatrix} 3\\ 2 \end{bmatrix} = \begin{bmatrix} 12\\ 8 \end{bmatrix}

Vamos plotar o vetor  resultante \begin{bmatrix} 12\\ 8 \end{bmatrix} no mesmo plano e ver o resultado:

Figure_3

Repare o que temos. Podemos traçar uma reta que passa pela origem e pelos dois pontos que representam os vetores.

Se multiplicar o vetor resultante  \begin{bmatrix} 12\\ 8 \end{bmatrix} pela mesma matriz outra vez obteremos o vetor \begin{bmatrix} 48\\ 32 \end{bmatrix} .

Repare que multiplicar o vetor \begin{bmatrix} 3\\ 2 \end{bmatrix} pela matriz \begin{bmatrix} 2 & 3\\ 2 & 1 \end{bmatrix} é o mesmo que multiplicar esse vetor por um escalar, que no nosso caso é 4.

Ou seja. Eu posso facilmente substituir a matriz pelo número 4.

Nesse caso um dos autovalores e um dos autovetores da matriz \begin{bmatrix} 2 & 3\\ 2 & 1 \end{bmatrix}  são respectivamente 4 e  \begin{bmatrix} 3\\ 2 \end{bmatrix}.

Existem muito mais definições em torno desse assunto, porem minha proposta aqui é apenas demonstrar uma introdução ao mesmo.

Outro ponto importante é como calcular os autovalores e os autovetores de uma matriz.

Introdução ao calculo de autovetores e autovalores

Como se trata apenas de uma introdução vamos utilizar o caso da matriz quadrada, vamos antes a algumas definições.

Dada uma matriz A seus autovalores \lambda e seu autovetores v são respectivamente os escalares e vetores que satisfazem a seguinte equação:

Av = \lambda v

Que podemos escrever da seguinte forma:

Av = \lambda v I

Onde I e a matriz identidade de ordem n x n.

Podemos assumir que:

Av - \lambda v I = 0

Fatorando temos:

(A - \lambda I)v = 0

E em seguida nos temos que satisfazer a equação:

det(A - \lambda I) = 0

Isso é.  a matriz resultante (A - \lambda I) tem que ser uma matriz singular, não é inversível.

Não irei entrar muito em detalhes sobre os procedimentos citados acima, pois isso iria consumir pelo menos mais uma 5 paginas, porem o mais importante é ultima equação. Pois usaremos ela para achar os autovalores de uma matriz e a partir disso os autovetores.

Vamos usar como exemplo a matriz \begin{bmatrix} 2 &3 \\ 2& 1 \end{bmatrix} .

Logo temos que achar que satisfaça a seguinte equação:

\begin{bmatrix} 2 &3 \\ 2& 1 \end{bmatrix} - \lambda \begin{bmatrix} 1 & 0\\ 0& 1 \end{bmatrix} = 0

o que nos da:

\begin{bmatrix} 2 &3 \\ 2& 1 \end{bmatrix} - \begin{bmatrix} \lambda & 0\\ 0& \lambda \end{bmatrix} =\begin{bmatrix} 2 - \lambda & 3 \\ 2 & 1 - \lambda \end{bmatrix} = 0

Como essa matriz tem que ser singular temos que:

det(\begin{bmatrix} 2 - \lambda & 3 \\ 2 & 1 - \lambda \end{bmatrix}) = 0

E então temos que resolver a seguinte equação do segundo grau:

\lambda^2 - 3\lambda - 4 = 0

Como sabemos uma equação do segundo grau possui duas raízes se o delta for maior que zero, uma se for igual ou nem uma se for menor que zero. Isso dentro do conjunto dos números Reais.

Logo temos que os dois autovalores dessa matriz são:

\lambda_{1} = 4\lambda_{2} = -1

Com isso em mãos podemos voltar e resolver a equação (A - \lambda I)v =0.

Sabemos que:

(A - \lambda I) = \begin{bmatrix} 2 - \lambda & 3 \\ 2 & 1 - \lambda \end{bmatrix}

Então temos que resolver:

\begin{bmatrix} 2 - \lambda & 3 \\ 2 & 1 - \lambda \end{bmatrix}v = 0

vamos definir o vetor v  como \begin{bmatrix} x\\ y \end{bmatrix} e teremos que resolver o seguinte sistema:

\begin{bmatrix} 2 - \lambda & 3\\ 2 & 1 - \lambda \end{bmatrix} \begin{bmatrix}x\\y \end{bmatrix} = 0

A partir da multiplicação de matrizes acima teremos o seguinte sistema linear para resolver:

\left\{\begin{matrix} x(2 - \lambda) + 3y = 0 \\ 2x + y(1 - \lambda) = 0 \end{matrix}\right.

para resolver o sistema linear acima vamos substituir o \lambda pelo número correspondente, não irei fazer a resolução passo a passo se você não sabe como resolver um sistema linear aconselho a estudar algebra linear que é  pré requisito básico para um aspirante a engenheiro de machine learning ou afim.

Mas tem um coisa com esse sistema em particular, se querermos achar a solução iremos no deparar com o vetor nulo, a ideia é achar uma função linear que gere os autovetores e pegamos a base.

Para \lambda = 4

temos que:

2x = 3y \Rightarrow x = \frac{3y}{2}

logo se pegarmos o vetor \begin{bmatrix} x\\ y \end{bmatrix} podemos associar valores a xy , no caso temos a sequencia de vetores:

\begin{bmatrix} \frac{3}{2}\\2 \end{bmatrix} e \begin{bmatrix} 3\\2 \end{bmatrix} e \begin{bmatrix} \frac{9}{2}\\3 \end{bmatrix} ... \begin{bmatrix} \frac{3y}{2}\\y \end{bmatrix}

Faça o teste e multiplique a matriz acima por qualquer um desses vetores, vera que é como se o estivesse multiplicando por quatro.

O primeiro vetor geralmente é o que nos interessa.

Para \lambda = -1 temos:

2x = -2y \Rightarrow x = -y

Então basta que o vetor tenha elementos com sinais opostos, no caso um dos autovetores seria:

\begin{bmatrix} -1\\ 1 \end{bmatrix}

Ou seja seria como multiplicar o vetor por -1. faça o teste!!!

 

 

Voltando ao nosso problema

Agora que você tem um base, mesmo que básica, de como funciona o calculo de autovetores e autovalores podemos aplicar em nossa matriz.

\begin{bmatrix} 0.61655556 & 0.61544444 \\ 0.61544444 & 0.71655556 \end{bmatrix}

Teremos os seguintes resultados:

autovalores = \begin{bmatrix} 0.490833989\\ 1.28402771 \end{bmatrix}

Coloquei em forma de vetor mas a ideia é a mesma, no caso são as raízes da equação de segundo grau resultante do desenvolvimento da equação principal.

autovetores = \begin{bmatrix} -0.735178656 & -0.677873399 \\ 0.677873399 & -0.735178656 \end{bmatrix}

É importante lembrar que os dois vetores acima, são vetores unitários, isso é como uma normalização para que sempre tenhamos vetores de modulo igual 1. Não expliquei sobre isso pois considero elementar.

Vamos plotar esses dois vetores nos dados em que tiramos á média para ver uma coisa interessante.

Figure_5

Então temos a tão famosa analise das componentes principais. Os dois vetores acima são perpendiculares um ao outro, infelizmente teve uma distorção no plot que ate o momento não tive paciência para resolver.

Repare que temos um dos vetores que se ajusta aos dados, enquanto o outro é perpendicular a ele, um nos da á informação de como os pontos estão organizados em torno de uma reta, enquanto o outro nos diz o quanto eles estão dispersos em torno do primeiro componente.

Mas ainda não acabou. Vamos utilizar esses vetores para reduzir as dimensões de nosso conjunto de dados.

Para passar os nossos dados para uma unica dimensão podemos utilizar a seguinte formula:

FD = DS * C

Onde FD é o resultado final, C é o vetor de componente principal selecionado e DS é nosso conjunto de dados ajustado. No nosso caso teremos que multiplicar a matriz que representa os dados pelo vetor ou matriz das componentes principais. 

No caso o vetor selecionado é geralmente aquele com o maior autovalor, se repararmos o segundo elemento do vetor de autovalores é aquele que tem o maior valor e ele representa á segunda coluna da matriz de autovetores, logo selecionamos o segundo autovetor para ser multiplicado pelo nossos dados em formato de matriz.

tabela_2

Ou seja pegamos cada linha da tabela acima e multiplicarmos por \begin{bmatrix} -0.677873399\\ -0.735178656 \end{bmatrix}  , basta lembrar do conceito de multiplicação de matrizes “linha vezes coluna”.

Depois disso iremos obter o seguinte conjunto de dados:

tabela_3

O que nos temos aqui é a melhor representação possível daqueles dados em uma dimensão,  por mais que seja uma aproximação.

É importante levar em conta que a Analise da Componente Principal deve ser usada com cautela e depois de um estudo mais robusto do conjunto de dados. Pois quanto mais perto de zero a covariância entre duas variáveis menos informações iremos conseguir preservar quando reduzirmos as dimensões. Alem claro de várias outras ideias derivadas desse conceito.

O post de hoje foi apenas um introdução superficial a esse conceito, e deve ser visto como um ponto de partida apenas.

Se você chegou ate aqui. Primeiramente obrigado e caso queira realmente se tornar um Engenheiro de Machine Learning é preciso também se aprofundar aos conceitos teórico. Pois existem momentos em que é preciso tomar decisões especificas que demandam um certo nível de pericia de quem esta manipulando o ferramental.

Se você gostou do assunto e ainda esta iniciando no mundo da manipulação e analise de dados um curso muito interessante para acelerar seu desenvolvimento que indico é:

Workshop in Probability and Statistics

Ate o próximo post. Qualquer duvida ou critica construtiva ou erro no poste não exite em deixar nos comentários.

Perceptron multicamadas com TensorFlow

Ola meus queridos oito leitores, sei que faz tempo que não publico mais nada, mas infelizmente o tempo tem se tornado um recurso escasso nos últimos meses devido a isso se tornou raro o tempo que paro para escrever alguma coisa.

Isso dito. Hoje resolvi escrever sobre uma ferramenta que você que se interessa pela área com certeza já ouviu falar. O TensorFlow.

Nada melhor que a própria descrição do site oficial para descrever a proposta de ferramenta:

TensorFlow™ is an open source software library for numerical computation using data flow graphs. Nodes in the graph represent mathematical operations, while the graph edges represent the multidimensional data arrays (tensors) communicated between them. The flexible architecture allows you to deploy computation to one or more CPUs or GPUs in a desktop, server, or mobile device with a single API. TensorFlow was originally developed by researchers and engineers working on the Google Brain Team within Google’s Machine Intelligence research organization for the purposes of conducting machine learning and deep neural networks research, but the system is general enough to be applicable in a wide variety of other domains as well.

Em síntese uma ferramenta ideal para se criar e aplicar modelos baseados em Redes Neurais.

Um dos desafios de aprender TensorFLow é entender sua mecânica, e foi pensando nisso que resolvi escrever esse post hoje, nele iremos criar uma rede simples mas poderosa quando bem aplicada.

A rede de hoje é o Multilayer Perceptron, um dos modelos mais simples de Redes Neurais mas mesmo assim muito poderoso quando bem usado.

imagem_1

É importante ressaltar também que não irei me ater aos aspectos teóricos da rede, prometo que faço isso nos próximos posts, mas sim na mecânica do tensorFlow e como podemos utiliza-lo para arquitetar e treinar nosso modelo.

O dataset que iremos utilizar hoje é o iris.data que pode ser conseguido aqui. Disponibilizei no Drive para evitar problemas de versões entre datasets.

Então vamos ao que interessa:

Primeiro vamos começar importando as libs e abrindo o dataset que iremos utilizar:

import tensorflow as tf

#Usaremos o pandas para algumas transformações basicas
import pandas as pd

#Numpy apenas para deixar as coisas mais rapidas
import numpy as np

#Irei utilizar essa função do sklearn para dividir o conjunto de treino e teste
from sklearn.cross_validation import train_test_split

#As metricas básica acuracia e confusio_matrix
from sklearn.metrics import confusion_matrix,accuracy_score

#Usamos pandas para abrir o dataset
data = pd.read_csv("iris_data.csv")

Isso feito vamos definir algumas funções para preparar o dataset para treino:

#Definimos essa função para normalizar o coluna Species
#Exemplo [preto,branco,preto,azul] => [0,1,0,2]
def normalizeIris(x):
    if x == "virginica":
        return 0
    elif x == "versicolor":
        return 1
    else:
        return 2

#fazemos a função para transforma as labels em one-hot vectors
#Exemplo [1,2,1,0] => [[0,1,0],[0,0,1],[0,1,0],[1,0,0]]
def makeHotvector(y_data):
    labels = []
    for x in range(len(y_data)):
        labels.append([0,0,0])
        labels[x][y_data[x]] = 1
    y_data = np.array(labels,dtype=np.float64)
    return y_data

Aconselho a dar uma explorado no dataset caso não conheça. Agora iremos aplicar as funções de normalização e preparar o dataset para treino.

#Usamos o método .apply do Pandas para aplicar a função normaLizeIris
#Na coluna "Species" lembre o que essa função faz
data["Species"] = data["Species"].apply(normalizeIris)

#Aqui separamos as features em uma variavel e as labels em outra
x_data = data.drop("Species",axis=1).values
y_data = data["Species"].values

#Aqui separamos um pouco do dataset para treino e a outra parte para teste
x_train,x_test,y_train,y_test = train_test_split(x_data,y_data,test_size=0.2)

Agora iremos começar a arquitetar a rede. Nossa rede possui dois hidden layers, o primeiro com 5 Nodes e o segundo com 3 Nodes, 4 inputs e 3 outputs como mostrado no esquema abaixo.

imagem_2

Essa não é a rede mais apropriada para esse tipo de problema, pois estamos lidando com uma tarefa de classificação de múltiplas classes, porem já que se trata de um dataset simples é perfeitamente possível treinarmos usando apenas a função logística sigmoide.

Obs: Na prática é muito comum colocar uma função softmax na saída da rede.

Se você reparou, acima transformamos os dados em one-hot-vectors, isso significa que cada output tem que ser um vetor do tipo [1,0,0],[0,1,0],[0,0,1] representando respectivamente a espécies 0, 1 e 2.

Essa estratégia de treino é chamada one-vs-rest assim podemos treinar a rede para fazer aproximações de um output específico.

Isso dito vamos começar a montar nossa rede, o primeiro passo é definir alguns parâmetros.

#A quantidade inputs na rede
n_input = 4
#A proporção em que a rede irá atualizar cada peso
#Chamamos também de alpha
learning_rate = 0.01
# A quantidade de unidades no primeiro layer
n_hidden_unites_1 = 5
#A quantidade de unidades no segundo layer
n_hidden_unites_2 = 3

O próximo passo tem aver com a mecânica do TensorFlow, quando montamos uma estrutura nele ela não será compilada em tempo de execução como estamos acostumados, mas sim será compilada primeiro e só depois começara a rodar de fato o programa, isso permite um desempenho muito melhor.

Então antes de começar a montar seu grafo é necessário instanciar uma sessão, pois dizemos que esse grafo pertence a uma determinada seção e assim ele pode reproduzir todas as operações necessárias.

Você pode ver um pouco como isso funciona nesse post: Uma introdução ao Tensorflow e suas operações básicas.

#Vamos criar o grapho da rede
#Primeiro definimos a sessão
sess = tf.Session()

Isso feito vamos a primeira parte da rede:

#Aqui são os inputs da rede
#Apenas os inputs do dataset são considerados.
X = tf.placeholder(shape=[None,n_input],dtype=tf.float64)

#Aqui ficam as Labels para treino
#Essa parte do grafo não aparece no esquema.
#mas aqui fica as labels que serão usadas como exemplo para a rede.
y_ =  tf.placeholder(dtype=tf.float64)

Abaixo a representação do placeholder X no esquema.

imagem_3

Agora iremos definir os pesos da nossa rede:

#Agora iremos definir os pesos da rede
#Esse é nosso primeira camada de pesos.
W = {"h1":tf.Variable(tf.random_normal([n_input,n_hidden_unites_1],dtype=tf.float64),dtype=tf.float64),
     "h2":tf.Variable(tf.random_normal([n_hidden_unites_1,n_hidden_unites_2],dtype=tf.float64),dtype=tf.float64)
    }

Os pesos nada mais são do que matrizes com o número de linhas igual ao número de inputs e o número de coluna igual a quantidade de unidades no primeiro hidden-layer.

imagem_4

Assima vemos a localização dos dois hidden-layers na rede. Uma outra maneira de representar isso seria por meio de matrizes.

Abaixo um exemplo da representação do primeiro conjunto de pesos, se multiplicar-mos esse vetor por essa matriz iremos obter um vetor contendo 5 elementos que é justamente a quantidade de unidades que nossa rede possui depois disso basta somar com os biases e aplicar a função sigmoide a cada um desses resultados.

\left [ x_{1},x_{2},x_{3},x_{4} \right ] * \begin{bmatrix} w_{1,1} &w_{1,2} &w_{1,3} &w_{1,4} & w_{1,5} \\ w_{2,1} &w_{2,2} &w_{2,3} &w_{2,4} &w_{2,5} \\ w_{3,1} &w_{3,2} &w_{3,3} &w_{3,4} &w_{3,5} \\ w_{4,1}&w_{4,2} &w_{4,3} &w_{4,4} &w_{4,5} \end{bmatrix}

E claro temos que definir os biases:

#Esse é o bias da rede
b = {"b1":tf.Variable(tf.random_normal([n_hidden_unites_1],dtype=tf.float64),dtype=tf.float64),
     "b2":tf.Variable(tf.random_normal([n_hidden_unites_2],dtype=tf.float64),dtype=tf.float64)
    }

Podemos acrecentar a localização deles ao nosso esquema:

imagem_5

Agora definimos as funções de ativação de cada camada. como se trata de um problema simples usaremos somente a sigmoide.

#Sigmoide aplicado a rede
#O output do hidden_1
out_hidden_1 = tf.nn.sigmoid(tf.matmul(X,W["h1"])+b["b1"])
#o output do hidden_2 que alias será o usado como nosso output.
out_hidden_2 = tf.nn.sigmoid(tf.matmul(out_hidden_1,W["h2"])+b["b2"])

Podemos ver a representação no esquema.

imagem_6

Isso definido vamos cuidar do treino dessa rede.

No caso o que temos abaixo é uma média do erro do output da rede em relação a label usada como exemplo, usamos isso para orientar a atualização dos pesos na rede.

#Definimos o erro quadratico do out_hidden_2 em relação y_
#isso permite deriva-lo e depois ajustar os pesos
mse = tf.losses.mean_squared_error(y_,out_hidden_2)

#Aqui eu uso o método gradientDescent para otimizar o parametro mse
#Ele ira derivar o erro em relação a cada pesso da rede e fazer um atualização
#para cima ou para abaixo proporcional ao learning_hate da rede
train_step = tf.train.GradientDescentOptimizer(learning_rate).minimize(mse)

Aqui iremos inicializar todas as variaveis que compõe o grafo.

#Uma maneira de carregar as variaveis para a memoria.
init = tf.global_variables_initializer()
sess.run(init)

Isso permite ao TensorFlow inicializar todas as operações que compoe o grafo gerando assim o output.

Agora vem uma das partes mais importantes que é a forma que a rede ira ser treinada. Eu escolhi um loop dentro de um loop onde o algoritmo treina de forma incremental exemplo por exemplo (online learning.) porem de maneira aleatória.

#Realizar 1000 interações no dataset
for x in range(1000):
    print(x)#para ver o andamento do treino

    #Iremos dar loop do tamanho do dataset
    for _ in range(len(x_train)):
        #Aqui seleciono uma instancia aliatória do dataset
        i = np.random.randint(len(x_train))
        x = np.array([x_train[i]])
        y = np.array([y_train[i]])

        #Aqui treinamos a rede uma iteração de cada vez
        train_step.run(feed_dict={X:x,y_:y},session=sess)

Observe a forma que a rede é treinada. Ela minimiza o erro exemplo por exemplo isso é conhecido camo Stochastic gradient descent.

Depois de treinar a rede podemos ver alguns indices básicos de acuracia.

#Aqui iremos medir a precisão do modelo
#colocamos a rede para validar os outputs no conjunto de teste
outputs = out_hidden_2.eval(feed_dict={X:x_test},session=sess)

#Aqui convertemos de one-hot-vector para discreto
labels = [x.argmax() for x in y_test]
predictions = [x.argmax() for x in outputs]

#aplicamos confusion matrix para ver o desepenho
cm = confusion_matrix(labels,predictions)

#aqui vemos a acuracia do algoritmo.
acuracy = accuracy_score(labels,predictions)

print(cm)
print(acuracy)

acuracia fica em torno de 98% o que não é nada mal dada a simplicidade da rede.

Muito bém pessoas esse foi o post de hoje, só pesso que tomem cuidado caso forem aplicar esse conceito em algo real pois é necessário normalizar os dados antes de qualquer coisa.

Outra observeção a quanto a verção do tensorFlow que você esta usando pois algumas funções podem estar com nomes diferentes

Expero que tenha sido util para você que esta estudando essa tecnologia e bom estudos.

Uma introdução ao TensorFlow e suas operações básicas

Ola pessoas. Se você veio ate aqui já deve ter uma ideia do que se trata o post de hoje. A um tempo que venho estudando TensorFlow e aprendendo aos poucos a sua lógica.

Primeiramente tentaremos definir o porque do nome TensorFlow. Eu acho que é provavelmente devido ao uso intenso das estruturas chamada Tensores.

Meio óbvio. Mas o que será realmente um Tensor? Bem definir um tensor de maneira formal não é uma tarefa fácil.

Mas vamos a algumas definições e exemplos básicos você verá que entender de maneira intuitiva não é uma missão tão difícil.

Mas vamos aproveitar e definir algumas estruturas mais básicas.

Vetores

Para quem já tem intimidade com Álgebra linear já é acostumado com essa estrutura.

Podemos por exemplo representar a linha ou coluna de uma tabela por meio de um vetor.

Imagem_1

A primeira linha da tabela pode ser representada por um vetor:

X_{n} = (20,12,13,1) ou X_{n} = \begin{bmatrix} 20\\ 12\\ 13\\ 1 \end{bmatrix}

Podemos representar essa estrutura de dados em Python e você já deve saber como:

A = [20,12,13,1]

Vamos parar por aqui e analisar mais uma estrutura interessante.

Matrizes

Umas das estruturas mais utilizadas em Machine Learning e principalmente Deep Learning é a tão famosa matriz.

Podemos representar toda a tabela acima por meio de uma matriz:

A_{i,j} = \begin{bmatrix} 20&12&13&1 \\ 20&13&5&0 \\ 15&10&12&0 \\ 3&9&6&1 \end{bmatrix}

Agora podemos nos referir a cada elemento dessa matriz por meio de um índice duplo. Por exemplo o elemento A_{2,3} é 5.Ou seja o terceiro elemento da segunda linha.

Também podemos representar essa estrutura em Python:

A = [[20,12,13,1],
 [20,13,5,0],
 [15,10,12,0],
 [3,9,6,1]
 ]

E claro podemos ter acesso aos seus elementos quase da mesma forma.

In [1]: A[1][2]
Out[4]: 5

Difere apenas no fato de que na maioria de linguagem de programação os indices começam em 0 enquanto na matemática começa e 1.

Isso dito vamos a uma estrutura não tão popular assim.

Tensor

Suponha que você queira representar uma imagem por meio de uma lista ou array. Suponha a imagem abaixo.

Imagem_2

Apenas a título de exemplo suponha que tenhamos a ideia de representar essa imagem usando um padrão RGB e para facilitar ainda mais as coisas suponha que seja uma imagem 4 por 4.

Imagem_3

Acima temos uma Matriz de rank 3. Em outras palavras um tensor, claro que a definição do ponto de vista matemático seria bem mais sofisticada que isso, mas por hoje podemos nos limitar a definir um tensor como uma matriz de 3 ou mais dimensões.

Uma outra forma de representar a imagem acima seria:

Imagem_4.jpg

Se pararmos para pensar. No ponto de vista da programação as estruturas de dados vistas aqui são bem semelhantes e podemos representá-las de várias maneiras diferentes.

Podemos, por exemplo, representar uma lista de duas dimensões(Matriz) por meio de um vetor.

In [1]: import numpy as np
In [2]: matrix = np.array([[1,2,3],[4,5,6]])
In [3]: vector = matrix.ravel()
vector
Out[4]: array([1, 2, 3, 4, 5, 6])

Entender essa estrutura é essencial para tirar o máximo proveito das redes neurais artificiais e sistemas de aprendizado mais complexos.

Imagem_5

Operações básicas em TensorFlow

É importante lembrar que o TensorFlow não compila e executa o programa ao mesmo tempo, ele primeiro compila e depois executa o programa, para isso temos a classe Session. Veja o exemplo abaixo onde multiplico dois números.

 

import tensorflow as tf

#Primeiro definimos os números

#criamos uma constante.
x_1 = tf.constant(2.0,dtype=tf.float32)

#A função tf.constant cria nossas constantes
x_2 = tf.constant(5.0,dtype=tf.float32)

#Aqui multiplicamos os dois resultados.
mult = x_1*x_2

#Aqui somamos as duas constante
soma = x_1+x_2

# Toda vez que quisermos executar usamos o método run
# Isso nos permite compilar e executar o código solicitado.
sess = tf.Session()

#Aqui executamos a multiplicação
print(sess.run(mult))

#Aqui executamos a soma.
print(sess.run(soma))

Quando rodar isso terá o seguinte resultado:

10.0
7.0

Na primeira linha o resultado da multiplicação e na segunda o resultado da soma.

O processo de soma e multiplicação de vetores não é muito diferente, porem é preciso lembrar das propriedades das operações com vetores.

criamos uma variável constante.
vetor_1 = tf.constant([2.0,3.0,7.0],dtype=tf.float32)

#A função tf.constant cria nossas constantes
vetor_2 = tf.constant([5.0,2.0,6.0],dtype=tf.float32)

#Aqui multiplicamos os dois vetores
#Utilizei as funções multply e reduce_sum.
#multply: multplica duas array elemento por elemento.
#reduce_sum: faz o somatório dos elementos de um vetor
mult = tf.reduce_sum(tf.multiply(vetor_1,vetor_2))

#Aqui somamos os dois vetores de forma tradicional
soma = vetor_1+vetor_2

# Toda vez que quisermos execultar o métono run
# Isso nos permite compilar e execultar o codigo solicitado.
sess = tf.Session()

#Aqui execultamos a multiplicação
print(sess.run(mult))

#Aqui execultamos a soma.
print(sess.run(soma))

Pronto fizemos as operações básicas com vetores basta rodar e ver o resultado.

58
[7. 5. 13.]

Vamos analisar uma outra função a tf.add essa função nos permite ligar de forma intuitiva vários Nodes de um grafo. Vejamos um exemplo simples.

Vamos processar a seguinte estrutura representada no esquema abaixo.

Imagem_6

Nesse exemplo duas variáveis x_{1}x_{2} entram como input e são somadas e logo depois gerado o output.

#tf.placeholder funciona como um input.
#isso quer dizer que essas variáveis virão de fora
x_1 = tf.placeholder(dtype=tf.float32)

#Aqui os valores entrarão somente ná hora que
# forem execultados.
x_2 = tf.placeholder(dtype=tf.float32)

output = tf.add(x_1,x_2)

# Toda vez que quisermos execultar o métono run
# Isso nos permite compilar e execultar o codigo solicitado.
sess = tf.Session()

#Aqui execultamos a soma.
#Repare que passamos os valores para os placeholders na forma
# de um dict. Eu acho isso interessante pois facilita a comunicação
# com a API.
print(sess.run(output,{x_1:1,x_2:2}))

Quando executar terá o seguinte output:

3.0

Vamos a uma estrutura um pouco mais intrigante.

Imagem_7

Nesse caso temos uma estrutura semelhante ha de uma rede neural artificial, claro falta algumas coisas, mas como o foco é o processamento da informação isso não é um problema.

Numerei os Nodes de 1 a 3 os inputs entram e são somados em cada Node e depois o resultado de cada um é multiplicado por uma constante e o output sai no utimo Node.

#Vamos declarar os inputs
x_1 = tf.placeholder(dtype=tf.float32)

x_2 = tf.placeholder(dtype=tf.float32)

#Crio o primeiro Node
#Ele soma os dois inputs
Node_1 = tf.add(x_1,x_2)

#Crio o segundo Node
Node_2 = tf.add(x_1,x_2)

#No terceiro Node esses valores entram Multplicados
#Pelos seus respectivos pesos
Node_3 = tf.add(Node_1*2,Node_2*5)

# Toda vez que quisermos execultar o métono run
# Isso nos permite compilar e execultar o codigo solicitado.
sess = tf.Session()

#Aqui execultamos a soma.
#Repare que passamos os valores para os placeholders na forma
# de um dict.
print(sess.run(Node_3,{x_1:8,x_2:9}))

 

O output deve ser:

119.0

Lembro que isso é apenas uma introdução simplista ao TensorFlow. A ideia aqui e te mostrar uma ligação entre os FrameWork e a Álgebra Linear.

Na Prática é comum trabalha com a ideia de  vetores e matrizes para fazer as operações de treinamento de um algoritmo.

Aconselho que antes de usar o TensorFlow domine muito bem os conceitos teóricos relacionados ao algoritmo, pois isso irá lhe permitir usar ao máximo o poder que essa ferramenta tem a oferecer.

Muito bem por hoje é só. Ate a próxima e bons estudo.

E como sempre qualquer dica, critica e inconsistencia no post deixe nos comentários o intuito é sempre a melhora do conteudo.

Perceptron em Python

Dando continuidade ao assunto sobre redes neurais artificiais, vamos analisar hoje a implementação do Perceptron em Python.

Caso você ainda não entenda o algoritmo de forma conceitual, aconselho a ler o post Introdução ao Perceptron passo a passo.

Como vimos, o perceptron é um modelo linear em classificação, ele só lida bem com conjuntos linearmente separáveis; mas, quando usado em conjunto com outros algoritmos pode gerar resultados interessantes. Existem variações dele para regressão também.

Isso dito, a tarefa de hoje consiste em classificar flores. Para isso, iremos usar o clássico dataset íris, com apenas duas classes de flores você pode consegui-lo aqui: Iris.csv.

O dataset está salvo em formato csv, mas ainda precisa de alguns ajustes. Vale a pena abrir em alguma ferramenta de sua escolha para dar uma conferida.

Irei prepará-lo usando o módulo CSV e NumPy do python, porém na prática o ideal seria usar scikit-learn e Pandas. Porém, para não dar trabalho ao leitor iniciante em Machine learning, manteremos as coisas básicas.

Os únicos pré-requisitos que precisarão ser instalados são: NumPy e Matplotlib. Fiz dessa forma porque o NumPy é a base da computação numérica, Python e Matplotlib é apenas para visualização.

Se você estiver cogitando aprender Machine Learning usando Python, essas duas libs devem ser estudadas primeiro. Claro que não precisa virar um expert, mas um entendimento básico já ajudará bastante. Em relação a visualização, existem muitas opções ao matplotlib.

Depois de ter instalado o NumPy e Matplotlib, vamos ao exemplo de hoje.

A primeira parte consiste em carregar e tratar o dataset, e então deixá-lo estruturado para treino.


def open_file(path):
    """Essa função organiza o dataset para treino
       considerando que as classes que serão o target
       estejam na última coluna."""

    with open(path) as dataset: #Usamos with pois garante fechar o documento.
        data = np.array(list(csv.reader(dataset)))#Armazenamos todo o dataset em uma array.
        labels = np.array(list(set(data[1:,-1])))#Essa operação é útil para eliminar valores repetidos.
        header  = data[0] #Esse é o cabeçário da tabela.
        x_data = np.zeros((len(data)-1,len(data[0])-1))#x_data são os dados para treino.
        y_data = np.empty(len(data)-1)#y_data são as classes alvo.

        for x in range(1,len(data)):#O for começa de 1 pois na primeira linha está o cabeçário.
            x_data[x-1] = data[x][:-1]#Armazeno em x_data apenas as features.

            for y in range(len(labels)):#dou um for na variável labels
                if labels[y] in data[x]:#avalio qual classe está contida na linha
                    y_data[x-1] = y#Substituo a string por um float no caso 0 ou 1.
    return header,x_data,y_data

A função acima foi pensada para ser o mais genérica possível. A única condição que foi imposta é que a classe que será aprendida esteja na última coluna.

Ela retorna três listas. A primeira contém o header do dataset, a segunda contendo as features para treino e a terceira contendo as labels.

É importante lembrar que essa função prepara os dados para tarefas de classificação, pois ela reescala a última coluna, no nosso caso ela transformou em 0 e 1 as respectivas classes.

Feito isso podemos declarar nossas variáveis:


#header: Contém o cabeçário do dataset sendo que a última coluna é a classe.
#x_data: Contém as features para treino ou seja as primeiras quatro colunas do conjunto.
#y_data: Contém as classes de cada linha de x_data, sendo 0 para setosa e 1 para versicolor.
header,x_data,y_data = open_file("iris.csv")

Com os nossos dados preparados vamos fazer uma simples visualização para ver se estar tudo certo.

import matplotlib.pyplot as plt

plt.scatter(x_data[:,1],x_data[:,0],c=y_data)
plt.title("Setosa x versicolor" )
plt.xlabel('Sepal.Width')
plt.ylabel('Sepal.Length')
Figure_1

O gráfico acima demonstra que nas dimensões dadas os pontos são linearmente separáveis. Aliás esse dataset contém duas classes linearmente separáveis justamente para termos uma ideia de como o algoritmo funciona.

Uma vez que temos os dados preparados o certo seria dividir em duas partes, uma para treino e uma para teste, porém hoje eu pretendo demonstrar apenas como o Perceptron é capaz de dividir os dados quando esses são linearmente separáveis ou achar uma boa aproximação caso não seja possível.

Fundamentos de Python para Machine Learning saiba mais

Vamos ao nosso algoritmo:

import numpy as np


#Um perceptron para 4 inputs
#os inputs são as colunas do dataset de treino
class Perceptron(object):
    """Essa classe consiste em um perceptron: um modelo
       linear concedido por  Frank Rosenblatt.
       Essa classe é apenas para classificação.
       alpha: defaut=0.01 # A taxa em que o erro será propagado.
       n_features: O número de features no seu dataset.
       n_iter: O número de iterações realizadas pelo perceptron."""

    def __init__(self,alpha=0.01,n_features=3,n_iter=2000):
        self.w = np.random.uniform(-1,1,n_features+1) #inicializa os pesos
        self.alpha = alpha#a taxa de aprendizado
        self.n_iter = n_iter#o número de iterações

    def _0_1_loss(self,x): #Função de perda que avalia o output
        if x >= 0.0:
            return 1
        else:
            return 0

    def fit(self,x_data,y_data):
        """Esse método é usado para treinar o perceptron.
           x_data: Uma numpy.array contendo as features para treino.
           y_data: Uma numpy.array contendo as classes(target)"""

        x_data = np.insert(x_data[:,],len(x_data[0]),1,axis=1)#acrescentamos o bias ao dataset
        for x in range(self.n_iter):                          #no caso mais uma coluna contendo apenas 1
            print("iteração número:{}".format(x))
            cum_erro = 0 #Aqui armazenamos o erro acumulado para parar a otimização
            for y in range(len(x_data)):
                output = self.w.dot(x_data[y])#O output é o produto dos pesos pela linha atual
                if self._0_1_loss(output) != y_data[y]: #avaliamos para ver se é correspondente.
                    cum_erro += 1 #Caso não seja acrescentamos a contagem de erro
                    erro = y_data[y] - output #medimos o erro da iteração de forma direta.(sem loss)
                    self.w += self.alpha*erro*x_data[y] #Aqui os pesos são atualizados
            if cum_erro == 0: #Aqui avaliamos o erro acumulado caso sejá 0 para o treinamento.
                print("Otimização terminada em {} iterações".format(x))
                break                 

    def predict(self,vector):
        """O método predict pode levar uma numpy.array de uma ou duas
           dimensões."""
        if np.ndim(vector) == 1:#Avaliamos a quantidade de dimensões.
            vector = np.insert(vector,len(vector),1)#inserimos o bias
            prediction = self._0_1_loss(self.w.dot(vector))#Fazemos a predição.
            return prediction
        else:#Caso contrário é feito o mesmo processo porém com uma array de duas dimensões.
            vector = np.insert(vector[:,],len(vector[0]),1,axis=1)
            prediction = [self._0_1_loss(self.w.dot(x)) for x in vector]
            return prediction

Pronto, temos a nossa classe Perceptron pronta para ser aplicada em um conjunto de dados. É importante lembrar que caso você não tenha entendido muito bem o processo, aconselho fortemente que leia o artigo indicado no início do post.

Uma coisa que aconselho a prestar atenção é no bias, que nada mais é do que uma nova coluna contendo apenas 1, ela funciona como nosso interceptor, criei a classe de forma a não precisar de declarar esse bias no conjunto de treino ele usa internamente sem alterar o dataset, e faz isso tanto na hora de treinar quanto na hora de fazer a predição.

Uma vez que temos nosso algoritmo podemos treiná-lo.

perceptron = Perceptron(n_features=4)
perceptron.fit(x_data,y_data)

Quando você usar o método fit ele irá treinar o perceptron até um ponto ótimo.

Para saber se estar tudo correto basta usar o método predict em uma dos elementos da array ou no dataset todo:

In [1]: perceptron.predict(x_data[51])
Out[1]: 0

In[2]: perceptron.predict(x_data) #isso irá retornar uma array contendo todas as labels

Mas antes de medir  precisão segue algumas considerações.

Como os pesos são iniciados de maneira aleatória usando um número de 0 a 1, pode acontecer de ele convergir muito rápido algumas vezes e em outras demorar muito.

Isso acontece pois existem intervalos de valores melhores para se inicializar os pesos, porém escolhi não implementar.

Pode acontecer que depois de treinado o perceptron ainda não classifique de forma correta o dataset. Isso pode acontecer devido a taxa de aprendizado inadequada ou quantidade de iterações insuficientes.

Agora vamos implementar uma simples função para medir a precisão de nosso algoritmo.

Primeiro vamos definir a função que será usada:

def showAccuracy(y_true,predictions):
    correct = 0
    for x in range(len(y_true)):
        if y_true[x] == predictions[x]:#Compara os respectivos valores
            correct += 1
    return float(correct)/len(y_true)

 

Agora armazenamos as predições em uma variável.

predi = perceptron.predict(x_data)

Pronto podemos aplicá-la para ver se está tudo certo.

In [2]: showAccuracy(y_data,predi)
Out[2]: 0.99 #a acurácia pode dar 100% também
 

Muito bem. Encerramos o post de hoje, meu conselho é que você teste o perceptron com várias configurações diferentes e até modifique a classe para acrescentar mais funções.

Notas sobre o design do algoritmo

Escolhi fazer o perceptron na forma de uma classe pois meu foco além dos algoritmos puros é também pensar nas formas de aplicá-los em situações reais e integrar com aplicações existentes.

Então quando construímos uma classe fica mais fácil organizar o projeto em módulos e reutilizá-lo depois.

Algumas dicas que tenho para dar sobre como construir e organizar seus algoritmos, mesmo que seja para estudo, são as seguintes.

Por exemplo: O perceptron é um modelo linear que pode ser usado inclusive para regressão, mas existem algoritmos como regressão linear e multilinear, regressão logística dentre outros que tem uma estrutura parecida. Nada impede de construirmos uma classe chamada por exemplo MainLinearModel que tem as características mais genéricas desses modelos. Dessa forma, as classes subsequentes sempre herdarão alguma característica dela.

-As funções de perda e ativação podem se construída de maneira separada:

O método _0_1_loss no exemplo acima foi construído de forma privada, mas só fiz isso para não precisarmos fazer código fora da classe.

Mas quando lidamos com machine learning temos vários tipos de funções de perda ou ativação, square loss, log loss, logistic, relu, dentre várias outras.

Então nada impede de deixarmos isso em outro módulo para simplesmente importá-los depois.

E claro a otimização do algoritmo é essencial, por mais simples que seja vale a pena sempre está procurando novas formas de deixar o algoritmo mais rápido por mais que esteja usando um framework. Vale muito a pena pensar em formas de melhorar a interação do algoritmo com outras partes do sistema, como o banco de dados e a API.

E quando seguimos um padrão para organizar nossos projetos essa tarefa se torna bem mais fácil pois dividimos a complexidade em partes.

Esse foi o post de hoje, qualquer dúvida, crítica ou sugestão é só deixar nos comentários! obrigada por ler até aqui.

Aliás, se encontrar algum erro ou inconsistência no post, não hesite em entrar em contato ou deixar um comentário pois estará ajudando futuros leitores a terem acesso a um conteúdo cada vez melhor.

Gostou do que viu? Que tal desenvolver os seus conhecimentos na área de Ciência de Dados?

 

Introdução ao Perceptron passo a passo

Hoje teremos o primeiro contato com Redes Neurais Artificiais, estudaremos o funcionamento do Perceptron e como treiná-lo.

Antes de mais nada é importante lembrar que existem várias maneiras de se treinar um Perceptron. A mais popular entre elas é a Delta Rule irei utilizá-la com alguns ajustes para tornar mais fácil a interpretação de cada passo envolvido no treinamento de um Perceptron.

O Perceptron é um classificador linear. Isso quer dizer que ele só irá lidar com problemas de classificação onde o conjunto de dados seja linearmente separável.

Problemas reais, entretanto, na maioria das vezes são mais complexos.

Apesar dessa limitação ele não é um algoritmo fraco. Pois por mais que seja difícil achar um conjunto de dados linearmente separável em aplicações reais, ele pode ser combinado com outros algoritmos para se aumentar a precisão.

E claro podemos analisar a relação entre as dimensões de um conjunto de dados pois dependendo da forma em que agrupamos as classes é possível que ele seja linearmente separável em uma combinação específica das dimensões dadas.

Ele também pode ser adaptado para problemas de regressão, nesse caso ele tem o mesmo efeito que uma regressão linear, como veremos em futuros posts. Porém é importante lembrar aqui que o resultado não será considerado um perceptron de acordo com a teoria.

Com algumas alterações também é possível aplicá-lo em problemas com mais de duas classes.

 

O Algoritmo

Antes de partirmos para o problema vamos analisar o algoritmo. 

Gostaria de lembrar aqui que em situações reais é adicionado um bias ao resultado do somatório. Porém para nosso exemplo iremos ignorar esse fato sem prejuízo do entendimento.

Figura_2

Acima temos o algoritmo representado de forma esquemática, onde x_1 e x_2 são os inputs e w_1 e w_2 são os respectivos pesos de cada input.

E \sum é uma combinação linear desses inputs ponderada pelos pesos, ela gera o output do node e é dada por:

o = \sum_{i=0}^{n}x_iw_i

Onde o é output desse Node. Depois disso ele é avaliado por uma função de perda, para problemas de classificação binária é muito comum o uso da 0-1 loss function:

l(o) = \left\{\begin{matrix} 1 & se & o \geq 0 \\ 0 & se & o < 0 \end{matrix}\right.

Para uma melhor ideia de como o perceptron processa a informação, vamos a um exemplo.

Suponha o seguinte:

x_1 = 2

x_2 = 4

w_1 = 0.4

w_2 = 0.2

Com isso, teremos o seguinte esquema:

Figura_3

A ideia aqui é focar apenas no processamento da informação realizado pelo perceptron. Na figura entre os inputs 2 e 4, eles são multiplicados pelos seus respectivos pesos e depois somados, o output dessa operação então é avaliado pela função de perda que dá o output final.

A operação é bem simples. Mas a ideia é saber como o perceptron chega nos valores ideais de cada peso fazendo assim com que cada instância seja classificada de forma correta.

Para isso vamos partir para um exemplo.

 

imagem_1

 

 

Classificação com duas classes.

Para o exemplo usarei um conjunto de quatro pontos no plano que são Linearmente separáveis e treinaremos o perceptron para achar um discriminante que divida o conjunto em duas partes.

Veja o gráfico abaixo:

Figura_4

Temos um conjunto de quatro pontos no gráfico acima, usaremos o Perceptron para achar um discriminante que divida o plano em duas áreas. Para assim podermos classificá-las de forma correta.

Para esse exemplo não usaremos bias, pois fica claro que a reta que divide os dois conjuntos de pontos passa na origem, e também o intuito é te mostrar como funciona o processo de treinamento do perceptron passo a passo.  Mas falaremos sobre bias em futuros posts.

Para facilitar vamos representar o conjunto de dados em uma tabela:

Sem título

Para nosso exemplo defini Azul igual 1 e Vermelho igual a 0.

O procedimento para se treinar uma perceptron para classes binárias não é complicado. Primeiro iremos definir pesos aleatórios para os vetores w_1 e w_2.

Logo teremos:

w_1 = 0.8

w_2 = -0.5

Feito isso temos que pensar em uma forma de atualizar os pesos. Mas antes disso vamos definir alguns conceitos. O primeiro deles é a taxa de aprendizado(Learning rate ou alpha.) ela dita qual a proporção em que a rede irá propagar o erro  e assim realizar a atualização dos pesos.

Uma taxa de aprendizagem alta acelera o treino porém pode fazer com a rede passe do ponto ótimo, uma taxa baixa diminui a velocidade de treino mas aumenta a precisão da busca.

Representarei a taxa de aprendizado por n e darei a ela o valor 0.5.

Logo temos:

n=0.5

Feito isso definiremos a regra de treino do perceptron.

Toda vez que ele gera um output ele compara com a classe esperada e computa o erro. Para nosso caso e devido à simplicidade do problema usaremos um limiar de -1 ou 1 para atualizar os pesos.

Logo o erro pode ser representado pela seguinte equação:

e = (t - y)

Onde t é classe esperada e y o output gerado pelo perceptron.

Uma vez medido o erro iremos atualizar cada peso utilizando a seguinte regra:

w_{new} = w_{old} + nex_i

Onde x_i é o elemento correspondente do vetor de input que gerou o erro. Se juntarmos tudo teremos o seguintes modelo:

w_1 = 0.8

w_2 = -0.5

n=0.5

e = (t - y)

w_{new} = w_{old} + nex_i

l(o) = \left\{\begin{matrix} 1 & se & o \geq 0 \\ 0 & se & o < 0 \end{matrix}\right.

Antes de começar o treino vamos plotar o gráfico da equação inicial que divide o dataset, isso irá nos dar uma ideia de como o perceptron funciona.

Figura_5

A reta ainda não se encontra no lugar ideal.

Usaremos o perceptron para encontrar esse lugar.

Vamos a nossa primeira iteração.

Os inputs são:

x_1 = 0.3   x_2 = 0.7 classe = 1

Os pesos atuais são:

w_1 = 0.8   w_2 = -0.5

Com isso calculemos o primeiro output:

output = 0.3*0.8 + 0.7*( -0.5 ) = -0.11

Avaliamos com a função de perda:

l( -0.11 ) = 0

Classificado de forma errada é necessário ajustar os pesos.

Medimos o erro:

e = ( 1 - 0 ) = 1

E agora atualizamos os pesos:

w_1 = 0.8 + 0.5*1*0.3 = 0.95  w_1 foi atualizado para 0.95

w_1 = -0.5 + 0.5*1*0.7 =- 0.15  w_2 foi atualizado para -0.15

Antes de partir para a próxima iteração vamos dar uma olhada em como fica o gráfico depois desse ajuste.

Figura_6

Repare que a reta começou a se deslocar para a posição ideal.

Vamos a nossa Segunda iteração.

Os inputs são:

x_1 = -0.6 x_2 = 0.3 classe = 0

Os pesos atuais são:

w_1 = 0.95  w_2 = -0.15

Com isso calculemos o primeiro output:

output = -0.6*0.8 + 0.3*( -0.15 ) = -0.525

Avaliamos com a função de perda:

l( -0.525 ) = 0

Classificado de forma correta não há necessidade de ajustar os pesos.

Vamos a nossa terceira iteração.

Os inputs são:

x_1 = -0.1 x_2 = -0.8 classe = 0

Os pesos atuais são:

w_1 = 0.95 w_2 = -0.15

Com isso calculemos o primeiro output:

output = -0.1*0.95 +(-0.8)*( -0.15 ) = 0.025

Avaliamos com a função de perda:

l( 0.025 ) = 1

Classificado de forma errada é necessário ajustar os pesos.

Medimos o erro:

le = ( 0 - 1 ) = -1

E agora atualizamos os pesos:

w_1 = 0.95 + 0.5*(-1)*(-0.1) = 1  w_1 foi atualizado para 1

w_2 = -0.15 + 0.5*(-1)*(-0.8) = 0.25  w_2 foi atualizado para 0.25

Vamos plotar o gráfico para conferir o que aconteceu.

Figura_7

Estamos quase lá!

Vamos a nossa Quarta iteração.

Os inputs são:

x_1 = 0.1 x_2 = -0.45 classe = 1

Os pesos atuais são:

w_1 = 1 w_2 = 0.25

Com isso calculemos o primeiro output:

output = 0.1*1 +(-0.45)*0.25  = -0.0125

Avaliamos com a função de perda:

l(-0.0125 ) = 0

Classificado de forma errada é necessário ajustar os pesos.

Medimos o erro:

le = ( 1 - 0 ) = 1

E agora atualizamos os pesos:

w_1 = 1 + 0.5*1*0.1 = 1.05  w_1 foi atualizado para 1.05

w_2 = 0.25 + 0.5*1*(-0.45) = 0.025  w_2 foi atualizado para 0.025

Vamos plotar o gráfico para conferir o que aconteceu.

Figura_8

Finalmente chegamos em um ponto ideal, isso quer dizer que o perceptron irá classificar todos os exemplos do conjunto de treino de forma correta a partir de agora.

Vamos ver se isso é verdade, irei testá-lo em duas instâncias.

x_1 = 0.3 x_2 = 0.7 classe = 1

output = 0.3*1.05 +0.7*0.025  = 0.3325

Como é um valor positivo a classe será 1. A classificação foi correta.

x_1 = -0.6 x_2 = 0.3 classe = 0

ouput = -0.6*1.05+0.3*0.025 = -0.6225

Como o valor é negativo a classe será igual a 0.

Deixo os outros valores para você conferir.

Bem, isso encerra o post de hoje. Mas antes de finalizar, gostaria de dizer que apesar de simples e limitado em alguns aspectos. O perceptron deve ser estudado em seus mínimos detalhes, ainda mais por quem pretende lidar com Deep Learning,  pois ele é a porta de entrada para se entender as redes neurais artificiais. como o Multilayer perceptron por exemplo, e claro é um algoritmo que pode ser combinado com outros para se construir um classificador mais eficiente.

Como sempre. Qualquer sugestão ou crítica é sempre bem vindo pois o meu intuito é trazer cada vez mais posts de qualidade para você leitor.

Gostou do que viu?. Que tal se  aprofundar um pouco na teoria matemática por trás dos algoritmos tradicionais e origens das redes neurais e de quebra aplicar em Python.

 

 

 

 

 

 

 

 

 

 

 

Classificação e regressão com K-nearest neighbors

Como muitos já devem saber, o algoritmo K-Nearest Neighbors e os métodos derivados são extremamente básicos para qualquer um que planeje se aventurar no Machine learning. E, por esse motivo, muitas pessoas passam direto por ele achando que é uma técnica fraca.

Apesar das limitações relacionadas a escala vs tempo de processamento, o K-NN nos permite resolver alguns problemas de forma simples. E quando temos uma boa seleção de features e amostras, ele pode nos render insights valiosos e se tornar um classificador poderoso.

Pensando nisso, resolvi escrever esse post sobre as estratégias do K-NN para problemas de classificação e regressão, também relatar um pouco sobre os modelos usados em cada problema.

Nas questões de classificação analisaremos dois cenários: um onde temos poucas classes, e o outro onde temos muitas classes. E discorrer sobre as estratégias para resolver cada um dos problemas.

Nos problemas de regressões analisaremos duas estratégias que podem ser usadas com o K-NN para se conseguir bons resultados.

Caso você ainda não conheça esse algoritmo, escrevi alguns posts sobre ele:

K-nearest neighbors com scikit learn

K-nearest neighbors seu primeiro algoritmo de machine learning

Vantagens e desvantagens

Antes de partir para uma análise mais aprofundada do modelo, é preciso alertar para algumas características do mesmo.

O primeiro problema que surge é relacionado a dimensionalidade do inglês (curse of dimensionality), isso deve ser observado, pois na prática, o número de atributos/variáveis envolvidos em um problema tendem a aumentar.  Isso faz com que os vetores usados pelo K-NN para cálculo das distâncias tenham dimensões muito grandes, o que acaba atrapalhando a localização de alguns padrões relevantes.

Irei reservar um post para falar sobre o tema curse of dimensionality.

A boa notícia é que uma vez que conseguir reduzir o número de dimensões, ou achar os atributos mais relevantes, ele irá funcionar muito bem, e você terá em mãos um modelo fácil de interpretar; o que é útil para a geração de insights.

Outro problema relacionado ao K-NN, é em relação ao processamento necessário na hora de fazer uma predição, por ser um algoritmo que não gera um modelo, mas ao invés disso calcula a distância da instância em relação a todas as outras do conjunto de treino que foi usado para treiná-lo. Isso gera uma enorme necessidade de processamento o que pode até inviabilizar o seu uso em aplicações reais.

Felizmente existem métodos como KD-tree e Ball-tree para lidar com esse tipo de problema, falarei desses métodos futuramente. E há casos em que a quantidade de dados não é tão grande, por isso podemos facilmente usar métodos de programação avançada para acelerar o tempo de predição.

Classificação

Com certeza a aplicação mais comum do K-NN é em problemas de classificação.

Nesse caso, a forma mais utilizada para se medir a distância entre os pontos, é a boa e velha distância Euclidiana.

e =\sqrt{ \sum_{i=0}^{n}(X_{i,j} - X_{i+n,j})^2}

Ela calcula a distância entre dois vetores (que no caso acima foram representados como sendo linhas de uma matriz), que é sua representação mais comum em aplicações reais.

O K-NN procura então achar os K vizinhos mais próximos de um dado ponto no espaço, então o mesmo procura pelo seguinte conjunto de vetores:

e = \underset{X_{i,j}\subset X}{argmin}\sum_{i=0}^{K}\sqrt{\sum_{j=0}^{N}(X_{i,j} - t_{i})^2}

Onde X_{i,j} são linhas da matriz X , enquanto que K e N são os números de vizinhos, e número de elemento do vetor respectivamente, e t é a instância que será classificada.

Isso retornará um conjunto de vetores K_{n} = \left \{x_{1},x_{2}..x_{n} \right \} no qual a soma das distâncias entre eles e a instância t que será classificada, seja a menor possível.

Por exemplo, caso definirmos K igual a 3 teremos três vetores nesse conjunto.

Feito isso, é preciso saber qual a classe dominante nesses vetores. Cada vetor do conjunto está associado a uma classe. Para descobrir a qual classe a instância t pertence, é feita uma votação para saber em qual classe ela se encaixa. Definimos a instância em questão como pertencendo a classe que tenha maior ocorrência no conjunto que retornar da iteração.

Podemos definir isso de maneira formal usando o conceito de proporção.

No vetor K_{n} podemos extrair c classes de elementos contidas no vetor, a partir daí basta aplicarmos o seguinte passo.

p(t=c_{j}) = \frac{1}{K}\sum_{i=0}^{K}l(c_{j}=c_{i})

Onde l(c_{j}=c_{i}) é uma função de perda do tipo:

l(x) = \left\{\begin{matrix} 0 & se & x=0 \\ 1 & se & x !=0 \end{matrix}\right.

Podemos ler a expressão acima como:

A probabilidade de t pertencer a classe c_{j} é igual a soma das ocorrências positivas dividida pela quantidade de vizinhos mais próximas da instância.

Classificamos a instância como sendo da classe com maior proporção de ocorrência.

Para problemas de classificação com poucas classes, esse método pode ser interessante, porém, em problemas com muitas classes pode acontecer de classificarmos uma instância erroneamente por falta de exemplos da classe que ela pertence. Em caso de K pequenos, ou o contrário, pode haver muitos vizinhos que estão distantes da instância terém um peso muito grande e atrapalhar a classificação da instância em casos onde K é muito grande.

Para resolver essa questão, podemos usar a distância ponderada. Uma maneira muito popular de lidar com esse problema é usar o inverso da distância euclidiana:

p(x,t) = \frac{1}{\sqrt{ \sum_{i=0}^{n}(X_{i,j} - X_{i+n,j})^2}}

Onde x é um vetor do conjunto dos vizinhos mais próximos, n o número de elementos/dimensões do vetor e t a instância que será classificada.

Agora, ao invés de avaliarmos as classes dos K vetores mais próximos, passamos a avaliar seus pesos. Daí avaliamos a instância t como pertencendo a classe na qual a soma dos inversos das distâncias em relação a ela seja a maior possível.

f(t) = \underset{c_{i} \subset K_{c}}{argmax} \sum_{i=0}^{K}p(c_{i},t)

Com isso em mãos, podemos lidar melhor com os problemas que envolvem muitas classes, pois damos menos importância aquelas que estão mais distantes da instância a ser classificada. Isso faz com que mesmo que uma classe mais distante tenha muitas ocorrências, ela pode perder para uma que esteja mais perto e com menos ocorrência.

Esse mecanismo é interessante pois possibilita repartir o espaço em torno da instância que o K-NN irá classificar em pedaços.

Sem título
Uma representação da divisão do plano em torno de um ponto.

É uma maneira interessante de pensar, mas perde precisões em grandes dimensões devido a perda de peso dos elementos do vetor, o que faz com que variáveis não tão importantes tenham a mesma influência que variáveis mais importante. O aumento da dimensão também faz com que a definição de longe e perto perca sentido devido a variação nos valores de cada elemento.

Isso acontece porque o K-NN (diferente de outros algoritmos), dá o mesmo peso para todos os elementos de um vetor. Por isso é interessante fazer uma análise de correlação nas variáveis que serão usadas para treiná-lo, e assim pode ser aplicado apenas nas mais relevantes.

Regressão.

Com algumas pequenas modificações, é possível aplicar o K-NN para problemas de regressões.

Assim como antes, podemos utilizar a distância Euclidiana para calcular a distância entre as instâncias, porém a diferença reside na forma em que será calculado o valor para instáncia, pois aqui, iremos associar um valor contínuo.

Ao invés de usarmos aquela classe que aparece com mais frequência no vetor dos vizinhos mais próximos, pegaremos uma média dos valores das classes dessas instâncias.

Agora suponha o conjunto de vetores pertencendo ao conjunto dos K vizinhos mais próximos:

K = \left \{ x_{1},x_{2},...,x_{n} \right \}

Como sabemos, cada vetor desse conjunto está associado à um valor real, pois o K-NN usa o próprio conjunto de treino como parte do algoritmo; logo, usaremos o vetor que representa esses valores. Então podemos associar a cada vetor do conjunto K à um valor real que representa seu valor.

Logo teremos o seguinte vetor que denomine V :

V =  (v_{1},v_{2},...,v_{n})

A primeira estratégia que pode ser usada para se achar o valor da instância t pode ser a média desses vizinhos mais próximos.

f(t) = \frac{1}{K}\sum_{i=0}^{K}(v_{i})

Então, o valor de saída nada mais seria do que a média dos K vizinhos mais próximos daquela instância.

Apesar de interessante, na prática tendemos a ter variáveis fortemente enviesadas, aquelas que não seguem uma distribuição normal. Nesses casos, além de se fazer uma análise estatística da distribuição de nossa variável de interesse, é importante pensar em um método que nos ajude a desviar desse problema.

Para resolver isso, podemos utilizar uma média ponderada pelo peso das distâncias, como vimos anteriormente:

Então nesse caso, a função que associa um valor a instância que será classificada seria dada pela seguinte expressão:

f(t)= \frac{\sum_{i=0}^{K}v_{i}p_{i}}{\sum{p_{i}}}

Onde p_{i} é o peso de cada valor real v_{i} associado ao vetor x_{i} dos vizinhos mais próximos.

E p_{i} pode ser calculado como vimos anteriormente pela seguinte equação:

p(x,t) = \frac{1}{\sqrt{ \sum_{i=0}^{n}(X_{i,j} - X_{i+n,j})^2}}

Onde t é o vetor que queremos estimar o valor.

Mais uma vez separamos o espaço em partes, assim as instâncias em diferentes lugares do espaço terão diferentes pesos na formação do valor da predição.

Conclusão:

O mais importante na hora de se estudar o K-NN não é ó modelo em si, mas a estratégia usada para se estimar um valor, e as potenciais aplicações que vão muito além de mera classificação e regressão.

Quando fazemos isso, fica claro que o Bias do K-NN é associar valores semelhantes a instâncias semelhantes. Porém ele considera semelhantes instâncias que tenham o mesmo valor para seus atributos. Ele também trata cada instância de forma isolada, ao contrário de modelos como Decisions tree, ou as redes neurais que criam um modelo único para classificar ou estimar um valor para todas as instâncias.

Ele também associa o mesmo peso a todos os  atributos de uma instância, por isso é importante preparar e normalizar o conjunto de treino antes de aplicá-lo, e até mesmo selecionar os atributos necessários para usá-lo.

Outro ponto importante é que o K-NN se torna mais útil quando usado em conjunto com outros algoritmos, assim, um pode corrigir a fraqueza do outro aumentando a precisão.

O K-NN é um dos vários métodos que o cientista de dados, e engenheiro de Machine Learning utilizam no dia a dia para resolver alguns problemas práticos.

Mas, para entrar na carreira de dados é necessário um conjunto de habilidades, que vão desde da matemática pura e computação cientifica, à interação com o usuário final e regras de negócio.

Esse foi o post de hoje, qualquer sugestão ou crítica é sempre bem vinda. Bons estudos!

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Uma introdução ao K-means passo a passo

Olá pessoas. No post de hoje iremos dar uma olhada de maneira conceitual em um dos mais famosos algoritmos de aprendizado não supervisionado o K-means.

Esse algoritmo simples e poderoso pode ser aplicado em várias tarefas interessantes como análise de ruídos extração de features e ate mesmo em análise de discriminantes como, por exemplo, achar alguma dimensão em que os dados são linearmente separáveis.

1499232946

Mas o foco de hoje é saber como esse algoritmo consegue achar na maioria das vezes o centroide ideal e assim separar o dataset em clusters. Para isso partirei de um Dataset teórico com 10 instâncias que representam a localização de pontos no plano.

Vamos representá-las com um gráfico. Você terá a seguinte figura:

Figura_1.png

A intenção é criar três clusters de modo a conseguir classificar cada ponto pertencendo a algum desses clusters e assim criar uma área de cluster.

Para isso vamos precisar carregar três centroides aleatório.

Figure_2

Os três pontos vermelhos que você ver na imagem são os centroides eles foram iniciados de maneira aleatória.

O primeiro passo para se resolver esse problema é calcular a distância entre os centroides e cada ponto e assim classificar cada ponto como pertencendo a aquele cluster no qual ele esteja mais perto para isso iremos nos utilizar da Distancia euclidiana.

Vamos também impor uma condição de parada para nosso exemplo, limitaremos a procura em duas iterações além da inicial.

Vamos realizar o processo e teremos a seguinte figura:

Figure_3

Repare que temos três áreas de cluster usei uma reta para separar pois os conjuntos são linearmente separáveis.

Com isso em mãos vamos mover os centroides(os pontinhos vermelhos.) para isso iremos move los para um local onde a soma das distâncias entre eles e os pontos da área de cluster seja a menor possível também podemos usar o local em que a média das distâncias seja a menor possível mas para esse simples exemplo não tem necessidade disso.

Se você olhou no link do wikpedia ou prestou atenção nas aulas de geometria analítica já sabe como funciona a distância euclidiana.

Para facilitar seu entendimento vamos fazer o processo de calculo para mover o centroide na área cinza.

Não sabemos onde é o melhor lugar para ele por isso essas coordenadas serão chamadas x e y logo temos uma função para minimizar.

f( x,y ) = \sqrt{( 2-x )^2+(1-y)^2} + \sqrt{ (1-x)^2+(2-y)^2 }+\sqrt{ (1-x)^2+(3-y)^2 }

As constantes da função são as coordenadas dos pontos que estão na área cinza. Vamos plotar o gráfico dessa função para termos uma ideia de como ele seleciona o centroide.

Figura_4

A grande jogada do K-means é que temos uma função convexa por isse motivo temos mínimos globais no nosso caso é a área azul quanto mais azul melhor pois é ali que esta as novas coordenas x e y da nova localização do centroide.

Não ire falar aqui dos detalhes dos cálculos necessários, depois de calculado os mínimos globais de cada centroide teremos a seguinte figura.

Figure_5

Repare que os pontos vermelhos mudaram de lugar, eles foram para os locais onde a soma das distâncias entre eles e os pontos nas suas respectivas área de clusters era a menor possível.

Isso encerra a primeira iteração.

Agora na segunda iteração as áreas de clusters serão redefinidas teremos a seguinte figura:

Figure_6

Agora fazemos a segunda iteração e teremos a seguinte figura:

Figure_7

Pronto temos os nossos clusters, claro que poderíamos continuar mais um pouco para ver aonde ele iria de fato parar mas para um exemplo didático já é mais que o suficiente.

Antes de finalizar o post é importante que você saiba de um dos pontos fracos K-means, apesar de ele convergir para um centro nem sempre isso acontece de primeira por isso é importante que você o rode várias e várias vezes antes de tirar qualquer conclusão.

Caso tenha se interessado e queira fazer algo na prática segue o link do post em que ele é implementado em Python:

K-means em Python

Se você leu ate aqui é provável que esteja interessado em entrar nessa área fascinante que é a ciência de dados e sabe que para isso precisa se dedicar aos estudos.  Pensando nisso a Faculdade ISEIB lançou a pós graduação em Ciência de dados que pode ser acessado no link abaixo. Vale muito a pena dar uma olhada no material que estão oferecendo.

Pós-Graduação Ciência de Dados

É isso pessoal. Qualquer duvida ou sugestão basta deixar seu comentário e bons estudos.

K-nearest neighbors com scikit learn

Ola pessoas que assim como eu se amarram em Machine Learning. Hoje o post é para quem não gosta muito de enrolação e já quer partir para a prática.

O algoritmo de hoje é o K-nearest neighboars e usaremos o framework scikit learn que nos permite pular a parte teórica e partir para a prática. Para quem não sabe de que algoritmo estou falando escrevi um post sobre ele:

K-nearest neighbors seu primeiro algoritmo de machine learning

KNN foi o primeiro algoritmo que aprendi sobre o assunto e aconselho fortemente que você o domine também pois além de ser extremamente simples e ser facilmente aplicável em uma grande gama de problemas, inclusive no reconhecimento de imagens, é um algoritmo muito útil para quem estar trabalhando com uma grande quantidade de dados.

Falo isso pois geralmente logo depois de conseguir um Dataset seja por meio de crawlers ou pesquisa sempre existem valores faltando. Existem varias formas de preencher esses valores: média, moda, mediana, processos de filtragem dentre outros. Mas um dos mais interessantes que achei ate agora e apresenta um resultado interessante foi usando o KNN. Claro que não da para usar em todas as situações mas mesmo assim da para aplicar em muitas.

Mas isso é história para outro post, hoje iremos aplicar o KNN para identificar dígitos, isso mesmo podemos usa-lo para classificar dígitos escrito a mão, com algumas alterações é claro, mas mesmo assim é consideravelmente eficiente quando comparado a outros algoritmos mais complexos como as redes neurais por exemplo.

Primeiramente vamos conhecer nosso conjunto de dados.

from sklearn.datasets import load_digits

#Aqui instanciamos a classe load_digits
digits = load_digits()

#Aqui armazeno as imagens para darmos uma olhada depois
imagens = digits.images

#Aqui estão as arrays 8 x 8 bites
x_data = digits.data

#Aqui temos nossos Labels que serão necessário para treinamento
y_data = digits.target 

O que fizemos instanciar o Dataset do próprio sklearn armazenei as imagens em uma variável pois daremos uma conferida nelas depois. Na variável x_data foi armazenado 1797 arrays cada uma contendo 64 números que representa as escala de cinza de cada algarismo ou seja cada digito é representado como uma pequena imagem 8 por 8.

E na variável y_data temos as nossas labels ou target que é o valor associado a cada array de x_data.

Antes de aplicar o algoritmo vamos dar uma olhada na variável imagens ela segue o mesmo esquema da x_data com a diferença de que cada número é representado por uma 2D-array o que facilita nossa plotagem.

A seguir um esquema para plotar alguns dessas imagens é te dar uma melhor visualização do problema.

import matplotlib.pyplot as plt

#Aqui criamos a figura principal do plot
fig = plt.figure(figsize=(5,5))

Aqui plotamos cada uma das imagens e suas respectivas labels
for x in range(1,11):
    fig.add_subplot(2,5,x,xticks=[],yticks=[])
    plt.imshow(imagens[x],cmap=plt.cm.bone,interpolation='gaussian')
    if x <= 9:
        plt.xlabel(str(x))
    else:
        plt.xlabel(str(0))

Caso você rode o código acima tera a seguinte imagem:

Plote dos digitos

Não se apegue muito a forma que plotei os dados foi uma escolha arbitrária, matplotlib é uma ferramenta muito completa para fazer a visualizações dos dados mas dominá-lo é uma tarefa que requer tempo e dedicação.

Uma vez que temos nossos dados e nosso problema bem definidos vamos partir para o próximo passo.

Vamos preparar nosso conjunto de treino e teste.

rom sklearn.cross_validation import train_test_split
#Irei preparar o conjunto de treino e teste.
#Deicharemos 200 para teste.
x_train,x_test,y_train,y_test = train_test_split(x_data,y_data,
                                                 test_size = 200)

A função train_test_split divide os DataSets em dois novos, um para treino e outro para testes e defini o tamanho do conjunto de teste em 200, e é importante lembrar que como não pré defini um random state ele sempre será reiniciado com instâncias aleatórias.

Agora vamos treinar nosso algoritmo.

from sklearn.neighbors import KNeighborsClassifier
#instanciamos a classe.
#Nada impede de treiná-lo logo em seguida.

knn = KNeighborsClassifier(n_neighbors=3)
knn.fit(x_train,y_train)

Simples assim! Temos um algoritmo treinado e pronto para aplicarmos mas antes de tomar conclusão é importantes que façamos alguns testes de precisão. Como se trata de um problema de classificação irei usar duas funções para medir o desempenho.

São elas: accuracy_score e confusion_matrix antes vamos criar uma variável que armazena as predições feitas pelo algoritmo.

#Aqui armazeno as predições do algoritmo
#No caso aplicamos ele ao conjunto de teste
#Essa variavel servira para fazermos algumas métricas.
predictions = knn.predict(x_test)

Primeiramente vamos medir a precisão com que ele esta fazendo as predições.

In  [1]: metrics.accuracy_score(y_test,predictions)
Out[1]: 0.98999999999999999

Temos uma taxa de acerto de 98%, excelente para um problema de classificação. Mas as vezes precisamos de algo mais detalhado vamos aplicar a próxima função.

In   [2]: metrics.confusion_matrix(y_test,predictions)
Out[2]:
array([[21,  0,  0,  0,  0,  0,  0,  0,  0,  0],
           [ 0, 21,  0,  0,  0,  0,  0,  0,  0,  0],
           [ 0,  0, 20,  0,  0,  0,  0,  0,  0,  0],
           [ 0,  0,  0, 21,  0,  0,  0,  0,  0,  0],
           [ 0,  0,  0,  0, 13,  0,  0,  0,  0,  0],
           [ 0,  0,  0,  0,  0, 17,  0,  0,  0,  0],
           [ 0,  0,  0,  0,  0,  0, 22,  0,  0,  0],
           [ 0,  0,  0,  0,  0,  0,  0, 23,  0,  1],
           [ 0,  1,  0,  0,  0,  0,  0,  0, 23,  0],
           [ 0,  0,  1,  0,  1,  0,  0,  0,  0, 17]])

Confusion matrix é uma maneira muito interessante de ver em problemas de classificação e como o algoritmo tende a se enganar com as classes.

Uma de suas utilidades é detectar qual classe ele estar com mais problemas para classificar bem como as classes que ele tende a confundir uma com a outra.

Na matriz acima olhe a primeira linha nela você o número 21 isso quer dizer que ele classificou o 0 como 0 todas as vezes que ele apareceu.

Para interpretar a matriz acima basta olhar as linhas e colunas as as linhas dizem qual classe deve ser e as colunas dizem o que o algoritmo classificou. Repara na ultima linha por exemplo era para ser 9 mas foi classificado uma vez como 2 e outra como 4.

Outra maneira interessante de analisar essa matriz é plotando ela.

Import matplotlib.pyplot as plt.
From sklearn impor metrics

cm = metrics.confusion_matrix(y_test,predictions)
plt.matshow(cm)

confussionMatrix

O interessante em sklearn é que depois que treinamos o algoritmo ele nos da uma gama de possibilidades para aplicar o mesmo. E claro vamos dar uma olhada em duas dessas possibilidades que eu pessoalmente jã apliquei na prática.

Por exemplo o método Kneighboars retornas as distâncias e os indices das instâncias que estão mais próximo de um determinado ponto.

In:  [1] x_test[0]
Out[1]:
array([  0.,   0.,   0.,  12.,  13.,   5.,   0.,   0.,   0.,   0.,   0.,
        11.,  16.,   9.,   0.,   0.,   0.,   0.,   3.,  15.,  16.,   6.,
         0.,   0.,   0.,   7.,  15.,  16.,  16.,   2.,   0.,   0.,   0.,
         0.,   1.,  16.,  16.,   3.,   0.,   0.,   0.,   0.,   1.,  16.,
        16.,   6.,   0.,   0.,   0.,   0.,   1.,  16.,  16.,   6.,   0.,
         0.,   0.,   0.,   0.,  11.,  16.,  10.,   0.,   0.])

In   [2]: knn.kneighbors(x_test[0],n_neighbors=10)
Out[2]:
(array([[ 14.24780685,  19.41648784,  19.67231557,  21.26029163,
          21.28379665,  21.37755833,  21.49418526,  21.88606863,
          22.        ,  22.13594362]]),
 array([[1194, 1436,  809,   56, 1081,   65, 1329,  116,   24,  187]]))

Acima eu primeiro demonstrei o que tem no índice 0 da variável x_test logo depois vi quais são os dez vizinhos mais próximos do mesmo. Repare que ele retorna duas arrays a primeira contendo as distâncias e a segunda contendo o índice de cada uma das instâncias.

Outra função muito interessante é o método kneighbors_graph() ele nos retorna uma matriz esparsa tanto de conexão quanto de distâncias. Isso nos ajuda a entender algumas relações interessantes e pode nos ajudar na visualização dos dados.

Por exemplo irei plotar um simples grafo para termos uma ideia de como estão distribuídos os três vizinhos mais próximos de cada instância do conjunto de teste.

import networkx as nx

matrix = knn.kneighbors_graph(x_test,mode='distance',n_neighbors=3)
matrix = matrix.toarray()

g = nx.Graph()

for x in range(len(matrix)):
    for y in range(len(matrix[x])):
        if matrix[x][y] != 0.0:
            g.add_edge(str(x),str(y),weight=matrix[x][y])

nx.draw_networkx(g,alpha=0.6,node_size=70,font_size=5)
plt.show()

Depois de plotar você tera a seguinte imagem.

graph

Infelizmente não da para ver muito bem. Mas cada Node do grafo é um índice da array que foi usada para treinar o algoritmo isso pode nos ajudar a achar padrões interessantes quando paramos para analisar essa estrutura.

Por exemplo podemos achar os números que são mais parecidos pela ocorrência de vezes em que são classificados como vizinhos mais próximos isso pode nos ajudar na tomada de decisão de qual atributos usar para melhora a precisão do algoritmo.

Mas não para por ae apesar de bonito a visualização do grafo é parte menos importante na hora de levantar insights dessa estrutura de dados. O mais interessante é que podemos fazer análises de correlações mais profundas e assim criar um algoritmo de valor.

Mas por hoje é só. Tomara que tenha sido util para você e o maior intuito foi mesmo que de forma introdutória mostrar o poder desse algoritmo que apesar de simples pode ser usado para realizar tarefas extremamente complexas. Por isso o KNN é um dos meus algoritmos preferidos tanto pela sua simplicidade como pela sua capacidade de tornar relações complexas mais palpáveis.

Ate o próximo post e obrigado por te parado para ler e bons estudos.