Skip to content

Deep Learning Book

  • Início
  • Índice
  • Cursos Online
  • Contato

Deep Learning Book

Em Português, Online e Gratuito

Deep Learning Book

Capítulo 20 – Overfitting e Regularização – Parte 2

by

Aumentar a quantidade de dados de treinamento é uma maneira de reduzir o overfitting. Mas existem outras maneiras de reduzir a extensão de ocorrência do overfitting? Uma abordagem possível é reduzir o tamanho da nossa rede. No entanto, redes grandes têm o potencial de serem mais poderosas do que redes pequenas e essa é uma opção que só adotaríamos com relutância.

Felizmente, existem outras técnicas que podem reduzir o overfitting, mesmo quando temos uma rede de tamanho fixo e dados de treinamento em quantidade limitada. Essas técnicas são conhecidos como técnicas de regularização. Neste capítulo descrevemos uma das técnicas de regularização mais comumente usadas, uma técnica às vezes conhecida como decaimento de peso (weight decay) ou Regularização L2. A ideia da Regularização L2 é adicionar um termo extra à função de custo, um termo chamado termo de regularização. Aqui está a entropia cruzada regularizada:

 

form

Equação 1

 

O primeiro termo é apenas a expressão usual para a entropia cruzada. Mas adicionamos um segundo termo, a soma dos quadrados de todos os pesos da rede. Isto é escalonado por um fator λ / 2n, onde λ > 0 é conhecido como o parâmetro de regularização e n é, como de costume, o tamanho do nosso conjunto de treinamento. Vou discutir mais tarde como λ é escolhido. É importante notar também que o termo de regularização não inclui os vieses. Eu também voltarei a isso mais frente.

Claro, é possível regularizar outras funções de custo, como o custo quadrático. Isso pode ser feito de maneira semelhante:

 

form2

Equação 2

 

Em ambos os casos, podemos escrever a função de custo regularizada como:

 

form3

Equação 3

 

onde C0 é a função de custo original e não regularizada.

Intuitivamente, o efeito da regularização é fazer com que a rede prefira aprender pequenos pesos, sendo todas as outras coisas iguais. Pesos grandes só serão permitidos se melhorarem consideravelmente a primeira parte da função de custo. Dito de outra forma, a regularização pode ser vista como uma forma de se comprometer entre encontrar pequenos pesos e minimizar a função de custo original. A importância relativa dos dois elementos do compromisso depende do valor de λ: quando λ é pequeno, preferimos minimizar a função de custo original, mas quando λ é grande, preferimos pesos pequenos.

Agora, não é de todo óbvio porque fazer este tipo de compromisso deve ajudar a reduzir o overfitting! Mas acontece que sim, reduz. Abordaremos a questão de porque isso ajuda na redução do overfitting no próximo capítulo, mas primeiro vamos trabalhar em um exemplo mostrando como a regularização reduz o overfitting.

Para construir um exemplo, primeiro precisamos descobrir como aplicar nosso algoritmo de aprendizado de descida de gradiente estocástico em uma rede neural regularizada. Em particular, precisamos saber como calcular as derivadas parciais ∂C/∂w e ∂C/∂b para todos os pesos e vieses na rede. Tomando as derivadas parciais da Equação 3 acima, temos:

 

form4

Equação 4

 

Os termos ∂C0/∂w e ∂C0/∂b podem ser calculados usando backpropagation, conforme descrito nos capítulos anteriores. E assim vemos que é fácil calcular o gradiente da função de custo regularizada, pois basta usar backpropagation, como de costume, e depois adicionar (λ/n).w à derivada parcial de todos os termos de peso. As derivadas parciais em relação aos vieses são inalteradas e, portanto, a regra de aprendizado de descida de gradiente para os vieses não muda da regra usual:

 

form5

Equação 5

 

A regra de aprendizado para os pesos se torna:

 

form6

Equação 6

 

Isto é exatamente o mesmo que a regra usual de aprendizado de descida de gradiente, exceto pelo fato de primeiro redimensionarmos o peso w por um fator 1 − (ηλ/n). Esse reescalonamento é, às vezes, chamado de redução de peso, uma vez que diminui os pesos. À primeira vista, parece que isso significa que os pesos estão sendo direcionados para zero, mas isso não é bem isso, uma vez que o outro termo pode levar os pesos a aumentar, se isso causar uma diminuição na função de custo não regularizada.

Ok, é assim que a descida de gradiente funciona. E quanto à descida de gradiente estocástica? Bem, assim como na descida de gradiente estocástica não-regularizada, podemos estimar ∂C0/∂w pela média de um mini-lote de m exemplos de treinamento. Assim, a regra de aprendizagem regularizada para a descida de gradiente estocástica torna-se:

 

form7

Equação 7

 

onde a soma é sobre exemplos de treinamento x no mini-lote, e Cx é o custo (não-regularizado) para cada exemplo de treinamento. Isto é exatamente o mesmo que a regra usual para descida de gradiente estocástico, exceto pelo fator de decaimento de peso de 1 − (ηλ/n). Finalmente, e por completo, deixe-me declarar a regra de aprendizagem regularizada para os vieses. Isto é, naturalmente, exatamente o mesmo que no caso não regularizado:

 

form8

Equação 8

 

onde a soma é sobre exemplos de treinamento x no mini-lote.

Vamos ver como a regularização altera o desempenho da nossa rede neural. Usaremos uma rede com 30 neurônios ocultos, um tamanho de mini-lote de 10, uma taxa de aprendizado de 0,5 e a função de custo de entropia cruzada. No entanto, desta vez vamos usar um parâmetro de regularização de λ = 0,1. Note que no código, usamos o nome da variável lmbda, porque lambda é uma palavra reservada em Python, com um significado não relacionado ao que estamos fazendo aqui (caso tenha dúvidas sobre as palavras reservadas em Python, acesse o curso gratuito de Python em nosso portal).

Eu também usei o test_data novamente, não o validation_data. Estritamente falando, devemos usar o validation_data, por todas as razões que discutimos anteriormente. Mas decidi usar o test_data porque ele torna os resultados mais diretamente comparáveis com nossos resultados anteriores e não regularizados. Você pode facilmente alterar o código para usar o validation_data e você verá que ele terá resultados semelhantes.

python

O custo com os dados de treinamento diminui durante todo o tempo, da mesma forma que no caso anterior, não regularizado no capítulo anterior:

 

regularized1

 

Mas desta vez a precisão no test_data continua a aumentar durante as 400 épocas:

 

regularized2

 

Claramente, o uso da regularização suprimiu o overfitting. Além do mais, a precisão é consideravelmente maior, com uma precisão de classificação de pico de 87.1%, em comparação com o pico de 82.27% obtido no caso não regularizado. De fato, quase certamente poderíamos obter resultados consideravelmente melhores, continuando a treinar mais de 400 épocas. Parece que, empiricamente, a regularização está fazendo com que nossa rede generalize melhor e reduza consideravelmente os efeitos do overfitting.

O que acontece se sairmos do ambiente artificial de ter apenas 1.000 imagens de treinamento e retornar ao conjunto completo de treinamento de 50.000 imagens? É claro, já vimos que o overfitting é muito menos problemático com as 50.000 imagens. A regularização ajuda ainda mais? Vamos manter os hiperparâmetros iguais ao exemplo anterior – 30 épocas, taxa de aprendizado de 0,5, tamanho de mini-lote de 10. No entanto, precisamos modificar o parâmetro de regularização. A razão é porque o tamanho n do conjunto de treinamento mudou de n = 1.000 para n = 50.000, e isso muda o fator de decaimento de peso 1 − (ηλ/n). Se continuássemos a usar λ = 0,1, isso significaria muito menos perda de peso e, portanto, muito menos efeito de regularização. Nós compensamos mudando para λ = 5.0.

Ok, vamos treinar nossa rede, parando primeiro para reinicializar os pesos:

python2

Obtemos os resultados:

regularized_full

Há muitas boas notícias aqui. Primeiro, nossa precisão de classificação nos dados de teste aumentou de 95.49%, quando não foi regularizada, para 96.49%. Isso é uma grande melhoria. Em segundo lugar, podemos ver que a diferença entre os resultados nos dados de treinamento e teste é muito menor do que antes, com um percentual abaixo de zero. Essa ainda é uma lacuna significativa, mas obviamente fizemos um progresso substancial para reduzir o overfitting.

Finalmente, vamos ver qual a precisão da classificação de teste que obtemos quando usamos 100 neurônios ocultos e um parâmetro de regularização de λ = 5.0. Eu não vou passar por uma análise detalhada de overfitting aqui, isso é puramente por diversão, só para ver a precisão que podemos obter quando usamos nossos novos truques: a função de custo de entropia cruzada e a Regularização L2.

python3

O resultado final é uma precisão de classificação de 97.92% nos dados de validação. É um grande salto do caso dos 30 neurônios ocultos. Na verdade, ajustando um pouco mais, para executar por 60 épocas com η = 0.1 e λ = 5.0, quebramos a barreira de 98%, alcançando uma precisão de classificação de 98.04% nos dados de validação. Nada mal para o que acaba sendo 152 linhas de código!

Descrevi a regularização como uma forma de reduzir o overfitting e aumentar as precisões de classificação. Na verdade, esse não é o único benefício. Empiricamente, ao executar várias execuções de nossas redes com o dataset MNIST, mas com diferentes inicializações de peso (aleatórias), descobrimos que as execuções não-regularizadas ocasionalmente ficarão “presas”, aparentemente capturadas em mínimos locais da função de custo. O resultado é que diferentes execuções às vezes fornecem resultados bastante diferentes. Por outro lado, as execuções regularizadas forneceram resultados muito mais facilmente replicáveis.

Por que isso está acontecendo? Heuristicamente, se a função de custo for desregularizada, o comprimento do vetor de peso provavelmente crescerá, todas as outras coisas sendo iguais. Com o tempo, isso pode levar o vetor de peso a ser realmente muito grande. Isso pode fazer com que o vetor de peso fique preso apontando mais ou menos na mesma direção, já que as mudanças devido a descida do gradiente fazem apenas pequenas alterações na direção, quando o comprimento é longo. Acredito que esse fenômeno esteja dificultando o nosso algoritmo de aprendizado para explorar adequadamente o espaço de pesos e, consequentemente, mais difícil encontrar bons mínimos da função de custo.

Ainda não acabamos sobre regularização. Mais sobre isso no próximo capítulo! Até lá!

Referências:

Dot Product 

Neural Networks & The Backpropagation Algorithm, Explained

Neural Networks and Deep Learning

Machine Learning

The Elements of Statistical Learning: Data Mining, Inference, and Prediction, Second Edition

Gradient Descent For Machine Learning

Pattern Recognition and Machine Learning

Understanding Activation Functions in Neural Networks

Redes Neurais, princípios e práticas

Deep Learning Book

Capítulo 19 – Overfitting e Regularização – Parte 1

by

O físico Enrico Fermi, ganhador do Prêmio Nobel de Física em 1938, foi questionado sobre sua opinião em relação a um modelo matemático que alguns colegas haviam proposto como a solução para um importante problema de física não resolvido. O modelo teve excelente performance no experimento, mas Fermi estava cético. Ele perguntou quantos parâmetros livres poderiam ser definidos no modelo. “Quatro” foi a resposta. Fermi respondeu: “Eu lembro que meu amigo Johnny Von Neumann costumava dizer: com quatro parâmetros eu posso encaixar um elefante, e com cinco eu posso fazê-lo mexer seu tronco” *. Com isso, ele quis dizer que não se deve ficar impressionado quando um modelo complexo se ajusta bem a um conjunto de dados. Com parâmetros suficientes, você pode ajustar qualquer conjunto de dados.

(* A citação vem de um artigo de Freeman Dyson, que é uma das pessoas que propôs o modelo. O artigo “Um elefante de quatro parâmetros” ou “A four-parameter elephant” pode ser encontrado aqui.)

O ponto, claro, é que modelos com um grande número de parâmetros podem descrever uma variedade incrivelmente ampla de fenômenos. Mesmo que tal modelo esteja de acordo com os dados disponíveis, isso não o torna um bom modelo. Isso pode significar apenas que há liberdade suficiente no modelo que pode descrever quase qualquer conjunto de dados de tamanho determinado, sem capturar nenhuma percepção genuína do fenômeno em questão. Quando isso acontece, o modelo funcionará bem para os dados existentes, mas não conseguirá generalizar para novas situações. O verdadeiro teste de um modelo é sua capacidade de fazer previsões em situações que não foram expostas antes.

Fermi e von Neumann suspeitavam de modelos com quatro parâmetros. Nossa rede de 30 neurônios ocultos para classificação de dígitos MNIST possui quase 24.000 parâmetros! Nossa rede de 100 neurônios ocultos tem cerca de 80.000 parâmetros e redes neurais profundas de última geração às vezes contêm milhões ou até bilhões de parâmetros. Devemos confiar nos resultados?

Vamos aguçar este problema construindo uma situação em que a nossa rede faz um mau trabalho ao generalizar para novas situações. Usaremos nossa rede de 30 neurônios ocultos, com seus 23.860 parâmetros. Mas não treinamos a rede usando todas as imagens de treinamento de 50.000 dígitos MNIST. Em vez disso, usaremos apenas as primeiras 1.000 imagens de treinamento. Usar esse conjunto restrito tornará o problema com a generalização muito mais evidente. Vamos treinar usando a função de custo de entropia cruzada, com uma taxa de aprendizado de η = 0,5 e um tamanho de mini-lote de 10. No entanto, vamos treinar por 400 épocas, pois não estamos usando muitos exemplos de treinamento. Vamos usar network2 para ver como a função de custo muda:

 

network

 

Usando os resultados, podemos traçar a maneira como o custo muda à medida que a rede aprende (o script overfitting.py contém o código que gera esse resultado):

overfitting1

Isso parece encorajador, mostrando uma redução suave no custo, exatamente como esperamos. Note que eu só mostrei as épocas de treinamento de 200 a 399. Isso nos dá uma boa visão dos últimos estágios do aprendizado, que, como veremos, é onde está a ação interessante.

Vamos agora ver como a precisão da classificação nos dados de teste muda com o tempo:

overfitting2

Mais uma vez, eu ampliei um pouco. Nas primeiras 200 épocas (não mostradas), a precisão sobe para pouco menos de 82%. O aprendizado então diminui gradualmente. Finalmente, por volta da época 280, a precisão da classificação praticamente pára de melhorar. As épocas posteriores meramente vêem pequenas flutuações estocásticas perto do valor da precisão na época 280. Compare isso com o gráfico anterior, em que o custo associado aos dados de treinamento continua a cair suavemente. Se olharmos apenas para esse custo, parece que nosso modelo ainda está ficando “melhor”. Mas os resultados da precisão do teste mostram que a melhoria é uma ilusão. Assim como o modelo que Fermi não gostava, o que nossa rede aprende após a época 280 não mais se generaliza para os dados de teste. E assim não é um aprendizado útil. Dizemos que a rede está super adaptando ou com sobreajuste ou ainda com overfitting, a partir da época 280.

Você pode se perguntar se o problema aqui é que eu estou olhando para o custo dos dados de treinamento, ao contrário da precisão da classificação nos dados de teste. Em outras palavras, talvez o problema seja que estamos fazendo uma comparação de maçãs e laranjas. O que aconteceria se comparássemos o custo dos dados de treinamento com o custo dos dados de teste, estaríamos comparando medidas semelhantes? Ou talvez pudéssemos comparar a precisão da classificação tanto nos dados de treinamento quanto nos dados de teste? Na verdade, essencialmente o mesmo fenômeno aparece, não importa como fazemos a comparação. Os detalhes mudam, no entanto. Por exemplo, vamos analisar o custo nos dados de teste:

overfitting3

Podemos ver que o custo nos dados de teste melhora até a época 15, mas depois disso ele realmente começa a piorar, mesmo que o custo nos dados de treinamento continue melhorando. Este é outro sinal de que nosso modelo está super adaptando (overfitting). No entanto, coloca um enigma, que é se devemos considerar a época 15 ou a época 280 como o ponto em que o overfitting está dominando a aprendizagem? Do ponto de vista prático, o que realmente nos importa é melhorar a precisão da classificação nos dados de teste, enquanto o custo dos dados de teste não é mais do que um proxy para a precisão da classificação. E assim faz mais sentido considerar a época 280 como o ponto além do qual o overfitting está dominando o aprendizado em nossa rede neural.

Outro sinal de overfitting pode ser visto na precisão da classificação nos dados de treinamento:

overfitting4

A precisão aumenta até 100%. Ou seja, nossa rede classifica corretamente todas as 1.000 imagens de treinamento! Enquanto isso, nossa precisão de teste atinge apenas 82,27%. Portanto, nossa rede realmente está aprendendo sobre as peculiaridades do conjunto de treinamento, não apenas reconhecendo os dígitos em geral. É quase como se nossa rede estivesse apenas memorizando o conjunto de treinamento, sem entender os dígitos suficientemente bem para generalizar o conjunto de testes.

Overfitting é um grande problema em redes neurais. Isso é especialmente verdadeiro em redes modernas, que geralmente têm um grande número de pesos e vieses. Para treinar de forma eficaz, precisamos de uma maneira de detectar quando o overfitting está acontecendo. E precisamos aplicar técnicas para reduzir os efeitos do overfitting (por todo esse trabalho e conhecimento necessário, Cientistas de Dados devem ser muito bem remunerados).

A maneira óbvia de detectar overfitting é usar a abordagem acima, mantendo o controle da precisão nos dados de teste conforme nossos treinos da rede. Se percebermos que a precisão nos dados de teste não está mais melhorando, devemos parar de treinar. É claro que, estritamente falando, isso não é necessariamente um sinal de overfitting. Pode ser que a precisão nos dados de teste e os dados de treinamento parem de melhorar ao mesmo tempo. Ainda assim, a adoção dessa estratégia impedirá o overfitting.

Na verdade, usaremos uma variação dessa estratégia. Lembre-se de que, quando carregamos os dados MNIST, carregamos em três conjuntos de dados:

 

code

 

Até agora, usamos o training_data e test_data e ignoramos o validation_data. O validation_data contém 10.000 imagens de dígitos, imagens que são diferentes das 50.000 imagens no conjunto de treinamento MNIST e das 10.000 imagens no conjunto de teste MNIST. Em vez de usar o test_data para evitar overfitting, usaremos o validation_data. Para fazer isso, usaremos praticamente a mesma estratégia descrita acima para o test_data. Ou seja, calcularemos a precisão da classificação nos dados de validação no final de cada época. Quando a precisão da classificação nos dados de validação estiver saturada, paramos de treinar. Essa estratégia é chamada de parada antecipada (Early-Stopping). É claro que, na prática, não sabemos imediatamente quando a precisão está saturada. Em vez disso, continuamos treinando até termos certeza de que a precisão está saturada.

Por que usar o validation_data para evitar overfitting, em vez de test_data? Na verdade, isso faz parte de uma estratégia mais geral, que é usar o validation_data para avaliar diferentes opções de avaliação de hiperparâmetros, como o número de épocas para treinamento, a taxa de aprendizado, a melhor arquitetura de rede e assim por diante. Usamos essas avaliações para encontrar e definir bons valores para os hiperparâmetros. De fato, embora eu não tenha mencionado isso até agora, isto é, em parte, como chegamos às escolhas de hiperparâmetros feitas anteriormente neste livro. (Mais sobre isso depois.)

Claro, isso não responde de forma alguma à pergunta de por que estamos usando o validation_data para evitar overfitting, em vez de test_data. Para entender o porquê, considere que, ao definir os hiperparâmetros, é provável que tentemos muitas opções diferentes para os hiperparâmetros. Se definirmos os hiperparâmetros com base nas avaliações do test_data, será possível acabarmos super adequando nossos hiperparâmetros ao test_data. Ou seja, podemos acabar encontrando hiperparâmetros que se encaixam em peculiaridades particulares dos dados de teste, mas onde o desempenho da rede não se generalizará para outros conjuntos de dados. Protegemos contra isso descobrindo os hiperparâmetros usando o validation_data. Então, uma vez que tenhamos os hiperparâmetros que queremos, fazemos uma avaliação final da precisão usando o test_data. Isso nos dá confiança de que nossos resultados nos dados de teste são uma medida real de quão bem nossa rede neural se generaliza. Para colocar de outra forma, você pode pensar nos dados de validação como um tipo de dados de treinamento que nos ajuda a aprender bons parâmetros. Essa abordagem para encontrar bons hiperparâmetros é às vezes conhecida como o método “hold out”, uma vez que os dados de validação são mantidos separados ou “mantidos” a partir dos dados de treinamento.

Agora, na prática, mesmo depois de avaliar o desempenho nos dados de teste, podemos mudar nossa opinião e tentar outra abordagem – talvez uma arquitetura de rede diferente – que envolva a descoberta de um novo conjunto de hiperparâmetros. Se fizermos isso, não há perigo de acabarmos com o test_data também? Precisamos de uma regressão potencialmente infinita de conjuntos de dados, para que possamos ter certeza de que nossos resultados serão generalizados? Abordar essa preocupação é um problema profundo e difícil. Mas para nossos objetivos práticos, não vamos nos preocupar muito com essa questão. Em vez disso, vamos nos concentrar no método básico de retenção, com base nos dados training_data, validation_data e test_data, conforme descrito acima.

Vimos que o overfitting ocorre quando estamos usando apenas 1.000 imagens de treinamento. O que acontece quando usamos o conjunto completo de treinamento de 50.000 imagens? Manteremos todos os outros parâmetros iguais (30 neurônios ocultos, taxa de aprendizado de 0,5, tamanho de mini-lote de 10), mas treinamos usando todas as 50.000 imagens por 30 épocas. Aqui está um gráfico mostrando os resultados da precisão de classificação nos dados de treinamento e nos dados de teste. Observe que usei os dados de teste aqui, em vez dos dados de validação, para tornar os resultados mais diretamente comparáveis aos gráficos anteriores.

overfitting_full

Como você pode ver, a precisão nos dados de teste e treinamento permanece muito mais próxima do que quando estávamos usando 1.000 exemplos de treinamento. Em particular, a melhor precisão de classificação de 97,86% nos dados de treinamento é apenas 2,53% maior do que os 95,33% nos dados de teste. Isso é comparado com a diferença de 17,73% que tivemos anteriormente! Overfitting ainda está acontecendo, mas foi bastante reduzido. Nossa rede está se generalizando muito melhor dos dados de treinamento para os dados de teste. Em geral, uma das melhores maneiras de reduzir o overfitting é aumentar o volume (tamanho) dos dados de treinamento. Com dados de treinamento suficientes, é difícil até mesmo uma rede muito grande sofrer de overfitting. Infelizmente, os dados de treinamento podem ser caros ou difíceis de adquirir, por isso nem sempre é uma opção prática.

Aumentar a quantidade de dados de treinamento é uma maneira de reduzir o overfitting. Mas existem outras maneiras de reduzir a extensão do overfitting? Uma abordagem possível é reduzir o tamanho da nossa rede. No entanto, redes grandes têm o potencial de serem mais poderosas do que redes pequenas e, portanto, essa é uma opção que só adotamos em último caso.

Felizmente, existem outras técnicas que podem reduzir o overfitting, mesmo quando temos uma rede fixa e dados de treinamento fixos. Estas técnicas são conhecidas como técnicas de regularização e serão assunto do próximo capítulo.

Até lá!

Referências:

Formação Engenheiro de IA

Dot Product 

Neural Networks & The Backpropagation Algorithm, Explained

Neural Networks and Deep Learning

Machine Learning

The Elements of Statistical Learning: Data Mining, Inference, and Prediction, Second Edition

Gradient Descent For Machine Learning

Pattern Recognition and Machine Learning

Understanding Activation Functions in Neural Networks

Redes Neurais, princípios e práticas

Deep Learning Book

Capítulo 18 – Entropia Cruzada Para Quantificar a Diferença Entre Distribuições de Probabilidade

by

A Cross-Entropy (ou entropia cruzada, se você preferir o termo em português) é fácil de implementar como parte de um programa que aprende usando gradiente descendente e backpropagation. Faremos isso nos próximos capítulos quando treinarmos uma rede completa, desenvolvendo uma versão melhorada do nosso programa anterior para classificar os dígitos manuscritos do dataset MNIST. O novo programa é chamado de network2.py e incorpora não apenas a entropia cruzada, mas também várias outras técnicas que estudaremos mais adiante. Agora, vejamos como usar a Entropia Cruzada Para Quantificar a Diferença Entre Duas Distribuições de Probabilidade.

Por enquanto, vamos ver como nosso novo programa classifica os dígitos MNIST. Usaremos uma rede com 30 neurônios ocultos, e usaremos um tamanho de mini-lote de 10. Definimos a taxa de aprendizado para η = 0,5 e nós treinamos por 30 épocas. A interface para o network2.py será um pouco diferente do network.py, mas ainda deve estar claro o que está acontecendo. Nos próximos capítulos apresentamos o código completo.

 

corss-entropy

 

Perceba que o comando net.large_weight_initializer() é usado para inicializar os pesos e vieses da mesma maneira que já descrevemos anteriormente. Precisamos executar este comando porque mais adiante vamos alterar o peso padrão para inicialização em nossas redes. O resultado da execução da sequência de comandos acima é uma rede com 95,49% de precisão.

Vejamos também o caso em que usamos 100 neurônios ocultos, a entropia cruzada, e mantemos os parâmetros da mesma forma. Neste caso, obtemos uma precisão de 96,82%. Essa é uma melhoria substancial em relação aos resultados que obtivemos nos capítulos anteriores, onde a precisão de classificação foi de 96,59%, usando o custo quadrático. Isso pode parecer uma pequena mudança, mas considere que a taxa de erro caiu de 3,41% para 3,18%. Ou seja, eliminamos cerca de um em quatorze dos erros originais. Isso é uma melhoria bastante útil.

É encorajador que o custo de entropia cruzada nos dê resultados semelhantes ou melhores do que o custo quadrático. No entanto, esses resultados não provam conclusivamente que a entropia cruzada é uma escolha melhor. A razão é que nós colocamos apenas um pequeno esforço na escolha de hyperparâmetros como taxa de aprendizado, tamanho de mini-lote e assim por diante. Para que a melhoria seja realmente convincente, precisaríamos fazer um trabalho completo de otimização desses hyperparâmetros. Ainda assim, os resultados são encorajadores e reforçam nosso argumento teórico anterior de que a entropia cruzada é uma escolha melhor do que o custo quadrático.

Isso, a propósito, é parte de um padrão geral que veremos nos próximos capítulos e, na verdade, em grande parte do restante do livro. Vamos desenvolver uma nova técnica, vamos experimentá-la e obteremos resultados “aprimorados”. É claro que é bom vermos essas melhorias, mas a interpretação de tais melhorias é sempre problemática. Elas só são verdadeiramente convincentes se virmos uma melhoria depois de nos esforçarmos para otimizar todos os outros hyperparâmetros. Isso é uma grande quantidade de trabalho, exigindo muito poder de computação, e normalmente não vamos fazer uma investigação tão exaustiva. Em vez disso, procederemos com base em testes informais como os realizados até aqui.

Até agora, discutimos a entropia cruzada de forma bem detalhada. Por que tanto esforço quando a entropia cruzada nos dá apenas uma pequena melhora em nossos resultados com o dataset MNIST? Mais adiante veremos outras técnicas, notadamente a regularização, que trazem melhorias muito maiores. Então, por que tanto foco na entropia cruzada? Parte da razão é que a entropia cruzada é uma função de custo amplamente utilizada e, portanto, vale a pena compreendê-la bem. Mas a razão mais importante é que a saturação dos neurônios é um problema importante nas redes neurais, um problema ao qual voltaremos repetidamente ao longo do livro. Por isso discutimos a entropia cruzada em extensão pois é um bom laboratório para começar a entender a saturação dos neurônios e como ela pode ser abordada.

O que significa a entropia cruzada? De onde isso vem?

Nossa discussão sobre a entropia cruzada se concentrou na análise algébrica e na implementação prática. Isso é útil, mas deixa questões conceituais mais amplas não respondidas, como: o que significa a entropia cruzada? Existe alguma maneira intuitiva de pensar sobre a entropia cruzada? E quanto ao significado intuitivo da entropia cruzada? Como devemos pensar sobre isso?

Explicar isso em profundidade nos levaria mais longe do que queremos ir neste livro. No entanto, vale ressaltar que existe uma maneira padrão de interpretar a entropia cruzada que vem do campo da teoria da informação. Vejamos.

Já sabemos que para treinar uma rede neural, você precisa encontrar o erro entre as saídas calculadas e as saídas alvo desejadas. A medida de erro mais comum é chamada de erro quadrático médio (ou Mean Square Error). No entanto, existem alguns resultados de pesquisa que sugerem o uso de uma medida diferente, denominada erro de entropia cruzada, como método preferível em relação ao erro quadrático médio.

A medida de entropia cruzada tem sido utilizada como alternativa ao erro quadrático médio. A entropia cruzada pode ser usada como uma medida de erro quando as saídas de uma rede podem ser pensadas como representando hipóteses independentes (por exemplo, cada nó significa um conceito diferente) e as ativações dos nós podem ser entendidas como representando a probabilidade (ou a confiança) que cada uma das hipóteses pode ser verdadeira. Nesse caso, o vetor de saída representa uma distribuição de probabilidade, e nossa medida de erro – entropia cruzada – indica a distância entre o que a rede acredita que essa distribuição deve ser e o que realmente deveria ser. Existe também uma razão prática para usar a entropia cruzada. Pode ser mais útil em problemas nos quais os alvos são 0 e 1. A entropia cruzada tende a permitir que erros alterem pesos mesmo quando houver nós saturados (o que significa que suas derivadas são próximas de 0). Vamos compreender melhor isso:

A entropia cruzada é comumente usada para quantificar a diferença entre duas distribuições de probabilidade. Geralmente, a distribuição “verdadeira” (dos dados usados para treinamento) é expressa em termos de uma distribuição One-Hot.

Por exemplo, suponha que para uma instância de treinamento específica (uma única linha no seu dataset), a classe seja B (de 3 possíveis possibilidades: A, B e C). A distribuição única para esta instância de treinamento é, portanto:

 

Pr(Class A)  Pr(Class B)  Pr(Class C)

0.0          1.0          0.0

 

Você pode interpretar a distribuição acima da seguinte forma: a instância de treinamento tem 0% de probabilidade de ser classe A, 100% de probabilidade de ser classe B e 0% de probabilidade de ser a classe C.

Agora, suponha que seu algoritmo de aprendizado de máquina tenha previsto a seguinte distribuição de probabilidade:

 

Pr(Class A)  Pr(Class B)  Pr(Class C)     

0.228          0.619           0.153

 

Quão próxima é a distribuição prevista da distribuição verdadeira? É isso que determina o erro de entropia cruzada. A entropia cruzada é representada por esta fórmula:

 

cross-entropy

 

A soma é sobre as três classes A, B e C. Se você completar o cálculo, você achará que a perda é 0.479. Então, é assim que “longe” está a sua previsão da distribuição verdadeira.

A entropia cruzada é uma das muitas funções de perda possíveis. Essas funções de perda são tipicamente escritas como J(theta) e podem ser usadas dentro da descida do gradiente, que é uma estrutura iterativa para mover os parâmetros (ou coeficientes) para os valores ótimos. A entropia cruzada descreve a perda entre duas distribuições de probabilidade.

Ao usar uma rede neural para realizar classificação e predição, geralmente é melhor usar o erro de entropia cruzada do que o erro de classificação e um pouco melhor usar o erro de entropia cruzada do que o erro quadrático médio para avaliar a qualidade da rede neural. É importante deixar claro que estamos lidando apenas com uma rede neural que é usada para classificar os dados, como a previsão da concessão de crédito (sim ou não), ou ainda outras classificações como idade, sexo ou dígitos no dataset MNIST e assim por diante. Não estamos lidando com uma rede neural que faz regressão, onde o valor a ser previsto é numérico.

Até o próximo capítulo!

Referências:

Dot Product 

Neural Networks & The Backpropagation Algorithm, Explained

Derivada

Machine Learning

The Elements of Statistical Learning: Data Mining, Inference, and Prediction, Second Edition

Gradient Descent For Machine Learning

Pattern Recognition and Machine Learning

Understanding Activation Functions in Neural Networks

Redes Neurais, princípios e práticas

Deep Learning Book

Capítulo 17 – Cross-Entropy Cost Function

by

Quando um jogador de tênis está aprendendo a praticar o esporte, ele geralmente passa a maior parte do tempo desenvolvendo o movimento do corpo. Apenas gradualmente ele desenvolve as tacadas, aprende a movimentar a bola com precisão para a quadra adversária e com isso vai construindo sua técnica, que se aprimora à medida que ele pratica. De maneira semelhante, até agora nos concentramos em entender o algoritmo de retropropagação (backpropagation), a base para aprender a maioria das atividades em redes neurais. A partir de agora, estudaremos um conjunto de técnicas que podem ser usadas para melhorar nossa implementação do backpropagation e, assim, melhorar a maneira como nossas redes aprendem.

As técnicas que desenvolveremos incluem: uma melhor escolha de função de custo, conhecida como função de custo de entropia cruzada (ou Cross-Entropy Cost Function); quatro métodos de “regularização” (regularização de L1 e L2, dropout e expansão artificial dos dados de treinamento), que melhoram nossas redes para generalizar além dos dados de treinamento; um método melhor para inicializar os pesos na rede; e um conjunto de heurísticas para ajudar a escolher bons hyperparâmetros para a rede. Também vamos analisar várias outras técnicas com menos profundidade. As discussões são em grande parte independentes umas das outras e, portanto, você pode avançar se quiser. Também implementaremos muitas das técnicas em nosso código e usaremos para melhorar os resultados obtidos no problema de classificação de dígitos manuscritos estudado nos capítulos anteriores.

Naturalmente, estamos cobrindo apenas algumas das muitas técnicas que foram desenvolvidas para uso em redes neurais. A filosofia é que o melhor acesso à multiplicidade de técnicas disponíveis é o estudo aprofundado de algumas das mais importantes. Dominar essas técnicas importantes não é apenas útil por si só, mas também irá aprofundar sua compreensão sobre quais problemas podem surgir quando você usa redes neurais. Isso deixará você bem preparado para aprender rapidamente outras técnicas, conforme necessário.

A Função de Custo

A maioria de nós acha desagradável estar errado. Logo depois de começar a aprender piano, minha filha fez sua primeira apresentação diante de uma platéia. Ela estava nervosa e começou a tocar a peça com uma oitava muito baixa. Ela ficou confusa e não pôde continuar até que alguém apontasse o erro. Ela ficou muito envergonhada. Ainda que desagradável, também aprendemos rapidamente quando estamos decididamente errados. Você pode apostar que a próxima vez que ela se apresentou diante de uma platéia, ela começou na oitava correta! Em contraste, aprendemos mais lentamente quando nossos erros são menos bem definidos.

Idealmente, esperamos que nossas redes neurais aprendam rapidamente com seus erros. Mas é isso que acontece na prática? Para responder a essa pergunta, vamos dar uma olhada em um exemplo simples. O exemplo envolve um neurônio com apenas uma entrada:

 

neuron

 

Nós vamos treinar esse neurônio para fazer algo ridiculamente fácil: obter a entrada 1 e gerar a saída 0. Claro, essa é uma tarefa tão trivial que poderíamos facilmente descobrir um peso apropriado e um viés (bias) de forma manual, sem usar um algoritmo de aprendizado. No entanto, vai nos ajudar a compreender melhor o processo de usar gradiente descendente para tentar aprender um peso e viés. Então, vamos dar uma olhada em como o neurônio aprende.

Para tornar as coisas definitivas, escolhemos o peso inicial como 0.6 e o ​​viés inicial como 0.9. Estas são escolhas genéricas usadas como um lugar para começar a aprender, eu não as escolhi para serem especiais de alguma forma. A saída inicial do neurônio é 0.82, então um pouco de aprendizado será necessário antes que nosso neurônio se aproxime da saída desejada 0,0.

No gráfico abaixo, podemos ver como o neurônio aprende uma saída muito mais próxima de 0.0. Durante o treinamento, o modelo está realmente computando o gradiente, e usando o gradiente para atualizar o peso e o viés, e exibir o resultado. A taxa de aprendizado é η = 0.15, o que acaba sendo lento o suficiente para que possamos acompanhar o que está acontecendo, mas rápido o suficiente para que possamos obter um aprendizado substancial em apenas alguns segundos. O custo é a função de custo quadrático, C, apresentada nos capítulos anteriores. Vou lembrá-lo da forma exata da função de custo em breve.

 

train

 

Como você pode ver, o neurônio aprende um peso e um viés que diminui o custo e dá uma saída do neurônio de cerca de 0.09 (Epoch, ou Época em português, é o número de passadas que nosso modelo faz pelos dados. A cada passada, os pesos são atualizados, o aprendizado ocorre e o custo, ou a taxa de erros, diminui). Isso não é exatamente o resultado desejado, 0.0, mas é muito bom.

Suponha, no entanto, que, em vez disso, escolhamos o peso inicial e o viés inicial como 2.0. Nesse caso, a saída inicial é 0.98, o que é muito ruim. Vamos ver como o neurônio aprende a gerar 0 neste caso:

 

train2

 

Embora este exemplo use a mesma taxa de aprendizado (η = 0.15), podemos ver que a aprendizagem começa muito mais devagar. De fato, nas primeiras 150 épocas de aprendizado, os pesos e vieses não mudam muito. Então o aprendizado entra em ação e, como em nosso primeiro exemplo, a saída do neurônio se aproxima rapidamente de 0.0.

Esse comportamento é estranho quando comparado ao aprendizado humano. Como eu disse no começo deste capítulo, muitas vezes aprendemos mais rápido quando estamos muito errados sobre algo. Mas acabamos de ver que nosso neurônio artificial tem muita dificuldade em aprender quando está muito errado – muito mais dificuldade do que quando está apenas um pouco errado. Além do mais, verifica-se que esse comportamento ocorre não apenas neste exemplo, mas em redes mais gerais. Por que aprender tão devagar? E podemos encontrar uma maneira de evitar essa desaceleração?

Para entender a origem do problema, considere que nosso neurônio aprende mudando o peso e o viés a uma taxa determinada pelas derivadas parciais da função custo, ∂C/∂w e ∂C/∂b. Então, dizer “aprender é lento” é realmente o mesmo que dizer que essas derivadas parciais são pequenas. O desafio é entender por que eles são pequenas. Para entender isso, vamos calcular as derivadas parciais. Lembre-se de que estamos usando a função de custo quadrático, que é dada por:

cost

onde a é a saída do neurônio quando a entrada de treinamento x = 1 é usada, e y = 0 é a saída desejada correspondente. Para escrever isso mais explicitamente em termos de peso e viés, lembre-se que a = σ(z), onde z = wx + b. Usando a regra da cadeia para diferenciar em relação ao peso e viés, obtemos:

 

cost2

 

onde substitui x = 1 e y = 0. Para entender o comportamento dessas expressões, vamos olhar mais de perto o termo σ ′ (z) no lado direito. Lembre-se da forma da função σ:

 

sig

 

Podemos ver neste gráfico que quando a saída do neurônio é próxima de 1, a curva fica muito plana, e então σ ′ (z) fica muito pequeno. As equações acima então nos dizem que ∂C/∂w e ∂C/∂b ficam muito pequenos. Esta é a origem da desaceleração da aprendizagem. Além do mais, como veremos mais adiante, a desaceleração do aprendizado ocorre basicamente pelo mesmo motivo em redes neurais mais genéricas, não apenas neste exemplo simples.

A Função de Custo de Entropia Cruzada

Como podemos abordar a desaceleração da aprendizagem? Acontece que podemos resolver o problema substituindo o custo quadrático por uma função de custo diferente, conhecida como entropia cruzada. Para entender a entropia cruzada, vamos nos afastar um pouco do nosso modelo super-simples. Vamos supor que estamos tentando treinar um neurônio com diversas variáveis de entrada, x1, x2,…, pesos correspondentes w1, w2,… e um viés, b:

 

neuron2

 

A saída do neurônio é, naturalmente, a = σ(z), onde z = ∑jwjxj + b é a soma ponderada das entradas. Nós definimos a função de custo de entropia cruzada para este neurônio assim:

 

entropy

 

onde n é o número total de itens de dados de treinamento, a soma é sobre todas as entradas de treinamento x, e y é a saída desejada correspondente. Não é óbvio que a expressão anterior resolva o problema de desaceleração do aprendizado. De fato, francamente, nem é óbvio que faz sentido chamar isso de uma função de custo! Antes de abordar a desaceleração da aprendizagem, vamos ver em que sentido a entropia cruzada pode ser interpretada como uma função de custo.

Duas propriedades em particular tornam razoável interpretar a entropia cruzada como uma função de custo. Primeiro, não é negativo, isto é, C > 0. Para visualizar isso, observe na fórmula anterior que: (a) todos os termos individuais na soma são negativos, já que ambos os logaritmos são de números no intervalo de 0 a 1; e (b) há um sinal de menos na frente da soma.

Segundo, se a saída real do neurônio estiver próxima da saída desejada para todas as entradas de treinamento x, então a entropia cruzada será próxima de zero. Para ver isso, suponha, por exemplo, que y = 0 e a ≈ 0 para alguma entrada x. Este é um caso quando o neurônio está fazendo um bom trabalho nessa entrada. Vemos que o primeiro termo (na fórmula acima) para o custo, desaparece, desde que y = 0, enquanto o segundo termo é apenas −ln (1 − a) ≈ 0. Uma análise semelhante é válida quando y = 1 e a ≈ 1. E assim, a contribuição para o custo será baixa, desde que a saída real esteja próxima da saída desejada.

Em suma, a entropia cruzada é positiva e tende a zero, à medida que o neurônio melhora a computação da saída desejada, y, para todas as entradas de treinamento, x.

Essas são as duas propriedades que esperamos intuitivamente para uma função de custo. De fato, ambas as propriedades também são satisfeitas pelo custo quadrático. Portanto, isso é uma boa notícia para a entropia cruzada. Mas a função custo de entropia cruzada tem o benefício de que, ao contrário do custo quadrático, evita o problema de desaceleração do aprendizado. Para ver isso, vamos calcular a derivada parcial do custo de entropia cruzada em relação aos pesos. Substituímos a = σ (z) na fórmula acima e aplicamos a regra da cadeia duas vezes, obtendo:

 

form1

 

Colocando tudo em um denominador comum e simplificando, isso se torna:

 

form2

 

Usando a definição da função sigmóide, σ (z) = 1 / (1 + ez), e um pouco de álgebra, podemos mostrar que σ (z) = σ (z) (1 − σ (z)). Vemos que os termos σ′ (z) e σ (z) (1 − σ (z)) se cancelam na equação acima, e simplificando torna-se:

 

form3

 

Esta é uma bela expressão. Ela nos diz que a taxa na qual o peso aprende é controlada por σ (z) −y, ou seja, pelo erro na saída. Quanto maior o erro, mais rápido o neurônio aprenderá. Isso é exatamente o que nós esperamos intuitivamente. Em particular, evita a lentidão de aprendizado causada pelo termo σ′ (z) na equação análoga para o custo quadrático. Quando usamos a entropia cruzada, o termo σ′ (z) é cancelado e não precisamos mais nos preocupar em ser pequeno. Este cancelamento é o milagre especial assegurado pela função de custo de entropia cruzada. Na verdade, não é realmente um milagre. Como veremos mais adiante, a entropia cruzada foi especialmente escolhida por ter apenas essa propriedade.

De maneira semelhante, podemos calcular a derivada parcial para o viés. Eu não vou passar por todos os detalhes novamente, mas você pode facilmente verificar que:

form4

 

Novamente, isso evita a lentidão de aprendizado causada pelo termo σ′ (z) na equação análoga para o custo quadrático.

Agora vamos retornar ao exemplo do início deste capítulo, e explorar o que acontece quando usamos a entropia cruzada em vez do custo quadrático. Para nos reorientarmos, começaremos com o caso em que o custo quadrático foi bom, com peso inicial de 0.6 e viés inicial de 0.9. Veja o que acontece quando substituímos o custo quadrático pela entropia cruzada:

 

train3

 

Como era de se esperar, o neurônio aprende perfeitamente bem neste caso, assim como fez anteriormente. E agora vamos olhar para o caso em que nosso neurônio ficou preso antes, com o peso e o viés ambos começando em 2.0:

 

train4

 

Sucesso! Desta vez, o neurônio aprendeu rapidamente, exatamente como esperávamos. Se você observar atentamente, pode ver que a inclinação da curva de custo era muito mais íngreme inicialmente do que a região plana inicial na curva correspondente para o custo quadrático. É essa inclinação que a entropia cruzada nos ajuda a resolver, impedindo-nos de ficar presos exatamente quando esperamos que nosso neurônio aprenda mais depressa, ou seja, quando o neurônio começa errado.

Eu não disse qual taxa de aprendizado foi usada nos exemplos que acabei de ilustrar. Anteriormente, com o custo quadrático, usamos η = 0.15. Deveríamos ter usado a mesma taxa de aprendizado nos novos exemplos? De fato, com a mudança na função de custo, não é possível dizer precisamente o que significa usar a “mesma” taxa de aprendizado; é uma comparação de maçãs e laranjas. Para ambas as funções de custo, simplesmente experimentei encontrar uma taxa de aprendizado que possibilitasse ver o que está acontecendo. Se você ainda estiver curioso, aqui está o resumo: usei η = 0.005 nos exemplos que acabei de fornecer.

Você pode contestar que a mudança na taxa de aprendizado torna os gráficos acima sem sentido. Quem se importa com a rapidez com que o neurônio aprende, quando a nossa escolha de taxa de aprendizado foi arbitrária, para começar ?! Mas essa objeção não procede. O ponto dos gráficos não é sobre a velocidade absoluta de aprendizagem. É sobre como a velocidade do aprendizado muda. Em particular, quando usamos o custo quadrático, a aprendizagem é mais lenta quando o neurônio está inequivocamente errado do que é mais tarde durante o treinamento, à medida que o neurônio se aproxima da saída correta; enquanto o aprendizado de entropia cruzada é mais rápido quando o neurônio está inequivocamente errado. Essas declarações não dependem de como a taxa de aprendizado é definida.

Estamos estudando a entropia cruzada para um único neurônio. No entanto, é fácil generalizar a entropia cruzada para redes multicamadas de muitos neurônios. Em particular, suponha que y = y1, y2,… são os valores desejados nos neurônios de saída, ou seja, os neurônios na camada final, enquanto aL1, aL2,… são os valores reais de saída. Então nós definimos a entropia cruzada por:

 

cost

 

Isso é o mesmo que nossa expressão anterior, exceto que agora nós temos o ∑j somando todos os neurônios de saída. Não vou explicitamente trabalhar com uma derivação, mas deve ser plausível que o uso da expressão anterior evite uma desaceleração na aprendizagem em muitas redes de neurônios.

A propósito, estou usando o termo “entropia cruzada” de uma maneira que confundiu alguns dos primeiros leitores, já que parece superficialmente entrar em conflito com outras fontes. Em particular, é comum definir a entropia cruzada para duas distribuições de probabilidade, pj e qj, como ∑jpjlnqj. Esta definição pode ser conectada a fórmula da entropia para um neurônio mostrada anteriormente, se tratarmos um único neurônio sigmóide como saída de uma distribuição de probabilidade que consiste na ativação a do neurônio ae seu complemento 1 − a.

No entanto, quando temos muitos neurônios sigmoides na camada final, o vetor aLj de ativações não costuma formar uma distribuição de probabilidade. Como resultado, uma definição como ∑jpjlnqj não faz sentido, já que não estamos trabalhando com distribuições de probabilidade. Em vez disso, você pode pensar na fórmula da entropia para múltiplos neurônios como um conjunto somado de entropias cruzadas por neurônio, com a ativação de cada neurônio sendo interpretada como parte de uma distribuição de probabilidade de dois elementos. Sim, eu sei que isso não é simples.

Nesse sentido, a fórmula da entropia para múltiplos neurônios é uma generalização da entropia cruzada para distribuições de probabilidade.

Quando devemos usar a entropia cruzada em vez do custo quadrático? De fato, a entropia cruzada é quase sempre a melhor escolha, desde que os neurônios de saída sejam neurônios sigmóides. Para entender por que, considere que, quando estamos configurando a rede, normalmente inicializamos os pesos e vieses usando algum tipo de aleatoriedade. Pode acontecer que essas escolhas iniciais resultem na rede sendo decisivamente errada para alguma entrada de treinamento – isto é, um neurônio de saída terá saturado próximo de 1, quando deveria ser 0, ou vice-versa. Se estamos usando o custo quadrático que irá desacelerar a aprendizagem, ele não vai parar de aprender completamente, já que os pesos continuarão aprendendo com outras entradas de treinamento, mas é obviamente indesejável.

Construir aplicações de IA é uma habilidade com demanda cada vez maior no mercado.

Até o próximo capítulo!

Referências:

Dot Product 

Neural Networks & The Backpropagation Algorithm, Explained

Derivada

Machine Learning

The Elements of Statistical Learning: Data Mining, Inference, and Prediction, Second Edition

Gradient Descent For Machine Learning

Pattern Recognition and Machine Learning

Understanding Activation Functions in Neural Networks

Redes Neurais, princípios e práticas

Deep Learning Book

Capítulo 16 – Algoritmo Backpropagation em Python

by

Depois de compreender como funciona o backpropagation, podemos agora entender o código usado em alguns capítulos anteriores para implementar o algoritmo (o qual vamos reproduzir aqui). 

Em nosso código nós temos os métodos update_mini_batch e backprop da classe Network. Em particular, o método update_mini_batch atualiza os pesos e bias da rede calculando o gradiente para o mini_batch atual de exemplos (dados) de treinamento:

 

metodo1

 

A maior parte do trabalho é feita pela linha:

delta_nabla_b, delta_nabla_w = self.backprop (x, y)

que usa o método backprop para descobrir as derivadas parciais ∂Cx / ∂blj e ∂Cx / ∂wljk. Isso invoca o algoritmo de backpropagation, que é uma maneira rápida de calcular o gradiente da função de custo. Portanto, update_mini_batch funciona simplesmente calculando esses gradientes para cada exemplo de treinamento no mini_batch e, em seguida, atualizando self.weights e self.biases adequadamente. Há uma pequena mudança – usamos uma abordagem ligeiramente diferente para indexar as camadas. Essa alteração é feita para aproveitar um recurso do Python, ou seja, o uso de índices de lista negativa para contar para trás a partir do final de uma lista, por exemplo, lst[-3] é a terceira última entrada em uma lista chamada lst. O código para backprop está abaixo, junto com algumas funções auxiliares, que são usadas para calcular a função σ, a derivada σ′ e a derivada da função de custo. Com essas inclusões, você deve ser capaz de entender o código de maneira independente:

 

backprop

 

Observe o método backprop. Começamos inicalizando as matrizes de pesos (nabla_w) e bias (nabla_b) com zeros. Essas  matrizes serão alimentadas com valores durante o processo de treinamento. Isso é o que a rede neural artificial efetivamente aprende. Depois de inicializar alguns objetos, temos um loop for para cada valor de b e w (que a esta altura você já sabe se trata de bias e pesos, respectivamente). Neste loop, usamos a função np.dot do Numpy para a multiplicação entre matrizes e adição do bias, colocamos o resultado na lista z e fazemos uma chamada à função de ativação Sigmóide. Ao final deste loop, teremos a lista com todas as ativações e finalizamos a passada para a frente.

Na passada para trás (Backward Pass) calculamos as derivadas e fazemos as multiplicações de matrizes mais uma vez (o funcionamento de redes neurais artificiais é baseado em um conceito elementar da Álgebra Linear, a multiplicação de matrizes). Repare que chamamos o método Transpose() para gerar a transposta da matriz e assim ajustar as dimensões antes de efetuar os cálculo. Por fim, retornamos bias e pesos.

Em que sentido backpropagation é um algoritmo rápido?

Para responder a essa pergunta, vamos considerar outra abordagem para calcular o gradiente. Imagine que é o início da pesquisa de redes neurais. Talvez seja a década de 1950 ou 1960, e você é a primeira pessoa no mundo a pensar em usar gradiente descendente para o aprendizado! Mas, para que a ideia funcione, você precisa de uma maneira de calcular o gradiente da função de custo. Você volta ao seu conhecimento de cálculo e decide se pode usar a regra da cadeia (chain rule) para calcular o gradiente. Mas depois de brincar um pouco, a álgebra parece complicada e você fica desanimado. Então você tenta encontrar outra abordagem. Você decide considerar o custo como uma função apenas dos pesos C = C(w) (voltaremos ao bias em um momento). Você numera os pesos w1, w2,… e deseja computar ∂C / ∂wj para um peso específico wj. Uma maneira óbvia de fazer isso é usar a aproximação

form

onde ϵ> 0 é um pequeno número positivo e ej é o vetor unitário na direção j. Em outras palavras, podemos estimar ∂C / ∂wj calculando o custo C para dois valores ligeiramente diferentes de wj e, em seguida, aplicando a equação. A mesma ideia nos permitirá calcular as derivadas parciais ∂C / ∂b em relação aos vieses (bias).

Essa abordagem parece muito promissora. É simples conceitualmente e extremamente fácil de implementar, usando apenas algumas linhas de código. Certamente, parece muito mais promissor do que a ideia de usar a regra da cadeia para calcular o gradiente!

Infelizmente, embora essa abordagem pareça promissora, quando você implementa o código, ele fica extremamente lento. Para entender porque, imagine que temos um milhão de pesos em nossa rede. Então, para cada peso distinto wj, precisamos computar C (w + ϵej) para calcular ∂C / ∂wj. Isso significa que, para calcular o gradiente, precisamos computar a função de custo um milhão de vezes diferentes, exigindo um milhão de passos para frente pela rede (por exemplo, treinamento). Precisamos calcular C(w) também, em um total de um milhão de vezes e em uma única passada pela rede.

O que há de inteligente no backpropagation é que ele nos permite calcular simultaneamente todas as derivadas parciais ∂C / ∂wj usando apenas uma passagem direta pela rede, seguida por uma passagem para trás pela rede. Grosso modo, o custo computacional do passe para trás é quase o mesmo que o do forward. Isso deve ser plausível, mas requer algumas análises para fazer uma declaração cuidadosa. É plausível porque o custo computacional dominante no passe para frente é multiplicado pelas matrizes de peso, enquanto no passo para trás é multiplicado pelas transpostas das matrizes de peso. Obviamente, essas operações têm um custo computacional similar. E assim, o custo total da retropropagação (backpropagation) é aproximadamente o mesmo que fazer apenas duas passagens pela rede. Compare isso com o milhão e um passe para frente que precisávamos para a abordagem que descrevi anteriormente. E assim, embora a retropropagação pareça superficialmente mais complexa do que a abordagem anterior, é na verdade muito, muito mais rápida.

Essa aceleração foi amplamente apreciada em 1986 e expandiu enormemente a gama de problemas que as redes neurais poderiam resolver. Isso, por sua vez, causou uma onda de pessoas usando redes neurais. Claro, a retropropagação não é uma panacéia. Mesmo no final da década de 1980, as pessoas enfrentavam limites, especialmente quando tentavam usar a retropropagação para treinar redes neurais profundas, ou seja, redes com muitas camadas ocultas. Mais adiante, no livro, veremos como os computadores modernos e algumas novas ideias inteligentes tornam possível usar a retropropagação para treinar redes neurais bem profundas.

Seu trabalho agora é estudar e compreender cada linha de código usada em nossa rede de amostra. Esse código é bem simples e o objetivo é mostrar a você como as coisas funcionam programaticamente. Ainda vamos treinar nossa rede, avaliar seu desempenho, otimizar algumas operações e compreender outros conceitos básicos. Temos muito mais vindo por aí! Até o próximo capítulo!

Referências:

Machine Learning

Dot Product 

How the backpropagation algorithm works

Delta Rule

An overview of gradient descent optimization algorithms

Neural Networks & The Backpropagation Algorithm, Explained

Derivada

Machine Learning

The Elements of Statistical Learning: Data Mining, Inference, and Prediction, Second Edition

Gradient Descent For Machine Learning

Pattern Recognition and Machine Learning

Understanding Activation Functions in Neural Networks

Redes Neurais, princípios e práticas

An overview of gradient descent optimization algorithms

Optimization: Stochastic Gradient Descent

Gradient Descent vs Stochastic Gradient Descent vs Mini-Batch Learning

Deep Learning Book

Capítulo 14 – Algoritmo Backpropagation Parte 1 – Grafos Computacionais e Chain Rule

by

No último capítulo, vimos como as redes neurais podem aprender seus pesos e bias usando o algoritmo de gradiente descendente. Houve, no entanto, uma lacuna na nossa explicação: não discutimos como calcular o gradiente da função de custo. Neste capítulo, explicaremos sobre um algoritmo usado para calcular esses gradientes, um algoritmo conhecido como backpropagation. Como esse tema é a essência do treinamento de redes neurais, vamos dividí-lo em dois capítulos. Vamos começar com Algoritmo Backpropagation Parte 1 – Grafos Computacionais e Chain Rule.

O backpropagation é indiscutivelmente o algoritmo mais importante na história das redes neurais – sem backpropagation, seria quase impossível treinar redes de aprendizagem profunda da forma que vemos hoje. O backpropagation pode ser considerado a pedra angular das redes neurais modernas e consequentemente do Deep Learning.

O algoritmo backpropagation foi originalmente introduzido na década de 1970, mas sua importância não foi totalmente apreciada até um famoso artigo de 1986 de David Rumelhart, Geoffrey Hinton e Ronald Williams. Esse artigo descreve várias redes neurais em que o backpropagation funciona muito mais rapidamente do que as abordagens anteriores de aprendizado, possibilitando o uso de redes neurais para resolver problemas que antes eram insolúveis.

O backpropagation é o algoritmo-chave que faz o treinamento de modelos profundos algo computacionalmente tratável. Para as redes neurais modernas, ele pode tornar o treinamento com gradiente descendente até dez milhões de vezes mais rápido, em relação a uma implementação ingênua. Essa é a diferença entre um modelo que leva algumas horas ou dias para treinar e e outro que poderia levar anos (sem exagero).

Além de seu uso em Deep Learning, o backpropagation é uma poderosa ferramenta computacional em muitas outras áreas, desde previsão do tempo até a análise da estabilidade numérica. De fato, o algoritmo foi reinventado pelo menos dezenas de vezes em diferentes campos. O nome geral, independente da aplicação, é “diferenciação no modo reverso”.

Fundamentalmente, backpropagation é uma técnica para calcular derivadas rapidamente (não sabe o que é derivada? Consulte o link para um excelente vídeo em português explicando esse conceito em detalhes nas referências ao final deste capítulo). E é um truque essencial, não apenas em Deep Learning, mas em uma ampla variedade de situações de computação numérica. E para compreender backpropagation de forma efetiva, vamos primeiro compreender o conceito de grafo computacional e chain rule.

Grafo Computacional

Grafos computacionais são uma boa maneira de pensar em expressões matemáticas. O conceito de grafo foi introduzido por Leonhard Euler em 1736 para tentar resolver o problema das Pontes de Konigsberg. Grafos são modelos matemáticos para resolver problemas práticos do dia a dia, com várias aplicações no mundo real tais como: circuitos elétricos, redes de distribuição, relações de parentesco entre pessoas, análise de redes sociais, logística, redes de estradas, redes de computadores e muito mais. Grafos são muito usados para modelar problemas em computação.

Um Grafo é um modelo matemático que representa relações entre objetos. Um grafo G = (V, E) consiste de um conjunto de vértices V (também chamados de nós), ligados por um conjunto de bordas ou arestas E. Para aprender sobre grafos em mais detalhes, clique aqui.

Por exemplo, considere a expressão:

 

e = (a + b) ∗ (b + 1)

 

Existem três operações: duas adições e uma multiplicação. Para facilitar a compreensão sobre isso, vamos introduzir duas variáveis intermediárias c e d para que a saída de cada função tenha uma variável. Nós agora temos:

 

c = a+b
d = b+1
e = c∗d

 

Para criar um grafo computacional, fazemos cada uma dessas operações nos nós, juntamente com as variáveis de entrada. Quando o valor de um nó é a entrada para outro nó, uma seta vai de um para outro e temos nesse caso um grafo direcionado.

 

tree-def

 

Esses tipos de grafos surgem o tempo todo em Ciência da Computação, especialmente ao falar sobre programas funcionais. Eles estão intimamente relacionados com as noções de grafos de dependência e grafos de chamadas. Eles também são a principal abstração por trás do popular framework de Deep Learning, o TensorFlow.

Podemos avaliar a expressão definindo as variáveis de entrada para determinados valores e computando os nós através do grafo. Por exemplo, vamos definir a = 2 e b = 1:

 

tree-eval

 

A expressão, nesse exemplo, é avaliada como 6.

Derivadas em Grafos Computacionais

Se alguém quiser entender derivadas em um grafo computacional, a chave é entender as derivadas nas bordas (arestas que conectam os nós no grafo). Se a afeta diretamente c, então queremos saber como isso afeta c. Se a muda um pouco, como c muda? Chamamos isso de derivada parcial de c em relação a a.

Para avaliar as derivadas parciais neste grafo, precisamos da regra da soma e da regra do produto:

 

derivada

 

Abaixo, o grafo tem a derivada em cada borda (aresta) rotulada.

 

 tree-eval-derivs

 

 

E se quisermos entender como os nós que não estão diretamente conectados afetam uns aos outros? Vamos considerar como e é afetado por a. Se mudarmos a uma velocidade de 1, c também muda a uma velocidade de 1. Por sua vez, c mudando a uma velocidade de 1 faz com que e mude a uma velocidade de 2. Então e muda a uma taxa de 1 ∗ 2 em relação a a (analise o diagrama acima para visualizar isso).

A regra geral é somar todos os caminhos possíveis de um nó para o outro, multiplicando as derivadas em cada aresta do caminho. Por exemplo, para obter a derivada de e em relação a b, obtemos:

form

Isso explica como b afeta e através de c e também como isso afeta d.

Essa regra geral de “soma sobre caminhos” é apenas uma maneira diferente de pensar sobre a regra da cadeia multivariada ou chain rule.

Fatorando os Caminhos

O problema com apenas “somar os caminhos” é que é muito fácil obter uma explosão combinatória no número de caminhos possíveis.

 

chain-def-greek

 

No diagrama acima, existem três caminhos de X a Y, e mais três caminhos de Y a Z. Se quisermos obter a derivada ∂Z/∂X somando todos os caminhos, precisamos calcular 3 ∗ 3 = 9 caminhos:

 

form2

 

O exemplo acima só tem nove caminhos, mas seria fácil o número de caminhos crescer exponencialmente à medida que o grafo se torna mais complicado. Em vez de apenas ingenuamente somar os caminhos, seria muito melhor fatorá-los:

 

form3

 

É aí que entram a “diferenciação de modo de avanço” (forward-mode differentiation ou forward pass) e a “diferenciação de modo reverso” (reverse-mode differentiation ou backpropagation). Eles são algoritmos para calcular a soma de forma eficiente fatorando os caminhos. Em vez de somar todos os caminhos explicitamente, eles calculam a mesma soma de forma mais eficiente, mesclando os caminhos juntos novamente em cada nó. De fato, os dois algoritmos tocam cada borda exatamente uma vez!

A diferenciação do modo de avanço inicia em uma entrada para o grafo e se move em direção ao final. Em cada nó, soma todos os caminhos que se alimentam. Cada um desses caminhos representa uma maneira na qual a entrada afeta esse nó. Ao adicioná-los, obtemos a maneira total em que o nó é afetado pela entrada, isso é a derivada.

 

chain-forward-greek

 

Embora você provavelmente não tenha pensado nisso em termos de grafos, a diferenciação no modo de avanço é muito parecida com o que você aprendeu implicitamente caso tenha feito alguma introdução a Cálculo.

A diferenciação no modo reverso, por outro lado, começa na saída do grafo e se move em direção ao início (ou seja, se retropropaga ou backpropagation). Em cada nó, ele mescla todos os caminhos originados nesse nó.

 

chain-backward-greek

 

A diferenciação do modo de avanço rastreia como uma entrada afeta todos os nós. A diferenciação no modo reverso rastreia como cada nó afeta uma saída. Ou seja, a diferenciação de modo de avanço aplica o operador ∂/∂X a cada nó, enquanto a diferenciação de modo reverso aplica o operador ∂Z/∂ a cada nó. Se isso parece o conceito de programação dinâmica, é porque é exatamente isso! (acesse um material sobre programação dinâmica nas referências ao final do capítulo)

Nesse ponto, você pode se perguntar porque alguém se importaria com a diferenciação no modo reverso. Parece uma maneira estranha de fazer a mesma coisa que o modo de avanço. Existe alguma vantagem? Vamos considerar nosso exemplo original novamente:

 

tree-eval-derivs

 

Podemos usar a diferenciação de modo de avanço de b para cima. Isso nos dá a derivada de cada nó em relação a b.

 

tree-forwradmode

 

Nós calculamos ∂e/∂b, a derivada de nossa saída em relação a um de nossos inputs. E se fizermos a diferenciação de modo reverso de e para baixo? Isso nos dá a derivada de e em relação a todos os nós:

tree-backprop

 

Quando digo que a diferenciação no modo reverso nos dá a derivada de e em relação a cada nó, eu realmente quero dizer cada nó. Temos tanto ∂e/∂a quanto ∂e/∂b, as derivadas de e em relação a ambas as entradas. A diferenciação no modo de avanço nos deu a derivada de nossa saída em relação a uma única entrada, mas a diferenciação no modo reverso nos dá todos eles.

Para este grafo, isso é apenas um fator de duas velocidades, mas imagine uma função com um milhão de entradas e uma saída. A diferenciação no modo de avanço exigiria que passássemos pelo grafo um milhão de vezes para obter as derivadas. Diferenciação no modo reverso pode fazer isso em uma só passada! Uma aceleração de um fator de um milhão é bem legal e explica porque conseguimos treinar um modelo de rede neural profunda em tempo razoável.

Ao treinar redes neurais, pensamos no custo (um valor que descreve o quanto uma rede neural é ruim) em função dos parâmetros (números que descrevem como a rede se comporta). Queremos calcular as derivadas do custo em relação a todos os parâmetros, para uso em descida do gradiente. Entretanto, muitas vezes, há milhões ou até dezenas de milhões de parâmetros em uma rede neural. Então, a diferenciação no modo reverso, chamada de backpropagation no contexto das redes neurais, nos dá uma velocidade enorme!

Existem casos em que a diferenciação de modo de avanço faz mais sentido? Sim, existem! Onde o modo reverso fornece as derivadas de uma saída em relação a todas as entradas, o modo de avanço nos dá as derivadas de todas as saídas em relação a uma entrada. Se tiver uma função com muitas saídas, a diferenciação no modo de avanço pode ser muito, muito mais rápida.

Agora faz sentido?

Quando aprendemos pela primeira vez o que é backpropagation, a reação é: “Oh, essa é apenas a regra da cadeia (chain rule)! Como demoramos tanto tempo para descobrir?”

Na época em que o backpropagation foi inventado, as pessoas não estavam muito focadas nas redes neurais feedforward. Também não era óbvio que as derivadas eram o caminho certo para treiná-las. Esses são apenas óbvios quando você percebe que pode calcular rapidamente derivadas. Houve uma dependência circular.

Treinar redes neurais com derivadas? Certamente você ficaria preso em mínimos locais. E obviamente seria caro computar todas essas derivadas. O fato é que só porque sabemos que essa abordagem funciona é que não começamos imediatamente a listar os motivos que provavelmente não funcionaria. Já sabemos que funciona, mas novas abordagens vem sendo propostas no avanço das pesquisas em Deep Learning e Inteligência Artificial.

Conclusão da Parte 1

O backpropagation também é útil para entender como as derivadas fluem através de um modelo. Isso pode ser extremamente útil no raciocínio sobre porque alguns modelos são difíceis de otimizar. O exemplo clássico disso é o problema do desaparecimento de gradientes em redes neurais recorrentes, que discutiremos mais diante neste livro.

Por fim, há uma lição algorítmica ampla a ser retirada dessas técnicas. Backpropagation e forward-mode differentiation usam um poderoso par de truques (linearização e programação dinâmica) para computar derivadas de forma mais eficiente do que se poderia imaginar. Se você realmente entende essas técnicas, pode usá-las para calcular com eficiência várias outras expressões interessantes envolvendo derivadas.

Mas este capítulo teve como objetivo apenas ajudá-lo a compreender o algoritmo, já que praticamente não existe documentação sobre isso em português. Falta ainda compreender como o backpropagation é aplicado no treinamento das redes neurais. Ansioso por isso? Então acompanhe o próximo capítulo!

Referências:

Me Salva! Cálculo – O que é uma derivada?

The Birth Of Graph Theory: Leonhard Euler And The Königsberg Bridge Problem

Learning representations by back-propagating errors

Chain Rule

Calculus on Computational Graphs: Backpropagation

How the backpropagation algorithm works

Dynamic programming

Nota: parte das imagens usadas neste capítulo foram extraídas no excelente post (citado nas referências acima) de Christopher Olah, pesquisador de Machine Learning do Google Brain, e com a devida autorização do autor.

Deep Learning Book

Capítulo 13 – Construindo Uma Rede Neural Com Linguagem Python

by

Ok. Chegou a hora. Vamos escrever um programa em linguagem Python que aprenda como reconhecer dígitos manuscritos, usando Stochastic Gradient Descent e o dataset de treinamento MNIST. Se você chegou até aqui sem ler os capítulos anteriores, então pare imediatamente, leia os últimos 12 capítulos e depois volte aqui! Não tenha pressa! Não existe atalho para o aprendizado!

 

******************************** Atenção ********************************

Este capítulo considera que você já tem o interpretador Python instalado no seu computador, seja ele com sistema operacional Windows, MacOS ou Linux. Recomendamos que você instale o Anaconda e que já possua conhecimentos em linguagem Python. Se esse não for seu caso, antes de ler este capítulo e executar os exemplos aqui fornecidos, acesse o curso gratuito Fundamentos de Linguagem Python Para Análise de Dados e Data Science.

Usaremos Python 3 e você deve construir os scripts no seu computador. Vamos começar!

*************************************************************************

 

Quando descrevemos o dataset MNIST anteriormente, dissemos que ele estava dividido em 60.000 imagens de treinamento e 10.000 imagens de teste. Essa é a descrição oficial do MNIST. Mas vamos dividir os dados de forma um pouco diferente. Deixaremos as imagens de teste como está, mas dividiremos o conjunto de treinamento MNIST de 60.000 imagens em duas partes: um conjunto de 50.000 imagens, que usaremos para treinar nossa rede neural e um conjunto separado de validação de 10.000 imagens. Não utilizaremos os dados de validação neste capítulo, porém mais tarde, aqui mesmo no livro, usaremos este dataset quando estivermos configurando certos hiperparâmetros da rede neural, como a taxa de aprendizado por exemplo. Embora os dados de validação não façam parte da especificação MNIST original, muitas pessoas usam o MNIST desta forma e o uso de dados de validação é comum em redes neurais. Quando eu me referir aos “dados de treinamento MNIST” de agora em diante, vou me referir ao nosso conjunto de dados de 50.000 imagens, e não ao conjunto de dados de 60.000 imagens. Fique atento!

Além dos dados MNIST, também precisamos de uma biblioteca Python chamada Numpy, para álgebra linear. Se você instalou o Anaconda, não precisa se preocupar, pois o Numpy já está instalado. Caso contrário, será necessário fazer a instalação do pacote.

Mas antes de carregar e dividir os dados, vamos compreender os principais recursos do nosso código para construção de uma rede neural. A peça central é uma classe chamada Network, que usamos para representar uma rede neural. Abaixo a classe Network e seu construtor:

 

 

Neste código, o parâmetro sizes contêm o número de neurônios nas respectivas camadas, sendo um objeto do tipo lista em Python. Então, por exemplo, se queremos criar um objeto da classe Network com 2 neurônios na primeira camada, 3 neurônios na segunda camada e 1 neurônio na camada final, aqui está o código que usamos para instanciar um objeto da classe Network::

 

rede1 = Network([2, 3, 1])

 

Os bias e pesos no objeto rede1 são todos inicializados aleatoriamente, usando a função Numpy np.random.randn para gerar distribuições gaussianas com 0 de média e desvio padrão 1. Esta inicialização aleatória dá ao nosso algoritmo de descida do gradiente estocástico um local para começar. Em capítulos posteriores, encontraremos melhores maneiras de inicializar os pesos e os bias. Observe que o código de inicialização de rede assume que a primeira camada de neurônios é uma camada de entrada e omite a definição de quaisquer bias para esses neurônios, uma vez que os bias são usados apenas para calcular as saídas de camadas posteriores.

Observe também que os bias e pesos são armazenados como listas de matrizes Numpy. Assim, por exemplo, rede1.weights[1] é uma matriz Numpy armazenando os pesos conectando a segunda e terceira camadas de neurônios. (Não é a primeira e segunda camadas, uma vez que a indexação da lista em Python começa em 0.) Uma vez que rede1.weights[1] é bastante detalhado, vamos apenas indicar essa matriz w. É uma matriz tal que wjk é o peso para a conexão entre o neurônio kth na segunda camada e o neurônio jth na terceira camada. Essa ordenação dos índices j e k pode parecer estranha – certamente teria mais sentido trocar os índices j e k? A grande vantagem de usar essa ordenação é que isso significa que o vetor de ativações da terceira camada de neurônios é:

 

Form

Equação 1

 

Onde, a é o vetor de ativações da segunda camada de neurônios. Para obter um a’ multiplicamos a pela matriz de peso w, e adicionamos o vetor b com os bias (se você leu os capítulos anteriores, isso não deve ser novidade agora). Em seguida, aplicamos a função σ de forma elementar a cada entrada no vetor wa + b. (Isto é chamado de vetorizar a função σ.)

Com tudo isso em mente, é fácil escrever código que computa a saída de uma instância de rede. Começamos definindo a função sigmoide:

 

 

Observe que quando a entrada z é um vetor ou uma matriz Numpy, Numpy aplica automaticamente a função sigmoid elementwise, ou seja, na forma vetorizada.

Em seguida, adicionamos um método feedforward à classe Network, que, dada a entrada a para a rede, retorna a saída corresponente. Basicamente o método feedforward aplica a Equação 1 mostrada acima, para cada camada:

 

 

A principal atividade que queremos que nossos objetos da classe Network façam é aprender. Para esse fim, criaremos um método SGD (Stochastic Gradient Descent). Aqui está o código. É um pouco misterioso em alguns lugares, mas vamos explicar em detalhes mais abaixo:

 

 

O training_data é uma lista de tuplas (x, y) que representam as entradas de treinamento e as correspondentes saídas desejadas. As variáveis epochs e mini_batch_size são o que você esperaria – o número de épocas para treinar e o tamanho dos mini-lotes a serem usados durante a amostragem, enquanto eta é a taxa de aprendizagem, η. Se o argumento opcional test_data for fornecido, o programa avaliará a rede após cada período de treinamento e imprimirá progresso parcial. Isso é útil para rastrear o progresso, mas retarda substancialmente as coisas.

O código funciona da seguinte forma. Em cada época, ele começa arrastando aleatoriamente os dados de treinamento e, em seguida, particiona-os em mini-lotes de tamanho apropriado. Esta é uma maneira fácil de amostragem aleatória dos dados de treinamento. Então, para cada mini_batch, aplicamos um único passo de descida do gradiente. Isso é feito pelo código self.update_mini_batch (mini_batch, eta), que atualiza os pesos e os bias da rede de acordo com uma única iteração de descida de gradiente, usando apenas os dados de treinamento em mini_batch. Aqui está o código para o método update_mini_batch:

 

 

A maior parte do trabalho é feita pela linha delta_nabla_b, delta_nabla_w = self.backprop (x, y). Isso invoca algo chamado algoritmo de backpropagation, que é uma maneira rápida de calcular o gradiente da função de custo. Portanto, update_mini_batch funciona simplesmente calculando esses gradientes para cada exemplo de treinamento no mini_batch e, em seguida, atualizando self.weights e self.biases adequadamente.

Abaixo você encontra o código para self.backprop, mas não estudaremos ele agora. Estudaremos em detalhes como funciona o backpropagation no próximo capítulo, incluindo o código para self.backprop. Por hora, basta assumir que ele se comporta conforme indicado, retornando o gradiente apropriado para o custo associado ao exemplo de treinamento x.

 

 

Além do self.backprop, o programa é auto-explicativo – todo o levantamento pesado é feito em self.SGD e self.update_mini_batch, que já discutimos. O método self.backprop faz uso de algumas funções extras para ajudar no cálculo do gradiente, nomeadamente sigmoid_prime, que calcula a derivada da função σ e self.cost_derivative.

A classe Network é em essência nosso algoritmo de rede neural. A partir dela criamos uma instância (como rede1), alimentamos com os dados de treinamento e realizamos o treinamento. Avaliamos então a performance da rede com dados de teste e repetimos todo o processo até alcançar o nível de acurácia desejado em nosso projeto. Quando o modelo final estiver pronto, usamos para realizar as previsões para as quais o modelo foi criado, apresentando a ele novos conjuntos de dados e extraindo as previsões. Perceba que este é um algoritmo de rede neural bem simples, mas que permite compreender como funcionam as redes neurais e mais tarde, aqui mesmo no livro, as redes neurais profundas ou Deep Learning.

No próximo capítulo vamos continuar trabalhando com este algoritmo e compreender como funciona o Backpropagation. Na sequência, vamos carregar os dados, treinar e testar nossa rede neural e então usá-la para reconhecer dígitos manuscritos. Até lá.

Referências:

MNIST

Derivada

Machine Learning

The Elements of Statistical Learning: Data Mining, Inference, and Prediction, Second Edition

Gradient Descent For Machine Learning

Pattern Recognition and Machine Learning

Understanding Activation Functions in Neural Networks

Redes Neurais, princípios e práticas

Neural Networks and Deep Learning

An overview of gradient descent optimization algorithms

Optimization: Stochastic Gradient Descent

Gradient Descent vs Stochastic Gradient Descent vs Mini-Batch Learning

Deep Learning Book

Capítulo 12 – Aprendizado Com a Descida do Gradiente

by

No capítulo anterior definimos o design para a nossa rede neural e agora podemos começar o processo de aprendizado de máquina. Neste capítulo você vai compreender o que é o Aprendizado Com a Descida do Gradiente.

A primeira coisa que precisamos é um conjunto de dados para o treinamento da rede. Usaremos o conjunto de dados MNIST, que contém dezenas de milhares de imagens digitalizadas de dígitos manuscritos, juntamente com suas classificações corretas. O nome MNIST vem do fato de que é um subconjunto modificado de dois conjuntos de dados coletados pelo NIST, o Instituto Nacional de Padrões e Tecnologia dos Estados Unidos. Aqui estão algumas imagens do MNIST:

 

 

O MNIST tem duas partes. A primeira parte contém 60.000 imagens para serem usadas como dados de treinamento. Essas imagens são amostras de manuscritos escaneados de 250 pessoas, metade dos quais funcionários do Bureau do Censo dos EUA e metade dos estudantes do ensino médio. As imagens estão em escala de cinza e 28 por 28 pixels de tamanho. A segunda parte do conjunto de dados MNIST tem 10.000 imagens a serem usadas como dados de teste, também 28 por 28 pixels em escala de cinza. Usaremos os dados do teste para avaliar o quão bem a nossa rede neural aprendeu a reconhecer os dígitos. Para fazer deste um bom teste de desempenho, os dados de teste foram retirados de um conjunto diferente de 250 pessoas em relação aos dados de treinamento originais (embora ainda seja um grupo dividido entre funcionários do Census Bureau e alunos do ensino médio). Isso nos ajuda a confiar que nosso sistema pode reconhecer dígitos de pessoas cuja escrita não viu durante o treinamento.

Usaremos a notação x para indicar uma entrada (input) de treinamento. Será conveniente considerar cada entrada de treinamento x (cada imagem) como um vetor de 784 posições (28 x 28 pixels). A imagem abaixo representa como este vetor é construído:

 

 

Cada entrada no vetor representa o valor de cinza para um único pixel na imagem. Vamos indicar a saída correspondente desejada por y = y(x), onde y é um vetor com dimensão 10. Por exemplo, se uma imagem de treinamento particular, x, representa um 3, então y(x) = (0,0,0,1,0,0,0,0,0,0)T é a saída desejada da rede . Observe que T aqui é a operação de transposição, transformando um vetor de linha em um vetor comum (coluna). Vamos deixar isso mais claro. Observe a figura abaixo:

 

 

Vamos usar os pixels de imagem correspondentes ao fluxo inteiro chamado “features”. Os rótulos são One-Hot Encoded 1-hot. O rótulo que representa a classe de saída da imagem com dígito 3 torna-se “0001000000” uma vez que temos 10 classes para os 10 dígitos possíveis, onde o primeiro índice corresponde ao dígito “0” e o último corresponde ao dígito “9”.

O que queremos é um algoritmo que nos permita encontrar pesos e bias para que a saída da rede se aproxime de y(x) para todas as entradas de treinamento x. Para quantificar o quão bem estamos alcançando esse objetivo, definimos uma função de custo:

 

Função Quadrático de Custo

 

Na fórmula acima, w indica a coleta de todos os pesos na rede, b todos os bias (viés), n é o número total de entradas de treinamento, a é o vetor de saídas da rede (quando x é entrada) e a soma é sobre todas as entradas de treinamento x. Claro, a saída a depende de x, w e b, mas para manter a notação simples, eu não indiquei explicitamente essa dependência. A notação ‖v‖ apenas indica a função de comprimento usual para um vetor v. Chamaremos C a função de custo quadrático, que também é conhecido como o erro quadrático médio ou apenas o MSE (Mean Squared Error). Inspecionando a forma da função de custo quadrático, vemos que C (w, b) não é negativo, pois cada termo na soma não é negativo. Além disso, o custo C (w, b) torna-se pequeno, isto é, C (w, b) ≈ 0, precisamente quando y(x) é aproximadamente igual à saída, a, para todas as entradas de treinamento x.

Portanto, nosso algoritmo de treinamento faz um bom trabalho se ele pode encontrar pesos e bias para que C (w, b) ≈ 0. Isso significa basicamente que nosso modelo fez as previsões corretas, ou seja, cada vez que apresentamos ao modelo uma imagem com dígito 3, ele é capaz de reconhecer que se trata do número 3.

Em contraste, o algoritmo não terá boa performance, quando C (w, b) for um valor maior que 0 – isso significaria que nosso algoritmo não está conseguindo fazer as previsões, ou seja, quando apresentado a imagem com o dígito 3, ele não é capaz de prever que se trata de um número 3. Isso ocorre, porque a diferença entre o valor real da saída e o valor previsto pelo modelo, é muito alta. Assim, o objetivo do nosso algoritmo de treinamento será minimizar o custo C(w, b) em função dos pesos e dos bias. Em outras palavras, queremos encontrar um conjunto de pesos e bias que tornem o custo o menor possível. Vamos fazer isso usando um algoritmo conhecido como Descida do Gradiente (Gradient Descent).

Mas antes, uma pergunta. Por que introduzir o custo quadrático? Afinal, não nos interessamos principalmente pelo número de imagens corretamente classificadas pela rede? Por que não tentar maximizar esse número diretamente, em vez de minimizar uma medida, como o custo quadrático? O problema com isso é que o número de imagens corretamente classificadas não é uma “smooth function” dos pesos e bias na rede. Geralmente, fazer pequenas mudanças nos pesos e bias não causará nenhuma alteração no número de imagens de treinamento classificadas corretamente. Isso torna difícil descobrir como mudar os pesos e os bias para melhorar o desempenho. Se, em vez disso, usamos uma “smooth cost function”, como o custo quadrático, revela-se fácil descobrir como fazer pequenas mudanças nos pesos e nos bias para obter uma melhoria no custo. É por isso que nos concentramos primeiro na minimização do custo quadrático e somente depois examinaremos a precisão da classificação.

Mesmo considerando que queremos usar uma “smooth cost function”, você ainda pode se perguntar por que escolhemos a função quadrática. Talvez se escolhêssemos uma função de custo diferente, obteríamos um conjunto totalmente diferente de pesos e bias? Esta é uma preocupação válida e, mais tarde, revisitaremos a função de custo e faremos algumas modificações. No entanto, a função de custo quadrático mostrada anteriormente funciona perfeitamente para entender os conceitos básicos de aprendizagem em redes neurais, então ficaremos com isso por enquanto.

Recapitulando, nosso objetivo na construção de uma rede neural é encontrar pesos e bias que minimizem a função de custo quadrático C (w, b).

Descida do Gradiente

A maioria das tarefas em Machine Learning são na verdade problemas de otimização e um dos algoritmos mais usados para isso é o Algoritmo de Descida do Gradiente. Para um iniciante, o nome Algoritmo de Descida do Gradiente pode parecer intimidante, mas espero que depois de ler o que está logo abaixo, isso deixe de ser um mistério para você.

A Descida do Gradiente é uma ferramenta padrão para otimizar funções complexas iterativamente dentro de um programa de computador. Seu objetivo é: dada alguma função arbitrária, encontrar um mínimo. Para alguns pequenos subconjuntos de funções – aqueles que são convexos – há apenas um único minumum que também acontece de ser global. Para as funções mais realistas, pode haver muitos mínimos, então a maioria dos mínimos são locais. Certifique-se de que a otimização encontre o “melhor” minimum e não fique preso em mínimos sub-otimistas (um problema comum durante o treinamento do algoritmo).

Para compreender a intuição da Descida do Gradiente, vamos simplificar um pouco as coisas. Vamos imaginar que simplesmente recebemos uma função de muitas variáveis e queremos minimizar essa função. Vamos desenvolver a técnica chamada Descida do Gradiente que pode ser usada para resolver tais problemas de minimização. Então, voltaremos para a função específica que queremos minimizar para as redes neurais.

Ok, suponhamos que estamos tentando minimizar alguma função, C(v). Esta poderia ser qualquer função de valor real de muitas variáveis, onde v = v1, v2, …. Observe que eu substitui a notação w e b por v para enfatizar que esta poderia ser qualquer função – não estamos mais pensando especificamente no contexto das redes neurais apenas. Para minimizar C (v), vamos imaginar C como uma função de apenas duas variáveis, que chamaremos v1 e v2, conforme pode ser visto na figura abaixo:

Descida do Gradiente

O que queremos é encontrar onde C atinge seu mínimo global. Fica claro, que para a função traçada no gráfico acima, podemos observar facilmente o gráfico e encontrar o mínimo. Mas uma função geral, C, pode ser uma função complicada de muitas variáveis, e geralmente não será possível apenas observar o gráfico para encontrar o mínimo.

Uma maneira de atacar o problema é usar Cálculo (especificamente Álgebra Linear) para tentar encontrar o mínimo de forma analítica. Podemos calcular derivadas e depois tentar usá-las para encontrar lugares onde C é um extremum. Isso pode funcionar quando C é uma função de apenas uma ou algumas variáveis. Mas vai se transformar em um pesadelo quando tivermos muitas outras variáveis. E para as redes neurais, muitas vezes queremos muito mais variáveis – as maiores redes neurais têm funções de custo que dependem de bilhões de pesos e bias de uma maneira extremamente complicada. Usando “apenas” Cálculo para minimizar isso, não funcionará e precisamos de algo mais! Precisamos de um algoritmo de otimização capaz de minimizar C (v).

Felizmente, há uma analogia que nos ajuda a compreender como encontrar a solução. Começamos por pensar em nossa função como uma espécie de vale e imaginamos uma bola rolando pela encosta do vale, conforme pode ser visto na figura abaixo. Nossa experiência diária nos diz que a bola acabará rolando para o fundo do vale. Talvez possamos usar essa ideia como forma de encontrar um mínimo para a função? Escolheríamos aleatoriamente um ponto de partida para uma bola (imaginária), e então simularíamos o movimento da bola enquanto ela rola até o fundo do vale. Poderíamos fazer essa simulação simplesmente por derivadas de computação da função C – essas derivadas nos diriam tudo o que precisamos saber sobre a “forma” local do vale, e, portanto, como nossa bola deve rolar.

 

Representação da Descida do Gradiente (com o objetivo de minimizar a função de custo)

 

Ou seja, a Descida do Gradiente é um algoritmo de otimização usado para encontrar os valores de parâmetros (coeficientes ou se preferir w e b – weight e bias) de uma função que minimizam uma função de custo. A Descida do Gradiente é melhor usada quando os parâmetros não podem ser calculados analiticamente (por exemplo, usando álgebra linear) e devem ser pesquisados por um algoritmo de otimização.

O procedimento começa com valores iniciais para o coeficiente ou coeficientes da função. Estes poderiam ser 0.0 ou um pequeno valor aleatório (a inicialização dos coeficiente é parte crítica do processo e diversas técnicas podem ser usadas, ficando a escolha a cargo do Engenheiro de IA ou Cientista de Dados e do problema a ser resolvido com o modelo). Poderíamos iniciar assim nossos coeficientes (valores de w e b):

 

coeficiente = 0,0

 

O custo dos coeficientes é avaliado ligando-os à função e calculando o custo.

 

custo = f (coeficiente)

 

ou

 

custo = avaliar (f (coeficiente))

 

A derivada do custo é calculada. A derivada é um conceito de Cálculo e refere-se à inclinação da função em um determinado ponto. Precisamos conhecer a inclinação para que possamos conhecer a direção (sinal) para mover os valores dos coeficientes para obter um custo menor na próxima iteração.

 

delta = derivado (custo)

 

Agora que sabemos da derivada em que direção está em declive, podemos atualizar os valores dos coeficientes. Um parâmetro de taxa de aprendizagem (alfa) deve ser especificado e controla o quanto os coeficientes podem mudar em cada atualização.

 

coeficiente = coeficiente – (alfa * delta)

 

Este processo é repetido até que o custo dos coeficientes (função de custo) seja 0,0 ou próximo o suficiente de zero, indicando que as saídas da rede estão cada vez mais próximas dos valores reais (saídas desejadas).

A Descida do Gradiente é simples, mas exige que seja calculado o gradiente da função de custo ou a função que você está otimizando, mas além disso, é muito direto. Em resumo:

Você divide seus dados em amostras e a cada amostra (sample), você passa as entradas pela rede, multiplica pelos pesos, soma, e no final você vai ter sua saÍda (a previsão da rede). Você então compara a saída da sua rede com o a resposta certa, calcula o erro, e então retroage esse erro (backpropagation), ajustando os pesos de cada neurônio de cada camada. Quando você acabar de fazer a atualização dos pesos, uma nova amostra é introduzida e ela será multiplicada pelos pesos já atualizados. Esse processo de atualizar os pesos é que é chamado de “aprendizado”.

Se você observar os algoritmos mais atuais, todos trabalham dentro de um conceito relativamente novo chamado de mini-lotes (mini-batches). Para otimizar a performance, o que se faz é passar pela rede múltiplas amostras (por exemplo 128 amostras), calcular o erro médio delas e então realizar o backpropagation e a atualização dos pesos. Do ponto de vista da atualização dos pesos, 1 amostra = 128 amostras. Esse é um conceito mais novo, necessário principalmente no treinamento de grandes modelos de Deep Learning.

Em seguida, veremos como podemos usar isso em algoritmos de aprendizado de máquina.

Batch Gradient Descent em Aprendizado de Máquina

O objetivo de todos os algoritmos supervisionados de aprendizagem de máquina é estimar uma função de destino (f) que mapeia dados de entrada (X) para as variáveis ​​de saída (Y). Isso descreve todos os problemas de classificação e regressão (aprendizagem supervisionada).

Alguns algoritmos de aprendizagem de máquina têm coeficientes que caracterizam a estimativa de algoritmos para a função alvo (f). Diferentes algoritmos têm diferentes representações e diferentes coeficientes, mas muitos deles requerem um processo de otimização para encontrar o conjunto de coeficientes que resultam na melhor estimativa da função alvo. Os exemplos comuns de algoritmos com coeficientes que podem ser otimizados usando descida do gradiente são Regressão linear e Regressão logística.

A avaliação de quão próximo um modelo de aprendizagem de máquina estima a função de destino pode ser calculada de várias maneiras, muitas vezes específicas para o algoritmo de aprendizagem de máquina. A função de custo envolve a avaliação dos coeficientes no modelo de aprendizagem de máquina calculando uma previsão para o modelo para cada instância de treinamento no conjunto de dados e comparando as previsões com os valores de saída reais e calculando uma soma ou erro médio (como a Soma de Residuais Quadrados ou SSR no caso de regressão linear).

A partir da função de custo, uma derivada pode ser calculada para cada coeficiente para que ele possa ser atualizado usando exatamente a equação de atualização descrita acima.

O custo é calculado para um algoritmo de aprendizado de máquina em todo o conjunto de dados de treinamento para cada iteração do algoritmo de descida de gradiente. Uma iteração do algoritmo é chamada de um lote e esta forma de descida do gradiente é referida como descida do gradiente em lote (Batch Gradient Descent).

A descida do gradiente em lote é a forma mais comum de descida do gradiente em Machine Learning.

Stochastic Gradient Descent em Aprendizado de Máquina

A Descida do Gradiente pode ser lenta para executar em conjuntos de dados muito grandes. Como uma iteração do algoritmo de descida do gradiente requer uma previsão para cada instância no conjunto de dados de treinamento, pode demorar muito quando você tem muitos milhões de instâncias.

Em situações em que você possui grandes quantidades de dados, você pode usar uma variação da descida do gradiente chamada Stochastic Gradient Descent.

Nesta variação, o procedimento de descida do gradiente descrito acima é executado, mas a atualização para os coeficientes é realizada para cada instância de treinamento, em vez do final do lote de instâncias.

O primeiro passo do procedimento exige que a ordem do conjunto de dados de treinamento seja randomizada. Isto é, misturar a ordem que as atualizações são feitas para os coeficientes. Como os coeficientes são atualizados após cada instância de treinamento, as atualizações serão barulhentas saltando por todo o lado, e assim o custo correspondente funcionará. Ao misturar a ordem para as atualizações dos coeficientes, ela aproveita essa caminhada aleatória e evita que ela fique “distraída” ou presa.

O procedimento de atualização para os coeficientes é o mesmo que o anterior, exceto que o custo não é somado em todos os padrões de treinamento, mas sim calculado para um padrão de treinamento.

A aprendizagem pode ser muito mais rápida com descida de gradiente estocástica para conjuntos de dados de treinamento muito grandes e muitas vezes você só precisa de um pequeno número de passagens através do conjunto de dados para alcançar um conjunto de coeficientes bom o suficiente.

Ufa, você ainda está aí? Entende agora porque Cientistas de Dados e Engenheiros de IA devem ser muito bem remunerados? Eles são os “magos” que estão ajudando a transformar o mundo com Machine Learning. E este capítulo foi apenas uma breve introdução! Voltaremos a este assunto mais a frente no livro, quando estudarmos outros algoritmos.

Tenho certeza que você está ansioso para criar e treinar sua primeira rede neural. Então, não perca o próximo capítulo!

Referências:

MNIST

Derivada

Machine Learning

The Elements of Statistical Learning: Data Mining, Inference, and Prediction, Second Edition

Gradient Descent For Machine Learning

Pattern Recognition and Machine Learning

Understanding Activation Functions in Neural Networks

Redes Neurais, princípios e práticas

Neural Networks and Deep Learning

An overview of gradient descent optimization algorithms

Optimization: Stochastic Gradient Descent

Gradient Descent vs Stochastic Gradient Descent vs Mini-Batch Learning

Deep Learning Book

Capítulo 11 – Design De Uma Rede Neural Para Reconhecimento de Dígitos

by

Na primeira parte deste livro online, durante os 10 primeiros capítulos, definimos e estudamos o universo das redes neurais artificias. Neste ponto você já deve ter uma boa compreensão sobre que são estes algoritmos e como podem ser usados, além da importância das redes neurais para a construção de sistemas de Inteligência Artificial. Estamos prontos para iniciar a construção de redes neurais e na sequência estudaremos as arquiteturas mais avançadas. Vamos começar definindo o Design De Uma Rede Neural Para Reconhecimento de Dígitos.

Nossa primeira tarefa será construir uma rede neural para reconhecer caligrafia, ou seja, dígitos escritos à mão que foram digitalizados em imagens no computador. Por que vamos começar com este tipo de tarefa? Porque ela permite percorrer todas as etapas e procedimentos matemáticos de uma rede neural, sendo portanto uma excelente introdução. Vamos começar?

Se você acompanha os cursos na Data Science Academy já sabe que: antes de pensar em escrever sua primeira linha de código, é preciso definir claramente o problema a ser resolvido. A tecnologia existe para resolver problemas e a definição clara do objetivo é o ponto de partida de qualquer projeto de sucesso! Neste capítulo definiremos o problema a ser resolvido, nesse caso o reconhecimento de dígitos manuscritos.

Podemos dividir o problema de reconhecer os dígitos manuscritos em dois sub-problemas. Primeiro, precisamos encontrar uma maneira de quebrar uma imagem que contenha muitos dígitos em uma sequência de imagens separadas, cada uma contendo um único dígito. Por exemplo, gostaríamos de quebrar a imagem:

 

 

em seis imagens separadas:

 

 

Nós, humanos, resolvemos esse problema de segmentação com facilidade, mas é um desafio para um programa de computador dividir corretamente a imagem. Uma vez que a imagem foi segmentada, o programa precisa classificar cada dígito individual. Então, por exemplo, gostaríamos que nosso programa reconhecesse automaticamente que o primeiro dígito acima é um 5:

 

 

Vamos nos concentrar em escrever um programa para resolver o segundo problema, isto é, classificar dígitos individuais. O problema da segmentação não é tão difícil de resolver, uma vez que você tenha uma boa maneira de classificar os dígitos individuais. Existem muitas abordagens para resolver o problema de segmentação. Uma abordagem é testar muitas maneiras diferentes de segmentar a imagem, usando o classificador de dígitos individuais para marcar cada segmentação de teste. Uma segmentação de teste obtém uma pontuação alta se o classificador de dígitos individuais estiver confiante de sua classificação em todos os segmentos e uma pontuação baixa se o classificador tiver muitos problemas em um ou mais segmentos. A ideia é que, se o classificador estiver tendo problemas em algum lugar, provavelmente está tendo problemas porque a segmentação foi escolhida incorretamente. Essa ideia e outras variações podem ser usadas para resolver o problema de segmentação. Então, em vez de se preocupar com a segmentação, nos concentraremos no desenvolvimento de uma rede neural que pode resolver o problema mais interessante e difícil, ou seja, reconhecer dígitos individuais manuscritos.

Para reconhecer dígitos individuais, usaremos uma rede neural de três camadas:

 

 

A camada de entrada da rede contém neurônios que codificam os valores dos pixels de entrada. Conforme iremos discutir no próximo capítulo, nossos dados de treinamento para a rede consistirão em muitas imagens de 28 por 28 pixels de dígitos manuscritos digitalizados e, portanto, a camada de entrada contém 28 × 28 = 784 neurônios (Nota: uma imagem nada mais é do que uma matriz, nesse caso de dimensões 28×28, que iremos converter em um vetor cujo tamanho será 784, onde cada item representa um pixel na imagem). Os pixels de entrada são de escala de cinza, com um valor de 0.0 representando branco e um valor de 1.0 representando preto. Valores intermediários representam tonalidades gradualmente escurecidas de cinza.

A segunda camada da rede é uma camada oculta. Representaremos o número de neurônios nesta camada oculta por n, e vamos experimentar diferentes valores para n. O exemplo mostrado acima ilustra uma pequena camada oculta, contendo apenas n = 15 neurônios.

A camada de saída da rede contém 10 neurônios. Se o primeiro neurônio “disparar” (for ativado), ou seja, tiver uma saída ≈ 1, então isso indicará que a rede acha que o dígito é 0. Se o segundo neurônio “disparar” (for ativado), isso indicará que a rede pensa que o dígito é um 1. E assim por diante. Em resumo, vamos numerar os neurônios de saída de 0 a 9 e descobrimos qual neurônio possui o maior valor de ativação. Se esse neurônio é, digamos, neurônio número 6, então nossa rede adivinhará que o dígito de entrada era um 6. E assim por diante para os outros neurônios de saída.

Você pode se perguntar por que usamos 10 neurônios de saída. Afinal, o objetivo da rede é nos dizer qual dígito (0,1,2, …, 9) corresponde à imagem de entrada. Uma maneira aparentemente natural de fazer isso é usar apenas 4 neurônios de saída, tratando cada neurônio como assumindo um valor binário, dependendo se a saída do neurônio está mais próxima de 0 ou 1. Quatro neurônios são suficientes para codificar a resposta, desde que 2ˆ4 = 16 é mais do que os 10 valores possíveis para o dígito de entrada. Por que nossa rede deve usar 10 neurônios em vez disso? Isso não é ineficiente? A justificativa final é empírica: podemos experimentar ambos os projetos de rede, e verifica-se que, para este problema específico, a rede com 10 neurônios de saída aprende a reconhecer dígitos melhor do que a rede com 4 neurônios de saída. Mas isso ainda deixa a pergunta por que o uso de 10 neurônios de saída funciona melhor. Existe alguma heurística que nos diga com antecedência que devemos usar a codificação de 10 saídas em vez da codificação de 4 saídas?

Entender porque fazemos isso, ajuda a pensar sobre o que a rede neural está realmente fazendo. Considere primeiro o caso em que usamos 10 neurônios de saída. Vamos nos concentrar no primeiro neurônio de saída, aquele que está tentando decidir se o dígito é ou não 0. Ele faz isso pesando evidências da camada oculta dos neurônios. O que esses neurônios ocultos estão fazendo? Bem, vamos supor que o primeiro neurônio na camada oculta detecta ou não uma imagem como a seguinte:

 

 

Isso pode ser feito pesando fortemente pixels de entrada que se sobrepõem à imagem e apenas ponderam ligeiramente as outras entradas. De forma semelhante, suponhamos que o segundo, terceiro e quarto neurônios na camada oculta detectem se as seguintes imagens estão ou não presentes:

 

 

Como você pode ter adivinhado, essas quatro imagens juntas compõem a imagem 0 que vimos na linha de dígitos mostrada anteriormente:

 

 

Então, se todos os quatro neurônios ocultos estão disparando, podemos concluir que o dígito é um 0. Claro, esse não é o único tipo de evidência que podemos usar para concluir que a imagem era um 0 – podemos legitimamente obter um 0 em muitas outras maneiras (por exemplo, através de traduções das imagens acima, ou pequenas distorções). Mas parece seguro dizer que, pelo menos neste caso, concluiríamos que a entrada era um 0.

Supondo que a rede neural funciona assim, podemos dar uma explicação plausível sobre porque é melhor ter 10 saídas da rede, em vez de 4. Se tivéssemos 4 saídas, o primeiro neurônio de saída tentaria decidir o que mais um bit significativo do dígito representa. E não existe uma maneira fácil de relacionar esse bit mais significativo com formas simples, como as mostradas acima. As formas componentes do dígito estarão intimamente relacionadas com (digamos) o bit mais significativo na saída.

Isso tudo é apenas uma heurística. Nada diz que a rede neural de três camadas tem que operar da maneira que descrevemos, com os neurônios ocultos detectando formas de componentes simples. Talvez um algoritmo de aprendizado inteligente encontre alguma atribuição de pesos que nos permita usar apenas 4 neurônios de saída. Mas, usar uma boa heurística pode economizar muito tempo na concepção de boas arquiteturas de redes neurais.

Já temos então um design para a nossa rede neural. Agora precisamos definir como será o processo de aprendizagem do algoritmo, antes de começar a codificar nossa rede em linguagem Python. Usaremos o treinamento com Gradiente Descendente, assunto do próximo capítulo, que aliás eu não perderia por nada, se fosse você, pois aí está a “magia” por trás das redes neurais. Até lá!

Para acompanhar os próximos capítulos e reproduzir os exemplos, você deve ter o Anaconda Python instalado no seu computador. Acesse o capítulo 1 do curso gratuito Fundamentos de Linguagem Python Para Análise de Dados e Data Science, para aprender como instalar o Anaconda.

Referências:

Função Sigmóide

Machine Learning

The Elements of Statistical Learning: Data Mining, Inference, and Prediction, Second Edition

Pattern Recognition and Machine Learning

Understanding Activation Functions in Neural Networks

Redes Neurais, princípios e práticas

Neural Networks and Deep Learning (alguns trechos extraídos e traduzidos com autorização do autor Michael Nielsen)

Deep Learning Book

Capítulo 8 – Função de Ativação

by

Neste capítulo estudaremos um importante componente de uma rede neural artificial, a Função de Ativação. Este capítulo é uma introdução ao tema e voltaremos a ele mais adiante quando estudarmos as arquiteturas avançadas de Deep Learning. Este capítulo pode ser um pouco desafiador, pois começaremos a introduzir conceitos mais avançados, que serão muito úteis na sequência dos capítulos. Relaxe, faça a leitura e aprenda um pouco mais sobre redes neurais artificiais.

Antes de mergulhar nos detalhes das funções de ativação, vamos fazer uma pequena revisão do que são redes neurais artificiais e como funcionam. Uma rede neural é um mecanismo de aprendizado de máquina (Machine Learning) muito poderoso que imita basicamente como um cérebro humano aprende. O cérebro recebe o estímulo do mundo exterior, faz o processamento e gera o resultado. À medida que a tarefa se torna complicada, vários neurônios formam uma rede complexa, transmitindo informações entre si. Usando uma rede neural artificial, tentamos imitar um comportamento semelhante. A rede que você vê abaixo é uma rede neural artificial composta de neurônios interligados.

 

Neural Network

 

Os círculos negros na imagem acima são neurônios. Cada neurônio é caracterizado pelo peso, bias e a função de ativação. Os dados de entrada são alimentados na camada de entrada. Os neurônios fazem uma transformação linear na entrada pelos pesos e bias. A transformação não linear é feita pela função de ativação. A informação se move da camada de entrada para as camadas ocultas. As camadas ocultas fazem o processamento e enviam a saída final para a camada de saída. Este é o movimento direto da informação conhecido como propagação direta. Mas e se o resultado gerado estiver longe do valor esperado? Em uma rede neural, atualizaríamos os pesos e bias dos neurônios com base no erro. Este processo é conhecido como backpropagation. Uma vez que todos os dados passaram por este processo, os pesos e bias finais são usados para previsões.

Calma, calma, calma. Muita informação em um único parágrafo, eu sei! Vamos por partes. As entradas, os pesos e bias nós já discutimos nos capítulos anteriores. A função de ativação vamos discutir agora e a propagação direta e o backpropagation discutimos nos próximos capítulos!

Função de Ativação

Os algoritmos de aprendizagem são fantásticos. Mas como podemos elaborar esses algoritmos para uma rede neural artificial? Suponhamos que tenhamos uma rede de Perceptrons que gostaríamos de usar para aprender a resolver algum problema. Por exemplo, as entradas para a rede poderiam ser os dados de pixel de uma imagem digitalizada, escrita à mão, de um dígito. Gostaríamos que a rede aprendesse pesos e bias para que a saída da rede classifique corretamente o dígito. Para ver como a aprendizagem pode funcionar, suponha que façamos uma pequena alteração em algum peso (ou bias) na rede. O que queremos é que esta pequena mudança de peso cause apenas uma pequena alteração correspondente na saída da rede. Como veremos em um momento, esta propriedade tornará possível a aprendizagem. Esquematicamente, aqui está o que queremos (obviamente, esta rede é muito simples para fazer reconhecimento de escrita, mas fique tranquilo que veremos redes bem mais complexas).

 

Esquema

 

Se fosse verdade que uma pequena alteração em um peso (ou bias) fizesse com que tivéssemos apenas uma pequena alteração no resultado, então poderíamos usar esse fato para modificar os pesos e os valores de bias para que a nossa rede se comporte mais da maneira que queremos. Por exemplo, suponha que a rede classificasse equivocadamente uma imagem como “8” quando deveria ser um “9”. Podemos descobrir como fazer uma pequena mudança nos pesos e bias para que a rede fique um pouco mais próxima da classificação da imagem como “9”. E então, repetiríamos isso, mudando os pesos e os valores de bias repetidamente para produzir melhor e melhor resultado. A rede estaria aprendendo.

O problema é que isso não é o que acontece quando nossa rede contém apenas Perceptrons, conforme estudamos nos capítulos anteriores. De fato, uma pequena alteração nos pesos de um único Perceptron na rede pode, por vezes, fazer com que a saída desse Perceptron mude completamente, digamos de 0 a 1. Essa mudança pode então causar o comportamento do resto da rede mudar completamente de uma maneira muito complicada. Então, enquanto o seu “9” pode agora ser classificado corretamente, o comportamento da rede em todas as outras imagens provavelmente mudará completamente de maneira difícil de controlar. Talvez haja uma maneira inteligente de resolver esse problema. Sim, há. E é conhecida como função de ativação.

Podemos superar esse problema através da introdução de um componente matemático em nosso neurônio artificial, chamado função de ativação. As funções de ativação permitem que pequenas mudanças nos pesos e bias causem apenas uma pequena alteração no output. Esse é o fato crucial que permitirá que uma rede de neurônios artificiais aprenda.

Vejamos como isso funciona:

 

Função de Ativação

 

As funções de ativação são um elemento extremamente importante das redes neurais artificiais. Elas basicamente decidem se um neurônio deve ser ativado ou não. Ou seja, se a informação que o neurônio está recebendo é relevante para a informação fornecida ou deve ser ignorada. Veja na fórmula abaixo como a função de ativação é mais uma camada matemática no processamento.

 

Função de Ativação

 

A função de ativação é a transformação não linear que fazemos ao longo do sinal de entrada. Esta saída transformada é então enviada para a próxima camada de neurônios como entrada. Quando não temos a função de ativação, os pesos e bias simplesmente fazem uma transformação linear. Uma equação linear é simples de resolver, mas é limitada na sua capacidade de resolver problemas complexos. Uma rede neural sem função de ativação é essencialmente apenas um modelo de regressão linear. A função de ativação faz a transformação não-linear nos dados de entrada, tornando-o capaz de aprender e executar tarefas mais complexas. Queremos que nossas redes neurais funcionem em tarefas complicadas, como traduções de idiomas (Processamento de Linguagem Natural) e classificações de imagens (Visão Computacional). As transformações lineares nunca seriam capazes de executar tais tarefas.

As funções de ativação tornam possível a propagação posterior desde que os gradientes sejam fornecidos juntamente com o erro para atualizar os pesos e bias. Sem a função não linear diferenciável, isso não seria possível. Caso o termo gradiente não seja familiar, aguarde os próximos capítulos, quando vamos explicar este conceito em detalhes, visto que ele é a essência do processo de aprendizagem em redes neurais artificiais.

Mas não existe apenas um tipo de função de ativação. Na verdade existem vários, cada qual a ser usado em diferentes situações. Vamos a uma breve descrição dos tipos mais populares.

Tipos Populares de Funções de Ativação

A função de ativação é um componente matemático incluído na estrutura de redes neurais artificiais a fim de permitir a solução de problemas complexos. Existem diversos tipos de funções de ativação e esta é uma área de pesquisa ativa, à medida que a Inteligência Artificial evolui (não é maravilhoso estar participando desta evolução, que vai transformar completamente o mundo?). Vejamos quais são os tipos mais populares.

Função de Etapa Binária (Binary Step Function)

A primeira coisa que vem à nossa mente quando temos uma função de ativação seria um classificador baseado em limiar (threshold), ou seja, se o neurônio deve ou não ser ativado. Se o valor Y estiver acima de um valor de limite determinado, ative o neurônio senão deixa desativado. Simples! Essa seria a regra:

f(x) = 1, x>=0

f(x) = 0, x<0

A função de etapa binária é isso mesmo, extremamente simples. Ela pode ser usada ao criar um classificador binário. Quando simplesmente precisamos dizer sim ou não para uma única classe, a função de etapa seria a melhor escolha, pois ativaria o neurônio ou deixaria zero.

A função é mais teórica do que prática, pois, na maioria dos casos, classificamos os dados em várias classes do que apenas uma única classe. A função de etapa não seria capaz de fazer isso.

Além disso, o gradiente da função de etapa é zero. Isso faz com que a função de etapa não seja tão útil durante o backpropagation quando os gradientes das funções de ativação são enviados para cálculos de erro para melhorar e otimizar os resultados. O gradiente da função de etapa reduz tudo para zero e a melhoria dos modelos realmente não acontece. Lembrando, mais uma vez, que veremos em detalhes os conceitos de gradiente e backpropagation mais adiante, nos próximos capítulos!

Função Linear

Nós vimos o problema com a função step, o gradiente sendo zero, é impossível atualizar o gradiente durante a backpropagation. Em vez de uma função de passo simples, podemos tentar usar uma função linear. Podemos definir a função como:

f(x) = ax

A derivada de uma função linear é constante, isto é, não depende do valor de entrada x. Isso significa que toda vez que fazemos backpropagation, o gradiente seria o mesmo. E este é um grande problema, não estamos realmente melhorando o erro, já que o gradiente é praticamente o mesmo. E não apenas suponha que estamos tentando realizar uma tarefa complicada para a qual precisamos de múltiplas camadas em nossa rede. Agora, se cada camada tiver uma transformação linear, não importa quantas camadas nós tenhamos, a saída final não é senão uma transformação linear da entrada. Portanto, a função linear pode ser ideal para tarefas simples, onde a interpretabilidade é altamente desejada.

Sigmóide

Sigmóide é uma função de ativação amplamente utilizada. É da forma:

f (x) = 1 / (1 + e ^ -x)

Esta é uma função suave e é continuamente diferenciável. A maior vantagem sobre a função de etapa e a função linear é que não é linear. Esta é uma característica incrivelmente interessante da função sigmóide. Isto significa essencialmente que quando eu tenho vários neurônios com função sigmóide como função de ativação – a saída também não é linear. A função varia de 0 a 1 tendo um formato S.

A função essencialmente tenta empurrar os valores de Y para os extremos. Esta é uma qualidade muito desejável quando tentamos classificar os valores para uma classe específica.

A função sigmóide ainda é amplamente utilizada até hoje, mas ainda temos problemas que precisamos abordar. Com a sigmóide temos problemas quando os gradientes se tornam muito pequenos. Isso significa que o gradiente está se aproximando de zero e a rede não está realmente aprendendo.

Outro problema que a função sigmóide sofre é que os valores variam apenas de 0 a 1. Esta medida que a função sigmóide não é simétrica em torno da origem e os valores recebidos são todos positivos. Nem sempre desejamos que os valores enviados ao próximo neurônio sejam todos do mesmo sinal. Isso pode ser abordado pela ampliação da função sigmóide. Isso é exatamente o que acontece na função tanh.

Tanh

A função tanh é muito semelhante à função sigmóide. Na verdade, é apenas uma versão escalonada da função sigmóide.

Tanh (x) = 2sigmoides (2x) -1

Pode ser escrito diretamente como:

tanh (x) = 2 / (1 + e ^ (- 2x)) -1

Tanh funciona de forma semelhante à função sigmóide, mas sim simétrico em relação à origem. varia de -1 a 1.

Basicamente, soluciona o nosso problema dos valores, sendo todos do mesmo sinal. Todas as outras propriedades são as mesmas da função sigmoide. É contínuo e diferenciável em todos os pontos. A função não é linear, então podemos fazer o backpropagation facilmente nos erros.

ReLU

A função ReLU é a unidade linear rectificada. É definida como:

f(x) = max (0, x)

ReLU é a função de ativação mais amplamente utilizada ao projetar redes neurais atualmente. Primeiramente, a função ReLU é não linear, o que significa que podemos facilmente copiar os erros para trás e ter várias camadas de neurônios ativados pela função ReLU.

A principal vantagem de usar a função ReLU sobre outras funções de ativação é que ela não ativa todos os neurônios ao mesmo tempo. O que isto significa ? Se você olhar para a função ReLU e a entrada for negativa, ela será convertida em zero e o neurônio não será ativado. Isso significa que, ao mesmo tempo, apenas alguns neurônios são ativados, tornando a rede esparsa e eficiente e fácil para a computação.

Mas ReLU também pode ter problemas com os gradientes que se deslocam em direção a zero. Mas quando temos um problema, sempre podemos pensar em uma solução. Aliás, isso é o que as empresas mais procuram nos dias de hoje: “resolvedores de problemas”. Seja um e sua empregabilidade estará garantida!

Leaky ReLU

A função Leaky ReLU não passa de uma versão melhorada da função ReLU. Na função ReLU, o gradiente é 0 para x < 0, o que fez os neurônios morrerem por ativações nessa região. Leaky ReLU ajuda a resolver este problema. Em vez de definir a função Relu como 0 para x inferior a 0, definimos como um pequeno componente linear de x. Pode ser definido como:

f(x) = ax, x < 0
f(x) = x, x > = 0

O que fizemos aqui é que simplesmente substituímos a linha horizontal por uma linha não-zero, não horizontal. Aqui um é um valor pequeno como 0,01 ou algo parecido. A principal vantagem de substituir a linha horizontal é remover o gradiente zero.

Softmax

A função softmax também é um tipo de função sigmóide, mas é útil quando tentamos lidar com problemas de classificação. A função sigmóide como vimos anteriormente é capaz de lidar com apenas duas classes. O que devemos fazer quando estamos tentando lidar com várias classes? Apenas classificar sim ou não para uma única classe não ajudaria. A função softmax transforma as saídas para cada classe para valores entre 0 e 1 e também divide pela soma das saídas. Isso essencialmente dá a probabilidade de a entrada estar em uma determinada classe. Pode ser definido como:

 

Softmax

 

Digamos, por exemplo, que temos as saídas como [1.2, 0.9, 0.75], quando aplicamos a função softmax, obteríamos [0.42, 0.31, 0.27]. Então, agora podemos usá-los como probabilidades de que o valor seja de cada classe.

A função softmax é idealmente usada na camada de saída do classificador, onde realmente estamos tentando gerar as probabilidades para definir a classe de cada entrada.

Escolhendo a Função de Ativação Correta

Ufa! Muita coisa, não? E ainda não vimos as questões matemáticas envolvidas nessas funções. Mas não tenhamos pressa, não existe atalho para o aprendizado e estudaremos tudo passo a passo, item a item, no padrão dos cursos na Data Science Academy.

Agora que já vimos tantas funções de ativação, precisamos de alguma lógica/heurística para saber qual função de ativação deve ser usada em qual situação. Não há uma regra de ouro e a escolha depende do problema no qual você estiver trabalhando.

No entanto, dependendo das propriedades do problema, poderemos fazer uma melhor escolha para uma convergência fácil e rápida da rede neural.

  • Funções Sigmóide e suas combinações geralmente funcionam melhor no caso de classificadores.
  • Funções Sigmóide e Tanh às vezes são evitadas devido ao problema de Vanishing Gradient (que estudaremos no capítulo sobre redes neurais recorrentes).
  • A função ReLU é uma função de ativação geral e é usada na maioria dos casos atualmente.
  • Se encontrarmos um caso de neurônios deficientes em nossas redes, a função Leaky ReLU é a melhor escolha.
  • Tenha sempre em mente que a função ReLU deve ser usada apenas nas camadas ocultas.
  • Como regra geral, você pode começar usando a função ReLU e depois passar para outras funções de ativação no caso da ReLU não fornecer resultados ótimos.

Está começando a sentir a vibração em trabalhar com Inteligência Artificial? Então continue acompanhando, pois estamos apenas no começo! Até o próximo capítulo!

Referências:

Função Sigmóide

Machine Learning

The Elements of Statistical Learning: Data Mining, Inference, and Prediction, Second Edition

Pattern Recognition and Machine Learning

Understanding Activation Functions in Neural Networks

Vanishing Gradient Problem

Redes Neurais, princípios e práticas

Neural Networks and Deep Learning (alguns trechos extraídos e traduzidos com autorização do autor Michael Nielsen)

Deep Learning Book

Capítulo 7 – O Perceptron – Parte 2

by

O Perceptron é um modelo matemático de um neurônio biológico. Enquanto nos neurônios reais o dendrito recebe sinais elétricos dos axônios de outros neurônios, no Perceptron estes sinais elétricos são representados como valores numéricos. Nas sinapses entre dendritos e axônio, os sinais elétricos são modulados em várias quantidades. Isso também é modelado no Perceptron multiplicando cada valor de entrada por um valor chamado peso. Um neurônio real dispara um sinal de saída somente quando a força total dos sinais de entrada excede um certo limiar. Nós modelamos esse fenômeno em um Perceptron calculando a soma ponderada das entradas para representar a força total dos sinais de entrada e aplicando uma função de ativação na soma para determinar sua saída. Tal como nas redes neurais biológicas, esta saída é alimentada em outros Perceptrons. Estudamos tudo isso no capítulo anterior. Agora vamos continuar nossa discussão sobre o Perceptron compreendendo mais alguns conceitos, que serão fundamentais mais a frente quando estudarmos as arquiteturas avançadas de Deep Learning.

Antes de iniciar, vamos definir dois conceitos que você vai ver com frequência daqui em diante, vetor de entrada e vetor de pesos:

Vetor de entrada –  todos os valores de entrada de cada Perceptron são coletivamente chamados de vetor de entrada desse Perceptron. Esses são seus dados de entrada.

Vetor de pesos – de forma semelhante, todos os valores de peso de cada Perceptron são coletivamente chamados de vetor de peso desse Perceptron. Iniciamos nossa rede neural artificial com valores aleatórios de pesos e durante o treinamento a rede neural aprende os valores de peso ideais. Como veremos, existem muitas formas de realizar esse processo.

Boa parte do trabalho de uma rede neural vai girar em torno das operações algébricas entre o vetor de entrada e o vetor de pesos. Em seguida, vamos adicionando outras camadas matemáticas ou estatísticas para realizar diferentes operações, de acordo com o problema que estamos tentando resolver com o modelo de rede neural. Você vai perceber que tudo não passa de Matemática, que pode ser implementada com linguagens de programação, grandes conjuntos de dados e processamento paralelo, para formar sistemas de Inteligência Artificial.

Mas o que um Perceptron pode fazer afinal?

No capítulo anterior descrevemos os Perceptrons como um método para pesar evidências a fim de tomar decisões. Outra forma em que os Perceptrons podem ser usados é para calcular as funções lógicas elementares tais como AND, OR e NAND (caso tenha dúvidas sobre as operações lógicas, consulte as referências ao final deste capítulo). Por exemplo, suponha que tenhamos um Perceptron com duas entradas, cada uma com peso -2 e um viés de 3. Aqui está o nosso Perceptron:

 

Perceptron

 

Então vemos que a entrada 00 produziria a saída 1, uma vez que (-2) * 0 + (- 2) * 0 + 3 = 3, é positivo (resultado positivo, gera saída 1 do Perceptron, lembra do capítulo anterior?). Aqui, incluímos o símbolo * para tornar as multiplicações explícitas. Cálculos similares mostram que as entradas 01 e 10 produzem a saída 1. Mas a entrada 11 produz a saída 0, uma vez que (-2) * 1 + (- 2) * 1 + 3 = -1, é negativo. E assim nosso Perceptron implementa um “portão” NAND, ou uma operação lógica binária NAND.

O exemplo NAND mostra que podemos usar Perceptrons para calcular funções lógicas simples. Na verdade, podemos usar redes de Perceptrons para calcular qualquer função lógica. A razão é que o portão NAND é universal para computação, ou seja, podemos construir qualquer computação com portões NAND.

Uma rede de Perceptrons pode ser usada para simular um circuito contendo muitos portões NAND. E como os portões NAND são universais para a computação, segue-se que os Perceptrons também são universais para a computação. Considerando que o Perceptron é o modelo mais simples de rede neural, imagine o que pode ser feito com modelos bem mais avançados! Acertou se você pensou em Inteligência Artificial.

A universalidade computacional dos Perceptrons é simultaneamente reconfortante e decepcionante. É reconfortante porque nos diz que redes de Perceptrons podem ser tão poderosas como qualquer outro dispositivo de computação. Mas também é decepcionante, porque parece que os Perceptrons são meramente um novo tipo de portão NAND. Isso não é uma grande noticia!

No entanto, a situação é melhor do que esta visão sugere. Acontece que podemos conceber algoritmos de aprendizado que podem ajustar automaticamente os pesos e os vieses de uma rede de neurônios artificiais. Este ajuste ocorre em resposta a estímulos externos, sem intervenção direta de um programador. Esses algoritmos de aprendizagem nos permitem usar neurônios artificiais de uma maneira que é radicalmente diferente dos portões lógicos convencionais. Em vez de colocar explicitamente um circuito de NAND e outros portões, nossas redes neurais podem simplesmente aprender a resolver problemas, às vezes problemas em que seriam extremamente difíceis de projetar diretamente usando um circuito convencional de lógica.

Operações Lógicas e Regiões Linearmente Separáveis

Conforme mencionado acima, um Perceptron calcula a soma ponderada dos valores de entrada. Por simplicidade, suponhamos que existem dois valores de entrada, x e y para um certo Perceptron P. Vamos definir os pesos de x e y, como sendo A e B, respectivamente. A soma ponderada pode ser representada como: A x + B y.

Uma vez que o Perceptron produz um valor não-zero somente quando a soma ponderada excede um certo limite C, pode-se escrever a saída deste Perceptron da seguinte maneira:

Regra Perceptron

Considerando que A x + B y > C e A x + B y < C são as duas regiões no plano xy separadas pela linha A x + B y + C = 0, e se considerarmos ainda a entrada (x, y) como um ponto em um plano, então o Perceptron realmente nos diz qual região no plano a que esse ponto pertence. Tais regiões, uma vez que são separadas por uma única linha, são chamadas de regiões linearmente separáveis.

Um único Perceptron consegue resolver somente funções linearmente separáveis. Em funções não linearmente separáveis, o Perceptron não consegue gerar um hiperplano, esta linha nos gráficos abaixo, para separar os dados. A questão é que no mundo real raramente os dados são linearmente separáveis, fazendo com o que o Perceptron não seja muito útil para atividades práticas (mas sendo ideal para iniciar o estudo em redes neurais artificiais). E como separamos os dados não linearmente separáveis? Continue acompanhando este livro e você irá descobrir.

Linear e Não-Linear

Mas ainda assim o Perceptron tem sua utilidade, porque resulta em algumas funções lógicas, como os operadores booleanos AND, OR e NOT, que são linearmente separáveis, isto é, eles podem ser realizadas usando um único Perceptron. Podemos ilustrar porque eles são linearmente separáveis ao traçar cada um deles em um gráfico:

 

Funções Lógicas

 

Nos gráficos acima, os dois eixos são as entradas que podem levar o valor de 0 ou 1 e os números no gráfico são a saída esperada para uma entrada específica. Usando um vetor de peso apropriado para cada caso, um único Perceptron pode executar todas essas funções.

No entanto, nem todos os operadores de lógica são linearmente separáveis. Por exemplo, o operador XOR não é linearmente separável e não pode ser alcançado por um único Perceptron. No entanto, esse problema poderia ser superado usando mais de um Perceptron organizado em redes neurais feed-forward, que veremos mais a frente nos próximos capítulos.

 

xor

 

Uma vez que é impossível desenhar uma linha para dividir as regiões contendo 1 ou 0, a função XOR não é linearmente separável, conforme pode ser visto no gráfico acima.

Agora fica mais fácil compreender porque precisamos de arquiteturas mais avançadas de redes neurais artificiais, uma vez que temos problemas complexos no mundo real, como Visão Computacional, Processamento de Linguagem Natural, Tradução, Detecção de Fraudes, Classificação e muitos outros. E veremos essas arquiteturas em detalhes. Mas antes, precisamos falar sobre um componente fundamental das redes neurais, a Função de Ativação. Não perca o próximo capítulo. Até lá.

Referências:

Operação Lógica AND

Operação Lógica OR

Operação Lógica NAND

Operação Lógica XOR

Machine Learning

The Elements of Statistical Learning: Data Mining, Inference, and Prediction, Second Edition

Pattern Recognition and Machine Learning

Redes Neurais, princípios e práticas

Neural Networks and Deep Learning (alguns trechos extraídos e traduzidos com autorização do autor Michael Nielsen)

Deep Learning Book

Capítulo 6 – O Perceptron – Parte 1

by

Você sabe quais são as principais arquiteturas de redes neurais artificias? Não. Então analise cuidadosamente a imagem abaixo (excelente trabalho criado pela equipe do Asimov Institute, cujo link você encontra na seção de referências ao final deste capítulo):

Deep Learning Zoo

Incrível, não? São diversas arquiteturas, usadas para resolver diferentes tipos de problemas, como por exemplo as arquiteturas de redes neurais convolucionais usadas em problemas de Visão Computacional e as redes neurais recorrentes usadas em problemas de Processamento de Linguagem Natural. Estudaremos quase todas essas arquiteturas aqui neste livro. Sim, isso mesmo que você leu. Estamos apenas começando!! Caso queira aprender a construir modelos e projetos usando essas arquiteturas e trabalhando com linguagem Python, PyTorch e TensorFlow, clique aqui.

Embora todas essas arquiteturas sejam de redes neurais artificias, nem todas são de Deep Learning. O que caracteriza modelos de aprendizagem profunda, como o nome sugere, são redes neurais artificias com muitas camadas ocultas (ou intermediárias). Mas antes de chegarmos lá, precisamos passar pela arquitetura mais simples de uma rede neural artificial, o Perceptron. Como diz o ditado: “Toda grande caminhada começa pelo primeiro passo”.

O Modelo Perceptron foi desenvolvido nas décadas de 1950 e 1960 pelo cientista Frank Rosenblatt, inspirado em trabalhos anteriores de Warren McCulloch e Walter Pitts. Hoje, é mais comum usar outros modelos de neurônios artificiais, mas o Perceptron permite uma compreensão clara de como funciona uma rede neural em termos matemáticos, sendo uma excelente introdução.

Então, como funcionam os Perceptrons? Um Perceptron é um modelo matemático que recebe várias entradas, x1, x2, … e produz uma única saída binária:

Perceptron

No exemplo mostrado, o Perceptron possui três entradas: x1, x2, x3. Rosenblatt propôs uma regra simples para calcular a saída. Ele introduziu pesos, w1, w2, …, números reais expressando a importância das respectivas entradas para a saída. A saída do neurônio, 0 ou 1, é determinada pela soma ponderada, Σjwjxj, menor ou maior do que algum valor limiar (threshold). Assim como os pesos, o threshold é um número real que é um parâmetro do neurônio. Para colocá-lo em termos algébricos mais precisos:

Output

Esse é o modelo matemático básico. Uma maneira de pensar sobre o Perceptron é que é um dispositivo que toma decisões ao comprovar evidências. Deixe-me dar um exemplo. Não é um exemplo muito realista, mas é fácil de entender, e logo chegaremos a exemplos mais realistas. Suponha que o fim de semana esteja chegando e você ouviu falar que haverá um festival de queijo em sua cidade. Você gosta de queijo e está tentando decidir se deve ou não ir ao festival. Você pode tomar sua decisão pesando três fatores:

  • O tempo está bom?
  • Seu namorado ou namorada quer acompanhá-lo(a)?
  • O festival está perto de transporte público? (Você não possui um carro)

Podemos representar estes três fatores pelas variáveis binárias correspondentes x1, x2 e x3. Por exemplo, teríamos x1 = 1 se o tempo estiver bom e x1 = 0 se o tempo estiver ruim. Da mesma forma, x2 = 1 se seu namorado ou namorada quiser ir ao festival com você, e x2 = 0, se não. E similarmente para x3 e transporte público.

Agora, suponha que você adore queijo e está disposto a ir ao festival, mesmo que seu namorado ou namorada não esteja interessado e o festival fica em um lugar de difícil acesso e sem transporte público amplamente disponível. Além disso, você realmente detesta mau tempo, e não há como ir ao festival se o tempo estiver ruim. Você pode usar Perceptrons para modelar esse tipo de tomada de decisão.

Uma maneira de fazer isso é escolher um peso w1 = 6 para o tempo e w2 = 2 e w3 = 2 para as outras condições. O valor maior de w1 indica que o tempo é muito importante para você, muito mais do que se seu namorado ou namorada vai acompanhá-lo(a) ou se o festival é próximo do transporte público. Finalmente, suponha que você escolha um threshold de 5 para o Perceptron. Com essas escolhas, o Perceptron implementa o modelo de tomada de decisão desejado, produzindo 1 sempre que o tempo estiver bom e 0 sempre que o tempo estiver ruim. Não faz diferença para o resultado se seu namorado ou namorada quer ir, ou se o transporte público está acessível.

Variando os pesos e o limiar, podemos obter diferentes modelos de tomada de decisão. Por exemplo, suponha que escolhemos um threshold de 3. Então, o Perceptron decidirá que você deveria ir ao festival sempre que o tempo estiver bom ou quando o festival estiver perto do transporte público e seu namorado ou namorada estiver disposto a se juntar a você. Em outras palavras, seria um modelo diferente de tomada de decisão. Reduzir o threshold significa que você está mais propenso a ir ao festival.

Obviamente, o Perceptron não é um modelo completo de tomada de decisão humana! Mas o que o exemplo ilustra é como um Perceptron pode pesar diferentes tipos de evidências para tomar decisões. E deve parecer plausível que uma rede complexa de Perceptrons possa tomar decisões bastante sutis.

Rede

Nesta rede, a primeira coluna de Perceptrons – o que chamaremos de primeira camada de Perceptrons – está tomando três decisões muito simples, pesando a evidência de entrada. E quanto aos Perceptrons na segunda camada? Cada um desses Perceptrons está tomando uma decisão ponderando os resultados da primeira camada de tomada de decisão. Desta forma, um Perceptron na segunda camada pode tomar uma decisão em um nível mais complexo e mais abstrato do que os Perceptrons na primeira camada. E as decisões ainda mais complexas podem ser feitas pelos Perceptrons na terceira camada. Desta forma, uma rede de Perceptrons de várias camadas pode envolver-se em uma tomada de decisão sofisticada.

Aliás, quando definimos os Perceptrons, dissemos que um Perceptron possui apenas uma saída. Na rede acima, os Perceptrons parecem ter múltiplos resultados. Na verdade, eles ainda são de saída única. As setas de saída múltiplas são meramente uma maneira útil de indicar que a saída de um Perceptron está sendo usada como entrada para vários outros Perceptrons.

Vamos simplificar a maneira como descrevemos os Perceptrons. No limite de condição Σjwjxj > threshold podemos fazer duas mudanças de notação para simplificá-lo. A primeira mudança é escrever Σjwjxj como um produto (dot product), w⋅x≡Σjwjxj, onde w e x são vetores cujos componentes são os pesos e entradas, respectivamente. A segunda mudança é mover o threshold para o outro lado da equação e substituí-lo pelo que é conhecido como o viés (bias) do Perceptron, ou b ≡ -threshold. Usando o viés em vez do threshold, a regra Perceptron pode ser reescrita:

Fórmula Perceptron

Você pode pensar no viés como uma medida de quão fácil é obter o Perceptron para produzir um 1. Ou para colocá-lo em termos mais biológicos, o viés é uma medida de quão fácil é fazer com que o Perceptron dispare. Para um Perceptron com um viés realmente grande, é extremamente fácil para o Perceptron emitir um 1. Mas se o viés é muito negativo, então é difícil para o Perceptron emitir um 1. Obviamente, a introdução do viés é apenas uma pequena mudança em como descrevemos Perceptrons, mas veremos mais adiante que isso leva a outras simplificações de notação. Por isso, no restante do livro, não usaremos o threshold, usaremos sempre o viés.

Agora começa a ficar mais fácil compreender o conceito por trás das redes neurais artificiais e isso será muito útil quando estudarmos arquiteturas mais avançadas! Um Perceptron segue o modelo “feed-forward”, o que significa que as entradas são enviadas para o neurônio, processadas e resultam em uma saída. No diagrama abaixo, isso significa que a rede (um neurônio) lê da esquerda para a direita.

Neurônio

O processo de treinamento de um modelo Perceptron consiste em fazer com que o modelo aprenda os valores ideais de pesos e bias. Apresentamos ao modelo os dados de entrada e as possíveis saídas, treinamos o modelo e pesos e bias são aprendidos. Com o modelo treinado, podemos apresentar novos dados de entrada e o modelo será capaz de prever a saída. Veremos isso em breve quando criarmos nosso primeiro modelo usando linguagem Python.

Perceptron é uma rede neural de camada única e um Perceptron de várias camadas é chamado de Rede Neural Artificial. O Perceptron é um classificador linear (binário). Além disso, é usado na aprendizagem supervisionada e pode ser usado para classificar os dados de entrada fornecidos.

Mas o Perceptron tem ainda outras características importantes, como a representação de condicionais lógicos (and, or, xor), problemas com dados não linearmente separáveis e as funções de ativação. Mas esses são temas para o próximo capítulo. Até lá!

Referências:

Formação Engenheiro de IA

The Neural Network Zoo

Machine Learning

The Elements of Statistical Learning: Data Mining, Inference, and Prediction, Second Edition

Pattern Recognition and Machine Learning

Redes Neurais, princípios e práticas

Neural Networks and Deep Learning (alguns trechos extraídos e traduzidos com autorização do autor Michael Nielsen)

Deep Learning Book

Capítulo 4 – O Neurônio, Biológico e Matemático

by

Para compreender a lógica de funcionamento das redes neurais, alguns conceitos básicos referentes ao funcionamento do cérebro humano e seus componentes, os neurônios, são de fundamental importância. A formação das conexões entre as células e algumas considerações sobre como se concebe teoricamente o funcionamento matemático, ajudam a entender as bases da aprendizagem de máquina e das redes neurais. Vejamos como funciona o neurônio biológico deixando Machine Learning de lado por um instante!

O Neurônio Biológico

O neurônio é a unidade básica do cérebro humano, sendo uma célula especializada na transmissão de informações, pois nelas estão introduzidas propriedades de excitabilidade e condução de mensagens nervosas. O neurônio é constituído por 3 partes principais: a soma ou corpo celular, do qual emanam algumas ramificações denominadas de dendritos, e por uma outra ramificação descendente da soma, porém mais extensa, chamada de axônio. Nas extremidades dos axônios estão os nervos terminais, pelos quais é realizada a transmissão das informações para outros neurônios. Esta transmissão é conhecida como sinapse.

 

Neurônio Biológico

Fig7 – Representação Simplificada do Neurônio Biológico

Nosso cérebro é formado por bilhões de neurônios. Mas eles não estão isolados. Pelo contrário, existem centenas de bilhões de conexões entre eles, formando uma enorme rede de comunicação, a rede neural. Cada neurônio possui um corpo central, diversos dendritos e um axônio. Os dendritos recebem sinais elétricos de outros neurônios através das sinapses, que constitui o processo de comunicação entre neurônios. O corpo celular processa a informação e envia para outro neurônio.

Observe que a soma e os dendritos formam a superfície de entrada do neurônio e o axônio a superfície de saída do fluxo de informação (esse fluxo de informação é importante para compreender o neurônio matemático daqui a pouco). A informação transmitida pelos neurônios na realidade são impulsos elétricos. O impulso elétrico é a mensagem que os neurônios transmitem uns aos outros, ou seja, é a propagação de um estímulo ao longo dos neurônios que pode ser qualquer sinal captado pelos receptores nervosos.

Os dendritos têm como função, receber informações, ou impulsos nervosos, oriundos de outros neurônios e conduzi-los até o corpo celular. Ali, a informação é processada e novos impulsos são gerados. Estes impulsos são transmitidos a outros neurônios, passando pelo axônio e atingindo os dendritos dos neurônios seguintes. O corpo do neurônio é responsável por coletar e combinar informações vindas de outros neurônios.

O ponto de contato entre a terminação axônica de um neurônio e o dendrito de outro é chamado sinapse. É pelas sinapses que os neurônios se unem funcionalmente, formando as redes neurais. As sinapses funcionam como válvulas, sendo capazes de controlar a transmissão de impulsos, isto é, o fluxo da informação entre os neurônios na rede neural. O efeito das sinapses é variável e é esta variação que dá ao neurônio capacidade de adaptação.

Sinais elétricos gerados nos sensores (retina ocular, papilas gustativas, etc…) caminham pelos axônios. Se esses sinais forem superiores a um limiar de disparo (threshold), seguem pelo axônio. Caso contrário, são bloqueados e não prosseguem (são considerados irrelevantes). A passagem desses sinais não é elétrica, mas química (através da substância serotonina). Se o sinal for superior a certo limite (threshold), vai em frente; caso contrário é bloqueado e não segue. Estamos falando aqui do neurônio biológico e preste bastante atenção a palavra threshold, pois ela é a essência do neurônio matemático.

Um neurônio recebe sinais através de inúmeros dendritos, os quais são ponderados e enviados para o axônio, podendo ou não seguir adiante (threshold). Na passagem por um neurônio, um sinal pode ser amplificado ou atenuado, dependendo do dendrito de origem, pois a cada condutor, está associado um peso pelo qual o sinal é multiplicado. Os pesos são o que chamamos de memória.

Cada região do cérebro é especializada em uma dada função, como processamento de sinais auditivos, sonoros, elaboração de pensamentos, desejos, etc… Esse processamento se dá através de redes particulares interligadas entre si, realizando processamento paralelo. Cada região do cérebro possui uma arquitetura de rede diferente: varia o número de neurônios, de sinapses por neurônio, valor dos thresholds e dos pesos, etc…Os valores dos pesos são estabelecidos por meio de treinamento recebido pelo cérebro durante a vida útil. É a memorização.

Inspirados no neurônio biológico, os pesquisadores desenvolveram um modelo de neurônio matemático que se tornou a base da Inteligência Artificial. A ideia era simples: “Se redes neurais formam a inteligência humana, vamos reproduzir isso e criar Inteligência Artificial”. E assim nasceu o neurônio matemático, o qual descrevemos abaixo.

O Neurônio Matemático

A partir da estrutura e funcionamento do neurônio biológico, pesquisadores tentaram simular este sistema em computador. O modelo mais bem aceito foi proposto por Warren McCulloch e Walter Pitts em 1943, o qual implementa de maneira simplificada os componentes e o funcionamento de um neurônio biológico. Em termos simples, um neurônio matemático de uma rede neural artificial é um componente que calcula a soma ponderada de vários inputs, aplica uma função e passa o resultado adiante.

Neste modelo de neurônio matemático, os impulsos elétricos provenientes de outros neurônios são representados pelos chamados sinais de entrada (a letra x nesse diagrama abaixo, que nada mais são do que os dados que alimentam seu modelo de rede neural artificial). Dentre os vários estímulos recebidos, alguns excitarão mais e outros menos o neurônio receptor e essa medida de quão excitatório é o estímulo é representada no modelo de Warren McCulloch e Walter Pitts através dos pesos sinápticos. Quanto maior o valor do peso, mais excitatório é o estímulo. Os pesos sinápticos são representados por wkn neste diagrama abaixo, onde k representa o índice do neurônio em questão e n se refere ao terminal de entrada da sinapse a qual o peso sináptico se refere.

A soma ou corpo da célula é representada por uma composição de dois módulos, o primeiro é uma junção aditiva, somatório dos estímulos (sinais de entrada) multiplicado pelo seu fator excitatório (pesos sinápticos), e posteriormente uma função de ativação, que definirá com base nas entradas e pesos sinápticos, qual será a saída do neurônio. O axônio é aqui representado pela saída (yk) obtida pela aplicação da função de ativação. Assim como no modelo biológico, o estímulo pode ser excitatório ou inibitório, representado pelo peso sináptico positivo ou negativo respectivamente.

Neurônio Matemático

Fig8 – Representação Simplificada do Neurônio Matemático

O modelo proposto possui uma natureza binária. Tanto os sinais de entrada quanto a saída, são valores binários. McCulloch acreditava que o funcionamento do sistema nervoso central possuía um carater binário, ou seja, um neurônio infuencia ou não outro neurônio, mas posteriormente mostrou-se que não era dessa forma.

O neurônio matemático é um modelo simplificado do neurônio biológico. Tais modelos inspirados a partir da análise da geração e propagação de impulsos elétricos pela membrana celular dos neurônios. O neurônio matemático recebe um ou mais sinais de entrada e devolve um único sinal de saída, que pode ser distribuído como sinal de saída da rede, ou como sinal de entrada para um ou vários outros neurônios da camada posterior (que formam a rede neural artificial). Os dendritos e axônios são representados matematicamente apenas pelas sinapses, e a intensidade da ligação é representada por uma grandeza denominada peso sináptico, simbolizada pela letra w. Quando as entradas, x são apresentadas ao neurônio, elas são multiplicadas pelos pesos sinápticos correspondentes, gerando as entradas ponderadas, ou seja, x1 que multiplica w1, etc… Isso descreve uma das bases matemáticas do funcionamento de uma rede neural artificial, a multiplicação de matrizes:

Matriz

Fig9 – Multiplicação de Matrizes Entre Sinais de Entrada x e Pesos Sinápticos w (versão simplificada)

O neurônio então totaliza todos os produtos gerando um único resultado. A esta função se denomina função de combinação. Este valor é então apresentado a uma função de ativação ou função de transferência, que tem, dentre outras, a finalidade de evitar o acréscimo progressivo dos valores de saída ao longo das camadas da rede, visto que tais funções possuem valores máximos e mínimos contidos em intervalos determinados. O uso de funções de transferência não-lineares torna a rede neural uma ferramenta poderosa. Sabe-se que uma rede perceptron de duas camadas com função de transferência não-linear como a função sigmóide (que veremos mais adiante), é denominada de aproximador universal.

Um neurônio dispara quando a soma dos impulsos que ele recebe ultrapassa o seu limiar de excitação chamado de threshold. O corpo do neurônio, por sua vez, é emulado por um mecanismo simples que faz a soma dos valores xi e wi recebidos pelo neurônio (soma ponderada) e decide se o neurônio deve ou não disparar (saída igual a 1 ou a 0) comparando a soma obtida ao limiar ou threshold do neurônio. A ativação do neurônio é obtida através da aplicação de uma “função de ativação”, que ativa a saída ou não, dependendo do valor da soma ponderada das suas entradas.

Note que este modelo matemático simplificado de um neurônio é estático, ou seja, não considera a dinâmica do neurônio natural. No neurônio biológico, os sinais são enviados em pulsos e alguns componentes dos neurônios biológicos, a exemplo do axônio, funcionam como filtros de frequência.

O modelo do neurônio matemático também pode incluir uma polarização ou bias de entrada. Esta variável é incluída ao somatório da função de ativação, com o intuito de aumentar o grau de liberdade desta função e, consequentemente, a capacidade de aproximação da rede. O valor do bias é ajustado da mesma forma que os pesos sinápticos. O bias possibilita que um neurônio apresente saída não nula ainda que todas as suas entradas sejam nulas. Por exemplo, caso não houvesse o bias e todas as entradas de um neurônio fossem nulas, então o valor da função de ativação seria nulo. Desta forma não poderíamos, por exemplo, fazer com o que o neurônio aprendesse a relação pertinente ao ”ou exclusivo” da lógica. Em resumo, temos esses componentes em um neurônio matemático:

Resumo do Neurônio

Fig10 – Representação do Neurônio Matemático

  • Sinais de entrada { X1, X2, …, Xn }: São os sinais externos normalmente normalizados para incrementar a eficiência computacional dos algoritmos de aprendizagem. São os dados que alimentam seu modelo preditivo.
  • Pesos sinápticos { W1, W2, …, Wn }: São valores para ponderar os sinais de cada entrada da rede. Esses valores são aprendidos durante o treinamento.
  • Combinador linear { Σ }: Agregar todos sinais de entrada que foram ponderados pelos respectivos pesos sinápticos a fim de produzir um potencial de ativação.
  • Limiar de ativação { Θ }: Especifica qual será o patamar apropriado para que o resultado produzido pelo combinador linear possa gerar um valor de disparo de ativação.
  • Potencial de ativação { u }: É o resultado obtido pela diferença do valor produzido entre o combinador linear e o limiar de ativação. Se o valor for positivo, ou seja, se u ≥ 0 então o neurônio produz um potencial excitatório; caso contrário, o potencial será inibitório.
  • Função de ativação { g }: Seu objetivo é limitar a saída de um neurônio em um intervalo valores.
  • Sinal de saída { y}: É o valor final de saída podendo ser usado como entrada de outros neurônios que estão sequencialmente interligados.

Os modelos baseados em redes neurais artificiais são os que mais ganharam atenção nos últimos anos por conseguirem resolver problemas de IA nos quais se conseguia pouco avanço com outras técnicas. A partir da concepção do neurônio matemático, várias arquiteturas e modelos com diferentes combinações entre esses neurônios, e aplicando diferentes técnicas matemáticas e estatísticas, surgiram e propiciaram a criação de arquiteturas avançadas de Deep Learning como Redes Neurais Convolucionais, Redes Neurais Recorrentes, Auto Encoders, Generative Adversarial Networks, Memory Networks, entre outras, que estudaremos ao longo deste livro online.

Referências:

Anatomia de um Neurônio

Bibliografia Machine Learning e IA

Deep Learning in Neural Networks: An Overview

Grokking Deep Learning

HAYKIN, S. Redes Neurais, princípios e práticas. Porto Alegre: Bookman, 2001.

JAIN, A. K, MAO, J., MOHIUDDIN, K.M. Artificial neural networks: a tutorial. IEEE Computer, v. 29, n. 3, p. 56-63, 1996.

 

Deep Learning Book

Capítulo 1 – Deep Learning e a Tempestade Perfeita

by

O interesse pela Aprendizagem de Máquina (Machine Learning) explodiu na última década. O mundo a nossa volta está passando por uma transformação e vemos uma interação cada vez maior das aplicações de computador com os seres humanos. Softwares de detecção de spam, sistemas de recomendação, marcação em fotos de redes sociais, assistentes pessoais ativados por voz, carros autônomos, smartphones com reconhecimento facial e muito mais.

E o interesse por Machine Learning se mostra ainda mais evidente pelo número cada vez maior de conferências, meetups, artigos, livros, cursos, buscas no Google e profissionais e empresas procurando compreender o que é e como usar aprendizagem de máquina, embora muitos ainda confundem o que podem fazer com o que desejam fazer. Não há como ficar indiferente a esta revolução trazida pela aprendizagem de máquina e, segundo o Gartner, até 2020 todos os softwares corporativos terão alguma funcionalidade ligada a Machine Learning.

Fundamentalmente, Machine Learning é a utilização de algoritmos para extrair informações de dados brutos e representá-los através de algum tipo de modelo matemático. Usamos então este modelo para fazer inferências a partir de outros conjuntos de dados. Existem muitos algoritmos que permitem fazer isso, mas um tipo em especial vem se destacando, as redes neurais artificiais.

As redes neurais artificiais não são necessariamente novas, existem pelo menos desde a década de 1950. Mas durante várias décadas, embora a arquitetura desses modelos tivesse evoluído, ainda faltavam ingredientes que fizessem os modelos realmente funcionar. E esses ingredientes surgiram quase ao mesmo tempo. Um deles você já deve ter ouvido: Big Data. O volume de dados, gerado em variedade e velocidade cada vez maiores, permite criar modelos e atingir altos níveis de precisão. Mas ainda falta um ingrediente. Faltava! Como processar grandes modelos de Machine Learning com grandes quantidades de dados? As CPUs não conseguiam dar conta do recado.

Foi quando os gamers e sua avidez por poder computacional e gráficos perfeitos, nos ajudaram a encontrar o segundo ingrediente: Programação Paralela em GPUs. As unidades de processamento gráfico, que permitem realizar operações matemáticas de forma paralela, principalmente operações com matrizes e vetores, elementos presentes em modelos de redes neurais artificias, formaram a tempestade perfeita, que permitiu a evolução na qual nos encontramos hoje: Big Data + Processamento Paralelo + Modelos de Aprendizagem de Máquina = Inteligência Artificial.

A unidade fundamental de uma rede neural artificial é um nó (ou neurônio matemático), que por sua vez é baseado no neurônio biológico. As conexões entre esses neurônios matemáticos também foram inspiradas em cérebros biológicos, especialmente na forma como essas conexões se desenvolvem ao longo do tempo com “treinamento”. Em meados da década de 1980 e início da década de 1990, muitos avanços importantes na arquitetura das redes neurais artificias ocorreram. No entanto, a quantidade de tempo e dados necessários para obter bons resultados retardou a adoção e, portanto, o interesse foi arrefecido, com o que ficou conhecimento como AI Winter (Inverno da IA).

No início dos anos 2000, o poder computacional expandiu exponencialmente e o mercado viu uma “explosão” de técnicas computacionais que não eram possíveis antes disso. Foi quando o aprendizado profundo (Deep Learning) emergiu do crescimento computacional explosivo dessa década como o principal mecanismo de construção de sistemas de Inteligência Artificial, ganhando muitas competições importantes de aprendizagem de máquina. O interesse por Deep Learning não para de crescer e hoje vemos o termo aprendizado profundo sendo mencionado com frequência cada vez maior e soluções comerciais surgindo a todo momento.

Este livro online, gratuito e em português, é uma iniciativa da Data Science Academy para ajudar aqueles que buscam conhecimento avançado e de qualidade em nosso idioma. Serão 100 capítulos, publicados no formato de posts. Desta forma, esperamos contribuir para o crescimento do Deep Learning e Inteligência Artificial no Brasil.

Nos acompanhe nesta incrível jornada!

Equipe DSA

www.datascienceacademy.com.br

 

Deep Learning Book

Início

by

Olá, Seja Bem-Vindo ao Deep Learning Book Brasil.

Esta é uma iniciativa da Data Science Academy, com o objetivo de ajudar a difundir uma das tecnologias mais revolucionárias do nosso tempo, Deep Learning. Como existe pouco material em português, esperamos que este livro online, em português e gratuito, o ajude a compreender um pouco melhor o que é a tecnologia e como ela pode ser usada na construção de aplicações de Inteligência Artificial.

Este livro vai abordar desde o básico em redes neurais artificias, modelos Perceptron e Multilayer Perceptron, passando pelo processo de treinamento com Backpropagation, até arquiteturas avançadas de Deep Learning, como Redes Neurais Convolucionais, Redes Neurais Recorrentes, Autoencoders, Generative Adversarial Network, Deep Reinforcement Learning e o estado da arte, Transformers.

Além disso, também abordaremos regras gerais de aprendizado de máquina, válidas para qualquer modelo de Machine Learning. E teremos ainda Álgebra Linear e Estatística.

São 100 capítulos, publicados no formato de posts, tudo 100% em português. Desta forma, esperamos contribuir para o crescimento do Deep Learning e Inteligência Artificial no Brasil.

Nos acompanhe nesta incrível jornada! Em Português, Online e Gratuito. Clique aqui para começar a leitura pelo capítulo 1.

Equipe DSA

www.datascienceacademy.com.br

Paginação de posts

  • Previous
  • 1
  • 2
  • 3

Capítulos Recentes

  • Capítulo 100 – Machine Learning – Guia Definitivo – Parte 10
  • Capítulo 99 – Machine Learning – Guia Definitivo – Parte 9
  • Capítulo 98 – Machine Learning – Guia Definitivo – Parte 8
  • Capítulo 97 – Machine Learning – Guia Definitivo – Parte 7
  • Capítulo 96 – Machine Learning – Guia Definitivo – Parte 6
  • Capítulo 95 – Machine Learning – Guia Definitivo – Parte 5
  • Capítulo 94 – Machine Learning – Guia Definitivo – Parte 4
  • Capítulo 93 – Machine Learning – Guia Definitivo – Parte 3
  • Capítulo 92 – Machine Learning – Guia Definitivo – Parte 2
  • Capítulo 91 – Machine Learning – Guia Definitivo – Parte 1

Buscar

Podcast DSA

© 2025 Data Science Academy - www.datascienceacademy.com.br