Você está na página 1de 194

Capítulo 1 – Deep Learning e a Tempestade

Perfeita

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 mais de 50 capítulos,
publicados no formato de posts e lançados semanalmente. Desta forma, esperamos contribuir para o
crescimento do Deep Learning e Inteligência Artificial no Brasil.

Nos acompanhe nesta incrível jornada!

Equipe DSA
Capítulo 2 – Uma Breve História das Redes
Neurais Artificiais

Para compreender onde estamos hoje, precisamos olhar para o passado e analisar como chegamos até aqui.
Vejamos então Uma Breve História das Redes Neurais Artificiais.

O cérebro humano é uma máquina altamente poderosa e complexa capaz de processar uma grande
quantidade de informações em tempo mínimo. As unidades principais do cérebro são os neurônios e é por
meio deles que as informações são transmitidas e processadas. As tarefas realizadas pelo cérebro intrigam os
pesquisadores, como por exemplo, a capacidade do cérebro de reconhecer um rosto familiar dentre uma
multidão em apenas milésimos de segundo. As respostas sobre alguns enigmas do funcionamento do cérebro
ainda não foram respondidas e se perpetuam ate os dias de hoje. O que é conhecido sobre o funcionamento
do cérebro é que o mesmo desenvolve suas regras através da experiência adquirida em situações vividas
anteriormente.

Fig1 – Cérebro humano, a máquina mais fantástica que existe no Planeta Terra.

O desenvolvimento do cérebro humano ocorre principalmente nos dois primeiros anos de vida, mas se
arrasta por toda a vida. Inspirando-se neste modelo, diversos pesquisadores tentaram simular o
funcionamento do cérebro, principalmente o processo de aprendizagem por experiência, a fim de criar
sistemas inteligentes capazes de realizar tarefas como classificação, reconhecimento de padrões,
processamento de imagens, entre outras atividades. Como resultado destas pesquisas surgiu o modelo do
neurônio artificial e posteriormente um sistema com vários neurônios interconectados, a chamada Rede
Neural.

Em 1943, o neurofisiologista Warren McCulloch e o matemático Walter Pitts escreveram um artigo sobre
como os neurônios poderiam funcionar e para isso, eles modelaram uma rede neural simples usando
circuitos elétricos.

Warren McCulloch e Walter Pitts criaram um modelo computacional para redes neurais baseadas em
matemática e algoritmos denominados lógica de limiar (threshold logic). Este modelo abriu o caminho para
a pesquisa da rede neural dividida em duas abordagens: uma abordagem focada em processos biológicos no
cérebro, enquanto a outra focada na aplicação de redes neurais à inteligência artificial.
Em 1949, Donald Hebb escreveu The Organization of Behavior, uma obra que apontou o fato de que os
caminhos neurais são fortalecidos cada vez que são usados, um conceito fundamentalmente essencial para a
maneira como os humanos aprendem. Se dois nervos dispararem ao mesmo tempo, argumentou, a conexão
entre eles é melhorada.

À medida que os computadores se tornaram mais avançados na década de 1950, finalmente foi possível
simular uma hipotética rede neural. O primeiro passo para isso foi feito por Nathanial Rochester dos
laboratórios de pesquisa da IBM. Infelizmente para ele, a primeira tentativa de fazê-lo falhou.

No entanto, ao longo deste tempo, os defensores das “máquinas pensantes” continuaram a argumentar suas
pesquisas. Em 1956, o Projeto de Pesquisa de Verão de Dartmouth sobre Inteligência Artificial
proporcionou um impulso tanto à Inteligência Artificial como às Redes Neurais. Um dos resultados deste
processo foi estimular a pesquisa em IA na parte de processamento neural.

Nos anos seguintes ao Projeto Dartmouth, John von Neumann sugeriu imitar funções simples de neurônios
usando relés telegráficos ou tubos de vácuo. Além disso, Frank Rosenblatt, um neurobiologista, começou a
trabalhar no Perceptron. Ele estava intrigado com o funcionamento do olho de uma mosca. Grande parte do
processamento feito por uma mosca ao decidir fugir, é feito em seus olhos. O Perceptron, que resultou dessa
pesquisa, foi construído em hardware e é a mais antiga rede neural ainda em uso hoje. Um Percetron de
camada única foi útil para classificar um conjunto de entradas de valor contínuo em uma de duas classes. O
Perceptron calcula uma soma ponderada das entradas, subtrai um limite e passa um dos dois valores
possíveis como resultado. Infelizmente, o Perceptron é limitado e foi comprovado como tal durante os “anos
desiludidos” por Marvin Minsky e o livro de Seymour Papert de 1969, Perceptrons.

Fig2 – Algumas Arquiteturas de Redes Neurais

 
Em 1959, Bernard Widrow e Marcian Hoff, de Stanford, desenvolveram modelos denominados
“ADALINE” e “MADALINE”. Em uma exibição típica do amor de Stanford por siglas, os nomes provêm
do uso de múltiplos elementos ADAptive LINear. ADALINE foi desenvolvido para reconhecer padrões
binários de modo que, se ele estivesse lendo bits de transmissão de uma linha telefônica, poderia prever o
próximo bit. MADALINE foi a primeira rede neural aplicada a um problema do mundo real, usando um
filtro adaptativo que elimina ecos nas linhas telefônicas. Embora o sistema seja tão antigo como os sistemas
de controle de tráfego aéreo, ele ainda está em uso comercial.

Infelizmente, esses sucessos anteriores levaram as pessoas a exagerar o potencial das redes neurais,
particularmente à luz da limitação na eletrônica, então disponível na época. Este exagero excessivo, que
decorreu do mundo acadêmico e técnico, infectou a literatura geral da época. Muitas promessas foram feitas,
mas o resultado foi o desapontamento. Além disso, muitos escritores começaram a refletir sobre o efeito que
teria “máquinas pensantes” no homem. A série de Asimov em robôs revelou os efeitos sobre a moral e os
valores do homem quando máquinas fossem capazes de fazer todo o trabalho da humanidade. Outros
escritores criaram computadores mais sinistros, como HAL do filme 2001.

Toda essa discussão sobre o efeito da Inteligência Artificial sobre a vida humana, aliada aos poucos
progressos, fizeram vozes respeitadas criticar a pesquisa em redes neurais. O resultado foi a redução drástica
de grande parte do financiamento em pesquisas. Esse período de crescimento atrofiado durou até 1981,
sendo conhecido como o Inverno da IA (AI Winter).

Em 1982, vários eventos provocaram um renovado interesse. John Hopfield da Caltech apresentou um
documento à Academia Nacional de Ciências. A abordagem de Hopfield não era simplesmente modelar
cérebros, mas criar dispositivos úteis. Com clareza e análise matemática, ele mostrou como essas redes
poderiam funcionar e o que poderiam fazer. No entanto, o maior recurso de Hopfield foi seu carisma. Ele era
articulado e simpático e isso colaborou bastante para que ele fosse ouvido.

Em 1985, o Instituto Americano de Física começou o que se tornou uma reunião anual – Redes Neurais para
Computação. Em 1987, a primeira Conferência Internacional sobre Redes Neurais do Institute of Electrical
and Electronic Engineer’s (IEEE) atraiu mais de 1.800 participantes.

Em 1986, com redes neurais de várias camadas nas notícias, o problema era como estender a regra Widrow-
Hoff para várias camadas. Três grupos independentes de pesquisadores, dentre os quais David Rumelhart,
ex-membro do departamento de psicologia de Stanford, apresentaram ideias semelhantes que agora são
chamadas de redes Backpropagation porque distribuem erros de reconhecimento de padrões em toda a rede.
As redes híbridas utilizavam apenas duas camadas, essas redes de Backpropagation utilizam muitas. O
resultado é que as redes de Backpropagation “aprendem” de forma mais lenta, pois necessitam,
possivelmente, de milhares de iterações para aprender, mas geram um resultado muito preciso.

Agora, as redes neurais são usadas em várias aplicações. A ideia fundamental por trás da natureza das redes
neurais é que, se ela funcionar na natureza, deve ser capaz de funcionar em computadores. O futuro das
redes neurais, no entanto, reside no desenvolvimento de hardware. As redes neurais rápidas e eficientes
dependem do hardware especificado para seu eventual uso.

O diagrama abaixo mostra alguns marcos importantes na evolução e pesquisa das redes neurais artificiais. O
fato, é que ainda estamos escrevendo esta história e muita evolução está ocorrendo neste momento, através
do trabalho de milhares de pesquisadores e profissionais de Inteligência Artificial em todo mundo. E você,
não quer ajudar a escrever esta história?
Fig3 – Marcos no desenvolvimento das redes neurais.

Podemos resumir assim os principais marcos na pesquisa e evolução das redes neurais artificiais até
chegarmos ao Deep Learning:

1943: Warren McCulloch e Walter Pitts criam um modelo computacional para redes neurais baseadas em
matemática e algoritmos denominados lógica de limiar.

1958: Frank Rosenblatt cria o Perceptron, um algoritmo para o reconhecimento de padrões baseado em uma
rede neural computacional de duas camadas usando simples adição e subtração. Ele também propôs camadas
adicionais com notações matemáticas, mas isso não seria realizado até 1975.

1980: Kunihiko Fukushima propõe a Neoconitron, uma rede neural de hierarquia, multicamada, que foi
utilizada para o reconhecimento de caligrafia e outros problemas de reconhecimento de padrões.

1989: os cientistas conseguiram criar algoritmos que usavam redes neurais profundas, mas os tempos de
treinamento para os sistemas foram medidos em dias, tornando-os impraticáveis para o uso no mundo real.

1992: Juyang Weng publica o Cresceptron, um método para realizar o reconhecimento de objetos 3-D
automaticamente a partir de cenas desordenadas.

Meados dos anos 2000: o termo “aprendizagem profunda” começa a ganhar popularidade após um artigo de
Geoffrey Hinton e Ruslan Salakhutdinov mostrar como uma rede neural de várias camadas poderia ser pré-
treinada uma camada por vez.

2009: acontece o NIPS Workshop sobre Aprendizagem Profunda para Reconhecimento de Voz e descobre-
se que com um conjunto de dados suficientemente grande, as redes neurais não precisam de pré-treinamento
e as taxas de erro caem significativamente.

2012: algoritmos de reconhecimento de padrões artificiais alcançam desempenho em nível humano em


determinadas tarefas. E o algoritmo de aprendizagem profunda do Google é capaz de identificar gatos.

2014: o Google compra a Startup de Inteligência Artificial chamada DeepMind, do Reino Unido, por £
400m
2015: Facebook coloca a tecnologia de aprendizado profundo – chamada DeepFace – em operação para
marcar e identificar automaticamente usuários do Facebook em fotografias. Algoritmos executam tarefas
superiores de reconhecimento facial usando redes profundas que levam em conta 120 milhões de
parâmetros.

2016: o algoritmo do Google DeepMind, AlphaGo, mapeia a arte do complexo jogo de tabuleiro Go e vence
o campeão mundial de Go, Lee Sedol, em um torneio altamente divulgado em Seul.

2017: adoção em massa do Deep Learning em diversas aplicações corporativas e mobile, além do avanço em
pesquisas. Todos os eventos de tecnologia ligados a Data Science, IA e Big Data, apontam Deep Learning
como a principal tecnologia para criação de sistemas inteligentes.

A promessa do aprendizado profundo não é que os computadores comecem a pensar como seres humanos.
Isso é como pedir uma maçã para se tornar uma laranja. Em vez disso, demonstra que, dado um conjunto de
dados suficientemente grande, processadores rápidos e um algoritmo suficientemente sofisticado, os
computadores podem começar a realizar tarefas que até então só podiam ser realizadas apenas por seres
humanos, como reconhecer imagens e voz, criar obras de arte ou tomar decisões por si mesmo.

Os estudos sobre as redes neurais sofreram uma grande revolução a partir dos anos 80 e esta área de estudos
tem se destacado, seja pelas promissoras características apresentadas pelos modelos de redes neurais
propostos, seja pelas condições tecnológicas atuais de implementação que permitem desenvolver arrojadas
implementações de arquiteturas neurais paralelas em hardwares dedicado, obtendo assim ótimas
performances destes sistemas (bastante superiores aos sistemas convencionais). A evolução natural das redes
neurais, são as redes neurais profundas (ou Deep Learning). Mas isso é o que vamos discutir no próximo
capítulo! Até lá.
Capítulo 3 – O Que São Redes Neurais Artificiais
Profundas ou Deep Learning?

Aprendizagem Profunda ou Deep Learning, é uma sub-área da Aprendizagem de Máquina, que emprega
algoritmos para processar dados e imitar o processamento feito pelo cérebro humano. Mas O Que São Redes
Neurais Artificiais Profundas ou Deep Learning? É o que veremos neste capítulo. Não se preocupe se alguns
termos mais técnicos não fizerem sentido agora. Todos eles serão estudados ao longo deste livro online.

Deep Learning  usa camadas de neurônios matemáticos para processar dados, compreender a fala humana e
reconhecer objetos visualmente. A informação é passada através de cada camada, com a saída da camada
anterior fornecendo entrada para a próxima camada. A primeira camada em uma rede é chamada de camada
de entrada, enquanto a última é chamada de camada de saída. Todas as camadas entre as duas são referidas
como camadas ocultas. Cada camada é tipicamente um algoritmo simples e uniforme contendo um tipo de
função de ativação.

Fig4 – Rede Neural Simples e Rede Neural Profunda (Deep Learning)

A aprendizagem profunda é responsável por avanços recentes em visão computacional, reconhecimento de


fala, processamento de linguagem natural e reconhecimento de áudio. O aprendizado profundo é baseado no
conceito de redes neurais artificiais, ou sistemas computacionais que imitam a maneira como o cérebro
humano funciona.

A extração de recursos é outro aspecto da Aprendizagem Profunda. A extração de recursos usa um algoritmo
para construir automaticamente “recursos” significativos dos dados para fins de treinamento, aprendizado e
compreensão. Normalmente, o Cientista de Dados, ou Engenheiro de IA, é responsável pela extração de
recursos.

O aumento rápido e o aparente domínio do aprendizado profundo sobre os métodos tradicionais de


aprendizagem de máquina em uma variedade de tarefas tem sido surpreendente de testemunhar e, às vezes,
difícil de explicar. Deep Learning é uma evolução das Redes Neurais, que por sua vez possuem uma história
fascinante que remonta à década de 1940, cheia de altos e baixos, voltas e reviravoltas, amigos e rivais,
sucessos e fracassos. Em uma história digna de um filme dos anos 90, uma ideia que já foi uma espécie de
patinho feio floresceu para se tornar a bola da vez.

Consequentemente, o interesse em aprendizagem profunda tem disparado, com cobertura constante na mídia
popular. A pesquisa de aprendizagem profunda agora aparece rotineiramente em revistas como Science,
Nature, Nature Methods e Forbes apenas para citar alguns. O aprendizado profundo conquistou Go,
aprendeu a dirigir um carro, diagnosticou câncer de pele e autismo, tornou-se um falsificador de arte mestre
e pode até alucinar imagens fotorrealistas.

Os primeiros algoritmos de aprendizagem profunda que possuíam múltiplas camadas de características não-
lineares podem ser rastreados até Alexey Grigoryevich Ivakhnenko (desenvolveu o Método do Grupo de
Manipulação de Dados) e Valentin Grigor’evich Lapa (autor de Cybernetics and Forecasting Techniques)
em 1965 (Figura 5), que usaram modelos finos mas profundos com funções de ativação polinomial os quais
eles analisaram com métodos estatísticos. Em cada camada, eles selecionavam os melhores recursos através
de métodos estatísticos e encaminhavam para a próxima camada. Eles não usaram Backpropagation para
treinar a rede de ponta a ponta, mas utilizaram mínimos quadrados camada-por-camada, onde as camadas
anteriores foram independentemente instaladas em camadas posteriores (um processo lento e manual).

Fig5 – Arquitetura da primeira rede profunda conhecida treinada por Alexey Grigorevich Ivakhnenko em
1965.

No final da década de 1970, o primeiro inverno de AI começou, resultado de promessas que não poderiam
ser mantidas. O impacto desta falta de financiamento limitou a pesquisa em Redes Neurais Profundas e
Inteligência Artificial. Felizmente, houve indivíduos que realizaram a pesquisa sem financiamento.
As primeiras “redes neurais convolutivas” foram usadas por Kunihiko Fukushima. Fukushima concebeu
redes neurais com múltiplas camadas de agrupamento e convoluções. Em 1979, ele desenvolveu uma rede
neural artificial, chamada Neocognitron, que usava um design hierárquico e multicamadas. Este design
permitiu ao computador “aprender” a reconhecer padrões visuais. As redes se assemelhavam a versões
modernas, mas foram treinadas com uma estratégia de reforço de ativação recorrente em múltiplas camadas,
que ganhou força ao longo do tempo. Além disso, o design de Fukushima permitiu que os recursos
importantes fossem ajustados manualmente aumentando o “peso” de certas conexões.

Muitos dos conceitos de Neocognitron continuam a ser utilizados. O uso de conexões de cima para baixo e
novos métodos de aprendizagem permitiram a realização de uma variedade de redes neurais. Quando mais
de um padrão é apresentado ao mesmo tempo, o Modelo de Atenção Seletiva pode separar e reconhecer
padrões individuais deslocando sua atenção de um para o outro (o mesmo processo que usamos em
multitarefa). Um Neocognitron moderno não só pode identificar padrões com informações faltantes (por
exemplo, um número 5 desenhado de maneira incompleta), mas também pode completar a imagem
adicionando as informações que faltam. Isso pode ser descrito como “inferência”.

O Backpropagation, o uso de erros no treinamento de modelos de Deep Learning, evoluiu significativamente


em 1970. Foi quando Seppo Linnainmaa escreveu sua tese de mestrado, incluindo um código FORTRAN
para Backpropagation. Infelizmente, o conceito não foi aplicado às redes neurais até 1985. Foi quando
Rumelhart, Williams e Hinton demonstraram o Backpropagation em uma rede neural que poderia fornecer
representações de distribuição “interessantes”. Filosoficamente, essa descoberta trouxe à luz a questão
dentro da psicologia cognitiva de saber se a compreensão humana depende da lógica simbólica
(computacionalismo) ou de representações distribuídas (conexão). Em 1989, Yann LeCun forneceu a
primeira demonstração prática de Backpropagation no Bell Labs. Ele combinou redes neurais convolutivas
com Backpropagation para ler os dígitos “manuscritos” (assunto do próximo capítulo). Este sistema foi
usado para ler o número de cheques manuscritos.

Fig6 – Os pioneiros da Inteligência Artificial. Da esquerda para a direita: Yann LeCun, Geoffrey
Hinton, Yoshua Bengio e Andrew Ng
 

Porém, tivemos neste período o que ficou conhecido como segundo Inverno da IA, que ocorreu entre 1985-
1990, que também afetou pesquisas em Redes Neurais e Aprendizagem Profunda. Vários indivíduos
excessivamente otimistas haviam exagerado o potencial “imediato” da Inteligência Artificial, quebrando as
expectativas e irritando os investidores. A raiva era tão intensa, que a frase Inteligência Artificial atingiu o
status de pseudociência. Felizmente, algumas pessoas continuaram trabalhando em IA e Deep Learning, e
alguns avanços significativos foram feitos. Em 1995, Dana Cortes e Vladimir Vapnik desenvolveram a
máquina de vetor de suporte ou Support Vector Machine (um sistema para mapear e reconhecer dados
semelhantes). O LSTM (Long-Short Term Memory) para redes neurais recorrentes foi desenvolvido em
1997, por Sepp Hochreiter e Juergen Schmidhuber.

O próximo passo evolutivo significativo para Deep Learning ocorreu em 1999, quando os computadores
começaram a se tornar mais rápidos no processamento de dados e GPUs (unidades de processamento de
gráfico) foram desenvolvidas. O uso de GPUs significou um salto no tempo de processamento, resultando
em um aumento das velocidades computacionais em 1000 vezes ao longo de um período de 10 anos.
Durante esse período, as redes neurais começaram a competir com máquinas de vetor de suporte. Enquanto
uma rede neural poderia ser lenta em comparação com uma máquina de vetor de suporte, as redes neurais
ofereciam melhores resultados usando os mesmos dados. As redes neurais também têm a vantagem de
continuar a melhorar à medida que mais dados de treinamento são adicionados.

Em torno do ano 2000, apareceu o problema conhecido como Vanishing Gradient. Foi descoberto que as
“características” aprendidas em camadas mais baixas não eram aprendidas pelas camadas superiores, pois
nenhum sinal de aprendizado alcançou essas camadas. Este não era um problema fundamental para todas as
redes neurais, apenas aquelas com métodos de aprendizagem baseados em gradientes. A origem do
problema acabou por ser certas funções de ativação. Uma série de funções de ativação condensavam sua
entrada, reduzindo, por sua vez, a faixa de saída de forma um tanto caótica. Isso produziu grandes áreas de
entrada mapeadas em uma faixa extremamente pequena. Nessas áreas de entrada, uma grande mudança será
reduzida a uma pequena mudança na saída, resultando em um gradiente em queda. Duas soluções utilizadas
para resolver este problema foram o pré-treino camada-a-camada e o desenvolvimento de uma memória
longa e de curto prazo.

Em 2001, um relatório de pesquisa do Grupo META (agora chamado Gartner) descreveu os desafios e
oportunidades no crescimento do volume de dados. O relatório descreveu o aumento do volume de dados e a
crescente velocidade de dados como o aumento da gama de fontes e tipos de dados. Este foi um apelo para
se preparar para a investida do Big Data, que estava apenas começando.

Em 2009, Fei-Fei Li, professora de IA em Stanford na Califórnia, lançou o ImageNet e montou uma base de
dados gratuita de mais de 14 milhões de imagens etiquetadas. Eram necessárias imagens marcadas para
“treinar” as redes neurais. A professora Li disse: “Nossa visão é que o Big Data mudará a maneira como a
aprendizagem de máquina funciona. Data drives learning.”. Ela acertou em cheio!

Até 2011, a velocidade das GPUs aumentou significativamente, possibilitando a formação de redes neurais
convolutivas “sem” o pré-treino camada por camada. Com o aumento da velocidade de computação, tornou-
se óbvio que Deep Learning tinha vantagens significativas em termos de eficiência e velocidade. Um
exemplo é a AlexNet, uma rede neural convolutiva, cuja arquitetura ganhou várias competições
internacionais durante 2011 e 2012. As unidades lineares retificadas foram usadas para melhorar a
velocidade.

Também em 2012, o Google Brain lançou os resultados de um projeto incomum conhecido como The Cat
Experiment. O projeto de espírito livre explorou as dificuldades de “aprendizagem sem supervisão”. A
Aprendizagem profunda usa “aprendizagem supervisionada”, o que significa que a rede neural convolutiva é
treinada usando dados rotulados. Usando a aprendizagem sem supervisão, uma rede neural convolucional é
alimentada com dados não marcados, e é então solicitada a busca de padrões recorrentes.
O Cat Experiment usou uma rede neural distribuída por mais de 1.000 computadores. Dez milhões de
imagens “sem etiqueta” foram tiradas aleatoriamente do YouTube, mostradas ao sistema e, em seguida, o
software de treinamento foi autorizado a ser executado. No final do treinamento, um neurônio na camada
mais alta foi encontrado para responder fortemente às imagens de gatos. Andrew Ng, o fundador do projeto,
disse: “Nós também encontramos um neurônio que respondeu fortemente aos rostos humanos”. A
aprendizagem não supervisionada continua a ser um um campo ativo de pesquisa em Aprendizagem
Profunda.

Atualmente, o processamento de Big Data e a evolução da Inteligência Artificial são ambos dependentes da
Aprendizagem Profunda. Com Deep Learning podemos construir sistemas inteligentes e estamos nos
aproximando da criação de uma IA totalmente autônoma. Isso vai gerar impacto em todas os segmentos da
sociedade e aqueles que souberem trabalhar com a tecnologia, serão os líderes desse novo mundo que se
apresenta diante de nós.

No próximo capítulo você vai começar a compreender tecnicamente como funciona a Aprendizagem
Profunda. Até o capítulo 4.
Capítulo 4 – O Neurônio, Biológico e Matemático

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.

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.

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:
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:

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.

 
Capítulo 5 – Usando Redes Neurais Para
Reconhecer Dígitos Manuscritos

O sistema visual humano é uma das maravilhas do mundo. Considere a seguinte sequência de dígitos
manuscritos:

A maioria das pessoas reconhece sem esforço esses dígitos como 504192. Essa facilidade é enganosa. Em
cada hemisfério do nosso cérebro, os seres humanos têm um córtex visual primário, também conhecido
como V1, contendo 140 milhões de neurônios, com dezenas de bilhões de conexões entre eles. E, no
entanto, a visão humana envolve não apenas V1, mas uma série inteira de cortices visuais – V2, V3, V4 e
V5 – fazendo processamento de imagem progressivamente mais complexo. Nós carregamos em nossas
cabeças um supercomputador, sintonizado pela evolução ao longo de centenas de milhões de anos, e
soberbamente adaptado para entender o mundo visual. Reconhecer os dígitos manuscritos não é fácil. Em
vez disso, nós humanos somos estupendos, surpreendentemente bons, em entender o que nossos olhos nos
mostram. Mas quase todo esse trabalho é feito inconscientemente. E, portanto, geralmente não apreciamos o
quão difícil é o problema dos nossos sistemas visuais.

A dificuldade de reconhecimento do padrão visual torna-se evidente se você tentar escrever um programa de
computador para reconhecer dígitos como os acima. O que parece fácil quando nós seres humanos fazemos,
de repente, se torna extremamente difícil. Intuições simples sobre como reconhecemos formas – “um 9 tem
um loop no topo e um curso vertical no canto inferior direito” – não é tão simples de se expressar
algoritmicamente. Quando você tenta construir essas regras de forma precisa, você se perde rapidamente em
diversas exceções, ressalvas e casos especiais. É meio desesperador.

As redes neurais abordam o problema de uma maneira diferente. A ideia é tomar uma grande quantidade de
dígitos manuscritos, conhecidos como exemplos de treinamento, e em seguida, desenvolver um sistema que
possa aprender com esses exemplos de treinamento. Em outras palavras, a rede neural usa os exemplos para
inferir automaticamente regras para o reconhecimento de dígitos manuscritos. Além disso, ao aumentar o
número de exemplos de treinamento, a rede pode aprender mais sobre a caligrafia, e assim melhorar sua
precisão. Podemos construir um reconhecedor de dígitos manuscritos melhor usando milhares, milhões ou
bilhões de exemplos de treinamento.

 
 

Ao longo dos próximos capítulos começaremos nossa jornada rumo às arquiteturas mais avançadas de Deep
Learning, desenvolvendo um programa de computador implementando uma rede neural que aprende a
reconhecer os dígitos manuscritos. O programa não usará bibliotecas de redes neurais especiais (usaremos
apenas linguagem Python). Mas este programa pode reconhecer dígitos com uma precisão de mais de 96%,
sem intervenção humana. Além disso, em capítulos posteriores, desenvolveremos ideias que podem
melhorar a precisão para mais de 99%. Na verdade, as melhores redes neurais comerciais são agora tão boas
que são usadas pelos bancos para processar cheques e por agências de correio para reconhecer endereços.

Estamos nos concentrando no reconhecimento de manuscrito porque é um excelente problema protótipo


para aprender sobre redes neurais em geral. Como um protótipo, ele atinge um ponto interessante: é
desafiador – não é tão simples reconhecer os dígitos manuscritos – mas também não é tão difícil e nem
requer uma solução extremamente complicada, ou um tremendo poder computacional. Além disso, é uma
ótima maneira de desenvolver técnicas mais avançadas, como a aprendizagem profunda. E assim, ao longo
do livro, retornaremos repetidamente ao problema do reconhecimento de dígitos manuscritos. Mais tarde, no
livro, vamos discutir como essas ideias podem ser aplicadas a outros problemas em visão computacional, e
também em reconhecimento da fala, processamento de linguagem natural e outras áreas.

Ao longo do caminho, desenvolveremos muitas ideias-chave sobre as redes neurais, incluindo dois tipos
importantes de neurônios artificiais (o perceptron e o neurônio sigmóide) e o algoritmo de aprendizagem
padrão para redes neurais, conhecido como descida estocástica do gradiente. Explicaremos porque as coisas
são feitas da maneira que elas são e na construção de sua intuição de redes neurais. Isso requer uma
discussão mais longa do que apenas apresentar a mecânica básica do que está acontecendo, mas vale a pena
para o entendimento mais profundo que você alcançará. E ao final deste livro, você terá uma boa
compreensão do que é aprendizado profundo e como isso está transformando o mundo!

Caso você não tenha conhecimento em linguagem Python, recomendamos o curso gratuito Python
Fundamentos Para Análise de Dados. Ele vai fornecer uma ótima base de tudo que você precisa para
começar a desenvolver suas redes neurais.

 
Capítulo 6 – O Perceptron – Parte 1
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):

 
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 e Google 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:

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:

 
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.

 
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:

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.

 
 

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á!

 
Capítulo 7 – O Perceptron – Parte 2

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:

 
 

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:
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.

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:

 
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.

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á.

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

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.

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).

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 anteiores. 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:

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.

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:

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!

 
Capítulo 9 – A Arquitetura das Redes Neurais

No capítulo 11 vamos desenvolver uma rede neural para classificação de dígitos manuscritos, usando
linguagem Python (caso ainda não saiba trabalhar com a linguagem, comece agora mesmo com nosso curso
online totalmente gratuito Python Fundamentos Para Análise de Dados). Mas antes, vamos compreender a
terminologia que será muito útil quando estivermos desenvolvendo nosso modelo, estudando a Arquitetura
das Redes Neurais. Suponha que tenhamos a rede abaixo:

A camada mais à esquerda nesta rede é chamada de camada de entrada e os neurônios dentro da camada são
chamados de neurônios de entrada. A camada mais à direita ou a saída contém os neurônios de saída ou,
como neste caso, um único neurônio de saída. A camada do meio é chamada de camada oculta, já que os
neurônios nessa camada não são entradas ou saídas. O termo “oculto” talvez soe um pouco misterioso – a
primeira vez que ouvi o termo, pensei que devesse ter algum significado filosófico ou matemático profundo
– mas isso realmente não significa nada mais do que “uma camada que não é entrada ou saída”. A rede
acima tem apenas uma única camada oculta, mas algumas redes possuem múltiplas camadas ocultas. Por
exemplo, a seguinte rede de quatro camadas tem duas camadas ocultas:

 
 

Tais redes de camadas múltiplas são chamados de Perceptrons Multicamadas ou MLPs (Multilayer
Perceptrons), ou seja, uma rede neural formada por Perceptrons (embora na verdade seja uma rede de
neurônios sigmóides, como veremos mais adiante).

O design das camadas de entrada e saída em uma rede geralmente é direto. Por exemplo, suponha que
estamos tentando determinar se uma imagem manuscrita representa um “9” ou não. Uma maneira natural de
projetar a rede é codificar as intensidades dos pixels da imagem nos neurônios de entrada. Se a imagem for
uma imagem em escala de cinza 64 x 64, teríamos 64 × 64 = 4.096  neurônios de entrada, com as
intensidades dimensionadas adequadamente entre 0 e 1. A camada de saída conterá apenas um único
neurônio com valores inferiores a 0,5 indicando que “a imagem de entrada não é um 9” e valores maiores
que 0,5 indicando que “a imagem de entrada é um 9”.

Embora o design das camadas de entrada e saída de uma rede neural seja frequentemente direto, pode haver
bastante variação para o design das camadas ocultas. Em particular, não é possível resumir o processo de
design das camadas ocultas com poucas regras simples. Em vez disso, pesquisadores de redes neurais
desenvolveram muitas heurísticas de design para as camadas ocultas, que ajudam as pessoas a obter o
comportamento que querem de suas redes. Conheceremos várias heurísticas de design desse tipo mais
adiante ao longo dos próximos capítulos. O design das camadas ocultas é um dos pontos cruciais em
modelos de Deep Learning.

Até agora, estamos discutindo redes neurais onde a saída de uma camada é usada como entrada para a
próxima camada. Essas redes são chamadas de redes neurais feedforward. Isso significa que não há loops na
rede – as informações sempre são alimentadas para a frente, nunca são enviadas de volta. Se tivéssemos
loops, acabaríamos com situações em que a entrada para a função σ dependeria da saída. Isso seria difícil de
entender e, portanto, não permitimos tais loops.

No entanto, existem outros modelos de redes neurais artificiais em que os circuitos de feedback são
possíveis. Esses modelos são chamados de redes neurais recorrentes. A ideia nestes modelos é ter neurônios
que disparem por algum período de tempo limitado. Disparar pode estimular outros neurônios, que podem
disparar um pouco mais tarde, também por uma duração limitada. Isso faz com que ainda mais neurônios
disparem e, ao longo do tempo, conseguimos uma cascata de disparos de neurônios. Loops não causam
problemas em tal modelo, uma vez que a saída de um neurônio afeta apenas sua entrada em algum momento
posterior, não instantaneamente.

Geralmente, as arquiteturas de redes neurais podem ser colocadas em 3 categorias específicas:


1 – Redes Neurais Feed-Forward

Estes são o tipo mais comum de rede neural em aplicações práticas. A primeira camada é a entrada e a
última camada é a saída. Se houver mais de uma camada oculta, nós as chamamos de redes neurais
“profundas” (ou Deep Learning). Esses tipos de redes neurais calculam uma série de transformações que
alteram as semelhanças entre os casos. As atividades dos neurônios em cada camada são uma função não-
linear das atividades na camada anterior.

2 – Redes Recorrentes

Estes tipos de redes neurais têm ciclos direcionados em seu grafo de conexão. Isso significa que às vezes
você pode voltar para onde você começou seguindo as setas. Eles podem ter uma dinâmica complicada e
isso pode torná-los muito difíceis de treinar. Entretanto, estes tipos são mais biologicamente realistas.

Atualmente, há muito interesse em encontrar formas eficientes de treinamento de redes recorrentes. As redes
neurais recorrentes são uma maneira muito natural de modelar dados sequenciais. Eles são equivalentes a
redes muito profundas com uma camada oculta por fatia de tempo; exceto que eles usam os mesmos pesos
em cada fatia de tempo e recebem entrada em cada fatia. Eles têm a capacidade de lembrar informações em
seu estado oculto por um longo período de tempo, mas é muito difícil treiná-las para usar esse potencial.

3 – Redes Conectadas Simetricamente

Estas são como redes recorrentes, mas as conexões entre as unidades são simétricas (elas têm o mesmo peso
em ambas as direções). As redes simétricas são muito mais fáceis de analisar do que as redes recorrentes.
Elas também são mais restritas no que podem fazer porque obedecem a uma função de energia. As redes
conectadas simetricamente sem unidades ocultas são chamadas de “Redes Hopfield”. As redes conectadas
simetricamente com unidades ocultas são chamadas de “Máquinas de Boltzmann”.

Dentre estas 3 categorias, podemos listar 10 arquiteturas principais de redes neurais:

 Redes Multilayer Perceptron


 Redes Neurais Convolucionais
 Redes Neurais Recorrentes
 Long Short-Term Memory (LSTM)
 Redes de Hopfield
 Máquinas de Boltzmann
 Deep Belief Network
 Deep Auto-Encoders
 Generative Adversarial Network
 Deep Neural Network Capsules (este é um tipo completamente novo de rede neural, lançado no final de
2017)

Quer aprender a construir essas arquiteturas de redes neurais de forma eficiente, profissional e totalmente
prática, com mini-projetos para solução de problemas do mundo real, em visão computacional,
processamento de linguagem natural, detecção de fraudes, previsão de séries temporais e muito mais? Então
confira os únicos cursos online do Brasil, 100% em português, onde você aprende tudo sobre essas
arquiteturas. Clique nos links abaixo para acessar os programas completos:  

No próximo capítulo, daremos a você uma visão geral sobre cada uma dessas 10 arquiteturas e ao longo dos
capítulos seguintes, estudaremos todas elas. Cada umas dessas arquiteturas tem sido usada para resolver
diferentes problemas e criar sistemas de Inteligência Artificial. Saber trabalhar com IA de forma eficiente,
será determinante para seu futuro profissional.
Capítulo 10 – As 10 Principais Arquiteturas de
Redes Neurais

O Aprendizado de Máquina (Machine Learning) é necessário para resolver tarefas que são muito complexas
para os humanos. Algumas tarefas são tão complexas que é impraticável, senão impossível, que os seres
humanos consigam explicar todas as nuances envolvidas. Então, em vez disso, fornecemos uma grande
quantidade de dados para um algoritmo de aprendizado de máquina e deixamos que o algoritmo funcione,
explorando esses dados e buscando um modelo que alcance o que os Cientistas de Dados e Engenheiros de
IA determinaram como objetivo. Vejamos estes dois exemplos:

 É muito difícil escrever programas que solucionem problemas como reconhecer um objeto tridimensional a
partir de um novo ponto de vista em novas condições de iluminação em uma cena desordenada. Nós não
sabemos qual programa de computador escrever porque não sabemos como ocorre o processo em nosso
cérebro. Mesmo se tivéssemos uma boa ideia sobre como fazê-lo, o programa poderia ser incrivelmente
complicado.

 É difícil escrever um programa para calcular a probabilidade de uma transação de cartão de crédito ser
fraudulenta. Pode não haver regras que sejam simples e confiáveis. Precisamos combinar um número muito
grande de regras fracas. A fraude é um alvo em movimento, mas o programa precisa continuar mudando.

É onde Machine Learning pode ser aplicado com sucesso. Em vez de escrever um programa à mão para cada
tarefa específica, nós coletamos muitos exemplos que especificam a saída correta para uma determinada
entrada. Um algoritmo de aprendizagem de máquina recebe esses exemplos e produz um programa que faz o
trabalho. O programa produzido pelo algoritmo de aprendizagem pode parecer muito diferente de um
programa típico escrito à mão. Pode conter milhões de números. Se o fizermos corretamente, o programa
funciona para novos casos (novos dados). Se os dados mudarem, o programa também pode mudar ao treinar
em novos dados. E com a redução de custos de computação (principalmente usando processamento em
nuvem), grande quantidade de dados (Big Data) e processamento paralelo em GPU, temos as condições
perfeitas para a evolução de Machine Learning. O maior problema, por incrível que pareça, será a falta de
profissionais qualificados em número suficiente para atender as demandas do mercado.

Alguns exemplos de tarefas melhor resolvidas pela aprendizagem de máquina incluem:

 Reconhecimento de padrões: objetos em cenas reais, identidades faciais ou expressões faciais, palavras
escritas ou faladas.
 Detecção de anomalias: sequências incomuns de transações de cartão de crédito, padrões incomuns de
leituras de sensores em máquinas de uma indústria têxtil.
 Previsão: preços de ações futuros ou taxas de câmbio, quais filmes uma pessoa gostaria de assistir, previsão
de vendas.

Machine Learning é um campo abrangente dentro da Inteligência Artificial. Mas uma sub-área de Machine
Learning, o Deep Learning (ou Redes Neurais Profundas), vem conseguindo resultados no estado da arte
para as tarefas acima mencionadas. Neste capítulo você encontra As 10 Principais Arquiteturas de Redes
Neurais, dentre elas as principais arquiteturas de Deep Learning.
 

1- Redes Multilayer Perceptrons

O Perceptron, conforme estudamos nos capítulos anteriores, é um algoritmo simples destinado a realizar a
classificação binária; isto é, prevê se a entrada pertence a uma determinada categoria de interesse ou não:
fraude ou não_fraude, gato ou não_gato.

Um Perceptron é um classificador linear; ou seja, é um algoritmo que classifica a entrada separando duas
categorias com uma linha reta. A entrada geralmente é um vetor de recursos x multiplicado por pesos w e
adicionado a um viés (ou bias) b. Aqui um exemplo do Perceptron: y = w * x + b. Um Perceptron produz
uma única saída com base em várias entradas de valor real, formando uma combinação linear usando os
pesos (e às vezes passando a saída através de uma função de ativação não linear).

Rosenblatt construiu um Perceptron de uma camada. Ou seja, seu algoritmo não inclui múltiplas camadas, o
que permite que as redes neurais modelem uma hierarquia de recursos. Isso impede que o Perceptron
consiga realizar classificação não linear, como a função XOR (um disparador do operador XOR quando a
entrada exibe uma característica ou outra, mas não ambas, significa “OR exclusivo” “), como Minsky e
Papert mostraram em seu livro.

Um Multilayer Perceptron (MLP) é uma rede neural artificial composta por mais de um Perceptron. Eles são
compostos por uma camada de entrada para receber o sinal, uma camada de saída que toma uma decisão ou
previsão sobre a entrada, e entre esses dois, um número arbitrário de camadas ocultas que são o verdadeiro
mecanismo computacional do MLP. MLPs com uma camada oculta são capazes de aproximar qualquer
função contínua.

O Multilayer Perceptron é uma espécie de “Hello World” da aprendizagem profunda: uma boa forma de
começar quando você está aprendendo sobre Deep Learning.

Os MLPs são frequentemente aplicados a problemas de aprendizagem supervisionados: treinam em um


conjunto de pares entrada-saída e aprendem a modelar a correlação (ou dependências) entre essas entradas e
saídas. O treinamento envolve o ajuste dos parâmetros, ou os pesos e bias, do modelo para minimizar o erro.
O backpropagation é usado para fazer os ajustes dos pesos e de bias em relação ao erro, e o próprio erro
pode ser medido de várias maneiras, inclusive pelo erro quadrático médio (MSE – Mean Squared Error).

As redes feed forward, como MLPs, são como ping-pong. Elas são principalmente envolvidas em dois
movimentos, uma constante de ida e volta.  Na passagem para a frente, o fluxo de sinal se move da camada
de entrada através das camadas ocultas para a camada de saída e a decisão da camada de saída é medida em
relação às saídas esperadas.

Na passagem para trás, usando o backpropagation e a regra da cadeia (Chain Rule), derivadas parciais da
função de erro dos vários pesos e bias são reproduzidos através do MLP. Esse ato de diferenciação nos dá
um gradiente, ao longo do qual os parâmetros podem ser ajustados à medida que movem o MLP um passo
mais perto do erro mínimo. Isso pode ser feito com qualquer algoritmo de otimização baseado em gradiente,
como descida estocástica do gradiente. A rede continua jogando aquele jogo de ping-pong até que o erro não
possa mais ser reduzido (chegou ao mínimo possível). Este estado é conhecido como convergência.

Parece muita coisa? Sim, é. Veremos esse processo em mais detalhes aqui mesmo neste livro e caso queira
aprender a construir modelos MLP para aplicações práticas, através de vídeos em português, clique aqui.

2- Redes Neurais Convolucionais

Em 1998, Yann LeCun e seus colaboradores desenvolveram um reconhecedor, realmente bom, para dígitos
manuscritos chamado LeNet. Ele usou o backpropagation em uma rede feed forward com muitas camadas
ocultas, muitos mapas de unidades replicadas em cada camada, agrupando as saídas de unidades próximas,
formando uma rede ampla que pode lidar com vários caracteres ao mesmo tempo, mesmo se eles se
sobrepõem e uma inteligente maneira de treinar um sistema completo, não apenas um reconhecedor. Mais
tarde, esta arquitetura foi formalizada sob o nome de redes neurais convolucionais.

As Redes Neurais Convolucionais (ConvNets ou CNNs) são redes neurais artificiais profundas que podem
ser usadas para classificar imagens, agrupá-las por similaridade (busca de fotos) e realizar reconhecimento
de objetos dentro de cenas. São algoritmos que podem identificar rostos, indivíduos, sinais de rua, cenouras,
ornitorrincos e muitos outros aspectos dos dados visuais.

As redes convolucionais realizam o reconhecimento óptico de caracteres (OCR) para digitalizar texto e
tornar possível o processamento de linguagem natural em documentos analógicos e manuscritos, onde as
imagens são símbolos a serem transcritos. CNNs também podem ser aplicadas a arquivos de áudio quando
estes são representados visualmente como um espectrograma. Mais recentemente, as redes convolucionais
foram aplicadas diretamente à análise de texto, bem como dados gráficos.

A eficácia das redes convolucionais no reconhecimento de imagem é uma das principais razões pelas quais o
mundo testemunhou a eficácia do aprendizado profundo. Este tipo de rede está impulsionando grandes
avanços em Visão Computacional, que tem aplicações óbvias em carros autônomos, robótica, drones,
segurança, diagnósticos médicos e tratamentos para deficientes visuais.

As redes convolucionais ingerem e processam imagens como tensores e tensores são matrizes de números
com várias dimensões. Eles podem ser difíceis de visualizar, então vamos abordá-los por analogia. Um
escalar é apenas um número, como 7; um vetor é uma lista de números (por exemplo, [7,8,9]); e uma matriz
é uma grade retangular de números que ocupam várias linhas e colunas como uma planilha.
Geometricamente, se um escalar é um ponto de dimensão zero, então um vetor é uma linha unidimensional,
uma matriz é um plano bidimensional, uma pilha de matrizes é um cubo tridimensional e quando cada
elemento dessas matrizes tem uma pilha de mapas de recursos ligados a ele, você entra na quarta dimensão.
Calma, não se desespere (ainda). Veremos isso mais a frente com calma, quando estudarmos exclusivamente
esta arquitetura. Em nossos cursos na Data Science Academy incluímos aulas completas sobre Álgebra
Linear, onde escalares, vetores, matrizes e tensores são estudados na teoria e prática, pois este conhecimento
é fundamental na construção de redes neurais profundas.

A primeira coisa a saber sobre redes convolucionais é que elas não percebem imagens como os humanos.
Portanto, você terá que pensar de uma maneira diferente sobre o que uma imagem significa quando é
alimentada e processada por uma rede convolucional.

As redes convolucionais percebem imagens como volumes; isto é, objetos tridimensionais, em vez de
estruturas planas a serem medidas apenas por largura e altura. Isso porque as imagens de cores digitais têm
uma codificação vermelho-verde-azul (RGB – Red-Green-Blue), misturando essas três cores para produzir o
espectro de cores que os seres humanos percebem. Uma rede convolucional recebe imagens como três
estratos separados de cores empilhados um em cima do outro.

Assim, uma rede convolucional recebe uma imagem como uma caixa retangular cuja largura e altura são
medidas pelo número de pixels ao longo dessas dimensões e cuja profundidade é de três camadas profundas,
uma para cada letra em RGB. Essas camadas de profundidade são referidas como canais.

À medida que as imagens se movem através de uma rede convolucional, descrevemos em termos de
volumes de entrada e saída, expressando-as matematicamente como matrizes de múltiplas dimensões dessa
forma: 30x30x3. De camada em camada, suas dimensões mudam à medida que atravessam a rede neural
convolucional até gerar uma série de probabilidades na camada de saída, sendo uma probabilidade para cada
possível classe de saída. Aquela com maior probabilidade, será a classe definida para a imagem de entrada,
um pássaro por exemplo.

Você precisará prestar muita atenção às medidas de cada dimensão do volume da imagem, porque elas são a
base das operações de álgebra linear usadas para processar imagens. Poderíamos dedicar dois capítulos
inteiros somente a esta arquitetura. Aliás, é o que faremos mais à frente aqui no livro e o que já fazemos na
prática aqui.

3- Redes Neurais Recorrentes

As redes recorrentes são um poderoso conjunto de algoritmos de redes neurais artificiais especialmente úteis
para o processamento de dados sequenciais, como som, dados de séries temporais ou linguagem natural.
Uma versão de redes recorrentes foi usada pelo DeepMind no projeto de videogames com agentes
autônomos.

As redes recorrentes diferem das redes feed forward porque incluem um loop de feedback, pelo qual a saída
do passo n-1 é alimentada de volta à rede para afetar o resultado do passo n, e assim por diante para cada
etapa subsequente. Por exemplo, se uma rede é exposta a uma palavra letra por letra, e é solicitado a
adivinhar cada letra a seguir, a primeira letra de uma palavra ajudará a determinar o que uma rede recorrente
pensa que a segunda letra pode ser.

Isso difere de uma rede feed forward, que aprende a classificar cada número manuscrito por exemplo,
independentemente, e de acordo com os pixels de que é exposto a partir de um único exemplo, sem se referir
ao exemplo anterior para ajustar suas previsões. As redes feed forward aceitam uma entrada por vez e
produzem uma saída. As redes recorrentes não enfrentam a mesma restrição um-para-um.

Embora algumas formas de dados, como imagens, não pareçam ser sequenciais, elas podem ser entendidas
como sequências quando alimentadas em uma rede recorrente. Considere uma imagem de uma palavra
manuscrita. Assim como as redes recorrentes processam a escrita manual, convertendo cada imagem em
uma letra e usando o início de uma palavra para adivinhar como essa palavra terminará, então as redes
podem tratar parte de qualquer imagem como letras em uma sequência. Uma rede neural que percorre uma
imagem grande pode aprender a partir de cada região, o que as regiões vizinhas, são mais prováveis de ser.

As redes recorrentes e as redes feed forward “lembram” algo sobre o mundo, modelando os dados que estão
expostos. Mas elas se lembram de maneiras muito diferentes. Após o treinamento, a rede feed forward
produz um modelo estático dos dados e esse modelo pode então aceitar novos exemplos e classificá-los ou
agrupá-los com precisão.

Em contraste, as redes recorrentes produzem modelos dinâmicos – ou seja, modelos que mudam ao longo do
tempo – de formas que produzem classificações precisas dependentes do contexto dos exemplos que estão
expostos.

Para ser preciso, um modelo recorrente inclui o estado oculto que determinou a classificação anterior em
uma série. Em cada etapa subsequente, esse estado oculto é combinado com os dados de entrada do novo
passo para produzir a) um novo estado oculto e, em seguida, b) uma nova classificação. Cada estado oculto é
reciclado para produzir seu sucessor modificado.

As memórias humanas também são conscientes do contexto, reciclando a consciência de estados anteriores
para interpretar corretamente novos dados. Por exemplo, vamos considerar dois indivíduos. Um está ciente
de que ele está perto da casa de Bob. O outro está ciente de que entrou em um avião. Eles interpretarão os
sons “Oi Bob!” de duas formas muito diferentes, precisamente porque retém um estado oculto afetado por
suas memórias de curto prazo e sensações precedentes.

Diferentes lembranças de curto prazo devem ser recontadas em momentos diferentes, a fim de atribuir o
significado certo à entrada atual. Algumas dessas memórias terão sido forjadas recentemente e outras
memórias terão forjado muitos passos antes de serem necessários. A rede recorrente que efetivamente
associa memórias e entrada remota no tempo é chamada de Memória de Longo Prazo (LSTM), a qual
veremos em seguida.

4- Long Short-Term Memory (LSTM)

Em meados dos anos 90, a proposta dos pesquisadores alemães Sepp Hochreiter e Juergen Schmidhuber
apresentou uma variação da rede recorrente com as chamadas unidades de Long Short-Term Memory, como
uma solução para o problema do vanishing gradient, problema comum em redes neurais recorrentes.

Os LSTMs ajudam a preservar o erro que pode ser copiado por tempo e camadas. Ao manter um erro mais
constante, eles permitem que as redes recorrentes continuem aprendendo durante vários passos de tempo
(mais de 1000), abrindo assim um canal para vincular causas e efeitos remotamente. Este é um dos desafios
centrais para a aprendizagem de máquina e a IA, uma vez que os algoritmos são frequentemente
confrontados por ambientes onde os sinais de recompensa são escassos e atrasados, como a própria vida. (Os
pensadores religiosos abordaram este mesmo problema com ideias de karma ou recompensas divinas,
teorizando consequências invisíveis e distantes para nossas ações).

Os LSTMs contêm informações fora do fluxo normal da rede recorrente em uma célula fechada. As
informações podem ser armazenadas, escritas ou lidas a partir de uma célula, como dados na memória de um
computador. A célula toma decisões sobre o que armazenar, e quando permitir leituras, gravações e
exclusões, através de portões abertos e fechados. Ao contrário do armazenamento digital em computadores,
no entanto, esses portões são analógicos, implementados com a multiplicação de elementos por sigmóides,
que estão todos na faixa de 0-1. Analógico tem a vantagem sobre o digital de ser diferenciável e, portanto,
adequado para backpropagation.

Esses portões atuam sobre os sinais que recebem e, de forma semelhante aos nós da rede neural, eles
bloqueiam ou transmitem informações com base em sua força e importação, que eles filtram com seus
próprios conjuntos de pesos. Esses pesos, como os pesos que modulam a entrada e estados ocultos, são
ajustados através do processo de aprendizagem das redes recorrentes. Ou seja, as células aprendem quando
permitir que os dados entrem, saiam ou sejam excluídos através do processo iterativo de fazer suposições,
calculando o erro durante o backpropagation e ajustando pesos através da descida do gradiente.

O diagrama abaixo ilustra como os dados fluem através de uma célula de memória e são controlados por
seus portões.
 

Os LSTM’s possuem muitas aplicações práticas, incluindo processamento de linguagem natural, geração
automática de texto e análise de séries temporais. Caso queira ver esses exemplos na prática, clique aqui.
Teremos um capítulo inteiro dedicado aos LSTM’s aqui no livro.

5- Redes de Hopfield

Redes recorrentes de unidades não lineares geralmente são muito difíceis de analisar. Elas podem se
comportar de muitas maneiras diferentes: se estabelecer em um estado estável, oscilar ou seguir trajetórias
caóticas que não podem ser preditas no futuro. Uma Rede Hopfield é composta por unidades de limite
binário com conexões recorrentes entre elas. Em 1982, John Hopfield percebeu que, se as conexões são
simétricas, existe uma função de energia global. Cada “configuração” binária de toda a rede possui energia,
enquanto a regra de decisão do limite binário faz com que a rede se conforme com um mínimo desta função
de energia. Uma excelente maneira de usar esse tipo de computação é usar memórias como energia mínima
para a rede neural. Usar mínimos de energia para representar memórias resulta em uma memória
endereçável ao conteúdo. Um item pode ser acessado por apenas conhecer parte do seu conteúdo. É robusto
contra danos no hardware.

 
 

Cada vez que memorizamos uma configuração, esperamos criar um novo mínimo de energia. Mas e se dois
mínimos próximos estão em um local intermediário? Isso limita a capacidade de uma Rede Hopfield. Então,
como aumentamos a capacidade de uma Rede Hopfield? Os físicos adoram a ideia de que a matemática que
eles já conhecem pode explicar como o cérebro funciona. Muitos artigos foram publicados em revistas de
física sobre Redes Hopfield e sua capacidade de armazenamento. Eventualmente, Elizabeth Gardner
descobriu que havia uma regra de armazenamento muito melhor que usa a capacidade total dos pesos. Em
vez de tentar armazenar vetores de uma só vez, ela percorreu o conjunto de treinamento muitas vezes e usou
o procedimento de convergência Perceptron para treinar cada unidade para ter o estado correto, dado os
estados de todas as outras unidades nesse vetor. Os estatísticos chamam essa técnica de “pseudo-
probabilidade”.

Existe outro papel computacional para as Redes Hopfield. Em vez de usar a rede para armazenar memórias,
usamos para construir interpretações de entrada sensorial. A entrada é representada pelas unidades visíveis, a
interpretação é representada pelos estados das unidades ocultas e o erro da interpretação é representado pela
energia.

6- Máquinas de Boltzmann

Uma Máquina de Boltzmann é um tipo de rede neural recorrente estocástica. Pode ser visto como a
contrapartida estocástica e generativa das Redes Hopfield. Foi uma das primeiras redes neurais capazes de
aprender representações internas e é capaz de representar e resolver problemas combinatórios difíceis.

O objetivo do aprendizado do algoritmo da Máquina de Boltzmann é maximizar o produto das


probabilidades que a Máquina de Boltzmann atribui aos vetores binários no conjunto de treinamento. Isso
equivale a maximizar a soma das probabilidades de log que a Máquina de Boltzmann atribui aos vetores de
treinamento. Também é equivalente a maximizar a probabilidade de obtermos exatamente os N casos de
treinamento se fizéssemos o seguinte: 1) Deixar a rede se estabelecer em sua distribuição estacionária no
tempo N diferente, sem entrada externa e 2) Mudar o vetor visível uma vez em cada passada.
Um procedimento eficiente de aprendizado de mini-lote foi proposto para as Máquinas de Boltzmann por
Salakhutdinov e Hinton em 2012.

Em uma Máquina de Boltzmann geral, as atualizações estocásticas de unidades precisam ser sequenciais.
Existe uma arquitetura especial que permite alternar atualizações paralelas que são muito mais eficientes
(sem conexões dentro de uma camada, sem conexões de camada ignorada). Este procedimento de mini-lote
torna as atualizações da Máquina de Boltzmann mais paralelas. Isso é chamado de Deep Boltzmann
Machines (DBM), uma Máquina de Boltzmann geral, mas com muitas conexões ausentes.

Em 2014, Salakhutdinov e Hinton apresentaram outra atualização para seu modelo, chamando-o de
Máquinas Boltzmann Restritas. Elas restringem a conectividade para facilitar a inferência e a aprendizagem
(apenas uma camada de unidades escondidas e sem conexões entre unidades ocultas). Em um RBM, é
preciso apenas um passo para alcançar o equilíbrio.

7- Deep Belief Network

O backpropagation é considerado o método padrão em redes neurais artificiais para calcular a contribuição
de erro de cada neurônio após processar um lote de dados (teremos um capítulo inteiro sobre isso). No
entanto, existem alguns problemas importantes no backpropagation. Em primeiro lugar, requer dados de
treinamento rotulados; enquanto quase todos os dados estão sem rótulos. Em segundo lugar, o tempo de
aprendizagem não escala bem, o que significa que é muito lento em redes com múltiplas camadas ocultas.
Em terceiro lugar, pode ficar preso em um “local optima”. Portanto, para redes profundas, o
backpropagation está longe de ser ótimo.

Para superar as limitações do backpropagation, os pesquisadores consideraram o uso de abordagens de


aprendizado sem supervisão. Isso ajuda a manter a eficiência e a simplicidade de usar um método de
gradiente para ajustar os pesos, mas também usá-lo para modelar a estrutura da entrada sensorial. Em
particular, eles ajustam os pesos para maximizar a probabilidade de um modelo gerador ter gerado a entrada
sensorial. A questão é que tipo de modelo generativo devemos aprender? Pode ser um modelo baseado em
energia como uma Máquina de Boltzmann? Ou um modelo causal feito de neurônios? Ou um híbrido dos
dois?
 

Uma Deep Belief Network pode ser definida como uma pilha de Máquinas de Boltzmann Restritas (RBM –
Restricted Boltzmann Machines), em que cada camada RBM se comunica com as camadas anterior e
posterior. Os nós de qualquer camada única não se comunicam lateralmente.

Esta pilha de RBMs pode terminar com uma camada Softmax para criar um classificador, ou simplesmente
pode ajudar a agrupar dados não gravados em um cenário de aprendizado sem supervisão.

Com a exceção das camadas inicial e final, cada camada em uma Deep Belief Network tem uma função
dupla: ela serve como a camada oculta para os nós que vem antes, e como a camada de entrada (ou
“visível”) para a nós que vem depois. É uma rede construída de redes de camada única.

As Deep Belief Networks são usadas para reconhecer, agrupar e gerar imagens, sequências de vídeos e
dados de captura de movimento. Outra aplicação das Deep Belief Networks é no Processamento de
Linguagem Natural. Esse tipo de rede foi apresentado por Geoff Hinton e seus alunos em 2006.

8- Deep Auto-Encoders

Um Deep Auto-Encoder é composto por duas redes simétricas Deep Belief que tipicamente têm quatro ou
cinco camadas rasas que representam a metade da codificação (encoder) da rede e o segundo conjunto de
quatro ou cinco camadas que compõem a metade da decodificação (decoder).

As camadas são Máquinas de Boltzmann Restritas, os blocos de construção das Deep Belief Networks, com
várias peculiaridades que discutiremos abaixo. Aqui está um esquema simplificado da estrutura de um Deep
Auto-Encoder:

 
 

Os Deep Auto-Encoders são uma maneira muito agradável de reduzir a dimensionalidade não linear devido
a alguns motivos: eles fornecem mapeamentos flexíveis em ambos os sentidos. O tempo de aprendizagem é
linear (ou melhor) no número de casos de treinamento. E o modelo de codificação final é bastante compacto
e rápido. No entanto, pode ser muito difícil otimizar Deep Auto-Encoders usando backpropagation. Com
pequenos pesos iniciais, o gradiente do backpropagation morre. Mas temos maneiras de otimizá-las, usando
o pré-treinamento camada-por-camada sem supervisão ou apenas inicializando os pesos com cuidado.

Os Deep Auto-Encoders são úteis na modelagem de tópicos ou modelagem estatística de tópicos abstratos
que são distribuídos em uma coleção de documentos. Isso, por sua vez, é um passo importante em sistemas
de perguntas e respostas como o IBM Watson.

Em resumo, cada documento em uma coleção é convertido em um Bag-of-Words (ou seja, um conjunto de
contagens de palavras) e essas contagens de palavras são dimensionadas para decimais entre 0 e 1, o que
pode ser pensado como a probabilidade de uma palavra ocorrer no documento.

As contagens de palavras em escala são então alimentadas em uma Deep Belief Network, uma pilha de
Máquinas de Boltzmann Restritas, que elas mesmas são apenas um subconjunto de Autoencoders. Essas
Deep Belief Networks, ou DBNs, comprimem cada documento para um conjunto de 10 números através de
uma série de transformações sigmóides que o mapeiam no espaço de recursos.

O conjunto de números de cada documento, ou vetor, é então introduzido no mesmo espaço vetorial, e sua
distância de qualquer outro vetor de documento medido. Em termos aproximados, os vetores de documentos
próximos se enquadram no mesmo tópico. Por exemplo, um documento poderia ser a “pergunta” e outros
poderiam ser as “respostas”, uma combinação que o software faria usando medidas de espaço vetorial.

Em resumo, existem agora muitas maneiras diferentes de fazer pré-treinamento camada-por-camada de


recursos. Para conjuntos de dados que não possuem um grande número de casos rotulados, o pré-
treinamento ajuda a aprendizagem discriminativa subsequente. Para conjuntos de dados muito grandes e
rotulados, não é necessário inicializar os pesos utilizados na aprendizagem supervisionada usando pré-
treinamento não supervisionado, mesmo para redes profundas. O pré-treinamento foi o primeiro bom
caminho para inicializar os pesos para redes profundas, mas agora existem outras formas. Mas se
construímos redes muito maiores, precisaremos de pré-treinamento novamente! Se quiser aprender a
construir Deep Auto-Encoders em Python, clique aqui.

9- Generative Adversarial Network

As Generative Adversarial Networks (GANs) são arquiteturas de redes neurais profundas compostas por
duas redes, colocando uma contra a outra (daí o nome, “adversária”).

Os GANs foram introduzidos em um artigo de Ian Goodfellow e outros pesquisadores da Universidade de


Montreal no Canadá, incluindo Yoshua Bengio, em 2014. Referindo-se aos GANs, o diretor de pesquisa de
IA do Facebook, Yann LeCun, chamou de treinamento adversário “a ideia mais interessante nos últimos 10
anos em Machine Learning”.

O potencial de GANs é enorme, porque eles podem aprender a imitar qualquer distribuição de dados. Ou
seja, os GANs podem ser ensinados a criar mundos estranhamente semelhantes aos nossos em qualquer
domínio: imagens, música, fala, prosa. Eles são artistas robôs em um sentido, e sua produção é
impressionante – até mesmo pungente.

Para entender os GANs, você deve saber como os algoritmos geradores funcionam, e para isso, contrastá-los
com algoritmos discriminatórios é útil. Os algoritmos discriminatórios tentam classificar dados de entrada;
isto é, dados os recursos de uma instância de dados, eles predizem um rótulo ou categoria a que esses dados
pertencem.

Por exemplo, tendo em conta todas as palavras em um e-mail, um algoritmo discriminatório pode prever se a
mensagem é spam ou not_spam. O spam é um dos rótulos, e o saco de palavras (Bag of Words) coletadas do
e-mail são os recursos que constituem os dados de entrada. Quando este problema é expresso
matematicamente, o rótulo é chamado y e os recursos são chamados de x. A formulação p (y | x) é usada
para significar “a probabilidade de y dado x”, que neste caso seria traduzido para “a probabilidade de um
email ser spam com as palavras que contém”.

Portanto, algoritmos discriminatórios mapeiam recursos para rótulos. Eles estão preocupados apenas com
essa correlação. Uma maneira de pensar sobre algoritmos generativos é que eles fazem o contrário. Em vez
de prever um rótulo com determinados recursos, eles tentam prever os recursos com um determinado rótulo.

A questão que um algoritmo gerador tenta responder é: assumir que este e-mail é spam, qual a probabilidade
dos recursos? Enquanto os modelos discriminativos se preocupam com a relação entre y e x, os modelos
generativos se preocupam com “como você obtém x”. Eles permitem que você capture p (x | y), a
probabilidade de x dado y, ou a probabilidade de características oferecidas em uma classe . (Dito isto, os
algoritmos geradores também podem ser usados como classificadores, embora eles podem fazer mais do que
categorizar dados de entrada.)

Outra maneira de pensar sobre isso é distinguir discriminativo de gerador assim:

 Modelos discriminativos aprendem o limite entre as classes


 Modelos generativos modelam a distribuição de classes individuais

 
 

Uma rede neural, chamada de gerador, gera novas instâncias de dados, enquanto a outra, o discriminador, as
avalia por autenticidade; ou seja, o discriminador decide se cada instância de dados que revisa pertence ao
conjunto de dados de treinamento real ou não.

Digamos que estamos tentando fazer algo mais banal do que imitar a Mona Lisa. Vamos gerar números
escritos à mão como os encontrados no conjunto de dados MNIST, que é retirado do mundo real. O objetivo
do discriminador, quando mostrado uma instância do verdadeiro conjunto de dados MNIST, é reconhecê-los
como autênticos.

Enquanto isso, o gerador está criando novas imagens que passa para o discriminador. Isso acontece com a
esperança de que eles, também, sejam considerados autênticos, embora sejam falsos. O objetivo do gerador
é gerar dígitos escritos à mão por si mesmo. O objetivo do discriminador é identificar as imagens
provenientes do gerador como falsas.

Aqui estão os passos que um GAN realiza:

 O gerador recebe números aleatórios e retorna uma imagem.


 Essa imagem gerada é alimentada no discriminador ao lado de um fluxo de imagens tiradas do conjunto de
dados real.
 O discriminador assume imagens reais e falsas e retorna probabilidades, um número entre 0 e 1, com 1
representando uma previsão de autenticidade e 0 representando falsas.

Então você tem um loop de feedback duplo:

 O discriminador está em um loop de feedback com as imagens verdadeiras, que conhecemos.


 O gerador está em um loop de feedback com o discriminador.

Quer aprender como construir GANs, uma das arquiteturas mais incríveis de Deep Learning, 100% em
português e 100% online, para gerar imagens de forma automática? Clique aqui.

 
10- Deep Neural Network Capsules

No final de 2017, Geoffrey Hinton e sua equipe publicaram dois artigos que introduziram um novo tipo de
rede neural chamada Capsules. Além disso, a equipe publicou um algoritmo, denominado roteamento
dinâmico entre cápsulas, que permite treinar essa rede.

Para todos na comunidade de Deep Learning, esta é uma grande notícia, e por várias razões. Em primeiro
lugar, Hinton é um dos fundadores do Deep Learning e um inventor de inúmeros modelos e algoritmos que
hoje são amplamente utilizados. Em segundo lugar, esses artigos apresentam algo completamente novo, e
isso é muito emocionante porque provavelmente estimulará a onda adicional de pesquisas e aplicativos
muito inovadores.

As Capsules introduzem um novo bloco de construção que pode ser usado na aprendizagem profunda para
modelar melhor as relações hierárquicas dentro da representação do conhecimento interno de uma rede
neural. A intuição por trás deles é muito simples e elegante.

Hinton e sua equipe propuseram uma maneira de treinar essa rede composta de cápsulas e treinou-a com
êxito em um conjunto de dados simples, alcançando desempenho de ponta. Isso é muito encorajador. No
entanto, há desafios. As implementações atuais são muito mais lentas do que outros modelos modernos de
aprendizado profundo. O tempo mostrará se as redes Capsules podem ser treinadas de forma rápida e
eficiente. Além disso, precisamos ver se elas funcionam bem em conjuntos de dados mais difíceis e em
diferentes domínios.

Em qualquer caso, a rede Capsule é um modelo muito interessante e já funcionando, que definitivamente se


desenvolverá ao longo do tempo e contribuirá para uma maior expansão de aplicações de aprendizagem
profunda.

Incluímos as Capsules entre as 10 principais arquiteturas de redes neurais, pois elas representam a inovação
e o avanço na incrível e vibrante área de Deep Learning e sistemas de Inteligência Artificial. Profissionais
que realmente desejem abraçar IA como carreira, devem estar atentos aos movimentos e inovações na área.

Esta não é uma lista definitiva de arquiteturas e existem outras, tais como Word2Vec, Doc2vec, Neural
Embeddings e variações das arquiteturas aqui apresentadas, como Denoising Autoencoders, Variational
Autoencoders, além de outras categorias como Deep Reinforcement Learning. Exatamente para auxiliar
aqueles que buscam conhecimento de ponta 100% em português e 100% online, que nós criamos a
Formação Inteligência Artificial, o único programa do Brasil completo, com todas as ferramentas que o
aluno precisa para aprender a trabalhar com IA de forma eficiente. O aluno aprende programação paralela
em GPU, Deep Learning e seus frameworks, estuda as principais arquiteturas com aplicações práticas e
desenvolve aplicações de Visão Computacional e Processamento de Linguagem Natural. Clique aqui e veja
mais detalhes sobre o programa.

Isso conclui a primeira parte deste livro, com uma introdução ao universo do Deep Learning. No próximo
capítulo começaremos a ver as redes neurais em ação. Até lá.
Capítulo 11 – Design De Uma Rede Neural Para
Reconhecimento de Dígitos

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 com Python versão 3.6.x. Acesse o capítulo 1 do curso gratuito Python
Fundamentos Para Análise de Dados, para aprender como instalar o Anaconda.

 
Capítulo 12 – Aprendizado Com a Descida do
Gradiente

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 de Custo Quadrático


 

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:

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 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 saida (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. Mas caso você queira aprender em detalhes como tudo isso funciona e criar
seus modelos usando linguagens R, Python, Scala ou Java, para aplicações comerciais, confira: Machine
Learning, Machine Learning com Scala e Spark, Deep Learning e Análise Preditiva com Machine Learning
em Java.

Tenho certeza que você está ansioso para criar e treinar sua primeira rede neural. Então, não perca o
próximo capítulo!
Capítulo 13 – Construindo Uma Rede Neural Com
Linguagem Python

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 (versão 3.6.x) 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 Python Fundamentos Para Análise de Dados.

Usaremos Python 3 e os scripts podem ser encontrados no repositório do livro no GitHub. 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 é:

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.

 
 

No programa completo disponível no Github você encontra comentários explicando como ocorre todo o
processo. 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á.
Capítulo 14 – Algoritmo Backpropagation Parte 1
– Grafos Computacionais e Chain Rule

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.

 
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:

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:

 
 

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

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:

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.

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:

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:

É 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.

 
 

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ó.

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:

 
 

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.

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:
 

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!

A Data Science Academy oferece um programa completo, onde esses e vários outros conceitos são
estudados em detalhes e com várias aplicações práticas e usando TensorFlow. A Formação Inteligência
Artificial é composta de 9 cursos, tudo 100% online e 100% em português, que aliam teoria e prática na
medida certa, com aplicações reais de Inteligência Artificial. Confira o programa completo dos
cursos: Formação Inteligência Artificial.
Capítulo 15 – Algoritmo Backpropagation Parte 2
– Treinamento de Redes Neurais

O backpropagation é indiscutivelmente o algoritmo mais importante na história das redes neurais – sem
backpropagation (eficiente), seria 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 aprendizagem
profunda. Neste capítulo, vamos compreender como o backpropagation é usado no treinamento das redes
neurais: Algoritmo Backpropagation Parte 2 – Treinamento de Redes Neurais.

O algoritmo de backpropagation consiste em duas fases:

1. O passo para frente (forward pass), onde nossas entradas são passadas através da rede e as previsões de
saída obtidas (essa etapa também é conhecida como fase de propagação).

2. O passo para trás (backward pass), onde calculamos o gradiente da função de perda na camada final (ou
seja, camada de previsão) da rede e usamos esse gradiente para aplicar recursivamente a regra da cadeia
(chain rule) para atualizar os pesos em nossa rede (etapa também conhecida como fase de atualização de
pesos ou retro-propagação).

Vamos analisar cada uma dessas fases e compreender como funciona o backpropagation no treinamento nas
redes neurais. No próximo capítulo, voltaremos ao script em Python para compreender como é a
implementação do algoritmo. Let’s begin!

 
Forward Pass

O propósito do passo para frente é propagar nossas entradas (os dados de entrada) através da rede aplicando
uma série de dot products (multiplicação entre os vetores) e ativações até chegarmos à camada de saída da
rede (ou seja, nossas previsões). Para visualizar esse processo, vamos primeiro considerar a tabela abaixo.
Podemos ver que cada entrada X na matriz é 2-dim (2 dimensões), onde cada ponto de dado é representado
por dois números. Por exemplo, o primeiro ponto de dado é representado pelo vetor de recursos (0, 0), o
segundo ponto de dado por (0, 1), etc. Em seguida, temos nossos valores de saída Y como a coluna da
direita. Nossos valores de saída são os rótulos de classe. Dada uma entrada da matriz, nosso objetivo é
prever corretamente o valor de saída desejado. Em resumo, X representa as entradas e Y a saída.

 
 

Para obter uma precisão de classificação perfeita nesse problema, precisamos de uma rede neural
feedforward com pelo menos uma camada oculta. Podemos então começar com uma arquitetura de 2-2-1
conforme a imagem abaixo.

Este é um bom começo, no entanto, estamos esquecendo de incluir o bias. Existem duas maneiras de incluir
o bias b em nossa rede. Nós podemos:

1. Usar uma variável separada.


2. Tratar o bias como um parâmetro treinável dentro da matriz, inserindo uma coluna de 1s nos vetores de
recursos.
Inserir uma coluna de 1s no nosso vetor de recursos é feito de forma programática, mas para garantir a
didática, vamos atualizar nossa matriz para ver isso explicitamente, conforme tabela abaixo. Como você
pode ver, uma coluna de 1s foi adicionada aos nossos vetores de recursos. Na prática você pode inserir essa
coluna em qualquer lugar que desejar, mas normalmente a colocamos como a primeira entrada no vetor de
recursos ou a última entrada no vetor de recursos.

Como nós mudamos o tamanho do nosso vetor de recursos de entrada (normalmente o que é realizado
dentro da implementação da rede em si, para que não seja necessário modificar explicitamente a nossa
matriz), isso muda nossa arquitetura de rede de 2-2-1 para uma arquitetura 3-3-1, conforme imagem
abaixo. Ainda nos referimos a essa arquitetura de rede como 2-2-1, mas quando se trata de implementação,
na verdade, é 3-3-1 devido à adição do termo de bias incorporado na matriz.
Finalmente, lembre-se de que tanto nossa camada de entrada quanto todas as camadas ocultas exigem um
termo de bias. No entanto, a camada de saída final não requer um bias. O bias agora é um parâmetro
treinável dentro da matriz de peso, tornando o treinamento mais eficiente e substancialmente mais fácil de
implementar. Para ver o forward pass em ação, primeiro inicializamos os pesos em nossa rede, conforme
figura abaixo. Observe como cada seta na matriz de peso tem um valor associado a ela – esse é o valor de
peso atual para um determinado nó e significa o valor em que uma determinada entrada é amplificada ou
diminuída. Este valor de peso será então atualizado durante a fase de backpropgation (lembre-se que ainda
estamos no forward pass). Existem várias formas de inicializar o vetor de pesos e isso pode influenciar
diretamente no treinamento da rede, como veremos mais abaixo.

Na extrema esquerda da figura abaixo, apresentamos o vetor de recursos (0, 1, 1) e também o valor de saída
1 para a rede, pois depois precisamos calcular os erros de previsão. Aqui podemos ver que 0,1 e 1 foram
atribuídos aos três nós de entrada na rede. Para propagar os valores através da rede e obter a classificação
final, nós precisamos do dot product entre as entradas e os valores de peso, seguido pela aplicação de um
função de ativação (neste caso, a função sigmóide s). Vamos calcular as entradas para os três nós nas
camadas ocultas:

1. s ((0 x 0.351) + (1 x 1.076) + (1 x 1.116)) = 0.899

2. s ((0x 0.097) + (1 x 0.165)+(1x 0.542)) = 0.593

3. s ((0x 0.457) + (1 x 0.165)+(1x 0.331)) = 0.378

 
 

Observando os valores dos nós das camadas ocultas (camadas do meio), podemos ver que os nós foram
atualizados para refletir nossa computação. Agora temos nossas entradas para os nós da camada oculta. Para
calcular a previsão de saída, uma vez mais usamos o dot product seguido por uma ativação sigmóide:

s ((0.899 x 0.383) + (0.593 x – 0.327) + (0.378 x -0.329)) = 0.506

A saída da rede é, portanto, 0.506. Podemos aplicar uma função de etapa (step function) para determinar se a
saída é a classificação correta ou não:

 
 

Aplicando a step function com saida = 0.506, vemos que nossa rede prevê 1 que é, de fato, o rótulo de classe
correto. No entanto, a nossa rede não está muito confiante neste rótulo de classe. O valor previsto 0.506 está
muito próximo do limite da etapa. Idealmente, esta previsão deve ser mais próxima de 0.98 ou 0.99.,
implicando que a nossa rede realmente aprendeu o padrão no conjunto de dados. Para que nossa rede
realmente “aprenda”, precisamos aplicar o backpropagation.

Backpropagation

Para qualquer problema de aprendizagem supervisionada, nós selecionamos pesos que fornecem a
estimativa ótima de uma função que modela nossos dados de treinamento. Em outras palavras, queremos
encontrar um conjunto de pesos W que minimize a saída de J(W), onde J(W) é a função de perda, ou o erro
da rede. Nos capítulos anteriores, discutimos o algoritmo de gradiente descendente, em que atualizamos
cada peso por alguma redução escalar negativa da derivada do erro em relação a esse peso. Se optarmos por
usar gradiente descendente (ou quase qualquer outro algoritmo de otimização convexo), precisamos
encontrar as derivadas na forma numérica.

O objetivo do backpropagation é otimizar os pesos para que a rede neural possa aprender a mapear
corretamente as entradas para as saídas.

Para outros algoritmos de aprendizado de máquina, como regressão logística ou regressão linear, o cálculo
das derivadas é uma aplicação elementar de diferenciação. Isso ocorre porque as saídas desses modelos são
apenas as entradas multiplicadas por alguns pesos escolhidos e, no máximo, alimentados por uma única
função de ativação (a função sigmóide na regressão logística). O mesmo, no entanto, não pode ser dito para
redes neurais. Para demonstrar isso, aqui está um diagrama de uma rede neural de dupla camada:

Como você pode ver, cada neurônio é uma função do anterior conectado a ele. Em outras palavras, se
alguém alterasse o valor de w1, os neurônios “hidden 1” e “hidden 2” (e, finalmente, a saída) mudariam.
Devido a essa noção de dependências funcionais, podemos formular matematicamente a saída como uma
função composta extensiva:

 
 

ou simplesmente:

Para aplicar o algoritmo de backpropagation, nossa função de ativação deve ser diferenciável, de modo que
possamos calcular a derivada parcial do erro em relação a um dado peso wi,j, loss(E), saída de nó oj e saída
de rede j.

Aqui, a saída é uma função composta dos pesos, entradas e função (ou funções) de ativação. É importante
perceber que as unidades / nós ocultos são simplesmente cálculos intermediários que, na realidade, podem
ser reduzidos a cálculos da camada de entrada. Se fôssemos então tirar a derivada da função com relação a
algum peso arbitrário (por exemplo, w1), aplicaríamos iterativamente a regra da cadeia (da qual eu tenho
certeza que você se lembra do capítulo anterior). O resultado seria semelhante ao seguinte:

Agora, vamos anexar mais uma operação à cauda da nossa rede neural. Esta operação irá calcular e retornar
o erro – usando a função de custo – da nossa saída:

 
Tudo o que fizemos foi adicionar outra dependência funcional; nosso erro é agora uma função da saída e,
portanto, uma função da entrada, pesos e função de ativação. Se fôssemos calcular a derivada do erro com
qualquer peso arbitrário (novamente, escolheríamos w1), o resultado seria:

Cada uma dessas derivações pode ser simplificada, uma vez que escolhemos uma função de ativação e erro,
de modo que todo o resultado represente um valor numérico. Nesse ponto, qualquer abstração foi removida e
a derivada de erro pode ser usada na descida do gradiente (como discutido anteriormente aqui no livro) para
melhorar iterativamente o peso. Calculamos as derivadas de erro w.r.t. para todos os outros pesos na rede e
aplicamos gradiente descendente da mesma maneira. Isso é backpropagation – simplesmente o cálculo de
derivadas que são alimentadas para um algoritmo de otimização convexa. Chamamos isso de
“retropropagação” porque estamos usando o erro de saída para atualizar os pesos, tomando passos iterativos
usando a regra da cadeia até que alcancemos o valor de peso ideal.

Depois de compreender o funcionamento do algoritmo backpropagation, você percebe sua simplicidade.


Claro, a aritmética/cálculos reais podem ser difíceis, mas esse processo é tratado pelos nossos computadores.
Na realidade, o backpropagation é apenas uma aplicação da regra da cadeia (chain rule). Como as redes
neurais são estruturas de modelo de aprendizado de máquina multicamadas complicadas, cada peso
“contribui” para o erro geral de uma maneira mais complexa e, portanto, as derivadas reais exigem muito
esforço para serem produzidas. No entanto, uma vez que passamos pelo cálculo, o backpropagation das
redes neurais é equivalente à descida de gradiente típica para regressão logística / linear.

Assim, como regra geral de atualizações de peso, podemos usar a Regra Delta (Delta Rule):

Novo Peso = Peso Antigo – Derivada * Taxa de Aprendizagem

A taxa de aprendizagem (learning rate) é introduzida como uma constante (geralmente muito pequena), a
fim de forçar o peso a ser atualizado de forma suave e lenta (para evitar grandes passos e comportamento
caótico).

Para validar esta equação:

 Se a Derivada for positiva, isso significa que um aumento no peso aumentará o erro, portanto, o novo peso
deverá ser menor.
 Se a Derivada é negativa, isso significa que um aumento no peso diminuirá o erro, portanto, precisamos
aumentar os pesos.
 Se a Derivada é 0, significa que estamos em um mínimo estável. Assim, nenhuma atualização nos pesos é
necessária -> chegamos a um estado estável.

Existem vários métodos de atualização de peso. Esses métodos são frequentemente chamados de
otimizadores. A regra delta é a mais simples e intuitiva, no entanto, possui várias desvantagens. Confira nas
referências ao final do capítulo, um excelente artigo sobre otimizadores.
Como atualizamos os pesos com uma pequena etapa delta de cada vez, serão necessárias várias iterações
para ocorrer o aprendizado. Na rede neural, após cada iteração, a força de descida do gradiente atualiza os
pesos para um valor cada vez menor da função de perda global. A atualização de peso na rede neural é
guiada pela força do gradiente descendente sobre o erro.

Quantas iterações são necessárias para convergir (ou seja, alcançar uma função de perda mínima global)?
Isso vai depender de diversos fatores:

 Depende de quão forte é a taxa de aprendizado que estamos aplicando. Alta taxa de aprendizado significa
aprendizado mais rápido, mas com maior chance de instabilidade.
 Depende também dos hyperparâmetros da rede (quantas camadas, quão complexas são as funções não-
lineares, etc..). Quanto mais variáveis, mais leva tempo para convergir, mas a precisão tende a ser maior.
 Depende do uso do método de otimização, pois algumas regras de atualização de peso são
comprovadamente mais rápidas do que outras.
 Depende da inicialização aleatória da rede. Talvez com alguma sorte você inicie a rede com pesos quase
ideais e esteja a apenas um passo da solução ideal. Mas o contrário também pode ocorrer.
 Depende da qualidade do conjunto de treinamento. Se a entrada e a saída não tiverem correlação entre si, a
rede neural não fará mágica e não poderá aprender uma correlação aleatória.

Ou seja, treinar uma rede neural não é tarefa simples. Imagine agora treinar uma rede profunda, com várias
camadas intermediárias e milhões ou mesmo bilhões de pontos de dados e você compreende o quão
trabalhoso isso pode ser e quantas decisões devem ser tomadas pelo Cientista de Dados ou Engenheiro de
IA. E aprender a trabalhar de forma profissional, requer tempo, dedicação e preparo e melhor ainda se isso
puder ser 100% em português para acelerar seu aprendizado. Construir aplicações de IA é uma habilidade
com demanda cada vez maior no mercado.

Pensando nisso, a Data Science Academy oferece um programa completo, onde esses e vários outros
conceitos são estudados em detalhes e com várias aplicações práticas, usando TensorFlow. A Formação
Inteligência Artificial é composta de 9 cursos, tudo 100% online e 100% em português, que aliam teoria e
prática na medida certa, com aplicações reais de Inteligência Artificial. Confira o programa completo dos
cursos: Formação Inteligência Artificial. Várias empresas em todo Brasil já estão treinando seus
profissionais conosco! Venha fazer parte da revolução da IA.

Agora que você já compreende como funciona o backpropagation, podemos retornar ao código Python e ver
tudo isso funcionando na prática. Mas isso é assunto para o próximo capítulo!
Capítulo 16 – Algoritmo Backpropagation em
Python

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). O arquivo com o
código completo pode ser encontrado no repositório do livro no Github.

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:

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:

 
 

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

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!
Capítulo 17 – Cross-Entropy Cost Function

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:

 
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.

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:

 
 

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:

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:

 
 

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 σ:

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:

 
 

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:

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:

 
 

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

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:

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:

 
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:

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:

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:

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.

Pensando nisso, a Data Science Academy oferece um programa completo, onde esses e vários outros
conceitos são estudados em detalhes e com várias aplicações práticas, usando TensorFlow. A Formação
Inteligência Artificial é composta de 9 cursos, tudo 100% online e 100% em português, que aliam teoria e
prática na medida certa, com aplicações reais de Inteligência Artificial. Confira o programa completo dos
cursos: Formação Inteligência Artificial. Várias empresas em todo Brasil já estão treinando seus
profissionais conosco! Venha fazer parte da revolução da IA.

Até o próximo capítulo!


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

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
no repositório do livro no Github.

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:

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!
Capítulo 19 – Overfitting e Regularização – Parte
1

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 (o código você encontra no repositório do curso no
Github):

 
 

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):

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:
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:
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:
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:

 
 

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.
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 (fica claro agora porque Big Data está revolucionando a Ciência de
Dados?). 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á!
Capítulo 20 – Overfitting e Regularização – Parte
2

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:

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:

Equação 2
 

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

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:

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:

Equação 5

A regra de aprendizado para os pesos se torna:

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:

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:

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 Python Fundamentos Para Análise de Dados – Capítulo 2).

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.
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:

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

 
 

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:
Obtemos os resultados:

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.

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á!
Capítulo 21 – Afinal, Por Que a Regularização
Ajuda a Reduzir o Overfitting?

Vimos no capítulo anterior que a regularização ajuda a reduzir o overfitting. Isso é encorajador, mas,
infelizmente, não é óbvio porque a regularização ajuda a resolver o overfitting! Uma história padrão que as
pessoas contam para explicar o que está acontecendo segue mais ou menos esse raciocínio: pesos menores
são, em certo sentido, de menor complexidade e, portanto, fornecem uma explicação mais simples e mais
poderosa para os dados e devem, normalmente, ser preferidos. É uma história bastante concisa e contém
vários elementos que talvez pareçam dúbios ou mistificadores. Vamos descompactar essa explicação e
examiná-la criticamente. Afinal, Por Que a Regularização Ajuda a Reduzir o Overfitting?

Para fazer isso, vamos supor que temos um conjunto de dados simples para o qual desejamos construir um
modelo:

Implicitamente, estamos estudando algum fenômeno do mundo real aqui, com x e y representando dados
desse fenômeno. Nosso objetivo é construir um modelo que nos permita prever y como uma função de x
(isso é o que fazemos em Machine Learning). Poderíamos tentar usar redes neurais para construir esse
modelo, mas vou fazer algo ainda mais simples: vou tentar modelar y como um polinômio em x. Estou
fazendo isso em vez de usar redes neurais porque usar polinômios tornará as coisas particularmente
transparentes. Uma vez que tenhamos entendido o caso polinomial, vamos traduzir para redes neurais.
Há dez pontos no gráfico acima, o que significa que podemos encontrar um único polinômio de 9ª ordem y =
a0x9 + a1x8 +… + a9 que se ajusta exatamente aos dados. Aqui está o gráfico desse polinômio:

Isso fornece um ajuste exato. Mas também podemos obter um bom ajuste usando o modelo linear y = 2x:

 
 

Qual destes é o melhor modelo? E qual modelo é mais provável de generalizar bem a outros exemplos do
mesmo fenômeno do mundo real?

Essas são questões difíceis. De fato, não podemos determinar com certeza a resposta para qualquer uma das
perguntas acima, sem muito mais informações sobre o fenômeno do mundo real que estamos analisando (é
onde entra a experiência do Cientista de Dados sobre áreas de negócio). Mas vamos considerar duas
possibilidades: (1) o polinômio de 9ª ordem é, de fato, o modelo que realmente descreve o fenômeno do
mundo real, e o modelo, portanto, generalizará perfeitamente; (2) o modelo correto é y = 2x, mas há um
pequeno ruído adicional devido a, digamos, erros de medição, e é por isso que o modelo não é um ajuste
exato.

Não é possível a priori dizer qual dessas duas possibilidades está correta (ou, na verdade, se alguma terceira
possibilidade é válida). Logicamente, qualquer uma poderia ser verdade. E não é uma diferença trivial. É
verdade que nos dados fornecidos há apenas uma pequena diferença entre os dois modelos. Mas suponha
que queremos predizer o valor de y correspondendo a um grande valor de x, muito maior do que qualquer
um mostrado nos gráficos acima. Se tentarmos fazer isso, haverá uma diferença dramática entre as previsões
dos dois modelos, já que o modelo polinomial de 9ª ordem passa a ser dominado pelo termo x9, enquanto o
modelo linear permanece, bem, linear.

Um ponto de vista é dizer que, na ciência, devemos seguir a explicação mais simples, a menos que sejamos
obrigados a fazer o contrário. Quando encontramos um modelo simples que parece explicar muitos dados,
somos tentados a gritar “Eureka!” Afinal, parece improvável que uma explicação simples ocorra apenas por
coincidência. Em vez disso, suspeitamos que o modelo deve estar expressando alguma verdade subjacente
sobre o fenômeno. No caso em questão, o modelo y = 2x + ruído parece muito mais simples que y = a0x9 +
a1x8 +…. Seria surpreendente se essa simplicidade tivesse ocorrido por acaso, e então suspeitamos que y =
2x + ruído expressa alguma verdade subjacente. Nesse ponto de vista, o modelo de 9ª ordem está realmente
aprendendo apenas os efeitos do ruído local. E assim, enquanto o modelo de 9ª ordem funciona
perfeitamente para esses pontos de dados particulares, o modelo não conseguirá generalizar para outros
pontos de dados, e o modelo linear terá maior poder preditivo.

Vamos ver o que esse ponto de vista significa para redes neurais. Suponha que nossa rede tenha, na maioria
das vezes, pequenos pesos, como tenderá a acontecer em uma rede regularizada. O tamanho menor dos
pesos significa que o comportamento da rede não mudará muito se alterarmos algumas entradas aleatórias
aqui e ali. Isso dificulta que uma rede regularizada aprenda os efeitos do ruído local nos dados. Pense nisso
como uma maneira de fazer com que as evidências não importem muito para a saída da rede. Em vez disso,
uma rede regularizada aprende a responder a tipos de evidências que são vistas com frequência em todo o
conjunto de treinamento. Por outro lado, uma rede com grandes pesos pode alterar bastante seu
comportamento em resposta a pequenas alterações na entrada. Assim, uma rede não regularizada pode usar
grandes pesos para aprender um modelo complexo que contém muitas informações sobre o ruído nos dados
de treinamento. Em suma, as redes regularizadas são levadas a construir modelos relativamente simples
baseados em padrões vistos frequentemente nos dados de treinamento e são resistentes às peculiaridades de
aprendizagem do ruído nos dados de treinamento. A esperança é que isso forçará nossas redes a aprender de
verdade sobre o fenômeno em questão e a generalizar melhor o que aprendem.

Com isso dito, e mantendo a necessidade de cautela em mente, é um fato empírico que as redes neurais
regularizadas geralmente generalizam melhor do que as redes não regularizadas. A verdade é que ninguém
ainda desenvolveu uma explicação teórica inteiramente convincente para explicar porque a regularização
ajuda a generalizar as redes. De fato, os pesquisadores continuam a escrever artigos nos quais tentam
abordagens diferentes à regularização, comparam-nas para ver qual funciona melhor e tentam entender por
que diferentes abordagens funcionam melhor ou pior. Embora muitas vezes ajude, não temos uma
compreensão sistemática inteiramente satisfatória do que está acontecendo, apenas heurísticas incompletas e
regras gerais.

Há um conjunto mais profundo de questões aqui, questões que vão para o coração da ciência. É a questão de
como generalizamos. A regularização pode nos dar uma varinha mágica computacional que ajuda nossas
redes a generalizar melhor, mas não nos dá uma compreensão baseada em princípios de como a
generalização funciona, nem de qual é a melhor abordagem.

Isso é particularmente irritante porque na vida cotidiana, nós humanos generalizamos bem. Mostradas
apenas algumas imagens de um elefante, uma criança aprenderá rapidamente a reconhecer outros elefantes.
É claro que eles podem ocasionalmente cometer erros, talvez confundindo um rinoceronte com um elefante,
mas em geral esse processo funciona notavelmente com precisão. Então nós temos um sistema – o cérebro
humano – com um grande número de parâmetros livres. E depois de ser mostrado apenas uma ou algumas
imagens de treinamento, o sistema aprende a generalizar para outras imagens. Nossos cérebros estão, em
certo sentido, se regularizando incrivelmente bem! Como fazemos isso? Neste ponto não sabemos. Espero
que nos próximos anos desenvolvamos técnicas mais poderosas de regularização em redes neurais artificiais,
técnicas que permitirão que as redes neurais generalizem bem, mesmo a partir de pequenos conjuntos de
dados.

De fato, nossas redes já generalizam melhor do que se poderia esperar a priori. Uma rede com 100 neurônios
ocultos tem quase 80.000 parâmetros. Temos apenas 50.000 imagens em nossos dados de treinamento. É
como tentar encaixar um polinômio de grau 80.000 em 50.000 pontos de dados. Consequentemente, nossa
rede deve se ajustar muito bem. E, no entanto, como vimos anteriormente, essa rede realmente faz um ótimo
trabalho generalizando. Por que esse é o caso? Não é bem entendido. Foi conjecturado que “a dinâmica do
aprendizado de gradiente descendente em redes multicamadas tem um efeito de ‘autorregulação'”. Isso é
excepcionalmente bom, mas também é um tanto inquietante que não entendemos porque exatamente isso
ocorre e por isso muitas vezes modelos de redes neurais profundas são chamados de “caixa preta”. Enquanto
isso, adotaremos a abordagem pragmática e usaremos a regularização sempre que pudermos. Nossas redes
neurais serão melhores assim.

Deixe-me concluir esta seção voltando a um detalhe que deixei inexplicado antes: o fato de que a
regularização L2 não restringe os vieses. É claro que seria fácil modificar o procedimento de regularização
para regularizar os vieses. Empiricamente, fazendo isso muitas vezes não muda muito os resultados, então,
em certa medida, é apenas uma convenção se regularizar os vieses ou não. No entanto, vale a pena notar que
ter um grande viés não torna um neurônio sensível às suas entradas da mesma maneira que ter pesos
grandes. Portanto, não precisamos nos preocupar com grandes vieses que permitem que nossa rede aprenda
o ruído em nossos dados de treinamento. Ao mesmo tempo, permitir grandes vieses dá às nossas redes mais
flexibilidade no comportamento – em particular, grandes vieses facilitam a saturação dos neurônios, o que às
vezes é desejável. Por essas razões, geralmente não incluímos termos de viés quando regularizamos a rede
neural.

Você já percebeu que regularização é um assunto importante quando tratamos de redes neurais. Nos
próximos capítulos estudaremos mais duas técnicas de regularização: Regularização L1 e Dropout! Não
perca!
Capítulo 22 – Regularização L1

Existem muitas técnicas de regularização além da Regularização L2 que vimos no capítulo anterior. De fato,
tantas técnicas foram desenvolvidas que é difícil resumir todas elas. Neste e nos próximos dois capítulos,
vamos descrever brevemente três outras abordagens para reduzir o overfitting: Regularização L1, Dropout e
aumento artificial do tamanho do conjunto de treinamento. Não aprofundaremos tanto nessas técnicas como
fizemos com a Regularização L2. Em vez disso, o objetivo é familiarizar você com as ideias principais e
apreciar a diversidade de técnicas de regularização disponíveis.

Regularização L1

Nesta abordagem, modificamos a função de custo não regularizada, adicionando a soma dos valores
absolutos dos pesos:

Equação 1

Intuitivamente, isso é semelhante à Regularização L2, penalizando grandes pesos e tendendo a fazer com
que a rede prefira pequenos pesos. Naturalmente, o termo de Regularização L1 não é o mesmo que o termo
de Regularização L2 e, portanto, não devemos esperar obter exatamente o mesmo comportamento. Vamos
tentar entender como o comportamento de uma rede treinada usando a Regularização L1 difere de uma rede
treinada usando a Regularização L2.

Para fazer isso, vejamos as derivadas parciais da função de custo. A partir da fórmula anterior obtemos:

 
Equação 2

onde sgn(w) é o sinal de w, isto é, +1 se w é positivo e −1 se w é negativo. Usando essa expressão, podemos
facilmente modificar a retropropagação (backpropagation) para fazer a descida de gradiente estocástica
usando a Regularização L1. A regra de atualização resultante para uma rede regularizada L1 é:

Equação 3 – Regra de atualização L1

onde, como de costume, podemos estimar ∂C0/∂w usando uma média de mini-lote, se desejarmos. Compare
isso com a regra de atualização para a Regularização L2:

Equação 4 – Regra de atualização L2

Em ambas as expressões, o efeito da regularização é diminuir os pesos. Isso está de acordo com a nossa
intuição de que ambos os tipos de regularização penalizam grandes pesos. Mas a maneira como os pesos
diminuem é diferente. Na Regularização L1, os pesos diminuem em uma quantidade constante para 0. Na
Regularização L2, os pesos diminuem em um valor proporcional a w. E assim, quando um peso específico
tem uma grande magnitude, a Regularização L1 reduz o peso muito menos do que a Regularização L2. Em
contraste, quando |w| é pequena, a Regularização L1 reduz o peso muito mais do que a Regularização L2. O
resultado é que a Regularização L1 tende a concentrar o peso da rede em um número relativamente pequeno
de conexões de alta importância, enquanto os outros pesos são direcionados para zero.

Mas há ainda um pequeno detalhe na discussão acima. A derivada parcial ∂C/∂w não é definida quando w =
0. A razão é que a função |w| tem um “canto” agudo em w = 0 e, portanto, não é diferenciável nesse ponto.
Tudo bem, no entanto. O que faremos é aplicar a regra usual (não regularizada) para descida de gradiente
estocástica quando w = 0. Isso ajuda a resolver a questão – intuitivamente, o efeito da regularização é
diminuir os pesos e, obviamente, não pode reduzir um peso que já é 0. Para colocá-lo com mais precisão,
usaremos as Equações (2) e (3) com a convenção que sgn(0) = 0. Isso dá uma regra legal e compacta para se
fazer uma descida gradiente estocástica com Regularização L1.

Agora vamos para o Dropout, no próximo capítulo!


Capítulo 23 – Como Funciona o Dropout?

Dropout é uma técnica radicalmente diferente para regularização. Ao contrário da Regularização L1 e L2, o
Dropout não depende da modificação da função de custo. Em vez disso, no Dropout, modificamos a própria
rede. Deixe-me descrever a mecânica básica de Como Funciona o Dropout? antes de entender porque ele
funciona e quais são os resultados. Suponha que estamos tentando treinar uma rede neural:

Em particular, suponha que tenhamos uma entrada de treinamento x e a saída desejada correspondente y.
Normalmente, nós treinamos pela propagação direta de x através da rede, e depois retrocedemos
(retropropagação) para determinar a contribuição do erro para o gradiente. Com o Dropout, esse processo é
modificado. Começamos por eliminar aleatoriamente (e temporariamente) alguns dos neurônios ocultos na
rede, deixando os neurônios de entrada e saída intocados. Depois de fazer isso, terminaremos com uma rede
da seguinte forma (observe as linhas tracejadas na figura abaixo). Note os neurônios que foram
temporariamente eliminados:

 
 

Nós encaminhamos para frente a entrada x através da rede modificada, e depois retropropagamos o
resultado, também através da rede modificada. Depois de fazer isso em um mini-lote de exemplos,
atualizamos os pesos e vieses apropriados. Em seguida, repetimos o processo, primeiro restaurando os
neurônios removidos, depois escolhendo um novo subconjunto aleatório de neurônios ocultos para excluir,
estimando o gradiente para um mini-lote diferente e atualizando os pesos e vieses na rede.

Ao repetir esse processo várias vezes, nossa rede aprenderá um conjunto de pesos e vieses. Naturalmente,
esses pesos e vieses terão sido aprendidos sob condições em que parte dos neurônios ocultos foram
descartados. Quando realmente executamos a rede completa, isso significa que mais neurônios ocultos
estarão ativos. Para compensar isso, reduzimos pela metade os pesos que saem dos neurônios ocultos.

Esse procedimento de desistência pode parecer estranho e ad-hoc. Por que esperamos que ajude com a
regularização? Para explicar o que está acontecendo, gostaria que você parasse brevemente de pensar sobre
o Dropout e, em vez disso, imagine o treinamento de redes neurais no modo padrão (sem Dropout). Em
particular, imagine que treinamos várias redes neurais diferentes, todas usando os mesmos dados de
treinamento. É claro que as redes podem não começar idênticas e, como resultado, após o treinamento, elas
podem, às vezes, dar resultados diferentes. Quando isso acontece, podemos usar algum tipo de esquema de
média ou votação para decidir qual saída aceitar. Por exemplo, se nós treinamos cinco redes, e três delas
estão classificando um dígito como um “3”, então provavelmente é um “3”. As outras duas redes
provavelmente estão cometendo um erro. Este tipo de esquema de média é frequentemente encontrado como
uma maneira poderosa (embora requeira mais capacidade computacional) de reduzir o overfitting. A razão é
que as diferentes redes podem se sobrepor de diferentes maneiras e a média pode ajudar a eliminar esse tipo
de overfitting.

O que isso tem a ver com o Dropout? Heuristicamente, quando abandonamos diferentes conjuntos de
neurônios, é como se estivéssemos treinando redes neurais diferentes. E assim, o procedimento de
eliminação é como calcular a média dos efeitos de um grande número de redes diferentes. As diferentes
redes se adaptarão de diferentes maneiras, e assim, esperançosamente, o efeito líquido do Dropout será
reduzir o overfitting.

Uma explicação heurística relacionada ao Dropout é dada em um dos primeiros artigos a usar a técnica:
“Esta técnica reduz co-adaptações complexas de neurônios, já que um neurônio não pode confiar na
presença de outros neurônios em particular. É, portanto, forçado a aprenda recursos mais robustos que são
úteis em conjunto com muitos subconjuntos aleatórios diferentes dos outros neurônios”. Em outras palavras,
se pensarmos em nossa rede como um modelo que está fazendo previsões, então podemos pensar no
Dropout como uma forma de garantir que o modelo seja robusto para a perda de qualquer evidência
individual. Nesse ponto, é um pouco semelhante à Regularização L1 e L2, que tendem a reduzir os pesos e,
assim, tornar a rede mais robusta para perder qualquer conexão individual na rede.

Naturalmente, a verdadeira medida do Dropout é que ele foi muito bem sucedido em melhorar o
desempenho das redes neurais. O artigo original, introduzindo a técnica, aplicou-a a muitas tarefas
diferentes. Para nós, é de particular interesse que eles aplicaram o Dropout na classificação de dígitos
MNIST, usando uma rede neural feedforward “vanilla” ao longo de linhas similares àquelas que estamos
considerando. O documento observou que o melhor resultado que alguém alcançou até aquele ponto usando
tal arquitetura foi a precisão de classificação de 98,4% no conjunto de testes. Eles melhoraram isso para
98,7% de precisão usando uma combinação de Dropout e uma forma modificada de Regularização L2. Da
mesma forma, resultados impressionantes foram obtidos para muitas outras tarefas, incluindo problemas de
reconhecimento de imagem e fala e processamento de linguagem natural. O Dropout tem sido especialmente
útil no treinamento de redes grandes e profundas, nas quais o problema do overfitting é frequentemente
agudo.

O Dropout é estudado em detalhes e na prática no curso Deep Learning II da Data Science Academy.

Até o próximo capítulo!


Capítulo 24 – Expandir Artificialmente os
Dados de Treinamento

Vimos anteriormente que a precisão da classificação com o dataset MNIST caiu para porcentagens em torno
de 80%, quando usamos apenas 1.000 imagens de treinamento. Não é de surpreender que isso aconteça, uma
vez que menos dados de treinamento significam que nossa rede será exposta a menos variações na forma
como os seres humanos escrevem dígitos. Vamos tentar treinar nossa rede de 30 neurônios ocultos com uma
variedade de diferentes tamanhos de conjuntos de dados de treinamento, para ver como o desempenho varia.
Nós treinaremos usando um tamanho de mini-lote de 10, uma taxa de aprendizado η = 0,5, um parâmetro de
regularização λ = 5.0 e a função de custo de entropia cruzada. Treinaremos por 30 épocas quando o conjunto
completo de dados de treinamento for usado e aumentaremos o número de épocas proporcionalmente
quando conjuntos de treinamento menores forem usados. Para garantir que o fator de decaimento do peso
(weight decay factor) permaneça o mesmo nos conjuntos de treinamento, usaremos um parâmetro de
regularização de λ = 5.0 quando o conjunto de dados de treinamento completo for usado, e reduziremos
proporcionalmente quando conjuntos de treinamento menores forem usados. Observe esse gráfico:

 
Como você pode ver analisando o gráfico acima, as precisões de classificação melhoram consideravelmente
à medida que usamos mais dados de treinamento. Presumivelmente, essa melhoria continuaria se houvesse
mais dados disponíveis. É claro que, olhando para o gráfico acima, parece que estamos chegando perto da
saturação. Suponha, no entanto, que refizemos o gráfico com o tamanho do conjunto de treinamento plotado
logaritmicamente:

Parece claro que o gráfico ainda está subindo em direção aos 100% de precisão. Isso sugere que, se
usássemos muito mais dados de treinamento – digamos, milhões ou até bilhões de amostras de dígitos
manuscritos, em vez de apenas 50.000, provavelmente teríamos um desempenho consideravelmente melhor,
mesmo nessa rede muito pequena.

Obter mais dados de treinamento é uma ótima ideia. Infelizmente, pode ser caro e nem sempre é possível na
prática. No entanto, há outra técnica que pode funcionar quase tão bem, que é expandir artificialmente os
dados de treinamento. Suponha, por exemplo, que tomemos uma imagem de treinamento MNIST, o dígito 5:

 
 

e rotacionamos por um pequeno ângulo, digamos 15 graus:

Ainda é reconhecivelmente o mesmo dígito. E ainda no nível do pixel é bem diferente de qualquer imagem
atualmente nos dados de treinamento MNIST. É possível que adicionar essa imagem aos dados de
treinamento possa ajudar nossa rede a aprender mais sobre como classificar os dígitos. Além do mais,
obviamente, não estamos limitados a adicionar apenas uma imagem. Podemos expandir nossos dados de
treinamento fazendo muitas rotações pequenas de todas as imagens de treinamento MNIST e, em seguida,
usando os dados de treinamento expandidos para melhorar o desempenho de nossa rede.

Essa técnica é muito poderosa e tem sido amplamente usada. Vejamos alguns dos resultados de
um artigo que aplicou diversas variações da técnica ao MNIST. Uma das arquiteturas de redes neurais que
eles consideraram foi similar às que estamos usando, uma rede feedforward com 800 neurônios ocultos e
usando a função de custo de entropia cruzada. Executando a rede com os dados de treinamento MNIST
padrão, eles obtiveram uma precisão de classificação de 98,4% em seu conjunto de testes. Eles então
expandiram os dados de treinamento, usando não apenas rotações, como descrevi acima, mas também
traduzindo e distorcendo as imagens. Ao treinar no conjunto de dados expandido, aumentaram a precisão de
sua rede para 98,9%. Eles também experimentaram o que chamaram de “distorções elásticas”, um tipo
especial de distorção de imagem destinada a emular as oscilações aleatórias encontradas nos músculos da
mão. Usando as distorções elásticas para expandir os dados, eles alcançaram uma precisão ainda maior,
99,3%. Efetivamente, eles estavam ampliando a experiência de sua rede, expondo-a ao tipo de variações
encontradas na caligrafia real. Caso queira aprender sobre estas técnicas, elas são estudadas em detalhes
em Visão Computacional e Reconhecimento de Imagens.

Variações sobre essa técnica podem ser usadas para melhorar o desempenho em muitas tarefas de
aprendizado, não apenas no reconhecimento de manuscrito. O princípio geral é expandir os dados de
treinamento aplicando operações que reflitam a variação do mundo real. Não é difícil pensar em maneiras de
fazer isso. Suponha, por exemplo, que você esteja construindo uma rede neural para fazer o reconhecimento
de fala. Nós humanos podemos reconhecer a fala mesmo na presença de distorções como ruído de fundo e
assim você pode expandir seus dados adicionando ruído de fundo. Também podemos reconhecer a fala se
ela estiver acelerada ou desacelerada. Então, essa é outra maneira de expandir os dados de treinamento.
Essas técnicas nem sempre são usadas – por exemplo, em vez de expandir os dados de treinamento
adicionando ruído, pode ser mais eficiente limpar a entrada para a rede aplicando primeiro um filtro de
redução de ruído. Ainda assim, vale a pena manter a ideia de expandir os dados de treinamento e buscar
oportunidades para aplicar a técnica.

Agora você compreende melhor o poder do Big Data, pois com mais dados, em maior variedade e gerados
em alta velocidade, conseguimos chegar a resultados nunca antes vistos em Inteligência Artificial. Vamos
ver novamente como a precisão da nossa rede neural varia com o tamanho do conjunto de treinamento:

Suponha que, em vez de usar uma rede neural, usemos alguma outra técnica de aprendizado de máquina
para classificar os dígitos. Por exemplo, vamos tentar usar as máquinas de vetores de suporte (SVMs). Não
se preocupe se você não estiver familiarizado com SVMs, não precisamos entender seus detalhes (caso
queira aprender sobre SVMs, elas são estudadas em detalhes em Machine Learning). Vamos usar o SVM
fornecido pela biblioteca scikit-learn. Veja como o desempenho do SVM varia em função do tamanho do
conjunto de treinamento. Eu tracei os resultados da rede neural também, para facilitar a comparação:

 
 

Provavelmente, a primeira coisa que chama a atenção sobre esse gráfico é que nossa rede neural supera o
SVM para cada tamanho de conjunto de treinamento. Isso é bom, embora tenhamos usado as configurações
prontas do SVM do scikit-learn, enquanto fizemos um bom trabalho customizando nossa rede neural. Um
fato sutil, porém interessante, sobre o gráfico é que, se treinarmos o SVM usando 50.000 imagens, ele terá
melhor desempenho (94,48% de precisão) do que a nossa rede neural quando treinado usando 5.000 imagens
(precisão de 93,24%). Em outras palavras, mais dados de treinamento podem, às vezes, compensar
diferenças no algoritmo de aprendizado de máquina usado.

Algo ainda mais interessante pode ocorrer. Suponha que estamos tentando resolver um problema usando
dois algoritmos de aprendizado de máquina, algoritmo A e algoritmo B. Às vezes acontece que o algoritmo
A superará o algoritmo B com um conjunto de dados de treinamento, enquanto o algoritmo B superará o
algoritmo A com um conjunto diferente de dados de treinamento. Não vemos isso acima – seria necessário
que os dois gráficos se cruzassem – mas a resposta correta à pergunta “O algoritmo A é melhor que o
algoritmo B?” seria: “Qual o tamanho do conjunto de dados de treinamento que você está usando?”

Tudo isso é uma precaução a ter em mente, tanto ao fazer o desenvolvimento quanto ao ler artigos de
pesquisa. Muitos artigos concentram-se em encontrar novos truques para obter melhor desempenho em
conjuntos de dados de referência padrão. “Nossa técnica XPTO nos deu uma melhoria de X por cento no
benchmark padrão Y” é uma forma canônica de alegação de pesquisa. Tais alegações são, com frequência,
genuinamente interessantes, mas devem ser entendidas como aplicáveis apenas no contexto do conjunto de
dados de treinamento específico usado. Imagine uma história alternativa na qual as pessoas que
originalmente criaram o conjunto de dados de referência tinham uma concessão de pesquisa maior. Eles
podem ter usado o dinheiro extra para coletar mais dados de treinamento. É perfeitamente possível que o
“aprimoramento” devido à técnica de “XPTO” desapareça em um conjunto maior de dados. Em outras
palavras, a suposta melhoria pode ser apenas um acidente da história. A mensagem a ser retirada,
especialmente em aplicações práticas, é que o que queremos é melhores algoritmos e melhores dados de
treinamento. Não há problema em procurar algoritmos melhores, mas certifique-se de não estar se
concentrando apenas em melhores algoritmos, excluindo a busca por mais ou melhores dados de
treinamento.

Com isso concluímos nosso mergulho no overfitting e na regularização. Claro, voltaremos novamente ao
assunto. Como já mencionamos várias vezes, o overfitting é um grande problema nas redes neurais,
especialmente à medida que os computadores se tornam mais poderosos e temos a capacidade de treinar
redes maiores. Como resultado, há uma necessidade premente de desenvolver técnicas poderosas de
regularização para reduzir o overfitting, e esta é uma área extremamente ativa de pesquisa.

No próximo capítulo vamos tratar de um outro importante assunto: a inicialização de pesos. Até lá.
Capítulo 25 – Inicialização de Pesos em Redes
Neurais Artificiais

Quando criamos nossas redes neurais, temos que fazer escolhas para os valores iniciais de pesos e vieses
(bias). Até agora, nós os escolhemos de acordo com uma prescrição que discutimos nos capítulos anteriores.
Só para lembrar, a prescrição era escolher tanto os pesos quanto os vieses usando variáveis aleatórias
Gaussianas independentes, normalizadas para ter a média 0 e desvio padrão 1 (esse é um conceito
fundamental em Estatística e caso queira adquirir conhecimento em Estatística, confira nossa mais nova
Formação: Formação Análise Estatística Para Cientistas de Dados).

Embora esta abordagem tenha funcionado bem, foi bastante ad-hoc, e vale a pena revisitar para ver se
podemos encontrar uma maneira melhor de definir nossos pesos e vieses iniciais, e talvez ajudar nossas
redes neurais a aprender mais rápido. É o que iremos estudar neste capítulo.

Para começar, vamos compreender porque podemos fazer um pouco melhor do que inicializar pesos e vieses
com valores Gaussianos normalizados. Para ver porque, suponha que estamos trabalhando com uma rede
com um grande número – digamos 1.000 – de neurônios de entrada. E vamos supor que usamos valores
Gaussianos normalizados para inicializar os pesos conectados à primeira camada oculta. Por enquanto, vou
me concentrar especificamente nos pesos que conectam os neurônios de entrada ao primeiro neurônio na
camada oculta e ignorar o restante da rede:

Vamos supor, por simplicidade, que estamos tentando treinar usando uma entrada de treinamento x na qual
metade dos neurônios de entrada estão ativados, isto é, configurados para 1, e metade dos neurônios de
entrada estão desligados, ou seja, ajustados para 0. O argumento a seguir aplica-se de forma mais geral, mas
você obterá a essência deste caso especial. Vamos considerar a soma ponderada z = ∑jwjxj + b de entradas
para nosso neurônio oculto. Ocorre que 500 termos nesta soma desaparecem, porque a entrada
correspondente xj é zero e, assim, z é uma soma sobre um total de 501 variáveis aleatórias Gaussianas
normalizadas, representando os 500 termos de peso e o termo extra de viés (bias). Logo, z é ele próprio uma
distribuição Gaussiana com média zero e desvio padrão ≈ 22.4 (raiz quadrada de 501). Ou seja, z tem uma
distribuição Gaussiana muito ampla, sem um pico agudo, conforme a figura abaixo:
 

Em particular, podemos ver neste gráfico que é bem provável que | z | será bastante grande, isto é, z > 1 ou z
< -1. Se for esse o caso, a saída σ(z) do neurônio oculto estará muito próxima de 1 ou 0. Isso significa que
nosso neurônio oculto terá saturado. E quando isso acontece, como sabemos, fazer pequenas mudanças nos
pesos fará apenas mudanças absolutamente minúsculas na ativação de nosso neurônio oculto. Essa mudança
minúscula na ativação do neurônio oculto, por sua vez, dificilmente afetará o resto dos neurônios na rede, e
veremos uma mudança minúscula correspondente na função de custo. Como resultado, esses pesos só
aprenderão muito lentamente quando usarmos o algoritmo de descida do gradiente. É semelhante ao
problema que discutimos anteriormente em outros capítulos, no qual os neurônios de saída que saturaram o
valor errado fizeram com que o aprendizado diminuísse. Abordamos esse problema anterior com uma
escolha inteligente de função de custo. Infelizmente, enquanto isso ajudou com os neurônios
de saídasaturados, ele não faz nada pelo problema dos neurônios ocultos saturados.

Temos falado sobre a entrada de pesos para a primeira camada oculta. Naturalmente, argumentos
semelhantes aplicam-se também a camadas ocultas posteriores: se os pesos em camadas ocultas posteriores
forem inicializados usando Gaussianos normalizados, então as ativações estarão frequentemente muito
próximas de 0 ou 1, e o aprendizado prosseguirá muito lentamente.

Existe alguma maneira de escolhermos melhores inicializações para os pesos e vieses, para que não
tenhamos esse tipo de saturação e, assim, evitar uma desaceleração na aprendizagem? Suponha que
tenhamos um neurônio com pesos de entrada nin. Então, inicializaremos esses pesos como variáveis
aleatórias gaussianas com média 0 e desvio padrão:

Isto é, vamos “esmagar os gaussianos”, tornando menos provável que nosso neurônio seja saturado.
Continuaremos a escolher o viés como um Gaussiano com média 0 e desvio padrão 1, por motivos pelos
quais voltaremos daqui a pouco. Com essas escolhas, a soma ponderada z = ∑jwjxj + b será novamente uma
variável aleatória Gaussiana com média 0, mas será muito mais aguda que antes. Suponha, como fizemos
anteriormente, que 500 das entradas são zero e 500 são 1. Então é fácil mostrar (veja o gráfico abaixo) que z
tem uma distribuição Gaussiana com média 0 e desvio padrão igual a 1,22…(raiz quadrada de 3/2). Isso é
muito mais agudo do que antes, tanto que até o gráfico abaixo subestima a situação, já que precisamos
redimensionar o eixo vertical, quando comparado ao gráfico anterior:

 
 

É muito menos provável que tal neurônio sature e, correspondentemente, é muito menos provável que tenha
problemas com a lentidão do aprendizado.

Eu afirmei acima que nós continuaremos a inicializar os vieses como antes, como variáveis aleatórias
Gaussianas com uma média de 0 e um desvio padrão de 1. Isto não tem problema, pois é pouco provável que
nossos neurônios vão saturar. Na verdade, não importa muito como inicializamos os vieses, desde que
evitemos o problema com a saturação dos neurônios. Algumas pessoas vão tão longe a ponto de inicializar
todos os vieses com 0, e dependem da descida de gradiente para aprender vieses apropriados. Mas como é
improvável que faça muita diferença, continuaremos com o mesmo procedimento de inicialização de antes.

Vamos comparar os resultados para as nossas abordagens antiga e nova para inicialização de peso, usando a
tarefa de classificação de dígitos MNIST. Como antes, usaremos 30 neurônios ocultos, um tamanho de mini-
lote de 10, um parâmetro de regularização λ = 5.0 e a função de custo de entropia cruzada. Diminuiremos
ligeiramente a taxa de aprendizado de η = 0,5 para 0,1, pois isso torna os resultados um pouco mais visíveis
nos gráficos. Podemos treinar usando o antigo método de inicialização de peso (o código pode ser
encontrado no repositório deste livro no Github):

 
Também podemos treinar usando a nova abordagem para inicializar o peso. Na verdade, isso é ainda mais
fácil, já que a maneira padrão de inicializar os pesos da rede2 é usar essa nova abordagem. Isso significa que
podemos omitir a chamada net.large_weight_initializer () acima:

Plotando os resultados, obtemos:

Em ambos os casos, acabamos com uma precisão de classificação um pouco acima de 96%. A precisão final
da classificação é quase exatamente a mesma nos dois casos, mas a nova técnica de inicialização é muito,
muito mais rápida. No final da primeira época de treinamento, a antiga abordagem de inicialização de peso
tem uma precisão de classificação abaixo de 87%, enquanto a nova abordagem já chega a quase 93%. O que
parece estar acontecendo é que nossa nova abordagem para a inicialização do peso nos leva a um processo
muito melhor, o que nos permite obter bons resultados muito mais rapidamente. O mesmo fenômeno
também é visto se traçarmos resultados com 100 neurônios ocultos:

 
 

Neste caso, as duas curvas não se encontram. No entanto, nossas experiências sugerem que, com apenas
mais algumas épocas de treinamento (não mostradas), as precisões se tornam quase exatamente as mesmas.
Portanto, com base nesses experimentos, parece que a inicialização do peso aprimorado apenas acelera o
aprendizado, não altera o desempenho final de nossas redes. No entanto, veremos mais a frente alguns
exemplos de redes neurais em que o comportamento de longo prazo é significativamente melhor com a
inicialização de peso usando:

Assim, não é apenas a velocidade de aprendizado que é melhorada, mas também o desempenho final.

A abordagem acima para a inicialização do peso ajuda a melhorar a maneira como nossas redes neurais
aprendem. Outras técnicas para inicialização de peso também foram propostas, muitas baseadas nessa ideia
básica. Não vamos rever as outras abordagens aqui, já que a descrita anteriormente funciona bem o
suficiente para nossos propósitos. Se você estiver interessado em pesquisar mais, recomendamos a leitura
das páginas 14 e 15 de um artigo de 2012 de Yoshua Bengio (um dos padrinhos do Deep Learning), bem
como as referências nele contidas: Practical Recommendations for Gradient-Based Training of Deep
Architectures. Nos cursos Deep Learning I e Deep Learning II esse tema também é estudado em detalhes.

Até o próximo capítulo!


Capítulo 26 – Como Escolher os
Hiperparâmetros de Uma Rede Neural

Até agora não explicamos como foram escolhidos os valores dos hiperparâmetros como a taxa de
aprendizado, η, o parâmetro de regularização, λ e assim por diante. Fornecemos valores que funcionaram
muito bem, mas, na prática, quando você está usando redes neurais para resolver um problema, pode ser
difícil encontrar bons parâmetros. Neste capítulo, começamos nosso estudo sobre Como Escolher os
Hiperparâmetros de Uma Rede Neural. Vamos começar?

Imagine, por exemplo, que acabamos de ser apresentados ao dataset MNIST e começamos a trabalhar nele,
sem saber nada sobre quais hiperparâmetros usar. Vamos supor que, por sorte, em nossos primeiros
experimentos, escolhemos muitos dos hiperparâmetros da mesma forma como foi feito nos capítulos
anteriores: 30 neurônios ocultos, um tamanho de mini-lote de 10, treinando por 30 épocas usando a entropia
cruzada. Mas escolhemos uma taxa de aprendizado η = 10.0 e o parâmetro de regularização λ = 1000.0.
Aqui está um exemplo de execução da rede (o script está disponível no repositório do livro no Github):

 
Nossas precisões de classificação não são melhores do que o acaso! Nossa rede está agindo como um
gerador de ruído aleatório!

“Bem, isso é fácil de consertar”, você pode dizer, “apenas diminua a taxa de aprendizado e os
hiperparâmetros de regularização”. Infelizmente, você não sabe a priori quais são os hiperparâmetros que
você precisa ajustar. Talvez o verdadeiro problema seja que nossa rede de neurônios ocultos nunca
funcionará bem, não importa como os outros hiperparâmetros sejam escolhidos? Talvez realmente
precisemos de pelo menos 100 neurônios ocultos? Ou 300 neurônios ocultos? Ou várias camadas ocultas?
Ou uma abordagem diferente para codificar a saída? Talvez nossa rede esteja aprendendo, mas precisamos
treinar em mais épocas? Talvez os mini-lotes sejam pequenos demais? Talvez seja melhor voltarmos para a
função de custo quadrático? Talvez precisemos tentar uma abordagem diferente para inicializar o peso? E
assim por diante. Se fosse fácil, não precisaríamos de um Cientista de Dados, não é verdade?

É fácil sentir-se perdido com tantas escolhas e combinações possíveis para os hiperparâmetros. Isso pode ser
particularmente frustrante se sua rede for muito grande ou usar muitos dados de treinamento, pois você pode
treinar por horas, dias ou semanas, apenas para não obter resultados. Se a situação persistir, prejudicará sua
confiança. Talvez as redes neurais sejam a abordagem errada para o seu problema? Talvez você devesse
largar o emprego e trabalhar com a apicultura?

Nos próximos capítulos, explicaremos algumas heurísticas que podem ser usadas para definir os
hiperparâmetros em uma rede neural. O objetivo é ajudá-lo a desenvolver um fluxo que permita que você
faça um bom trabalho definindo hiperparâmetros. Claro, não vamos cobrir tudo sobre otimização de
hiperparâmetros. Esse é um assunto enorme, e não é, de qualquer forma, um problema que já está
completamente resolvido, nem existe um acordo universal entre os profissionais sobre as estratégias corretas
a serem usadas. Há sempre mais um truque que você pode tentar para obter um pouco mais de desempenho
da sua rede. Mas temos algumas heurísticas com as quais podemos começar.

Compreendendo a Situação – Estratégia Geral


Ao usar redes neurais para atacar um novo problema, o primeiro desafio é obter qualquer aprendizado não-
trivial, ou seja, para que a rede obtenha resultados melhores que o acaso. Isso pode ser surpreendentemente
difícil, especialmente ao confrontar uma nova classe de problemas. Vejamos algumas estratégias que você
pode usar se tiver esse tipo de problema.

Suponha, por exemplo, que você esteja atacando o MNIST pela primeira vez. Você começa entusiasmado,
mas fica um pouco desanimado quando sua primeira rede falha completamente, como no exemplo acima. O
caminho a percorrer é reduzir o tamanho do problema. Livre-se de todas as imagens de treinamento e
validação, exceto imagens de 0s ou 1s. Em seguida, tente treinar uma rede para distinguir 0s de 1s. Não só
isso é um problema inerentemente mais fácil do que distinguir todos os dez dígitos, como também reduz a
quantidade de dados de treinamento em 80%, acelerando o treinamento por um fator de 5. Isso permite
experimentações muito mais rápidas e, portanto, fornece uma visão mais rápida sobre como construir uma
boa rede.

Você pode acelerar ainda mais a experimentação, desmembrando sua rede na rede mais simples,
provavelmente fazendo aprendizado significativo. Se você acredita que uma rede [784, 10] provavelmente
faz uma classificação melhor que o acaso com o dataset de dígitos MNIST, então comece sua
experimentação com essa rede. Vai ser muito mais rápido do que treinar uma rede [784, 30, 10], e você pode
“falhar” mais rápido (este é um conceito muito comum nos EUA: “fail fast”, ou seja, cometa falhas o mais
rápido possível e aprenda com elas. Não se preocupe em tentar atingir a perfeição, pois você não vai
conseguir de qualquer forma).

Você pode acelerar mais na experimentação aumentando a frequência de monitoramento. No network2.py,


monitoramos o desempenho no final de cada época de treinamento. Com 50.000 imagens por época, isso
significa esperar um pouco – cerca de dez segundos por época, no meu laptop, ao treinar uma rede [784, 30,
10] – antes de obter feedback sobre o quanto a rede está aprendendo. É claro que dez segundos não são
muito longos, mas se você quiser testar dezenas de opções de hiperparâmetros, é irritante, e se você quiser
testar centenas ou milhares de opções, isso começa a ficar debilitante. Podemos obter feedback mais
rapidamente, monitorando a precisão da validação com mais frequência, digamos, a cada 1.000 imagens de
treinamento. Além disso, em vez de usar o conjunto completo de 10.000 imagens de validação para
monitorar o desempenho, podemos obter uma estimativa muito mais rápida usando apenas 100 imagens de
validação. Tudo o que importa é que a rede veja imagens suficientes para aprender de verdade e obter uma
boa estimativa aproximada de desempenho. Claro, nosso programa network2.py atualmente não faz esse tipo
de monitoramento. Mas, como um clímax para obter um efeito semelhante para fins de ilustração, vamos
reduzir nossos dados de treinamento para apenas as primeiras 1.000 imagens de treinamento MNIST. Vamos
tentar e ver o que acontece. (Para manter o código abaixo simples, não implementei a ideia de usar apenas
imagens 0 e 1. Claro, isso pode ser feito com um pouco mais de trabalho).

Ainda estamos recebendo puro ruído! Mas há uma grande vitória: agora estamos obtendo feedback em uma
fração de segundo, em vez de uma vez a cada dez segundos. Isso significa que você pode experimentar mais
rapidamente outras opções de hiperparâmetros, ou até mesmo conduzir experimentos testando muitas opções
diferentes de hiperparâmetros quase simultaneamente.

No exemplo acima, eu deixamos λ como λ = 1000.0, como usamos anteriormente. Mas como mudamos o
número de exemplos de treinamento, deveríamos realmente mudar λ para manter weight decay o mesmo.
Isso significa mudar λ para 20.0. Se fizermos isso, então é o que acontece:

 
 

Ah! Nós temos um sinal. Não é um sinal muito bom, mas um sinal, no entanto. Isso é algo que podemos
construir, modificando os hiperparâmetros para tentar melhorar ainda mais. Talvez nós achemos que nossa
taxa de aprendizado precisa ser maior. (Como você talvez perceba, é um palpite bobo, por razões que
discutiremos em breve, mas chegaremos lá. Não existe atalho para o aprendizado). Então, para testar nosso
palpite, tentamos alterar η até 100.0:

Isso não é bom, pois sugere que nosso palpite estava errado e o problema não era que a taxa de aprendizado
fosse muito baixa. Então, em vez disso, tentamos alterar η para η = 1.0:

 
 

Agora ficou melhor! E assim podemos continuar, ajustando individualmente cada hiperparâmetro,
melhorando gradualmente o desempenho. Uma vez feita a exploração para encontrar um valor melhor para
η, seguimos para encontrar um bom valor para λ. Em seguida, experimente uma arquitetura mais complexa,
digamos uma rede com 10 neurônios ocultos e ajuste os valores para η e λ novamente. Depois, aumente para
20 neurônios ocultos e então, ajuste outros hiperparâmetros um pouco mais e assim por diante, em cada
estágio avaliando o desempenho usando nossos dados de validação e usando essas avaliações para encontrar
melhores hiperparâmetros. Ao fazer isso, normalmente leva mais tempo para testemunhar o impacto devido
a modificações dos hiperparâmetros, e assim podemos diminuir gradualmente a frequência de
monitoramento.

Tudo isso parece muito promissor como uma estratégia ampla. No entanto, quero voltar a esse estágio inicial
de encontrar hiperparâmetros que permitem que uma rede aprenda qualquer coisa. De fato, mesmo a
discussão acima transmite uma perspectiva muito positiva. Pode ser extremamente frustrante trabalhar com
uma rede que não está aprendendo nada. Você pode ajustar os hiperparâmetros por dias e ainda não obter
uma resposta significativa. Por isso, gostaria de enfatizar novamente que, durante os primeiros estágios,
você deve se certificar de que pode obter um feedback rápido dos experimentos. Intuitivamente, pode
parecer que simplificar o problema e a arquitetura apenas irá atrasá-lo. Na verdade, isso acelera as coisas,
pois você encontra muito mais rapidamente uma rede com um sinal significativo. Uma vez que você tenha
recebido tal sinal, muitas vezes você pode obter melhorias rápidas aprimorando os hiperparâmetros. Assim
como em tudo na vida, começar pode ser a coisa mais difícil a se fazer.

Ok, essa é a estratégia geral. Vamos agora olhar algumas recomendações específicas para definir
hiperparâmetros. Vou me concentrar na taxa de aprendizado, η, no parâmetro de regularização L2, λ e no
tamanho do mini-lote. No entanto, muitas das observações também se aplicam a outros hiperparâmetros,
incluindo aqueles associados à arquitetura de rede, outras formas de regularização e alguns hiperparâmetros
que encontraremos mais adiante aqui no Deep Learning Book, como o coeficiente momentum.

Oh não! O capítulo acabou! Fique tranquilo, continuamos no próximo. Até lá!


Capítulo 27 – A Taxa de Aprendizado de Uma
Rede Neural

Vamos continuar a discussão do capítulo anterior sobre a escolha dos hiperparâmetros de um modelo de rede
neural, estudando um dos mais importantes, a taxa de aprendizado.

Suponha que executemos três redes neurais artificiais sendo treinadas com o dataset MNIST com três taxas
de aprendizado diferentes, η = 0.025, η = 0.25 e η = 2.5, respectivamente. Vamos definir os outros
hiperparâmetros de acordo com as experiências nos capítulos anteriores, executando mais de 30 epochs, com
um tamanho de mini-lote de 10 e com λ = 5.0. Também voltaremos a usar todas as 50.000 imagens de
treinamento. Aqui está um gráfico mostrando o comportamento do custo de treinamento enquanto
treinamos:

Com η = 0.025, o custo diminui suavemente até a época final. Com η = 0.25 o custo inicialmente diminui,
mas após cerca de 20 épocas ele está próximo da saturação, e daí em diante a maioria das mudanças são
meramente pequenas e aparentemente oscilações aleatórias. Finalmente, com η = 2.5, o custo faz grandes
oscilações desde o início. Para entender o motivo das oscilações, lembre-se de que a descida estocástica do
gradiente supostamente nos levará gradualmente a um vale da função de custo (conforme explicado aqui):
No entanto, se η for muito grande, os passos serão tão grandes que poderão, na verdade, ultrapassar o
mínimo, fazendo com que o algoritmo simplesmente fique perdido durante o treinamento. Isso é
provavelmente o que está causando a oscilação do custo quando η = 2.5. Quando escolhemos η = 0.25, os
passos iniciais nos levam a um mínimo da função de custo, e é só quando chegamos perto desse mínimo que
começamos a sofrer com o problema de overshooting. E quando escolhemos η = 0.025, não sofremos este
problema durante as primeiras 30 épocas.

Claro, escolher η tão pequeno cria outro problema, que reduz a velocidade da descida estocástica do
gradiente, aumentando o tempo total de treinamento. Uma abordagem ainda melhor seria começar com η =
0.25, treinar por 20 épocas e então mudar para η = 0.025. Discutiremos essas tabelas de taxas de
aprendizado variáveis posteriormente. Por enquanto, porém, vamos nos ater a descobrir como encontrar um
único valor bom para a taxa de aprendizado, η.

Com esta imagem em mente, podemos definir η da seguinte maneira. Primeiro, estimamos o valor limite
para η no qual o custo nos dados de treinamento começa imediatamente a diminuir, em vez de oscilar ou
aumentar. Essa estimativa não tem que ser muito precisa. Você pode estimar a ordem de magnitude
começando com η = 0.01. Se o custo diminuir durante as primeiras épocas, então você deve sucessivamente
tentar η = 0.1, 1.0,… até encontrar um valor para η onde o custo oscile ou aumente durante as primeiras
poucas épocas (isso faz parte do trabalho de um Cientista de Dados).

Alternativamente, se o custo oscilar ou aumentar durante as primeiras épocas, quando η = 0.01, então tente η
= 0.001 ,0.0001,… até encontrar um valor para η onde o custo diminui durante as primeiras poucas épocas.
Seguindo este procedimento, obteremos uma estimativa da ordem de magnitude para o valor limite de η.
Você pode, opcionalmente, refinar sua estimativa, para escolher o maior valor de η no qual o custo diminui
durante as primeiras poucas épocas, digamos η = 0.5 ou η = 0.2 (não há necessidade de que isso seja super-
preciso). Isso nos dá uma estimativa para o valor limite de η. E claro, documente tudo!!!!

Obviamente, o valor real de η que você usa não deve ser maior que o valor limite. De fato, se o valor de η
permanecer utilizável ao longo de muitas épocas, então você provavelmente desejará usar um valor para η
que seja menor, digamos, um fator de dois abaixo do limite. Essa escolha normalmente permitirá que você
treine por muitas épocas, sem causar muita lentidão no aprendizado.
No caso dos dados MNIST, seguir esta estratégia leva a uma estimativa de 0.1 para a ordem de magnitude
do valor limite de η. Depois de um pouco mais de refinamento, obtemos um valor limite η = 0.5. Seguindo a
prescrição acima, isso sugere usar η = 0.25 como nosso valor para a taxa de aprendizado. De fato, eu
descobri que usar η = 0.5 funcionava bem o suficiente em 30 épocas que, na maioria das vezes, eu não me
preocupava em usar um valor menor de η.

Tudo isso parece bastante simples. No entanto, usar o custo de treinamento para escolher η parece
contradizer o que dissemos anteriormente, que escolheríamos os hiperparâmetros avaliando o desempenho
usando nossos dados de validação. Na verdade, usaremos a precisão de validação para escolher o
hiperparâmetro de regularização, o tamanho do mini-lote e os parâmetros de rede, como o número de
camadas e neurônios ocultos, e assim por diante (estudaremos isso nos próximos capítulos).

Por que as coisas diferem para a taxa de aprendizado? Francamente, essa escolha é uma preferência estética
pessoal e talvez seja um tanto idiossincrática. O raciocínio é que os outros hiperparâmetros são destinados a
melhorar a precisão final da classificação no conjunto de testes, e por isso faz sentido selecioná-los com base
na precisão da validação. No entanto, a taxa de aprendizado é apenas para influenciar a precisão final da
classificação. Sua finalidade principal é realmente controlar o tamanho da etapa na descida do gradiente e
monitorar o custo do treinamento é a melhor maneira de detectar se o tamanho da etapa é muito grande.
Com isso dito, essa é uma preferência pessoal. No início, durante o aprendizado, o custo do treinamento
geralmente diminui apenas se a precisão da validação melhorar e assim, na prática, é improvável que faça
muita diferença em qual critério você usa.

No próximo capítulo tem mais. Até lá!


Capítulo 28 – Usando Early Stopping Para
Definir o Número de Épocas de Treinamento

Ao treinar redes neurais, várias decisões precisam ser tomadas em relação às configurações
(hiperparâmetros) usadas, a fim de obter um bom desempenho. Um desses hiperparâmetros é o número de
épocas de treinamento: ou seja, quantas passagens completas do conjunto de dados (épocas) devem ser
usadas? Se usarmos poucas épocas, poderemos ter problemas de underfitting (ou seja, não aprender tudo o
que pudermos com os dados de treinamento); se usarmos muitas épocas, podemos ter o problema oposto,
overfitting (“aprender demais”, ou seja, ajustar o “ruído” nos dados de treinamento, e não o sinal).

Usamos o Early Stopping (“Parada Antecipada” ou “Parada Precoce”) exatamente para tentar definir
manualmente esse valor. Também pode ser considerado um tipo de método de regularização (como L1/L2
weight decay e dropout estudados anteriormente aqui no livro), pois pode impedir o overfitting da rede
neural. A imagem abaixo ajuda a definir claramente o que é o Early Stopping:

Ao treinar uma rede neural, geralmente se está interessado em obter uma rede com desempenho ideal de
generalização. No entanto, todas as arquiteturas de rede neural padrão, como o perceptron multicamada
totalmente conectado, são propensas a overfitting. Enquanto a rede parece melhorar, isto é, o erro no
conjunto de treinamento diminui, em algum momento durante o treinamento na verdade começa a piorar
novamente, ou seja, o erro em exemplos invisíveis aumenta.

Normalmente, o erro de generalização é estimado pelo erro de validação, isto é, o erro médio em um
conjunto de validação, um conjunto fixo de exemplos que não são do conjunto de treino. Existem
basicamente duas maneiras de combater o overfitting: reduzindo o número de dimensões do espaço de
parâmetros ou reduzindo o tamanho efetivo de cada dimensão. Técnicas para reduzir o número de
parâmetros são aprendizagem construtiva gananciosa, poda ou compartilhamento de peso. Técnicas para
reduzir o tamanho de cada dimensão de parâmetro são a regularização, como weight decay ou dropout, ou a
Parada Precoce (Early Stopping). A parada precoce é amplamente usada porque é simples de entender e
implementar e foi relatada como sendo superior aos métodos de regularização em muitos casos.

Usar Early Stopping significa que, no final de cada época, devemos calcular a precisão da classificação nos
dados de validação. Quando a precisão parar de melhorar, terminamos o treinamento. Isso torna a
configuração do número de épocas muito simples. Em particular, isso significa que não precisamos nos
preocupar em descobrir explicitamente como o número de épocas depende dos outros hiperparâmetros, pois
isso é feito automaticamente. Além disso, a Parada Antecipada também impede automaticamente o
overfitting. Isto é, obviamente, uma coisa boa, embora nos estágios iniciais da experimentação possa ser útil
desligar a Parada Antecipada, para que você possa ver quaisquer sinais de overfitting e usá-los para definir
sua abordagem de regularização.

Para implementar a Parada Antecipada, precisamos dizer com mais precisão o que significa que a precisão
da classificação parou de melhorar. Como já vimos, a precisão pode se mover um pouco, mesmo quando a
tendência geral é melhorar. Se pararmos pela primeira vez, a precisão diminui, então quase certamente
pararemos quando houver mais melhorias a serem feitas. Uma regra melhor é terminar se a melhor precisão
de classificação não melhorar por algum tempo. Suponha, por exemplo, que estamos trabalhando com o
dataset MNIST. Poderíamos optar por terminar se a precisão da classificação não melhorou durante as
últimas dez épocas. Isso garante que não paremos cedo demais, em resposta à má sorte no treinamento, mas
também que não estamos esperando para sempre uma melhoria que nunca acontece.

Esta regra de “parar o treinamento se não melhorar em dez épocas” é boa para a exploração inicial do
MNIST. No entanto, as redes podem às vezes estabilizar-se perto de uma determinada precisão de
classificação por algum tempo, apenas para começar a melhorar novamente. Se você está tentando obter um
desempenho realmente bom, a regra de “parar o treinamento se não melhorar em dez épocas” pode ser muito
agressiva. Nesse caso, sugerimos usar essa regra para a experimentação inicial e, gradualmente, adotar
regras mais brandas, conforme entender melhor a maneira como sua rede treina: sem melhoria em vinte
épocas, sem melhoria em cinquenta épocas e assim por diante. Claro, isso introduz um novo hiperparâmetro
para otimizar! Na prática, no entanto, geralmente é fácil definir esse hiperparâmetro para obter bons
resultados. Da mesma forma, para problemas diferentes do MNIST, a regra de não-melhoria-em-dez pode
ser agressiva demais ou não ser agressiva o suficiente, dependendo dos detalhes do problema. No entanto,
com um pouco de experimentação, geralmente é fácil encontrar uma boa estratégia para o Early Stopping.
Isso faz parte do trabalho do Cientista de Dados ou do Engenheiro de Inteligência Artificial.

Até aqui, nós não usamos o Early Stopping em nossos experimentos MNIST. A razão é que temos feito
muitas comparações entre diferentes abordagens de aprendizado. Para tais comparações, é útil usar o mesmo
número de épocas em cada caso. No entanto, vale a pena modificar o network2.py (disponível no repositório
do curso no Github) para implementar o Early Stopping, e deixaremos isso como tarefa para você. Se
precisar de ajuda, o Early Stopping é estudado em detalhes e com atividades práticas em Deep Learning na
DSA, no curso Deep Learning II.

Até o próximo capítulo!


Capítulo 29 – Definindo o Tamanho do Mini-
Batch

Quando os dados de treinamento são divididos em pequenos lotes, cada lote recebe o nome de Mini-Batch
(ou Mini-Lote). Suponha que os dados de treinamento tenham 32.000 instâncias e que o tamanho de um
Mini-Batch esteja definido como 32. Então, haverá 1.000 Mini-Batches. Mas qual deve ser o tamanho do
Mini-Batch? Isso é o que veremos neste capítulo: Definindo o Tamanho do Mini-Batch.

Mas porque usamos Mini-Batches? Digamos que você tenha cerca de 1 bilhão de dados de treinamento. Se
você decidir usar o conjunto completo de treinamento em cada época, você precisará de muita memória
RAM e armazenamento para processar esses dados, sendo bem provável que sua máquina (ou mesmo
um cluster de computadores) não tenha memória suficiente. Se você decidir usar um exemplo de
treinamento em cada época, de um bilhão de dados, de uma só vez, você está ignorando a filosofia de
vetorização e isso tornará o processo de treinamento muito mais lento.

Portanto, usamos um subconjunto de dados de treinamento (chamamos de “Mini-Batch”) de cada vez em


cada época. Isso nos permitirá manter os dois objetivos: ajustar dados suficientes na memória do
computador e manter a filosofia de vetorização ao mesmo tempo. Uma coisa importante sobre o Mini-Batch
é que, é melhor escolher o tamanho do Mini-Batch como múltiplo de 2 e os valores comuns são: 64, 128,
256 e 512. Sinta-se à vontade para usar outros valores e discutiremos mais sobre isso mais a frente aqui
mesmo neste capítulo.

Veja um exemplo: digamos que você tenha 1 bilhão de dados de treinamento. Você define seu tamanho de
Mini-Batch para, digamos, 512. Portanto, em cada época você tem 512 dados de treinamento para processar.
Esta configuração levará aproximadamente: (1.000.000.000 / 512) = 1.953.125 épocas para ser concluída.
Portanto, o tamanho do Mini-Batch é a quantidade de dados que você deseja processar em cada época.

Se atualizarmos os parâmetros do modelo após o processamento de todos os dados de treinamento (ou seja,
época), levaria muito tempo para obter uma atualização do modelo no treinamento, e os dados de
treinamento inteiros provavelmente não caberiam na memória. Se atualizarmos os parâmetros do modelo
após o processamento de cada instância (por exemplo, descida de gradiente estocástico), as atualizações do
modelo seriam demasiado ruidosas e o processo não seria computacionalmente eficiente.

Portanto, a utilização do Mini-Batch (principalmente na descida do gradiente) é introduzida como um trade-


off entre {atualizações rápidas do modelo, eficiência de memória} e {atualizações precisas do modelo,
eficiência computacional}. É trabalho do Cientista de Dados ajustar mais esse parâmetro no processo de
treinamento.

Mas Como Devemos Definir o Tamanho do Mini-


Batch?
Para responder a essa pergunta, vamos primeiro supor que estamos fazendo aprendizado on-line, ou seja,
que estamos usando um tamanho de Mini-Batch igual a 1.

A preocupação óbvia sobre o aprendizado online é que o uso de Mini-Lotes que contêm apenas um único
exemplo de treinamento causará erros significativos em nossa estimativa do gradiente. A razão é que as
estimativas graduais individuais não tem que ser super precisas. Tudo o que precisamos é de uma estimativa
precisa o suficiente para que nossa função de custo continue diminuindo. É como se você estivesse tentando
chegar ao Pólo Norte, mas tivesse uma bússola informando 10 a 20 graus cada vez que você olhasse para
ela. Desde que você pare para checar a bússola com frequência, e a bússola acerte na direção, você acabará
chegando ao Pólo Norte.

Com base nesse argumento, parece que devemos usar o aprendizado on-line. De fato, a situação acaba sendo
mais complicada do que isso. Em um problema do último capítulo, mostramos que é possível usar técnicas
de matriz para calcular a atualização de gradiente para todos os exemplos em um Mini-Lote
simultaneamente, em vez de fazer um loop sobre eles. Dependendo dos detalhes de seu hardware e da
biblioteca de álgebra linear, pode ser um pouco mais rápido calcular a estimativa de gradiente para um Mini-
Lote de (por exemplo) tamanho 100, em vez de computar a estimativa de gradiente Mini-Lote fazendo um
loop sobre os 100 exemplos de treinamento separadamente. Pode levar (digamos) apenas 50 vezes mais
tempo, em vez de 100 vezes mais tempo.

Agora, a princípio, parece que isso não nos ajuda muito. Com nosso Mini-Lote de tamanho 100, a regra de
aprendizado para os pesos se parece com:

onde a soma é sobre exemplos de treinamento no Mini-Lote. Isso é equivalente a:

para aprendizagem online. Mesmo que demore 50 vezes mais para fazer a atualização do Mini-Batch, ainda
parece ser melhor fazer o aprendizado online, porque estaríamos atualizando com muito mais frequência.
Suponha, no entanto, que no caso do Mini-Lote nós aumentemos a taxa de aprendizado por um fator 100,
então a regra de atualização se torna:

Isso é muito parecido com 100 instâncias separadas de aprendizado online com uma taxa de aprendizado de
η. Mas leva apenas 50 vezes mais tempo do que fazer uma única instância de aprendizado online.
Naturalmente, não é exatamente o mesmo que 100 instâncias de aprendizado online, já que no Mini-Lote os
∇Cxs são todos avaliados para o mesmo conjunto de pesos, ao contrário do aprendizado cumulativo que
ocorre no caso online. Ainda assim, parece claramente possível que o uso do Mini-Lote maior acelere as
coisas.

Com esses fatores em mente, escolher o melhor tamanho de Mini-Lote é um trade-off (escolha). Muito
pequeno, e você não consegue aproveitar ao máximo os benefícios de boas bibliotecas de matrizes
otimizadas para hardware veloz. Demasiado grande e você simplesmente não está atualizando seus pesos
com frequência suficiente. O que você precisa é escolher um valor que maximize a velocidade de
aprendizado. Felizmente, a escolha do tamanho do Mini-Lote no qual a velocidade é maximizada é
relativamente independente dos outros hiperparâmetros (além da arquitetura geral), portanto, você não
precisa ter otimizado esses hiperparâmetros para encontrar um bom tamanho Mini-Lote. O caminho a
percorrer é, portanto, usar alguns valores aceitáveis (mas não necessariamente ideais) para os outros
hiperparâmetros, e então testar vários tamanhos diferentes de Mini-Lotes, escalando η como fizemos no
exemplo acima. Plote a precisão da validação em relação ao tempo (como em tempo real decorrido, não em
época!) e escolha o tamanho do Mini-Lote que forneça a melhoria mais rápida no desempenho. Com o
tamanho do Mini-Lote escolhido, você pode continuar a otimizar os outros hiperparâmetros. Entendeu agora
porque Cientistas de Dados devem ser muito bem remunerados?

Claro, como você, sem dúvida, percebeu, não fizemos essa otimização em nossa rede de exemplo (que você
encontra no Github). De fato, nossa implementação não usa a abordagem mais rápida para atualizações de
Mini-Batch. Nós simplesmente usamos um tamanho de Mini-Lote de 10 sem comentários ou explicações em
quase todos os exemplos. Por causa disso, poderíamos ter acelerado o aprendizado reduzindo o tamanho do
Mini-Lote. Não fizemos isso, em parte porque queríamos ilustrar o uso de Mini-Lotes além do tamanho 1, e
em parte porque nossos experimentos preliminares sugeriam que a aceleração seria bastante modesta, uma
vez que nossa rede de exemplo é bem simples. Em implementações práticas, no entanto, certamente
implementaríamos a abordagem mais rápida para atualizações de Mini-Batch e, em seguida, faríamos um
esforço para otimizar o tamanho do Mini-Lote, a fim de maximizar nossa velocidade geral.

Nos cursos da Formação Inteligência Artificial, os alunos trabalho com Mini-Batches pois os datasets
usados são muito grandes e precisamos otimizar o tempo de treinamento. Todos os alunos da Formação tem
acesso remoto gratuito ao super servidor da DSA com duas GPUs e ensinamos como otimizar o treinamento
e usar os recursos computacionais de forma eficiente. A definição dos Mini-Batches é uma das atividades
principais. Acesse o programa completo dos cursos aqui: Formação Inteligência Artificial e comece sua
capacitação hoje mesmo.
Capítulo 30 – Variações do Stochastic Gradient
Descent – Hessian Optimization e Momentum

Cada técnica mostrada até aqui é valiosa e deve ser dominada por aqueles que pretendem trabalhar com
redes neurais artificiais e aplicações de Inteligência Artificial, mas essa não é a única razão pela qual nós as
explicamos. O ponto principal é familiarizar você com alguns dos problemas que podem ocorrer nas redes
neurais e com um estilo de análise que pode ajudar a superar esses problemas. De certo modo, aprendemos a
pensar sobre redes neurais. Agora neste capítulo, esquematizamos brevemente algumas outras técnicas.
Esses esboços são menos aprofundados do que as discussões anteriores, mas devem transmitir algum
sentimento pela diversidade de técnicas disponíveis para uso em redes neurais. Lembrando que você sempre
pode estudar todas essas técnicas em detalhes nos cursos da Formação Inteligência Artificial.

Variações do Stochastic Gradient Descent


A descida de gradiente estocástico pela retropropagação tem nos servido bem no ataque ao problema de
classificação de dígitos do dataset MNIST. No entanto, existem muitas outras abordagens para otimizar a
função de custo e, às vezes, essas outras abordagens oferecem desempenho superior ao gradiente estocástico
em mini-lote. Neste capítulo discutiremos duas dessas abordagens, Hessian Optimization e Momentum.

Hessian Optimization
Para iniciar nossa discussão, ajuda a colocar as redes neurais de lado por um tempo. Em vez disso, vamos
apenas considerar o problema abstrato de minimizar uma função de custo C que é uma função de muitas
variáveis, w = w1, w2,…, então C = C(w). Pelo teorema de Taylor, a função custo pode ser aproximada
perto de um ponto w por:

Fórmula 1

Podemos reescrever isso de forma mais compacta:


Fórmula 2

onde ∇C é o vetor gradiente usual e H é uma matriz conhecida como Matriz Hessiana. Suponha que nós
aproximemos C descartando os termos de ordem superior representados por … acima:

Fórmula 3

Usando o cálculo, podemos mostrar que a expressão do lado direito pode ser minimizada escolhendo:

Fórmula 4

Considerando que a Fórmula 3 é uma boa expressão aproximada para a função custo, então esperamos que a

mudança do ponto w para  deva diminuir


significativamente a função custo. Isso sugere um algoritmo possível para minimizar o custo:

 Escolha um ponto de partida, w.


 Atualize w para um novo ponto w ′ = w − H ^ − 1 ∇C, onde o Hessian H e ∇C são calculados em w.
 Atualize w′ para um novo ponto w′′ = w′ − H′ ^ − 1 ∇′C, onde o Hessian H′ e ∇′C são calculados em w′.

Na prática, a Fórmula 3 é apenas uma aproximação e é melhor dar passos menores. Fazemos isso alterando

repetidamente w por uma quantidade  onde η é conhecido


como taxa de aprendizado.
Essa abordagem para minimizar uma função de custo é conhecida como Hessian Technique ou Hessian
Optimization. Existem resultados teóricos e empíricos mostrando que os métodos de Hessian convergem em
um mínimo em menos etapas do que a descida de gradiente padrão. Em particular, ao incorporar
informações sobre mudanças de segunda ordem na função de custo, é possível que a abordagem Hessiana
evite muitas patologias que podem ocorrer na descida de gradiente. Além disso, há versões do algoritmo de
retropropagação que podem ser usadas para computar o Hessian.

Se a Hessian Optimization é tão bom, por que não a estamos usando em nossas redes neurais? Infelizmente,
embora tenha muitas propriedades desejáveis, tem uma propriedade muito indesejável: é muito difícil de
aplicar na prática. Parte do problema é o tamanho da matriz Hessiana. Suponha que você tenha uma rede
neural com 107 pesos e vieses. Em seguida, a matriz Hessiana correspondente conterá 107 × 107 = 1014
entradas. Isso é um número grande de entradas! E isso torna a computação H ^ − 1 ∇C extremamente difícil
na prática. No entanto, isso não significa que não seja útil entender. De fato, há muitas variações na descida
de gradiente que são inspiradas pela Hessian Optimization, mas que evitam o problema com matrizes
excessivamente grandes. Vamos dar uma olhada em uma dessas técnicas, a descida do gradiente baseada em
Momentum.

Momentum
Intuitivamente, a vantagem da Hessian Optimization é que ela incorpora não apenas informações sobre o
gradiente, mas também informações sobre como o gradiente está mudando. A descida do gradiente baseada
no Momentum baseia-se em uma intuição similar, mas evita grandes matrizes de derivadas secundárias. Para
entender a técnica de Momentum, pense em nossa imagem original de descida do gradiente, na qual
consideramos uma bola rolando em um vale (veja figura abaixo). Observamos que a descida do gradiente é,
apesar de seu nome, apenas vagamente semelhante a uma bola caindo no fundo de um vale.

A técnica de Momentum modifica a descida do gradiente de duas maneiras que a tornam mais semelhante à
imagem física. Primeiro, introduz uma noção de “velocidade” para os parâmetros que estamos tentando
otimizar. O gradiente atua para alterar a velocidade, não (diretamente) a “posição”, da mesma maneira que
as forças físicas alteram a velocidade, afetando apenas indiretamente a posição. Em segundo lugar, o método
Momentum introduz um tipo de termo de fricção, que tende a reduzir gradualmente a velocidade.

Vamos dar uma descrição matemática mais precisa. Introduzimos variáveis de velocidade v = v1, v2,…,
uma para cada variável wj correspondente. Então nós substituímos a regra de atualização de descida de
gradiente w → w′ = w − η∇C por:

Nessas equações, μ é um hiperparâmetro que controla a quantidade de amortecimento ou atrito no sistema.


Para entender o significado das equações, é útil considerar primeiro o caso onde μ = 1, o que corresponde a
nenhum atrito. Quando esse é o caso, a inspeção das equações mostra que a “força” ∇C está agora
modificando a velocidade, v, e a velocidade está controlando a taxa de variação de w. Intuitivamente, nós
aumentamos a velocidade adicionando repetidamente termos de gradiente a ela. Isso significa que se o
gradiente estiver na (aproximadamente) mesma direção através de várias rodadas de aprendizado,
poderemos desenvolver um pouco de vapor movendo-se nessa direção. Pense, por exemplo, no que acontece
se estivermos nos movendo diretamente por um declive:

A cada passo a velocidade se torna maior no declive, então nos movemos mais e mais rapidamente para o
fundo do vale. Isso pode permitir que a técnica de Momentum funcione muito mais rapidamente do que a
descida de gradiente padrão. Claro, um problema é que, uma vez que chegarmos ao fundo do vale, vamos
ultrapassar. Ou, se o gradiente deve mudar rapidamente, então podemos nos encontrar indo na direção
errada. Essa é a razão para o hiperparâmetro µ nas equações acima.

Eu disse anteriormente que μ controla a quantidade de atrito no sistema; para ser um pouco mais preciso,
você deve pensar em 1 − μ como a quantidade de atrito no sistema. Quando μ = 1, como vimos, não há atrito
e a velocidade é completamente controlada pelo gradiente ∇C. Em contraste, quando μ = 0 há muito atrito, a
velocidade não pode se acumular e as equações acima reduzem à equação usual para o gradiente
descendente, w → w ′ = w − η∇C. Na prática, usar um valor intermediário entre 0 e 1 pode nos dar muito do
benefício de ser capaz de aumentar a velocidade, mas sem causar overshooting. Podemos escolher um valor
para μ usando os dados de validação retidos, da mesma maneira que selecionamos η e λ. Essa técnica é
estudada em detalhes aqui.

Evitei nomear o hiperparâmetro μ até agora. A razão é que o nome padrão para μ é mal escolhido: é
chamado de coeficiente de momentum. Isso é potencialmente confuso, já que μ não é de maneira alguma a
noção de momento da física. Pelo contrário, está muito mais relacionado ao atrito. No entanto, o termo
coeficiente de momentum é amplamente utilizado, por isso continuaremos a usá-lo.

Uma coisa boa sobre a técnica do Momentum é que não é preciso quase nenhum trabalho para modificar
uma implementação de descida de gradiente para incorporar o Momentum. Ainda podemos usar a
retropropagação para calcular os gradientes, assim como antes, e usar ideias como a amostragem de mini-
lotes estocasticamente escolhidos. Desta forma, podemos obter algumas das vantagens da Hessian
Optimization, usando informações sobre como o gradiente está mudando, mas sem as desvantagens e com
apenas pequenas modificações no nosso código. Na prática, a técnica do Momentum é comumente usada e,
muitas vezes, acelera o aprendizado.
Vejo você no próximo capítulo!
Capítulo 31 – As Redes Neurais Artificiais
Podem Computar Qualquer Função?
Um dos fatos mais impressionantes sobre redes neurais é que elas podem computar qualquer função. Isto é,
suponha que alguém lhe dê alguma função complicada, f(x):

Não importa qual seja a função, é garantido que existe uma rede neural de modo que, para cada entrada
possível, x, o valor f(x) (ou alguma aproximação) seja transmitido da rede, por exemplo:

Este resultado é válido mesmo se a função tiver muitas entradas, f = f(x1,…, xm) e muitas saídas. Por
exemplo, aqui está uma rede computando uma função com m = 3 entradas e n = 2 saídas:
 

Este resultado nos diz que as redes neurais têm um tipo de universalidade. Não importa qual função
queremos computar, sabemos que existe uma rede neural que pode fazer o trabalho.

Além do mais, esse teorema da universalidade é válido mesmo se restringirmos nossas redes a ter apenas
uma única camada intermediária entre os neurônios de entrada e de saída – uma chamada camada oculta
única. Portanto, mesmo arquiteturas de rede muito simples podem ser extremamente poderosas e isso ajuda
a explicar porque as redes neurais vem sendo usadas em aplicações avançadas de Inteligência Artificial.

O teorema da universalidade é bem conhecido por pessoas que usam redes neurais. Mas porque é verdade
não é tão amplamente compreendido. A maioria das explicações disponíveis é bastante técnica. Por
exemplo, um dos artigos originais que comprovou o resultado utilizou o teorema de Hahn-Banach, o
teorema da representação de Riesz e alguma análise de Fourier. Se você é um matemático, o argumento não
é difícil de seguir, mas não é tão fácil para a maioria das pessoas. É uma pena, já que as razões subjacentes à
universalidade são simples e belas.

Nos próximos capítulos, faremos uma explicação simples e principalmente visual do teorema da
universalidade. Nós vamos passo a passo através das idéias principais. Você entenderá porque é verdade que
as redes neurais podem computar qualquer função. Você entenderá algumas das limitações do resultado. E
você entenderá como o resultado se relaciona com redes neurais profundas (Deep Learning).

Os capítulos serão estruturados para ser agradáveis e objetivos. Desde que você tenha apenas um pouco de
familiaridade básica com redes neurais, você deve ser capaz de seguir a explicação. No entanto, iremos
fornecer links ocasionais para materiais anteriores, para ajudar a preencher quaisquer lacunas em seu
conhecimento.

Os teoremas da universalidade são um lugar comum na ciência da computação, tanto que às vezes nos
esquecemos do quão surpreendentes eles são. Mas vale a pena lembrar-nos: a capacidade de calcular uma
função arbitrária é verdadeiramente notável. Quase qualquer processo que você possa imaginar pode ser
considerado como computação de função. Considere o problema de nomear uma peça musical com base em
uma pequena amostra da peça. Isso pode ser pensado como computação de uma função. Ou considere o
problema de traduzir um texto chinês para o inglês. Mais uma vez, isso pode ser pensado como computação
de uma função. Ou considere o problema de analisar um arquivo de filme mp4 e gerar uma descrição do
enredo do filme e uma discussão sobre a qualidade da atuação dos atores. Novamente, isso pode ser pensado
como um tipo de computação de função. Universalidade significa que, em princípio, as redes neurais podem
fazer tudo isso e muito mais.

É claro, só porque sabemos que existe uma rede neural que pode (por exemplo) traduzir o texto chinês para
o inglês, isso não significa que temos boas técnicas para construir ou mesmo reconhecer tal rede. Essa
limitação se aplica também aos teoremas da universalidade tradicionais para modelos como circuitos
booleanos. Mas, como vimos anteriormente no livro, as redes neurais possuem algoritmos poderosos para
funções de aprendizado. Essa combinação de algoritmos de aprendizado + universalidade é uma mistura
atraente. Até agora, o livro se concentrou nos algoritmos de aprendizado. Nos próximos capítulos, nos
concentramos na universalidade e no que ela significa.

A compreensão desse conceito é a chave para as arquiteturas mais avançadas de Deep Learning, que estão
por vir mais a frente, neste livro!
Capítulo 32 – Como Uma Rede Neural
Artificial Encontra a Aproximação de Uma
Função

Este é um capítulo muito importante para compreender como as redes neurais realmente funcionam e Como
Uma Rede Neural Artificial Encontra a Aproximação de Uma Função. Acompanhe a explicação passo a
passo analisando cada um dos gráficos apresentados.

Mas antes de explicar porque o teorema da universalidade é verdadeiro, quero mencionar duas advertências
a esta declaração informal: “uma rede neural pode computar qualquer função”, que vimos no
capítulo anterior.

Primeiro, isso não significa que uma rede possa ser usada para calcular exatamente qualquer função. Em vez
disso, podemos obter uma aproximação que seja tão boa quanto desejamos. Aumentando o número de
neurônios ocultos, podemos melhorar a aproximação. Por exemplo, anteriormente ilustramos uma rede
computando alguma função f(x) usando três neurônios ocultos. Para a maioria das funções, apenas uma
aproximação de baixa qualidade será possível usando três neurônios ocultos. Ao aumentar o número de
neurônios ocultos (digamos, para cinco), podemos obter uma melhor aproximação:

E podemos melhorar ainda mais aumentando o número de neurônios ocultos.

Para tornar esta afirmação mais precisa, suponha que tenhamos uma função f(x) que gostaríamos de
computar com alguma precisão desejada ϵ > 0. A garantia é que usando neurônios ocultos suficientes sempre
podemos encontrar uma rede neural cuja saída g(x) satisfaça | g(x) − f(x) | < ϵ, para todas as entradas x. Em
outras palavras, a aproximação será boa dentro da precisão desejada para cada entrada possível.

A segunda ressalva é que a classe de funções que podem ser aproximadas da maneira descrita são as funções
contínuas. Se uma função é descontínua, isto é, faz saltos bruscos e repentinos, então, em geral, não será
possível aproximar usando uma rede neural. Isso não é surpreendente, já que nossas redes neurais calculam
funções contínuas de sua entrada. No entanto, mesmo que a função que realmente gostaríamos de computar
fosse descontínua, muitas vezes a aproximação contínua é boa o suficiente. Se é assim, então podemos usar
uma rede neural. Na prática, isso geralmente não é uma limitação importante.

Em suma, uma afirmação mais precisa do teorema da universalidade é que redes neurais com uma única
camada oculta podem ser usadas para aproximar qualquer função contínua a qualquer precisão desejada.
Neste e no próximo capítulo, vamos provar uma versão desse resultado.

Universalidade Com Uma Entrada e Uma Saída


Para entender por que o teorema da universalidade é verdadeiro, vamos começar entendendo como construir
uma rede neural que se aproxima de uma função com apenas uma entrada e uma saída:

Este é o cerne do problema da universalidade. Uma vez que entendemos esse caso especial, é realmente fácil
estender para funções com muitas entradas e muitas saídas (tema do próximo capítulo).

Para construir um insight sobre como construir uma rede para calcular f, vamos começar com uma rede
contendo apenas uma camada oculta, com dois neurônios ocultos e uma camada de saída contendo um único
neurônio de saída:

Para ter uma ideia de como funcionam os componentes da rede, vamos nos concentrar no neurônio oculto
superior. No diagrama abaixo, aumentando o valor de w, podemos ver imediatamente como a função
computada pelo neurônio oculto superior muda:
Como aprendemos anteriormente no livro, o que está sendo computado pelo neurônio oculto é σ(wx + b),
onde σ(z) ≡ 1 / (1 + e^-z) é a função sigmóide. Até agora, fizemos uso frequente dessa forma algébrica. Mas,
para a prova da universalidade, obteremos mais discernimento ignorando inteiramente a álgebra e, em vez
disso, manipulando e observando a forma mostrada no gráfico. Isso não apenas nos dará uma ideia melhor
do que está acontecendo, mas também nos dará uma prova de universalidade que se aplica a outras funções
de ativação que não a função sigmóide.

Para começar esta prova, podemos aumentar o bias, b, no diagrama acima. Você verá que, conforme o bias
aumenta, o gráfico se move para a esquerda, mas sua forma não muda.

Em seguida, podemos diminuir o viés (bias). Você verá que conforme o viés diminui, o gráfico se move para
a direita, mas, novamente, sua forma não muda. Em seguida, diminuímos o peso para cerca de 2 ou 3. Você
verá que à medida que diminui o peso, a curva se alarga. Talvez seja necessário alterar o bias também, para
manter a curva no quadro.

Finalmente, aumentamos o peso acima de w = 100. A curva fica mais íngreme, até que, eventualmente, ela
começa a parecer uma função de passo (Step Function). A imagem a seguir mostra como deve ser resultado:
Podemos simplificar um pouco nossa análise aumentando o peso para que a saída realmente seja uma Step
Function, para uma aproximação muito boa. Abaixo eu plotei a saída do neurônio oculto superior quando o
peso é w = 999. 

Na verdade, é um pouco mais fácil trabalhar com funções step do que com funções gerais sigmóides. A
razão é que, na camada de saída, somamos contribuições de todos os neurônios ocultos. É fácil analisar a
soma de várias funções step, mas é mais difícil pensar sobre o que acontece quando você adiciona um monte
de curvas em forma de sigmóide. E assim torna as coisas muito mais fáceis de assumir que nossos neurônios
ocultos estão emitindo funções step. Mais concretamente, fazemos isso fixando o peso w como sendo um
valor muito grande e, em seguida, definindo a posição da etapa modificando o bias. É claro que tratar a saída
como uma função step é uma aproximação, mas é uma aproximação muito boa e, por enquanto, vamos tratá-
la como exata. Voltarei mais tarde para discutir o impacto dos desvios dessa aproximação.

Em que valor de x a etapa ocorre? Em outras palavras, como a posição da etapa depende do peso e do viés?
Para responder a essa pergunta, podemos modificar o peso e o viés no diagrama acima. Você consegue
descobrir como a posição da etapa depende de w e b. Com um pouco de trabalho, você deve ser capaz de se
convencer de que a posição da etapa é proporcional a b e inversamente proporcional a w.

Na verdade, a etapa está na posição s = −b / w, como você pode ver modificando o peso e o bias no
diagrama a seguir:

Isso simplificará muito nossas vidas para descrever os neurônios ocultos usando apenas um único parâmetro,
s, que é a posição do passo, s = −b / w. 

Como mencionado acima, nós implicitamente definimos o peso w na entrada como um valor grande –
grande o suficiente para que a função de passo seja uma boa aproximação. Podemos facilmente converter
um neurônio parametrizado dessa maneira de volta ao modelo convencional, escolhendo o viés b = −ws.

Até agora, nos concentramos na saída apenas do neurônio oculto superior. Vamos dar uma olhada no
comportamento de toda a rede. Em particular, vamos supor que os neurônios ocultos estejam computando
funções de passos parametrizadas pelos pontos de degrau s1 (neurônio superior) e s2 (neurônio de baixo). E
eles terão os respectivos pesos de saída w1 e w2. Aqui está a rede:

O que está sendo plotado à direita é a saída ponderada w1a1 + w2a2 da camada oculta. Aqui, a1 e a2 são as
saídas dos neurônios ocultos superior e inferior, respectivamente. Essas saídas são frequentemente
conhecidas como ativações dos neurônios.

Podemos aumentar ou diminuir o ponto de passo s1 do neurônio oculto superior e isso nos dá uma ideia de
como isso altera a saída ponderada da camada oculta. Vale a pena entender o que acontece quando o s1
passa do s2. Você verá que o gráfico muda de forma quando isso acontece, já que nos movemos de uma
situação em que o neurônio oculto superior é o primeiro a ser ativado para uma situação em que o neurônio
oculto na parte inferior é o primeiro a ser ativado.

Da mesma forma, podemos manipular o ponto de passo s2 do neurônio oculto na parte inferior e ter uma
ideia de como isso altera a saída combinada dos neurônios ocultos.

Finalmente, podemos definir w1 como 0.8 e w2 como −0.8. Você recebe uma função “bump”, que começa
no ponto s1, termina no ponto s2 e tem a altura 0.8. Por exemplo, a saída ponderada pode ser assim:
Claro, podemos redimensionar o bump para ter qualquer altura. Vamos usar um único parâmetro, h, para
indicar a altura. Para reduzir a confusão, também removerei as notações “s1 = …” e “w1 = …”.

Podemos alterar o valor de h para cima e para baixo, para ver como a altura do bump muda. 

Você notará, a propósito, que estamos usando nossos neurônios de uma forma que pode ser pensada não
apenas em termos gráficos, mas em termos de programação mais convencionais, como uma espécie de
declaração if-then-else, por exemplo:
Na maior parte eu vou ficar com o ponto de vista gráfico. Mas, no que se segue, às vezes você pode achar
útil trocar pontos de vista e pensar sobre as coisas em termos de se-então-senão (uma das bases da
programação convencional).

Podemos usar o nosso truque de fazer bump para obter dois solavancos, colando dois pares de neurônios
ocultos na mesma rede:

Eu suprimi os pesos aqui, simplesmente escrevendo os valores h para cada par de neurônios ocultos. 

De maneira mais geral, podemos usar essa ideia para obter o máximo de picos que quisermos, de qualquer
altura. Em particular, podemos dividir o intervalo [0,1] em um número grande, N, de subintervalos, e usar N
pares de neurônios ocultos para configurar picos de qualquer altura desejada. Vamos ver como isso funciona
para N = 5. Desculpa pela a complexidade do diagrama abaixo (eu poderia esconder a complexidade
abstraindo mais, mas acho que vale a pena colocar um pouco de complexidade, para obter uma ideia mais
concreta de como essas redes funciona):
Você pode ver que existem cinco pares de neurônios ocultos. Os pontos escalonados para os respectivos
pares de neurônios são 0,1 / 5, depois 1 / 5,2 / 5 e assim por diante, para 4 / 5,5 / 5. Esses valores são fixos –
eles fazem com que tenhamos cinco saliências uniformemente espaçadas no gráfico.

Cada par de neurônios tem um valor de h associado a ele. Lembre-se, as conexões saídas dos neurônios têm
pesos h e −h (não marcados). Ao alterar os pesos de saída, estamos realmente projetando a função!

Conforme alteramos as alturas, é possível ver a mudança correspondente nos valores h. E há também uma
mudança nos pesos de saída correspondentes, que são + h e −h.

Em outras palavras, podemos manipular diretamente a função que aparece no gráfico à direita e ver isso
refletido nos valores h à esquerda. 
Mas aqui consideramos uma entrada e uma saída, o que é bem simples. Com múltiplas entradas o conceito é
basicamente o mesmo, mas iremos discutir as particularidades nos próximos capítulos, quando
mergulharmos nas redes neurais profundas. Até lá.

Capítulo 33 – Por que as Redes Neurais


Profundas São Difíceis de Treinar?
Iniciamos agora a terceira e última parte deste livro, em que estudaremos como funciona Deep Learning e os
principais modelos e arquiteturas de redes neurais profundas, com diversos exemplos e aplicações. Mas
primeiro temos que responder a seguinte pergunta: Por que as Redes Neurais Profundas São Difíceis de
Treinar?

Imagine que você é um engenheiro que foi solicitado a projetar um computador do zero. Um dia, você está
trabalhando em seu escritório, projetando circuitos lógicos, estabelecendo portas AND e OU, e assim por
diante, quando seu chefe chega com más notícias. O cliente acaba de adicionar um requisito de design
surpreendente: o circuito para o computador inteiro deve ter apenas duas camadas de profundidade:

Você fica estupefato e diz ao seu chefe: “O cliente está louco!”

Seu chefe responde: “Eu acho que eles são loucos também. Mas precisamos atender este requisito.”

Na verdade, há um sentido limitado em que o cliente não é louco. Suponha que você tenha permissão para
usar uma porta lógica especial que permite a você aplicar o AND (o “e” da lógica) e juntar quantas entradas
desejar. E você também tem permissão para uma porta NAND com muitas entradas, ou seja, uma porta que
pode aplicar o AND a várias entradas e depois nega a saída. Com essas portas especiais, é possível calcular
qualquer função usando um circuito com apenas duas camadas de profundidade.

Mas só porque algo é possível, não é uma boa ideia. Na prática, quando resolvemos problemas de projeto de
circuitos (ou quase todos os tipos de problemas algorítmicos), geralmente começamos descobrindo como
resolver sub-problemas, e então gradualmente integramos as soluções. Em outras palavras, criamos uma
solução através de várias camadas de abstração.
Por exemplo, suponha que estamos projetando um circuito lógico para multiplicar dois números.
Provavelmente, queremos construí-lo a partir de sub-circuitos, fazendo operações como adicionar dois
números. Os sub-circuitos para adicionar dois números serão, por sua vez, construídos a partir de sub-sub-
circuitos para adicionar dois bits. Muito grosso modo, nosso circuito será parecido com:

Ou seja, nosso circuito final contém pelo menos três camadas de elementos de circuito. Na verdade,
provavelmente conterá mais de três camadas, pois dividimos as sub-tarefas em unidades menores do que as
descritas anteriormente. Mas você compreendeu a ideia geral.

Então circuitos profundos facilitam o processo de design. Mas eles não são apenas úteis para o design.
Existem, de fato, provas matemáticas mostrando que, para algumas funções, circuitos muito superficiais
requerem exponencialmente mais elementos de circuitos para serem computados do que circuitos profundos.
Por exemplo, uma famosa série de artigosno início dos anos 1980 mostrou que calcular a paridade de um
conjunto de bits requer muitos portões exponencialmente, se feito com um circuito superficial. Por outro
lado, se você usa circuitos mais profundos, é fácil calcular a paridade usando um pequeno circuito: basta
calcular a paridade de pares de bits, depois usar esses resultados para calcular a paridade de pares de pares
de bits e assim por diante. construindo rapidamente a paridade geral. Os circuitos profundos, portanto,
podem ser intrinsecamente muito mais poderosos que os circuitos superficiais.

Até agora, este livro abordou redes neurais como o cliente louco. Quase todas as redes com as quais
trabalhamos têm apenas uma camada oculta de neurônios (mais as camadas de entrada e saída):

 
 

Essas redes simples têm sido extraordinariamente úteis: nos capítulos anteriores, usamos redes como essa
para classificar dígitos manuscritos com precisão superior a 98%! No entanto, intuitivamente, esperamos
que as redes com muito mais camadas ocultas sejam mais poderosas:

Tais redes poderiam usar as camadas intermediárias para construir múltiplas camadas de abstração, assim
como fazemos em circuitos booleanos. Por exemplo, se estamos fazendo reconhecimento de padrões visuais,
então os neurônios da primeira camada podem aprender a reconhecer bordas, os neurônios da segunda
camada podem aprender a reconhecer formas mais complexas, digamos, triângulo ou retângulos, construídos
a partir de bordas. A terceira camada reconheceria formas ainda mais complexas. E assim por diante. Essas
múltiplas camadas de abstração parecem propiciar às redes profundas uma vantagem convincente em
aprender a resolver problemas complexos de reconhecimento de padrões. Além disso, assim como no
caso dos circuitos, existem resultados teóricos sugerindo que as redes profundas são intrinsecamente mais
poderosas do que as redes superficiais.

Como podemos treinar essas redes profundas? Nos próximos capítulos, tentaremos treinar redes profundas
usando nosso algoritmo de aprendizado: descendente de gradiente estocástico por retropropagação (que já
estudamos em detalhes nos capítulos anteriores, mas que agora aplicaremos em redes neurais profundas).
Mas vamos nos deparar com problemas, com nossas redes profundas não realizando muito (se for o caso)
melhor do que redes rasas.
Essa falha parece surpreendente à luz da discussão acima. Em vez de desistir de redes profundas, vamos nos
aprofundar e tentar entender o que está dificultando o treinamento de nossas redes profundas. Quando
olharmos de perto, descobriremos que as diferentes camadas da nossa rede profunda estão aprendendo em
velocidades muito diferentes. Em particular, quando as camadas posteriores da rede estão aprendendo bem,
as camadas iniciais geralmente ficam presas durante o treinamento, aprendendo quase nada. Este empecilho
não é simplesmente devido à má sorte. Em vez disso, descobriremos que existem razões fundamentais para a
lentidão do aprendizado, conectadas ao nosso uso de técnicas de aprendizado baseadas em gradientes.

À medida que nos aprofundamos no problema, aprenderemos que o fenômeno oposto também pode ocorrer:
as primeiras camadas podem estar aprendendo bem, mas as camadas posteriores podem ficar presas. Na
verdade, descobriremos que existe uma instabilidade intrínseca associada ao aprendizado por gradiente
descendente em redes neurais profundas de muitas camadas. Essa instabilidade tende a resultar em camadas
anteriores ou posteriores ficando presas durante o treinamento.

Mas, ao nos debruçarmos sobre essas dificuldades, podemos começar a entender o que é necessário para
treinar redes profundas de maneira eficaz. E isso é exatamente o que faremos nos próximos capítulos.

Agora é que começa a diversão. Até lá.


Capítulo 34 – O Problema da Dissipação do
Gradiente
Então, por que as redes neurais profundas são difíceis de treinar?

Para responder a essa pergunta, primeiro revisitemos o caso de uma rede com apenas uma camada oculta.
Como de costume, usaremos o problema de classificação de dígitos MNIST o mesmo já estudado nos
capítulos anteriores e que você encontra no repositório deste livro no Github.

A partir de um shell do Python, nós carregamos os dados MNIST:

Montamos nossa rede:

Esta rede possui 784 neurônios na camada de entrada, correspondendo a 28 × 28 = 784 pixels na imagem de
entrada. Utilizamos 30 neurônios ocultos, assim como 10 neurônios de saída, correspondentes às 10
classificações possíveis para os dígitos MNIST (‘0’, ‘1’, ‘2’,…, ‘9’).

Vamos tentar treinar nossa rede por 30 épocas completas, usando mini-lotes de 10 exemplos de treinamento
por vez, uma taxa de aprendizado η = 0.1 e um parâmetro de regularização λ = 5.0. À medida que
treinarmos, monitoramos a precisão da classificação no conjunto de dados validation_data. Podemos
executar o script test.py com todos os comandos. Via prompt de comando ou terminal, digitamos: python
test.py (o treinamento pode levar muitos minutos dependendo da velocidade do computador).

 
 

Ao final do treinamento, obtemos uma precisão de classificação de 96,48% (aproximadamente), comparável


a nossos resultados anteriores com uma configuração semelhante. Agora, vamos adicionar outra camada
oculta, também com 30 neurônios, e tentar treinar com os mesmos hiperparâmetros. Usamos:

net = network2.Network([784, 30, 30, 10])

Isto dá uma melhor precisão de classificação 96,90%. Isso é encorajador: um pouco mais de profundidade
está ajudando. Vamos adicionar outra camada oculta de 30 neurônios.

net = network2.Network([784, 30, 30, 30, 10])

Isso não ajuda em nada. Na verdade, o resultado cai para 96,57%, próximo à nossa rede original. E suponha
que inserimos mais uma camada oculta.

net = network2.Network([784, 30, 30, 30, 30, 10])

Esse comportamento parece estranho. Intuitivamente, camadas ocultas extras devem tornar a rede capaz de
aprender funções de classificação mais complexas e, assim, fazer uma melhor classificação. Certamente, as
coisas não devem piorar, já que as camadas extras podem, no pior dos casos, simplesmente não fazer nada.
Mas não é isso que está acontecendo.

Então, o que está acontecendo? Vamos supor que as camadas ocultas extras realmente possam ajudar em
princípio e o problema é que nosso algoritmo de aprendizado não está encontrando os pesos e vieses
corretos. Gostaríamos de descobrir o que está errado em nosso algoritmo de aprendizado e como fazer
melhor.

Para entender melhor o que está errado, vamos visualizar como a rede aprende. Abaixo, traçamos parte de
uma rede [784,30,30,10], ou seja, uma rede com duas camadas ocultas, cada uma contendo 30 neurônios
ocultos. Cada neurônio no diagrama tem uma pequena barra nele, representando a rapidez com que o
neurônio está mudando à medida que a rede aprende. Uma barra grande significa que o peso e o viés do
neurônio estão mudando rapidamente, enquanto uma barra pequena significa que os pesos e o viés estão
mudando lentamente. Mais precisamente, as barras indicam o gradiente ∂C / ∂b para cada neurônio, ou seja,
a taxa de mudança do custo em relação ao viés do neurônio. Nos capítulos anteriores, vimos que essa
quantidade de gradiente controlava não apenas a rapidez com que o viés muda durante o aprendizado, mas
também a rapidez com que os pesos inseridos no neurônio também mudam. Não se preocupe se você não se
lembrar dos detalhes: a única coisa a ter em mente é simplesmente que essas barras mostram a rapidez com
que os pesos e os vieses de cada neurônio mudam conforme a rede aprende.

Para manter o diagrama simples, mostrei apenas os seis principais neurônios nas duas camadas ocultas. Eu
omiti os neurônios de entrada, pois eles não têm pesos nem viés para aprender. Eu também omiti os
neurônios de saída, já que estamos fazendo comparações por camadas, e faz mais sentido comparar camadas
com o mesmo número de neurônios. Os resultados foram plotados no início do treinamento, ou seja,
imediatamente após a inicialização da rede. Aqui estão eles:

A rede foi inicializada aleatoriamente e, portanto, não é surpreendente que haja muita variação na rapidez
com que os neurônios aprendem. Ainda assim, uma coisa que vale ressaltar é que as barras na segunda
camada oculta são em sua maioria muito maiores que as barras na primeira camada oculta. Como resultado,
os neurônios da segunda camada oculta aprendem um pouco mais rápido que os neurônios da primeira
camada oculta. Isso é meramente uma coincidência, ou os neurônios da segunda camada oculta
provavelmente aprenderão mais rápido do que os neurônios na primeira camada oculta em geral?
Para determinar se esse é o caso, é útil ter uma maneira global de comparar a velocidade de aprendizado na
primeira e segunda camadas ocultas. Para fazer isso, vamos indicar o gradiente como δlj = ∂C / ∂blj, ou seja,
o gradiente para o neurônio jth na camada lth.

Podemos pensar no gradiente δ1 como um vetor cujas entradas determinam a rapidez com que a primeira
camada oculta aprende, e δ2 como um vetor cujas entradas determinam a rapidez com que a segunda
camada oculta aprende. Em seguida, usaremos os comprimentos desses vetores como medidas globais da
velocidade na qual as camadas estão aprendendo. Assim, por exemplo, o comprimento “δ1” mede a
velocidade na qual a primeira camada oculta está aprendendo, enquanto o comprimento “δ2” mede a
velocidade na qual a segunda camada oculta está aprendendo.

Com essas definições, e na mesma configuração que foi plotada acima, encontramos δδ1 = 0.07… e δδ2 =
0.31…. Isso confirma nossa suspeita anterior: os neurônios na segunda camada oculta realmente estão
aprendendo muito mais rápido que os neurônios da primeira camada oculta.

O que acontece se adicionarmos mais camadas ocultas? Se tivermos três camadas ocultas, em uma rede
[784,30,30,30,10], então as respectivas velocidades de aprendizado serão 0,012, 0,060 e 0,283. Novamente,
as camadas ocultas anteriores estão aprendendo muito mais lentamente que as camadas ocultas posteriores.
Suponha que adicionemos mais uma camada com 30 neurônios ocultos. Nesse caso, as respectivas
velocidades de aprendizado são 0,003, 0,017, 0,070 e 0,285. O padrão é válido: as camadas iniciais
aprendem mais lentamente que as camadas posteriores.

Temos observado a velocidade de aprendizado no início do treinamento, ou seja, logo após as redes serem
inicializadas. Como a velocidade do aprendizado muda à medida que treinamos nossas redes? Vamos voltar
para ver a rede com apenas duas camadas ocultas. A velocidade de aprendizado muda da seguinte forma:

 
 

Para gerar esses resultados, usamos a descida do gradiente em lote com apenas 1.000 imagens de
treinamento, treinadas em mais de 500 épocas. Isso é um pouco diferente do que normalmente treinamos nos
capítulos anteriores, mas acontece que o uso de gradiente estocástico em mini-lote dá resultados muito mais
ruidosos (embora muito similares, quando você mede o ruído). Usar os parâmetros que escolhemos é uma
maneira fácil de suavizar os resultados, para que possamos ver o que está acontecendo.

Em qualquer caso, como você pode ver, as duas camadas começam a aprender em velocidades muito
diferentes (como já sabemos). A velocidade em ambas as camadas cai muito rapidamente, antes de se
recuperar. Mas, apesar de tudo, a primeira camada oculta aprende muito mais lentamente do que a segunda
camada oculta.

E quanto a redes mais complexas? Aqui estão os resultados de uma experiência semelhante, mas desta vez
com três camadas ocultas (uma rede [784,30,30,30,10]):

Mais uma vez, as primeiras camadas ocultas aprendem muito mais lentamente do que as camadas ocultas
posteriores. Finalmente, vamos adicionar uma quarta camada oculta (uma rede [784,30,30,30,30,10]) e ver o
que acontece quando treinamos:

 
 

Mais uma vez, as primeiras camadas ocultas aprendem muito mais lentamente do que as camadas ocultas
posteriores. Nesse caso, a primeira camada oculta está aprendendo aproximadamente 100 vezes mais lenta
que a camada oculta final. Natural que estivéssemos tendo problemas para treinar essas redes antes!

Temos aqui uma observação importante: em pelo menos algumas redes neurais profundas, o gradiente tende
a diminuir à medida que nos movemos para trás através das camadas ocultas. Isso significa que os neurônios
nas camadas anteriores aprendem muito mais lentamente que os neurônios nas camadas posteriores. E,
embora tenhamos visto isso em apenas uma única rede, há razões fundamentais pelas quais isso acontece em
muitas redes neurais. O fenômeno é conhecido como O Problema da Dissipação do Gradiente ou The
Vanishing Gradient Problem. Esse é um problema muito comum e ainda mais evidente em Redes Neurais
Recorrentes, usadas em aplicações de Processamento de Linguagem Natural.

Por que o problema de dissipação do gradiente ocorre? Existem maneiras de evitar isso? E como devemos
lidar com isso no treinamento de redes neurais profundas? Na verdade, aprenderemos rapidamente que não é
inevitável, embora a alternativa também não seja muito atraente: às vezes, o gradiente fica muito maior nas
camadas anteriores! Este problema é chamado de explosão do gradiente, e não é uma notícia muito melhor
do que o problema da dissipação do gradiente. Geralmente, verifica-se que o gradiente em redes neurais
profundas é instável, tendendo a explodir ou a desaparecer nas camadas anteriores. Essa instabilidade é um
problema fundamental para o aprendizado baseado em gradiente em redes neurais profundas. É algo que
precisamos entender e, se possível, tomar medidas para resolver.

Momentaneamente se afastando das redes neurais, imagine que estamos tentando minimizar numericamente
uma função f(x) de uma única variável. Não seria uma boa notícia se a derivada f′(x) fosse pequena? Isso
não significaria que já estávamos perto de um extremo? De forma semelhante, o pequeno gradiente nas
primeiras camadas de uma rede profunda pode significar que não precisamos fazer muito ajuste dos pesos e
vieses?

Claro, isso não é o caso. Lembre-se de que inicializamos aleatoriamente o peso e os vieses na rede. É
extremamente improvável que nossos pesos e vieses iniciais façam um bom trabalho em qualquer coisa que
desejamos que nossa rede faça. Para ser concreto, considere a primeira camada de pesos em uma rede
[784,30,30,30,10] para o problema MNIST. A inicialização aleatória significa que a primeira camada
elimina a maior parte das informações sobre a imagem de entrada. Mesmo que as camadas posteriores
tenham sido extensivamente treinadas, elas ainda acharão extremamente difícil identificar a imagem de
entrada, simplesmente porque elas não possuem informações suficientes. E assim, não é possível que não
seja preciso aprender muito na primeira camada. Se vamos treinar redes profundas, precisamos descobrir
como resolver o problema da dissipação do gradiente.

Se eu fosse você, não perderia o próximo capítulo com uma explicação matemática para esse importante
fenômeno no treinamento de redes neurais profundas (Deep Learning).
Capítulo 35 – A Matemática do Problema de
Dissipação do Gradiente em Deep Learning

Vamos continuar a discussão iniciada no capítulo anterior. Para entender porque o problema da dissipação
do gradiente ocorre, vamos considerar a rede neural profunda mais simples: uma com apenas um único
neurônio em cada camada. Aqui está uma rede com três camadas ocultas:

Aqui, w1, w2,… são os pesos, b1, b2,… são os vieses e C é alguma função de custo. Apenas para lembrar
como isso funciona, a saída aj do neurônio j é σ(zj), onde σ é a função de ativação sigmóide usual, e zj =
wjaj − 1 + bj é a entrada ponderada para o neurônio. Eu desenhei o custo C no final para enfatizar que o
custo é uma função da saída da rede, a4: se a saída real da rede estiver próxima da saída desejada, então o
custo será baixo, enquanto se estiver longe, o custo será alto.

Vamos estudar o gradiente ∂C / ∂b1 associado ao primeiro neurônio oculto. Definiremos uma expressão para
∂C / ∂b1 e, estudando essa expressão, entenderemos porque o problema da dissipação do gradiente ocorre.

Vou começar simplesmente mostrando a expressão para ∂C / ∂b1. Parece assustador, mas na verdade tem
uma estrutura simples, que descreverei em breve. Aqui está a expressão (ignore a rede, por enquanto, e note
que σ ′ é apenas a derivada da função σ):

A estrutura na expressão é a seguinte: existe um termo σ ′ (zj) no produto para cada neurônio na rede; um
peso wj para cada peso na rede; e um termo final ∂C / ∂a4, correspondente à função de custo no final.
Observe que coloquei cada termo na expressão acima da parte correspondente da rede. Então a rede em si é
um mnemônico para a expressão.

Há também uma explicação simples de porque a expressão acima é verdadeira e, portanto, é divertido (e
talvez esclarecedor) dar uma olhada nessa explicação.

Imagine que fazemos uma pequena mudança Δb1 no viés b1. Isso irá desencadear uma série de mudanças
em cascata no resto da rede. Primeiro, causa uma mudança Δa1 na saída do primeiro neurônio oculto. Isso,
por sua vez, causará uma mudança Δz2 na entrada ponderada para o segundo neurônio oculto. Então, uma
mudança Δa2 na saída do segundo neurônio oculto. E assim por diante, até chegar a uma mudança de C no
custo na saída. Nós temos
Isso sugere que podemos descobrir uma expressão para o gradiente ∂C / ∂b1, acompanhando
cuidadosamente o efeito de cada etapa dessa cascata.

Para fazer isso, vamos pensar em como Δb1 faz com que a saída a1 do primeiro neurônio oculto mude. Nós
temos a1 = σ (z1) = σ (w1a0 + b1), então:

Esse termo σ ′ (z1) deve parecer familiar: é o primeiro termo em nossa expressão reivindicada para o
gradiente ∂C / ∂b1. Intuitivamente, esse termo converte uma mudança Δb1 no viés em uma mudança Δa1 na
ativação de saída. Essa mudança Δa1 por sua vez causa uma mudança na entrada ponderada z2 = w2a1 + b2
para o segundo neurônio oculto:

Combinando nossas expressões para Δz2 e Δa1, vemos como a mudança no viés b1 se propaga ao longo da
rede para afetar z2:
Novamente, isso deve parecer familiar: agora temos os dois primeiros termos em nossa expressão
reivindicada para o gradiente ∂C / ∂b1.

Podemos continuar dessa maneira, rastreando a maneira como as alterações se propagam pelo resto da rede.
Em cada neurônio, pegamos um termo σ ′ (zj) e, em cada peso, escolhemos um termo wj. O resultado final é
uma expressão que relaciona a mudança final ΔC no custo para a mudança inicial Δb1 no viés:

Dividindo por Δb1, de fato, obtemos a expressão desejada para o gradiente:

Por que o problema da dissipação do gradiente ocorre


afinal?
Para entender porque o problema da dissipação do gradiente ocorre, vamos escrever explicitamente a
expressão inteira para o gradiente:

Com exceção do último termo, essa expressão é um produto de termos da forma wjσ ′ (zj). Para entender
como cada um desses termos se comporta, vamos ver um gráfico da função σ ′:
A derivada atinge um máximo em σ ′ (0) = 1/4. Agora, se usarmos nossa abordagem padrão para inicializar
os pesos na rede, escolheremos os pesos usando uma distribuição normal (Gaussiana) com média 0 e desvio
padrão 1. Assim, os pesos geralmente satisfazem | wj | < 1. Reunindo essas observações, vemos que os
termos wjσ ′ (zj) geralmente satisfazem | wjσ ′ (zj) | < 1/4. E quando tomamos um produto de muitos desses
termos, o produto tenderá a diminuir exponencialmente: quanto mais termos, menor será o produto. Isso está
começando a cheirar como uma possível explicação para o problema da dissipação do gradiente.

Para tornar tudo isso um pouco mais explícito, vamos comparar a expressão para ∂C / ∂b1 com uma
expressão para o gradiente em relação a um viés posterior, digamos ∂C / ∂b3. Naturalmente, não explicamos
explicitamente uma expressão para ∂C / ∂b3, mas segue o mesmo padrão descrito acima para ∂C / ∂b1. Aqui
está a comparação das duas expressões:
As duas expressões compartilham muitos termos. Mas o gradiente ∂C / ∂b1 inclui dois termos extras, cada
um da forma wjσ ′ (zj). Como vimos, esses termos são tipicamente menores que 1/4 de magnitude. E assim o
gradiente ∂C / ∂b1 normalmente será um fator de 16 (ou mais) menor que ∂C / ∂b3. Esta é a origem
essencial do problema da dissipação do gradiente.

É claro que este é um argumento informal, não uma prova rigorosa de que o problema da dissipação do
gradiente ocorrerá. Existem várias cláusulas de escape possíveis. Em particular, podemos nos perguntar se
os pesos wj poderiam crescer durante o treinamento. Se o fizerem, é possível que os termos wjσ ′ (zj) no
produto deixem de satisfazer | wjσ ′ (zj) | < 1/4. De fato, se os termos se tornarem grandes o suficiente –
maiores que 1 – então não teremos mais um problema de dissipação do gradiente. Em vez disso, o gradiente
crescerá exponencialmente à medida que nos movemos para trás pelas camadas. Em vez de um problema de
dissipação do gradiente, teremos um problema de explosão do gradiente. Mas isso é assunto para o próximo
capítulo!

Para aprender todos os detalhes matemáticos por trás desse processo, confira nosso curso único e exclusivo
no Brasil: Matemática Para Machine Learning. Até o próximo capítulo.

Capítulo 36 – Outros Problemas com o


Gradiente em Redes Neurais Artificiais
No capítulo anterior descrevemos para você a Matemática que ajuda a explicar a causa do problema da
dissipação do gradiente. Mas a dissipação não é o único problema que pode ocorrer. Neste capítulo vamos
descrever outros possíveis problemas com o gradiente em redes neurais artificiais.

Explosão do Gradiente
Vamos ver um exemplo explícito em que ocorre a explosão dos gradientes. O exemplo é bem simples e
vamos alterar alguns parâmetros na rede de maneira a garantir que tenhamos a explosão do gradiente.
Mesmo fazendo essa alteração para forçar o problema, a explosão do gradiente não é apenas uma
possibilidade hipotética e realmente pode acontecer.

Há duas etapas para obter a explosão do gradiente. Primeiro, escolhemos todos os pesos na rede como
grandes, digamos w1 = w2 = w3 = w4 = 100. Segundo, vamos escolher os vieses para que os termos σ ′ (zj)
não sejam muito pequenos. Isso é realmente muito fácil de fazer: tudo o que precisamos é escolher os vieses
para garantir que a entrada ponderada para cada neurônio seja zj = 0 (e então σ ′ (zj) = 1/4). Então, por
exemplo, queremos z1 = w1a0 + b1 = 0. Podemos conseguir isso ajustando b1 = −100 ∗ a0. Podemos usar a
mesma ideia para selecionar os outros vieses. Quando fazemos isso, vemos que todos os termos wjσ ′ (zj)
são iguais a 100 ∗ 1/4 = 25. Com estas escolhas ocorre o problema da explosão do gradiente.

Instabilidade do Gradiente
O problema fundamental aqui não é tanto o problema do gradiente que desaparece ou o problema do
gradiente que explode. É que o gradiente nas camadas iniciais é o produto dos termos de todas as camadas
posteriores. Quando há muitas camadas, essa é uma situação intrinsecamente instável. A única maneira que
todas as camadas podem aprender perto da mesma velocidade é se todos esses produtos de termos estiverem
próximos de se equilibrar. Sem algum mecanismo ou razão subjacente para que o equilíbrio ocorra, é
altamente improvável que aconteça simplesmente por acaso. Em suma, o problema real aqui é que as redes
neurais sofrem de um problema de instabilidade do gradiente. Como resultado, se usarmos técnicas de
aprendizado baseadas em gradiente padrão, camadas diferentes na rede tenderão a aprender em velocidades
totalmente diferentes.

O Problema Mais Comum é Mesmo a Dissipação do Gradiente


Vimos que o gradiente pode desaparecer ou explodir nas camadas iniciais de uma rede profunda. De fato, ao
usar neurônios sigmóides, o gradiente geralmente desaparece. Para ver porque, considere novamente a
expressão | wσ ′ (z) |. Para evitar o problema da dissipação do gradiente, precisamos | wσ ′ (z) | ≥1. Você
pode pensar que isso pode acontecer facilmente se w for muito grande. No entanto, é mais difícil do que
parece. A razão é que o termo σ ′ (z) também depende de w: σ ′ (z) = σ ′ (wa + b), onde a é a ativação da
entrada. Então, quando fazemos w grande, precisamos ter cuidado para que não tornemos simultaneamente σ
′ (wa + b) pequeno. Isso acaba sendo uma restrição considerável. A razão é que quando fazemos w grande,
tendemos a tornar o wa + b muito grande. Olhando para um gráfico de σ ′ você pode ver que isso nos coloca
fora das “asas” da função σ ′, onde é preciso valores muito pequenos. A única maneira de evitar isso é se a
ativação de entrada estiver dentro de um intervalo bastante estreito de valores. Às vezes isso vai acontecer,
mas frequentemente, porém, isso não acontece. E assim, no caso genérico, temos a dissipação dos
gradientes.

Gradientes Instáveis em Redes Mais Complexas


Temos estudado redes simples como exemplo, com apenas um neurônio em cada camada oculta. E quanto a
redes profundas mais complexas, com muitos neurônios em cada camada oculta (tipicamente Deep
Learning)?
 

De fato, o mesmo comportamento ocorre em tais redes. Nos capítulos anteriores onde estudamos
retropropagação, vimos que o gradiente na camada l de uma rede de camada L é dado por:

Aqui, Σ ′ (zl) é uma matriz diagonal cujas entradas são os valores σ ′ (z) para as entradas ponderadas para a
camada l. As wl são as matrizes de peso para as diferentes camadas. E ∇aC é o vetor de derivadas parciais de
C em relação às ativações de saída.

Essa é uma expressão muito mais complicada do que no caso de um único neurônio. Ainda assim, se você
olhar de perto, a forma essencial é muito semelhante, com muitos pares da forma:

Além disso, as matrizes Σ′ (zj) possuem pequenas entradas na diagonal, nenhuma maior que 1/4. Desde que

as matrizes de peso wj não sejam muito grandes, cada termo adicional:

tende a fazer o vetor gradiente menor, levando a uma dissipação do gradiente. Mais genericamente, o grande
número de termos no produto tende a levar a um gradiente instável, assim como no nosso exemplo anterior.
Na prática isso é tipicamente encontrado em redes sigmóides que os gradientes desaparecem
exponencialmente de forma rápida nas camadas anteriores. Como resultado, o aprendizado diminui nessas
camadas. Essa desaceleração não é apenas um acidente ou uma inconveniência: é uma consequência
fundamental da abordagem que estamos adotando para o aprendizado da rede.

Outros Obstáculos Para a Aprendizagem Profunda


Nestes últimos capítulos, nos concentramos na dissipação de gradientes – e, em geral, gradientes instáveis –
como um obstáculo à aprendizagem profunda. De fato, gradientes instáveis são apenas um obstáculo para o
aprendizado profundo, embora seja um importante obstáculo fundamental. Muitas pesquisas em andamento
têm como objetivo entender melhor os desafios que podem ocorrer quando se treinam redes profundas.
Vamos mencionar brevemente alguns artigos, para dar a você o sabor de algumas das perguntas que as
pessoas estão fazendo.

Como primeiro exemplo, em 2010 Glorot e Bengio encontraram evidências sugerindo que o uso de funções
de ativação sigmóide pode causar problemas ao treinamento de redes profundas. Em particular, eles
encontraram evidências de que o uso de sigmóides fará com que as ativações na camada oculta final saturem
perto de 0 no início do treinamento, diminuindo substancialmente o aprendizado. Eles sugeriram algumas
funções de ativação alternativas, que parecem não sofrer tanto com esse problema de saturação.

Como um segundo exemplo, em 2013, Sutskever, Martens, Dahl e Hinton estudaram o impacto na


aprendizagem profunda tanto da inicialização de peso aleatório quanto do cronograma de momentum na
descida de gradiente estocástica baseada no momento. Em ambos os casos, fazer boas escolhas fez uma
diferença substancial na capacidade de treinar redes profundas.

Esses exemplos sugerem que “O que dificulta o treinamento de redes profundas?” é uma questão complexa.
Nestes últimos capítulos, nos concentramos nas instabilidades associadas ao aprendizado baseado em
gradiente em redes profundas. Os resultados dos dois últimos parágrafos sugerem que há também um papel
desempenhado pela escolha da função de ativação, a forma como os pesos são inicializados e até mesmo
detalhes de como a aprendizagem por gradiente descendente é implementada. E, claro, a escolha da
arquitetura de rede e outros hiperparâmetros também é importante. Assim, muitos fatores podem
desempenhar um papel em dificultar a formação de redes profundas, e a compreensão de todos esses fatores
ainda é objeto de pesquisas em andamento. A boa notícia é que, a partir do próximo capítulo, vamos mudar
isso e desenvolver várias abordagens para o aprendizado profundo que, até certo ponto, conseguem superar
ou direcionar todos esses desafios.

Não é incrível o que estamos vivenciando neste exato momento da história humana? Tudo isso que
estudamos até aqui forma a base de aplicações de Inteligência Artificial que já são encontradas no mercado,
em diversas aplicações e até mesmo em nossos smartphones. E ainda estamos apenas no começo. 

E você, quer ou não fazer parte desta incrível revolução trazida pela Inteligência Artificial? Se a resposta for
sim, o que está esperando?

Você também pode gostar