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.
Hoje iremos focar no tipo de problema representado à direita.
Para isso vamos usar um conjunto de dados fictício representado no gráfico abaixo:
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:
Na figura acima temos o layer de input com os dois inputs e mais o bias .
Uma observação sobre o bias é que ele sempre levara o input 1, o que importa de fato são os pesos e . Logo depois temos os pesos da primeira camada que vai de ate .
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:
Repare que na coluna 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.
Definimos os biases:
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.
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 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 qualquer o seu valor será dado por:
Aqueles da área de estatística podem estranhar a fórmula acima pois falta 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.
Aqui o erro é dado pela média da soma dos quadrados das diferenças dos outputs da rede em relação a label original . 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 e 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
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:
E repare que temos o vetor de peso, as arestas em vermelho que é representado por:
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.
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.
No estado atual esse seria o output do neurônio vermelho.
Fazemos o mesmo procedimento para obter o output do outro neurônio.
depois basta aplicar a sigmoid:
Dessa forma obtemos o output da primeira camada. Veja no desenho abaixo.
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 e mais o bias que leva como input o número 1.
Agora basta substituir os valores:
Aplicamos a função sigmóide para gerar o output final da rede:
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.
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.
Com isso feito podemos medir o erro quadrado médio:
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:
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.
Onde será o valor do peso após a atualização, o valor atual do peso, a taxa de aprendizado que definimos,aqui é 0.01, e é o gradiente do erro em relação ao peso que iremos atualizar.
Mas antes voltemos ao esquema da rede.
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 em relação ao output . Basta definir a derivada da função de perda.
Logo:
A conclusão que chegamos aqui é que a derivada de em relação a é nada mais do que erro médio.
Antes de ir para o próximo passo observe o desenho a abaixo:
Acrescentei no final a letra 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 . Lembre-se que após a multiplicação pelos pesos é aplicado a função sigmóide ao resultado:
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 em relação ao resultado da camada anterior é dada pela parcial:
Onde é 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.
Repare que é 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 e pelos pesos e 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 em relação ao um dos pesos.
vamos atualizar agora:
Observe a figura abaixo:
Aqui concluímos que a derivada parcial do resultado em relação ao peso é o próprio input 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:
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 em relação ao output da rede :
O próximo passo é calcular o o gradiente do output em relação a .
É 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:
O gradiente para o primeiro input seria:
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:
Agora basta achar a derivada do resultado em relação ao peso :
Como queremos a média de todos outputs do batch basta somar todas as saídas de :
Logo teremos:
Agora pela regra da cadeia temos que:
Com isso temos o gradiente do erro em relação ao peso . 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 antigo que foi inicializado como 1.32.
logo temos:
E agora basta aplicar a regra de atualização:
Com isso concluímos a atualização do primeiro peso da rede.
Temos agora:
Vamos fazer o mesmo procedimento para, abaixo temos o valor inicial do peso:
Lembrando que a derivada parcial de em relação a é dada por:
Dae basta pegar a média dos gradientes de de em relação a :
Aplicamos a regra da cadeia:
Se aplicarmos o mesmo procedimento ao bias teremos:
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.
É 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:
Com isso em mãos podemos atualizar o bias dessa camada:
Até aqui já atualizamos a última camada da rede.
Porém agora atualizaremos os pesos da primeira camada. partiremos do peso .
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 porem agora encaixaremos mais uma peça na nossa figura.
Para calcular a variação do erro em relação a é necessário obter as seguintes derivadas.
Repare que não derivamos o erro mais em relação a pois nessa etapa ele é considerado como uma constante e o 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 calculados.
Logo vamos adicioná las a fórmula:
Lembrando que os valores se referem a média das derivadas do batch todo. Vamos calcular a próxima parte dessa derivada:
Como vimos:
Logo:
A diferença dessa derivada em relação a que derivamos na outra vez é que aqui derivamos em relação a e não em relação a .
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.
Agora vamos à próxima derivada.
Sabemos que:
Logo:
Agora pegamos a média dos gradientes para o batch:
Agora basta encaixar esse resultado na equação:
O próximo passo é achar o gradiente de em relação a :
logo:
Calculamos a média de todos os inputs para :
Encaixamos isso na equação para obter o resultado final, a variação do erro em relação ao peso .
Com isso em mãos pode-se aplicar a regra de atualização. (Delta rule.) e atualizar o peso
Irei aplicar o mesmo procedimento ao peso e o bias . 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 basta apenas calcular a derivada em relação a que como já vimos é o próprio input .
Temos que:
Fazemos a atualização:
Vamos atualizar o bias agora:
Com isso feito. Atualizamos os pesos que geram o output de primeiro neurônio da primeira camada oculta .
Ainda falta fazer a atualização dos pesos , e o bias que geram o output . Nesse momento iremos ter que achar a derivada de em relação a .
Veja a figura abaixo:
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 :
Voltando a equação da cadeia com os gradientes que já possuímos vemos que:
Vamos calcular a próxima derivada:
Calculamos a média de para todos os elementos do batch que no caso é igual ao próprio .
E encaixamos isso na nossa equação:
Agora é hora de achar a próxima derivada da equação.
sabemos que:
logo:
Agora basta pegar a média dos gradientes para o neurônio :
Encaixamos mais esse resultado em nosso equação:
Agora iremos derivar a última parte. E como já vimos:
Agora basta adicionar a média dos quatro inputs em nossa equação:
Com isso em mão podemos atualizar o peso :
Como já sabemos que o processo é o mesmo para o peso e o bias irei já colocar o valor atualizado de cada um:
E com isso temos todos os pesos e biases atualizados e a primeira iteração terminada:
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:
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.