Você está na página 1de 902

Prefácio

Através da exposição às notícias e mídias sociais, você provavelmente está


familiarizado com o fato de que o aprendizado de máquina se tornou uma das
tecnologias mais empolgantes de nosso tempo e era. Grandes empresas, como
Microsoft, Google, Meta, Apple, Amazon, IBM e muitas outras, investem pesado
em pesquisas e aplicativos de aprendizado de máquina por bons motivos. Embora
possa parecer que o aprendizado de máquina se tornou a palavra da moda de
nosso tempo e era, certamente não é hype. Este campo emocionante abre
caminho para novas possibilidades e tornou-se indispensável para o nosso dia a
dia. Conversando com o assistente de voz em nossos smartphones,
recomendando o produto certo para nossos clientes, evitando fraudes com cartão
de crédito, filtrando spam de nossas caixas de entrada de e-mail, detectando e
diagnosticando doenças médicas, a lista não para.

Se você quer se tornar um praticante de aprendizado de máquina, um melhor


solucionador de problemas ou até mesmo considerar uma carreira em pesquisa de
aprendizado de máquina, então este livro é para você! No entanto, para um
novato, os conceitos teóricos por trás do aprendizado de máquina podem ser
bastante esmagadores. No entanto, muitos livros práticos que foram publicados
nos últimos anos ajudarão você a começar no aprendizado de máquina,
implementando algoritmos de aprendizado poderosos.

Ficar exposto a exemplos práticos de código e trabalhar com aplicativos de


exemplo de aprendizado de máquina é uma ótima maneira de mergulhar nesse
campo. Exemplos concretos ajudam a ilustrar os conceitos mais amplos,
colocando o material aprendido diretamente em ação. No entanto, lembre-se que
com grande poder vem grande responsabilidade! Além de oferecer experiência
prática com aprendizado de máquina usando bibliotecas de aprendizado de
máquina baseadas em Python e Python, este livro também apresenta os conceitos
matemáticos por trás dos algoritmos de aprendizado de máquina, o que é
essencial para usar o aprendizado de máquina com sucesso. Assim, este livro é
diferente de um livro puramente prático; É um livro que discute os detalhes
necessários sobre conceitos de aprendizado de máquina, oferece explicações
intuitivas e informativas sobre como os algoritmos de aprendizado de máquina
funcionam, como usá-los e, o mais importante, como evitar as armadilhas mais
comuns.

Neste livro, vamos embarcar em uma jornada emocionante que abrange todos os
tópicos e conceitos essenciais para lhe dar uma vantagem inicial neste campo. Se
você achar que sua sede de conhecimento não está satisfeita, este livro faz
referência a muitos recursos úteis que você pode usar para acompanhar os
avanços essenciais neste campo.

A quem se destina este livro


Este livro é o companheiro ideal para aprender a aplicar o aprendizado de
máquina e o aprendizado profundo a uma ampla gama de tarefas e conjuntos de
dados. Se você é um programador que quer acompanhar as tendências recentes
em tecnologia, este livro é definitivamente para você. Além disso, se você é um
estudante ou está considerando uma transição de carreira, este livro será sua
introdução e um guia abrangente para o mundo do aprendizado de máquina.

O que este livro aborda


O Capítulo 1, Dando aos computadores a capacidade de aprender com os dados,
apresenta as principais subáreas do aprendizado de máquina para lidar com
várias tarefas problemáticas. Além disso, ele discute as etapas essenciais para
criar um pipeline típico de construção de modelo de aprendizado de máquina que
nos guiará pelos capítulos a seguir.

O Capítulo 2, Treinando Algoritmos Simples de Aprendizado de Máquina para


Classificação, remonta às origens do aprendizado de máquina e introduz
classificadores de perceptron binários e neurônios lineares adaptativos. Este
capítulo é uma introdução suave aos fundamentos da classificação de padrões e
se concentra na interação de algoritmos de otimização e aprendizado de máquina.

O Capítulo 3, A Tour of Machine Learning Classifiers Using Scikit-Learn, descreve


os algoritmos essenciais de aprendizado de máquina para classificação e fornece
exemplos práticos usando uma das bibliotecas de aprendizado de máquina de
código aberto mais populares e abrangentes, scikit-learn.

O Capítulo 4, Building Good Training Datasets – Data Preprocessing, discute


como lidar com os problemas mais comuns em conjuntos de dados não
processados, como dados ausentes. Ele também discute várias abordagens para
identificar os recursos mais informativos em conjuntos de dados e ensina como
preparar variáveis de diferentes tipos como entradas adequadas para algoritmos
de aprendizado de máquina.

O Capítulo 5, Compactando dados via redução de dimensionalidade, descreve as


técnicas essenciais para reduzir o número de recursos em um conjunto de dados
para conjuntos menores, mantendo a maioria de suas informações úteis e
discriminatórias. Discute a abordagem padrão para redução de dimensionalidade
via análise de componentes principais e a compara com técnicas de
transformação supervisionada e não linear.

O Capítulo 6, Learning Best Practices for Model Evaluation and Hyperparameter


Tuning, discute o que fazer e o que não fazer para estimar o desempenho de
modelos preditivos. Além disso, discute diferentes métricas para medir o
desempenho de nossos modelos e técnicas para ajustar algoritmos de
aprendizado de máquina.

O Capítulo 7, Combinando Diferentes Modelos para Aprendizagem em Conjunto,


apresenta os diferentes conceitos de combinação de múltiplos algoritmos de
aprendizagem de forma eficaz. Ele ensina como construir conjuntos de
especialistas para superar as fraquezas de cada aluno, resultando em previsões
mais precisas e confiáveis.

O capítulo 8, Aplicando o aprendizado de máquina à análise de sentimento,


discute as etapas essenciais para transformar dados textuais em representações
significativas para algoritmos de aprendizado de máquina para prever as opiniões
das pessoas com base em sua escrita.

O Capítulo 9, Prevendo Variáveis Alvo Contínuas com Análise de Regressão,


discute as técnicas essenciais para modelar relações lineares entre variáveis-alvo
e variáveis de resposta para fazer previsões em escala contínua. Depois de
introduzir diferentes modelos lineares, também fala sobre regressão polinomial e
abordagens baseadas em árvores.

O Capítulo 10, Trabalhando com dados não rotulados – Análise de clustering,


muda o foco para uma subárea diferente do aprendizado de máquina, o
aprendizado não supervisionado. Aplicamos algoritmos de três famílias
fundamentais de algoritmos de agrupamento para encontrar grupos de objetos que
compartilham um certo grau de similaridade.

O Capítulo 11, Implementando uma Rede Neural Artificial Multicamada do Zero,


estende o conceito de otimização baseada em gradiente, que introduzimos pela
primeira vez no Capítulo 2, Treinando Algoritmos Simples de Aprendizado de
Máquina para Classificação, para construir redes neurais poderosas e
multicamadas baseadas no popular algoritmo de backpropagation em Python.

O Capítulo 12, Paralelizando o Treinamento de Redes Neurais com o PyTorch,


baseia-se no conhecimento do capítulo anterior para fornecer um guia prático para
treinar redes neurais de forma mais eficiente. O foco deste capítulo é o PyTorch,
uma biblioteca Python de código aberto que nos permite utilizar vários núcleos de
GPUs modernas e construir redes neurais profundas a partir de blocos de
construção comuns por meio de uma API amigável e flexível.

O capítulo 13, Going Deeper – The Mechanics of PyTorch, retoma de onde o


capítulo anterior parou e introduz conceitos e funcionalidades mais avançados do
PyTorch. O PyTorch é uma biblioteca extraordinariamente vasta e sofisticada, e
este capítulo orienta você através de conceitos como gráficos de computação
dinâmica e diferenciação automática. Você também aprenderá como usar a API
orientada a objetos do PyTorch para implementar redes neurais complexas e
como o PyTorch Lightning o ajuda com as práticas recomendadas e minimizando
o código clichê.

O Capítulo 14, Classificando Imagens com Redes Neurais Convolucionais


Profundas, introduz as redes neurais convolucionais (CNNs). Uma CNN
representa um tipo particular de arquitetura de rede neural profunda que é
particularmente adequada para trabalhar com conjuntos de dados de imagem.
Devido ao seu desempenho superior em comparação com as abordagens
tradicionais, as CNNs são agora amplamente utilizadas em visão computacional
para alcançar resultados de última geração para várias tarefas de reconhecimento
de imagem. Ao longo deste capítulo, você aprenderá como as camadas
convolucionais podem ser usadas como poderosos extratores de recursos para
classificação de imagens.

O Capítulo 15, Modeling Sequential Data Using Recurrent Neural Networks,


introduz outra arquitetura de rede neural popular para aprendizado profundo que é
especialmente adequada para trabalhar com texto e outros tipos de dados
sequenciais e dados de séries temporais. Como um exercício de aquecimento,
este capítulo introduz redes neurais recorrentes para prever o sentimento das
críticas de filmes. Em seguida, ensinaremos redes recorrentes a digerir
informações de livros a fim de gerar textos inteiramente novos.

O capítulo 16, Transformers – Improving Natural Language Processing with


Attention Mechanisms, concentra-se nas últimas tendências em processamento de
linguagem natural e explica como os mecanismos de atenção ajudam na
modelagem de relacionamentos complexos em longas sequências. Em particular,
este capítulo descreve a influente arquitetura de transformadores e modelos de
transformadores de última geração, como BERT e GPT.

O Capítulo 17, Generative Adversarial Networks for Synthesizing New Data,


introduz um regime de treinamento adversarial popular para redes neurais que
pode ser usado para gerar novas imagens de aparência realista. O capítulo
começa com uma breve introdução aos autoencoders, que é um tipo particular de
arquitetura de rede neural que pode ser usada para compactação de dados. O
capítulo então mostra como combinar a parte decodificadora de um autoencoder
com uma segunda rede neural que pode distinguir entre imagens reais e
sintetizadas. Ao permitir que duas redes neurais compitam entre si em uma
abordagem de treinamento adversarial, você implementará uma rede adversarial
generativa que gera novos dígitos manuscritos.

O Capítulo 18, Redes neurais de grafos para capturar dependências em dados


estruturados de gráficos, vai além de trabalhar com conjuntos de dados tabulares,
imagens e texto. Este capítulo apresenta redes neurais de grafos que operam em
dados estruturados por grafos, como redes de mídia social e moléculas. Depois de
explicar os fundamentos das convoluções de grafos, este capítulo inclui um tutorial
mostrando como implementar modelos preditivos para dados moleculares.
O Capítulo 19, Aprendizado por Reforço para Tomada de Decisão em Ambientes
Complexos, abrange uma subcategoria de aprendizado de máquina que é
comumente usada para treinar robôs e outros sistemas autônomos. Este capítulo
começa apresentando os fundamentos da aprendizagem por reforço (RL) para
se familiarizar com as interações agente/ambiente, o processo de recompensa dos
sistemas de RL e o conceito de aprender com a experiência. Depois de aprender
sobre as principais categorias de RL, você implementará e treinará um agente que
pode navegar em um ambiente de mundo de grade usando o algoritmo Q-learning.
Finalmente, este capítulo introduz o algoritmo de aprendizagem profunda Q, que é
uma variante da aprendizagem Q que usa redes neurais profundas.

Para tirar o máximo proveito deste livro


Idealmente, você já está confortável com a programação em Python para
acompanhar os exemplos de código que fornecemos para ilustrar e aplicar vários
algoritmos e modelos. Para tirar o máximo proveito deste livro, uma compreensão
firme da notação matemática também será útil.

Um laptop ou computador desktop comum deve ser suficiente para executar a


maior parte do código neste livro, e fornecemos instruções para seu ambiente
Python no primeiro capítulo. Os capítulos posteriores introduzirão bibliotecas
adicionais e recomendações de instalação quando necessário.

Uma unidade de processamento gráfico (GPU) recente pode acelerar os


tempos de execução de código nos capítulos posteriores de aprendizado
profundo. No entanto, uma GPU não é necessária, e também fornecemos
instruções para usar recursos de nuvem gratuitos.

Baixar os arquivos de código de exemplo


Todos os exemplos de código estão disponíveis para download no GitHub
em https://github.com/rasbt/machine-learning-book. Também temos outros
pacotes de códigos do nosso rico catálogo de livros e vídeos disponíveis
no https://github.com/PacktPublishing/. Confira!
Embora seja recomendável usar o Jupyter Notebook para executar código
interativamente, todos os exemplos de código estão disponíveis em um script
Python (por exemplo, ) e um formato Jupyter Notebook (por exemplo, ). Além
disso, recomendamos a visualização do arquivo que acompanha cada capítulo
individual para obter informações adicionais e
atualizaçõesch02/ch02.pych02/ch02.ipynbREADME.md

Baixe as imagens coloridas


Nós também fornecemos um arquivo PDF que tem imagens coloridas das
capturas de tela / diagramas usados neste livro. Você pode baixá-lo
aqui: https://static.packt-cdn.com/downloads/9781801819312_ColorImages.pdf.
Além disso, imagens coloridas de resolução mais baixa são incorporadas nos
blocos de anotações de código deste livro que vêm junto com os arquivos de
código de exemplo.

Convenções
Há uma série de convenções de texto usadas ao longo deste livro.

Aqui estão alguns exemplos desses estilos e uma explicação de seu significado.
As palavras de código no texto são mostradas da seguinte maneira: "E os pacotes
já instalados podem ser atualizados por meio do sinalizador." --upgrade

Um bloco de código é definido da seguinte maneira:

def __init__(self, eta=0.01, n_iter=50, random_state=1):

self.eta = eta

self.n_iter = n_iter

self.random_state = random_state
CopyExplain

Qualquer entrada no interpretador Python é escrita da seguinte forma (observe o


símbolo). A saída esperada será mostrada sem o símbolo: >>>>>>
>>> v1 = np.array([1, 2, 3])

>>> v2 = 0.5 * v1

>>> np.arccos(v1.dot(v2) / (np.linalg.norm(v1) *

... np.linalg.norm(v2)))

0.0
CopyExplain

Qualquer entrada ou saída de linha de comando é gravada da seguinte maneira:

pip install gym==0.20


CopyExplain

Novos termos e palavras importantes são mostrados em negrito. As palavras


que você vê na tela, por exemplo, em menus ou caixas de diálogo, aparecem no
texto assim: "Clicar no botão Avançar move você para a próxima tela".

Avisos ou notas importantes aparecem em uma caixa como esta.

Dicas e truques aparecem assim.

Entre em contato
Feedback de nossos leitores é sempre bem-vindo.

Feedback geral: feedback@packtpub.com por e-mail e mencione o título do livro


no assunto da sua mensagem. Se você tiver dúvidas sobre qualquer aspecto
deste livro, envie um e-mail para questions@packtpub.com.

Errata: Embora tenhamos tomado todos os cuidados para garantir a precisão do


nosso conteúdo, erros acontecem. Se você encontrou um erro neste livro,
ficaríamos gratos se você nos relatasse isso. Por favor,
visite, http://www.packtpub.com/submit-errata, selecionando seu livro, clicando no
link Formulário de Submissão de Errata e inserindo os detalhes.
Pirataria: Se você se deparar com cópias ilegais de nossos trabalhos de qualquer
forma na Internet, ficaríamos gratos se você nos fornecesse o endereço do local
ou o nome do site. Entre em contato conosco pelo copyright@packtpub.com com
um link para o material.

Se você está interessado em se tornar um autor: Se há um tópico em que você


tem experiência e você está interessado em escrever ou contribuir para um livro,
visite http://authors.packtpub.com.

Compartilhe seus pensamentos


Depois de ler Machine Learning com PyTorch e Scikit-Learn, adoraríamos ouvir
seus pensamentos! Clique aqui para ir direto para a página de revisão da
Amazon para este livro e compartilhar seus comentários.

Sua avaliação é importante para nós e para a comunidade de tecnologia e nos


ajudará a garantir que estamos entregando conteúdo de excelente qualidade.

Faça o download gratuito de uma cópia em PDF


deste livro
Obrigado por comprar este livro!

Você gosta de ler em movimento, mas não consegue levar seus livros impressos
para todos os lugares? Sua compra de eBook não é compatível com o dispositivo
de sua escolha?

Não se preocupe, agora com cada livro Packt você recebe uma versão PDF sem
DRM desse livro sem nenhum custo.

Leia em qualquer lugar, em qualquer lugar, em qualquer dispositivo. Pesquise,


copie e cole código de seus livros técnicos favoritos diretamente em seu aplicativo.

As vantagens não param por aí, você pode ter acesso exclusivo a descontos,
newsletters e ótimos conteúdos gratuitos em sua caixa de entrada diariamente
Siga estes passos simples para obter os benefícios:

1. Escaneie o código QR ou acesse o link abaixo

https://packt.link/free-ebook/9781801819312

2. Envie seu comprovante de compra


3. É isso! Enviaremos seu PDF gratuito e outros benefícios diretamente para
seu e-mail

Dando aos computadores a capacidade de


aprender com os dados
Na minha opinião, o aprendizado de máquina, a aplicação e ciência de algoritmos que
dão sentido aos dados, é o campo mais emocionante de todas as ciências da computação!
Estamos vivendo em uma era em que os dados vêm em abundância; Usando algoritmos de
autoaprendizagem do campo do aprendizado de máquina, podemos transformar esses dados
em conhecimento. Graças às muitas bibliotecas de código aberto poderosas que foram
desenvolvidas nos últimos anos, provavelmente nunca houve um momento melhor para
entrar no campo do aprendizado de máquina e aprender a utilizar algoritmos poderosos para
identificar padrões em dados e fazer previsões sobre eventos futuros.

Neste capítulo, você aprenderá sobre os principais conceitos e diferentes tipos de


aprendizado de máquina. Juntamente com uma introdução básica à terminologia relevante,
estabeleceremos as bases para o uso bem-sucedido de técnicas de aprendizado de máquina
para a solução prática de problemas.

Neste capítulo, abordaremos os seguintes tópicos:


 Os conceitos gerais de aprendizado de máquina
 Os três tipos de aprendizagem e terminologia básica
 Os blocos de construção para projetar sistemas de aprendizado de máquina com
sucesso
 Instalando e configurando o Python para análise de dados e aprendizado de máquina

Construindo máquinas inteligentes para


transformar dados em conhecimento
Nesta era da tecnologia moderna, há um recurso que temos em abundância: uma grande
quantidade de dados estruturados e não estruturados. Na segunda metade do século 20, o
aprendizado de máquina evoluiu como um subcampo da inteligência artificial (IA)
envolvendo algoritmos de autoaprendizagem que derivam conhecimento de dados para
fazer previsões.

Em vez de exigir que os humanos derivem manualmente regras e criem modelos a partir da
análise de grandes quantidades de dados, o aprendizado de máquina oferece uma alternativa
mais eficiente para capturar o conhecimento em dados para melhorar gradualmente o
desempenho de modelos preditivos e tomar decisões baseadas em dados.

Não só o aprendizado de máquina está se tornando cada vez mais importante na pesquisa
em ciência da computação, mas também está desempenhando um papel cada vez maior em
nossas vidas cotidianas. Graças ao aprendizado de máquina, desfrutamos de filtros robustos
de spam de e-mail, software conveniente de reconhecimento de texto e voz, mecanismos de
pesquisa confiáveis na web, recomendações sobre filmes divertidos para assistir, depósitos
de cheques móveis, tempos estimados de entrega de refeições e muito mais.
Esperançosamente, em breve, adicionaremos carros autônomos seguros e eficientes a esta
lista. Além disso, progressos notáveis foram feitos em aplicações médicas; Por exemplo, os
pesquisadores demonstraram que os modelos de aprendizagem profunda podem detectar o
câncer de pele com precisão quase humana (https://www.nature.com/articles/nature21056).
Outro marco foi alcançado recentemente por pesquisadores da DeepMind, que usaram o
aprendizado profundo para prever estruturas de proteínas 3D, superando abordagens
baseadas em física por uma margem substancial
(https://deepmind.com/blog/article/alphafold-a-solution-to-a-50-year-old-grand-challenge-
in-biology). Embora a previsão precisa da estrutura de proteínas 3D desempenhe um papel
essencial na pesquisa biológica e farmacêutica, houve muitas outras aplicações
importantes do aprendizado de máquina na área da saúde recentemente. Por exemplo, os
pesquisadores projetaram sistemas para prever as necessidades de oxigênio de pacientes
COVID-19 com até quatro dias de antecedência para ajudar os hospitais a alocar recursos
para aqueles que precisam (https://ai.facebook.com/blog/new-ai-research-to-help-predict-
covid-19-resource-needs-from-a-series-of-x-rays/). Outro tema importante dos nossos dias
são as alterações climáticas, que apresentam um dos maiores e mais críticos desafios. Hoje,
muitos esforços estão sendo direcionados para o desenvolvimento de sistemas inteligentes
para combatê-la (https://www.forbes.com/sites/robtoews/2021/06/20/these-are-the-startups-
applying-ai-to-tackle-climate-change). Uma das muitas abordagens para combater as
mudanças climáticas é o campo emergente da agricultura de precisão. Aqui, os
pesquisadores pretendem projetar sistemas de aprendizado de máquina baseados em visão
computacional para otimizar a implantação de recursos para minimizar o uso e o
desperdício de fertilizantes.

Os três tipos diferentes de aprendizado de


máquina
Nesta seção, vamos dar uma olhada nos três tipos de aprendizado de máquina:
aprendizado supervisionado, aprendizado não supervisionado e aprendizado por
reforço. Aprenderemos sobre as diferenças fundamentais entre os três diferentes tipos de
aprendizagem e, usando exemplos conceituais, desenvolveremos uma compreensão dos
domínios de problemas práticos onde eles podem ser aplicados:
Figura 1.1: Os três tipos diferentes de aprendizado de máquina

Fazer previsões sobre o futuro com aprendizagem


supervisionada
O principal objetivo no aprendizado supervisionado é aprender um modelo a partir de
dados de treinamento rotulados que nos permita fazer previsões sobre dados invisíveis ou
futuros. Aqui, o termo "supervisionado" refere-se a um conjunto de exemplos de
treinamento (entradas de dados) onde os sinais de saída desejados (rótulos) já são
conhecidos. O aprendizado supervisionado é, então, o processo de modelagem da relação
entre as entradas de dados e os rótulos. Assim, também podemos pensar na aprendizagem
supervisionada como "aprendizagem de rótulo".

A Figura 1.2 resume um fluxo de trabalho típico de aprendizado supervisionado, em que os


dados de treinamento rotulados são passados para um algoritmo de aprendizado de máquina
para ajustar um modelo preditivo que pode fazer previsões em entradas de dados novas e
não rotuladas:
Figura 1.2: Processo de aprendizagem supervisionado

Considerando o exemplo da filtragem de spam de e-mail, podemos treinar um modelo


usando um algoritmo de aprendizado de máquina supervisionado em um corpus de e-mails
rotulados, que são corretamente marcados como spam ou não-spam, para prever se um
novo e-mail pertence a qualquer uma das duas categorias. Uma tarefa de aprendizado
supervisionado com rótulos de classe discretos, como no exemplo anterior de filtragem de
spam de e-mail, também é chamada de tarefa de classificação. Outra subcategoria da
aprendizagem supervisionada é a regressão, onde o sinal de resultado é um valor
contínuo.

Classificação para prever rótulos de classe

A classificação é uma subcategoria de aprendizagem supervisionada onde o objetivo é


prever os rótulos de classe categórica de novas instâncias ou pontos de dados com base em
observações passadas. Esses rótulos de classe são valores discretos e não ordenados que
podem ser entendidos como as associações de grupo dos pontos de dados. O exemplo
mencionado anteriormente de detecção de spam de e-mail representa um exemplo típico de
uma tarefa de classificação binária, onde o algoritmo de aprendizado de máquina aprende
um conjunto de regras para distinguir entre duas classes possíveis: e-mails de spam e não-
spam.
A figura 1.3 ilustra o conceito de uma tarefa de classificação binária com 30 exemplos de
treinamento; 15 exemplos de treinamento são rotulados como classe A e 15 exemplos de
treinamento são rotulados como classe B. Nesse cenário, nosso conjunto de dados é
bidimensional, o que significa que cada exemplo tem dois valores associados a ele: x1 e x2.
Agora, podemos usar um algoritmo de aprendizado de máquina supervisionado para
aprender uma regra — o limite de decisão representado como uma linha tracejada — que
pode separar essas duas classes e classificar novos dados em cada uma dessas duas
categorias, dado seu x1 e x2 Valores:

Figura 1.3: Classificando um novo ponto de dados

No entanto, o conjunto de rótulos de classe não precisa ser de natureza binária. O modelo
preditivo aprendido por um algoritmo de aprendizado supervisionado pode atribuir
qualquer rótulo de classe que foi apresentado no conjunto de dados de treinamento a um
novo ponto de dados ou instância não rotulado.

Um exemplo típico de uma tarefa de classificação multiclasse é o reconhecimento de


caracteres manuscritos. Podemos coletar um conjunto de dados de treinamento que consiste
em vários exemplos manuscritos de cada letra do alfabeto. As letras ("A", "B", "C" e assim
por diante) representarão as diferentes categorias não ordenadas ou rótulos de classe que
queremos prever. Agora, se um usuário fornecer um novo caractere manuscrito por meio de
um dispositivo de entrada, nosso modelo preditivo será capaz de prever a letra correta no
alfabeto com certa precisão. No entanto, nosso sistema de aprendizado de máquina será
incapaz de reconhecer corretamente qualquer um dos dígitos entre 0 e 9, por exemplo, se
eles não fizerem parte do conjunto de dados de treinamento.

Regressão para predição de desfechos contínuos


Aprendemos na seção anterior que a tarefa da classificação é atribuir rótulos categóricos e
não ordenados às instâncias. Um segundo tipo de aprendizagem supervisionada é a
predição de resultados contínuos, que também é chamada de análise de regressão. Na
análise de regressão, recebemos um número de variáveis preditoras (explicativas) e uma
variável de resposta contínua (desfecho), e tentamos encontrar uma relação entre essas
variáveis que nos permita predizer um desfecho.

Observe que, no campo do aprendizado de máquina, as variáveis preditoras são comumente


chamadas de "recursos" e as variáveis de resposta são geralmente chamadas de "variáveis
de destino". Adotaremos essas convenções ao longo deste livro.

Por exemplo, vamos supor que estamos interessados em prever as pontuações de SAT de
matemática dos alunos. (O SAT é um teste padronizado frequentemente usado para
admissões universitárias nos Estados Unidos.) Se houver uma relação entre o tempo gasto
estudando para o teste e as pontuações finais, poderíamos usá-lo como dados de
treinamento para aprender um modelo que usa o tempo de estudo para prever as pontuações
do teste de futuros alunos que planejam fazer esse teste.

Regressão para a média


O termo "regressão" foi criado por Francis Galton em seu artigo Regression towards
Mediocrity in Hereditary Stature em 1886. Galton descreveu o fenômeno biológico de que
a variância de altura em uma população não aumenta ao longo do tempo.
Ele observou que a altura dos pais não é passada para seus filhos, mas sim, a altura de seus
filhos regride em relação à média da população.

A figura 1.4 ilustra o conceito de regressão linear. Dada uma variável de recurso, x, e uma
variável de destino, y, ajustamos uma linha reta a esses dados que minimiza a distância —
mais comumente a distância média ao quadrado — entre os pontos de dados e a linha
ajustada.

Agora podemos usar a interceptação e a inclinação aprendidas com esses dados para prever
a variável de destino de novos dados:

Figura 1.4: Um exemplo de regressão linear


Resolvendo problemas interativos com aprendizagem
por reforço
Outro tipo de aprendizado de máquina é o aprendizado por reforço. Na aprendizagem por
reforço, o objetivo é desenvolver um sistema (agente) que melhore seu desempenho a partir
das interações com o ambiente. Como as informações sobre o estado atual do ambiente
normalmente também incluem o chamado sinal de recompensa, podemos pensar na
aprendizagem por reforço como um campo relacionado à aprendizagem supervisionada. No
entanto, na aprendizagem por reforço, esse feedback não é o rótulo ou valor correto da
verdade, mas uma medida de quão bem a ação foi medida por uma função de recompensa.
Por meio de sua interação com o ambiente, um agente pode então usar o aprendizado por
reforço para aprender uma série de ações que maximizam essa recompensa por meio de
uma abordagem exploratória de tentativa e erro ou planejamento deliberativo.
Um exemplo popular de aprendizagem por reforço é um programa de xadrez. Aqui, o
agente decide uma série de movimentos dependendo do estado do tabuleiro (o ambiente), e
a recompensa pode ser definida como ganhar ou perder no final do jogo:

Figura 1.5: Processo de aprendizagem por reforço

Existem muitos subtipos diferentes de aprendizagem por reforço. No entanto, um esquema


geral é que o agente na aprendizagem por reforço tenta maximizar a recompensa através de
uma série de interações com o ambiente. Cada estado pode ser associado a uma recompensa
positiva ou negativa, e uma recompensa pode ser definida como a realização de um
objetivo geral, como ganhar ou perder um jogo de xadrez. Por exemplo, no xadrez, o
resultado de cada movimento pode ser pensado como um estado diferente do ambiente.

Para explorar mais o exemplo do xadrez, vamos pensar em visitar certas configurações no
tabuleiro de xadrez como sendo associadas a estados que provavelmente levarão à vitória –
por exemplo, removendo a peça de xadrez de um oponente do tabuleiro ou ameaçando a
rainha. Outras posições, no entanto, estão associadas a estados que provavelmente
resultarão em perder o jogo, como perder uma peça de xadrez para o adversário no turno
seguinte. Agora, no jogo de xadrez, a recompensa (positiva por ganhar ou negativa por
perder o jogo) não será dada até o final do jogo. Além disso, a recompensa final também
dependerá de como o adversário joga. Por exemplo, o oponente pode sacrificar a rainha,
mas eventualmente ganhar o jogo.

Em suma, o aprendizado por reforço se preocupa em aprender a escolher uma série de


ações que maximizam a recompensa total, que pode ser obtida imediatamente após a
tomada de uma ação ou por meio de feedback atrasado.

Descobrindo estruturas ocultas com aprendizado não


supervisionado
Na aprendizagem supervisionada, sabemos a resposta certa (o rótulo ou a variável alvo) de
antemão quando treinamos um modelo e, na aprendizagem por reforço, definimos uma
medida de recompensa por determinadas ações realizadas pelo agente. Na aprendizagem
não supervisionada, no entanto, estamos lidando com dados não rotulados ou dados de
estrutura desconhecida. Usando técnicas de aprendizagem não supervisionada, somos
capazes de explorar a estrutura de nossos dados para extrair informações significativas sem
a orientação de uma variável de resultado conhecida ou função de recompensa.

Localizando subgrupos com clustering


Clustering é uma técnica exploratória de análise de dados ou descoberta de padrões que
nos permite organizar uma pilha de informações em subgrupos significativos (clusters)
sem ter qualquer conhecimento prévio de suas associações de grupo. Cada cluster que surge
durante a análise define um grupo de objetos que compartilham um certo grau de
similaridade, mas são mais diferentes de objetos em outros clusters, e é por isso que o
agrupamento também é às vezes chamado de classificação não supervisionada. O
clustering é uma ótima técnica para estruturar informações e derivar relacionamentos
significativos a partir de dados. Por exemplo, permite que os profissionais de marketing
descubram grupos de clientes com base em seus interesses, a fim de desenvolver programas
de marketing distintos.
A figura 1.6 ilustra como o clustering pode ser aplicado à organização de dados não
rotulados em três grupos ou clusters distintos (A, B e C, em ordem arbitrária) com base na
semelhança de suas características, x1 e x2:

Figure 1.6: How clustering works

Dimensionality reduction for data compression


Another subfield of unsupervised learning is dimensionality reduction. Often, we are
working with data of high dimensionality—each observation comes with a high number of
measurements—that can present a challenge for limited storage space and the
computational performance of machine learning algorithms. Unsupervised dimensionality
reduction is a commonly used approach in feature preprocessing to remove noise from data,
which can degrade the predictive performance of certain algorithms. Dimensionality
reduction compresses the data onto a smaller dimensional subspace while retaining most of
the relevant information.
Sometimes, dimensionality reduction can also be useful for visualizing data; for example, a
high-dimensional feature set can be projected onto one-, two-, or three-dimensional feature
spaces to visualize it via 2D or 3D scatterplots or histograms. Figure 1.7 shows an example
where nonlinear dimensionality reduction was applied to compress a 3D Swiss roll onto a
new 2D feature subspace:

Figura 1.7: Um exemplo de redução da dimensionalidade de três para duas dimensões

Introdução à terminologia básica e notações


Agora que discutimos as três grandes categorias de aprendizado de máquina –
supervisionado, não supervisionado e aprendizado por reforço – vamos dar uma olhada na
terminologia básica que usaremos ao longo deste livro. A subseção a seguir aborda os
termos comuns que usaremos ao nos referirmos a diferentes aspectos de um conjunto de
dados, bem como a notação matemática para se comunicar de forma mais precisa e
eficiente.

Como o aprendizado de máquina é um campo vasto e muito interdisciplinar, você


certamente encontrará muitos termos diferentes que se referem aos mesmos conceitos mais
cedo ou mais tarde. A segunda subseção coleta muitos dos termos mais comumente usados
que são encontrados na literatura de aprendizado de máquina, que podem ser úteis para
você como uma seção de referência ao ler publicações de aprendizado de máquina.

Notação e convenções usadas neste livro


A figura 1.8 mostra um trecho do conjunto de dados Iris, que é um exemplo clássico no
campo do aprendizado de máquina (mais informações podem ser encontradas
em https://archive.ics.uci.edu/ml/datasets/iris). O conjunto de dados Iris contém as
medidas de 150 flores de Iris de três espécies diferentes – Setosa, Versicolor e Virginica.
Aqui, cada exemplo de flor representa uma linha em nosso conjunto de dados, e as medidas
de flor em centímetros são armazenadas como colunas, que também chamamos
de recursos do conjunto de dados:

Figura 1.8: O conjunto de dados Iris

Para manter a notação e implementação simples, mas eficiente, faremos uso de alguns dos
fundamentos da álgebra linear. Nos capítulos seguintes, usaremos uma notação matricial
para nos referirmos aos nossos dados. Seguiremos a convenção comum para representar
cada exemplo como uma linha separada em uma matriz de recursos, X, onde cada recurso é
armazenado como uma coluna separada.

O conjunto de dados Iris, consistindo de 150 exemplos e quatro recursos, pode então ser

escrito como uma matriz 150×4, formalmente denotada como :

Convenções notacionais
Para a maioria das partes deste livro, a menos que indicado de outra forma, usaremos o
sobrescrito i para nos referirmos ao i-ésimo exemplo de treinamento, e o subscrito j para
nos referirmos à j-ésimadimensão do conjunto de dados de treinamento.
Usaremos letras minúsculas, em negrito, para nos referirmos a vetores () e letras

maiúsculas, em negrito, para nos referirmos a matrizes (

). Para nos referirmos a elementos únicos em um vetor ou matriz,

escreveremos as letras em itálico (x(n) ou  , respectivamente).

Por exemplo, refere-se à primeira dimensão da flor exemplo 150,   o


comprimento da sépala. Cada linha na matriz X representa uma ocorrência de flor e pode

ser escrita como um vetor de linha quadridimensional,  :


E cada dimensão de recurso é um vetor de coluna de 150

dimensões,  . Por exemplo:

Da mesma forma, podemos representar as variáveis de destino (aqui, rótulos de classe)


como um vetor de coluna de 150 dimensões:

Terminologia de aprendizado de máquina


O aprendizado de máquina é um campo vasto e também muito interdisciplinar, pois reúne
muitos cientistas de outras áreas de pesquisa. Acontece que muitos termos e conceitos
foram redescobertos ou redefinidos e podem já ser familiares para você, mas aparecem com
nomes diferentes. Para sua conveniência, na lista a seguir, você pode encontrar uma seleção
de termos comumente usados e seus sinônimos que você pode achar útil ao ler este livro e a
literatura de aprendizado de máquina em geral:
 Exemplo de treinamento: uma linha em uma tabela que representa o conjunto de
dados e é sinônimo de uma observação, registro, instância ou amostra (na maioria
dos contextos, amostra refere-se a uma coleção de exemplos de treinamento).
 Treinamento: Ajuste de modelos, para modelos paramétricos semelhantes à
estimação de parâmetros.
 Recurso, abbrev. x: Uma coluna em uma tabela de dados ou matriz de dados
(design). Sinônimo de preditor, variável, entrada, atributo ou covariável.
 Alvo, abbrev. y: Sinônimo de resultado, saída, variável de resposta, variável
dependente, rótulo (classe) e verdade básica.
 Função de perda: Muitas vezes usada como sinônimo de uma função de custo. Às
vezes, a função de perda também é chamada de função de erro. Em algumas
literaturas, o termo "perda" refere-se à perda medida para um único ponto de dados,
e o custo é uma medida que calcula a perda (média ou somada) ao longo de todo o
conjunto de dados.

Um roteiro para a construção de sistemas de


aprendizado de máquina
Nas seções anteriores, discutimos os conceitos básicos de aprendizado de máquina e os três
tipos diferentes de aprendizado. Nesta seção, discutiremos as outras partes importantes de
um sistema de aprendizado de máquina que acompanha o algoritmo de aprendizado.

A Figura 1.9 mostra um fluxo de trabalho típico para usar o aprendizado de máquina na


modelagem preditiva, que discutiremos nas seguintes subseções:
Figura 1.9: Fluxo de trabalho de modelagem preditiva

Pré-processamento – colocando os dados em forma


Vamos começar discutindo o roteiro para a construção de sistemas de aprendizado de
máquina. Os dados brutos raramente vêm na forma e na forma necessárias para o
desempenho ideal de um algoritmo de aprendizagem. Assim, o pré-processamento dos
dados é uma das etapas mais cruciais em qualquer aplicação de aprendizado de máquina.

Se tomarmos o conjunto de dados de flores Iris da seção anterior como exemplo, podemos
pensar nos dados brutos como uma série de imagens de flores das quais queremos extrair
recursos significativos. As características úteis podem ser centradas em torno da cor das
flores ou da altura, comprimento e largura das flores.

Muitos algoritmos de aprendizado de máquina também exigem que os recursos


selecionados estejam na mesma escala para um desempenho ideal, o que geralmente é
alcançado transformando os recursos no intervalo [0, 1] ou uma distribuição normal padrão
com média zero e variância unitária, como veremos nos próximos capítulos.
Algumas das características selecionadas podem ser altamente correlacionadas e, portanto,
redundantes até certo ponto. Nesses casos, as técnicas de redução de dimensionalidade são
úteis para comprimir as feições em um subespaço de dimensão inferior. Reduzir a
dimensionalidade do nosso espaço de recursos tem a vantagem de que menos espaço de
armazenamento é necessário, e o algoritmo de aprendizado pode ser executado muito mais
rápido. Em certos casos, a redução da dimensionalidade também pode melhorar o
desempenho preditivo de um modelo se o conjunto de dados contiver um grande número de
recursos irrelevantes (ou ruído); ou seja, se o conjunto de dados tiver uma baixa relação
sinal-ruído.

Para determinar se nosso algoritmo de aprendizado de máquina não apenas tem um bom
desempenho no conjunto de dados de treinamento, mas também generaliza bem para novos
dados, também queremos dividir aleatoriamente o conjunto de dados em conjuntos de
dados de treinamento e teste separados. Usamos o conjunto de dados de treinamento para
treinar e otimizar nosso modelo de aprendizado de máquina, enquanto mantemos o
conjunto de dados de teste até o final para avaliar o modelo final.

Treinamento e seleção de um modelo preditivo


Como você verá nos próximos capítulos, muitos algoritmos diferentes de aprendizado de
máquina foram desenvolvidos para resolver diferentes tarefas de problemas. Um ponto
importante que pode ser resumido a partir do famoso teorema No free lunch de David
Wolpert é que não podemos aprender "de graça" (The Lack of A Priori Distinctions
Between Learning Algorithms, D.H. Wolpert, 1996; Sem teoremas de almoço grátis para
otimização, D.H. Wolpert e W.G. Macready, 1997). Podemos relacionar esse conceito com
o ditado popular, suponho que seja tentador, se a única ferramenta que se tem é um
martelo, tratar tudo como se fosse um prego (Abraham Maslow, 1966). Por exemplo, cada
algoritmo de classificação tem seus vieses inerentes, e nenhum modelo de classificação
único goza de superioridade se não fizermos suposições sobre a tarefa. Na prática, portanto,
é essencial comparar pelo menos um punhado de algoritmos de aprendizagem diferentes
para treinar e selecionar o modelo de melhor desempenho. Mas antes de podermos
comparar diferentes modelos, primeiro temos que decidir sobre uma métrica para medir o
desempenho. Uma métrica comumente usada é a precisão de classificação, que é definida
como a proporção de instâncias corretamente classificadas.
Uma pergunta legítima a ser feita é: como sabemos qual modelo tem um bom desempenho
no conjunto de dados de teste final e nos dados do mundo real se não usamos esse conjunto
de dados de teste para a seleção do modelo, mas o mantemos para a avaliação final do
modelo? Para abordar a questão embutida nesta questão, diferentes técnicas resumidas
como "validação cruzada" podem ser usadas. Na validação cruzada, dividimos um conjunto
de dados em subconjuntos de treinamento e validação para estimar o desempenho de
generalização do modelo.

Finalmente, também não podemos esperar que os parâmetros padrão dos diferentes


algoritmos de aprendizagem fornecidos pelas bibliotecas de software sejam ideais para
nossa tarefa de problema específico. Portanto, faremos uso frequente de técnicas de
otimização de hiperparâmetros que nos ajudarão a ajustar o desempenho de nosso modelo
nos próximos capítulos.

Podemos pensar nesses hiperparâmetros como parâmetros que não são aprendidos com os
dados, mas representam os botões de um modelo que podemos recorrer para melhorar seu
desempenho. Isso ficará muito mais claro nos próximos capítulos, quando virmos exemplos
reais.

Avaliando modelos e prevendo instâncias de dados


invisíveis
Depois de selecionarmos um modelo que foi ajustado no conjunto de dados de treinamento,
podemos usar o conjunto de dados de teste para estimar o desempenho desses dados
invisíveis para estimar o chamado erro de generalização. Se estivermos satisfeitos com seu
desempenho, agora podemos usar esse modelo para prever novos dados futuros. É
importante observar que os parâmetros para os procedimentos mencionados anteriormente,
como dimensionamento de recursos e redução de dimensionalidade, são obtidos
exclusivamente do conjunto de dados de treinamento, e os mesmos parâmetros são
posteriormente reaplicados para transformar o conjunto de dados de teste, bem como
quaisquer novas instâncias de dados — o desempenho medido nos dados de teste pode ser
excessivamente otimista de outra forma.

Usando Python para aprendizado de máquina


Python é uma das linguagens de programação mais populares para ciência de dados, e
graças à sua comunidade de desenvolvedores e código aberto muito ativa, um
grande número de bibliotecas úteis para computação científica e aprendizado de máquina
foram desenvolvidas.

Embora o desempenho de linguagens interpretadas, como Python, para tarefas de


computação intensiva seja inferior às linguagens de programação de nível inferior,
bibliotecas de extensão como NumPy e SciPy foram desenvolvidas que se baseiam em
implementações Fortran e C de camada inferior para operações vetorizadas rápidas em
matrizes multidimensionais.

Para tarefas de programação de aprendizado de máquina, vamos nos referir principalmente


à biblioteca scikit-learn, que é atualmente uma das bibliotecas de aprendizado de máquina
de código aberto mais populares e acessíveis. Nos próximos capítulos, quando nos
concentrarmos em um subcampo do aprendizado de máquina chamado aprendizado
profundo, usaremos a versão mais recente da biblioteca PyTorch, especializada em treinar
os chamados modelos de redes neurais profundas de forma muito eficiente, utilizando
placas gráficas.

Instalando Python e pacotes a partir do Python Package


Index
Python está disponível para todos os três principais sistemas operacionais - Microsoft
Windows, macOS e Linux - e o instalador, bem como a documentação, pode ser baixado do
site oficial do Python: https://www.python.org.
Os exemplos de código fornecidos neste livro foram escritos e testados no Python 3.9, e
geralmente recomendamos que você use a versão mais recente do Python 3 que está
disponível. Parte do código também pode ser compatível com Python 2.7, mas como o
suporte oficial para Python 2.7 terminou em 2019, e a maioria das bibliotecas de código
aberto já parou de suportar Python 2.7 (https://python3statement.org), recomendamos
fortemente que você use Python 3.9 ou mais recente.

Você pode verificar sua versão do Python executando

python --version
CopyExplain
ou

python3 --version
CopyExplain

no seu terminal (ou PowerShell, se você estiver usando o Windows).

Os pacotes adicionais que usaremos ao longo deste livro podem ser instalados através do
programa instalador, que faz parte da Biblioteca Padrão Python desde o Python 3.3. Mais
informações podem ser encontradas
em https://docs.python.org/3/installing/index.html.pippip
Depois de instalarmos o Python com sucesso, podemos executar a partir do terminal para
instalar pacotes Python adicionais:pip
pip install SomePackage
CopyExplain
Os pacotes já instalados podem ser atualizados através do sinalizador:--upgrade
pip install SomePackage --upgrade
CopyExplain

Usando a distribuição Anaconda Python e o gerenciador


de pacotes
Um sistema de gerenciamento de pacotes de código aberto altamente recomendado para
instalar Python para contextos de computação científica é conda pela Continuum Analytics.
Conda é livre e licenciado sob uma licença permissiva de código aberto. Seu objetivo é
ajudar com a instalação e o gerenciamento de versões de pacotes Python para ciência de
dados, matemática e engenharia em diferentes sistemas operacionais. Se você quiser usar
conda, ele vem em diferentes sabores, ou seja, Anaconda, Miniconda e Miniforge:

 Anaconda vem com muitos pacotes de computação científica pré-instalados. O


instalador do Anaconda pode ser baixado
em https://docs.anaconda.com/anaconda/install/, e um guia de início rápido do
Anaconda está disponível
em https://docs.anaconda.com/anaconda/user-guide/getting-started/.
 Miniconda é uma alternativa mais enxuta à Anaconda
(https://docs.conda.io/en/latest/miniconda.html). Essencialmente, é semelhante ao
Anaconda, mas sem quaisquer pacotes pré-instalados, o que muitas pessoas
(incluindo os autores) preferem.
 O Miniforge é semelhante ao Miniconda, mas mantido pela comunidade e usa um
repositório de pacotes diferente (conda-forge) do Miniconda e do Anaconda.
Descobrimos que o Miniforge é uma ótima alternativa ao Miniconda. As instruções
de download e instalaçãopodem ser encontradas no repositório do GitHub
em https://github.com/conda-forge/miniforge.

Depois de instalar com sucesso o conda através do Anaconda, Miniconda ou Miniforge,


podemos instalar novos pacotes Python usando o seguinte comando:

conda install SomePackage


CopyExplain

Os pacotes existentes podem ser atualizados usando o seguinte comando:

conda update SomePackage


CopyExplain
Os pacotes que não estão disponíveis através do canal oficial da conda podem estar
disponíveis através do projeto conda-forge (https://conda-forge.org) apoiado pela
comunidade, que pode ser especificado através da bandeira. Por exemplo:--channel
conda-forge
conda install SomePackage --channel conda-forge
CopyExplain
Os pacotes que não estão disponíveis através do canal conda padrão ou conda-forge podem
ser instalados através de como explicado anteriormente. Por exemplo:pip
pip install SomePackage
CopyExplain

Pacotes para computação científica, ciência de dados e


aprendizado de máquina
Ao longo da primeira metade deste livro, usaremos principalmente as matrizes
multidimensionais do NumPy para armazenar e manipular dados. Ocasionalmente,
faremos uso de pandas, que é uma biblioteca construída sobre o NumPy que
fornece ferramentas adicionais de manipulação de dados de nível superior que tornam o
trabalho com dados tabulares ainda mais conveniente. Para aumentar sua experiência de
aprendizado e visualizar dados quantitativos, que muitas vezes são extremamente úteis para
dar sentido a eles, usaremos a biblioteca Matplotlib muito personalizável.

A principal biblioteca de aprendizado de máquina usada neste livro é scikit-learn


(Capítulos 3 a 11). O Capítulo 12, Paralelizando o Treinamento de Redes Neurais com
o PyTorch, apresentará a biblioteca PyTorch para aprendizado profundo.

Os números de versão dos principais pacotes Python que foram usados para escrever este
livro são mencionados na lista a seguir. Certifique-se de que os números de versão dos
pacotes instalados sejam, idealmente, iguais a esses números de versão para garantir que os
exemplos de código sejam executados corretamente:

 NumPy 1.21.2
 SciPy 1.7.0
 Scikit-aprender 1.0
 Matplotlib 3.4.3
 Pandas 1.3.2
Depois de instalar esses pacotes, você pode verificar novamente a versão instalada
importando o pacote em Python e acessando seu atributo, por exemplo:__version__
>>> import numpy

>>> numpy.__version__

'1.21.2'
CopyExplain
Para sua conveniência, incluímos um script no repositório de código gratuito deste livro
no https://github.com/rasbt/machine-learning-book para que você possa verificar sua versão
do Python e as versões do pacote executando esse script. python-environment-check.py
Alguns capítulos exigirão pacotes adicionais e fornecerão informações sobre as instalações.
Por exemplo, não se preocupe em instalar o PyTorch neste momento. O Capítulo
12 fornecerá dicas e instruções quando você precisar delas.
Se você encontrar erros, mesmo que seu código corresponda exatamente ao código no
capítulo, recomendamos que você primeiro verifique os números de versão dos pacotes
subjacentes antes de gastar mais tempo na depuração ou entrar em contato com o editor ou
autores. Às vezes, versões mais recentes de bibliotecas introduzem alterações incompatíveis
com versões anteriores que podem explicar esses erros.

Se você não quiser alterar a versão do pacote em sua instalação principal do Python,
recomendamos usar um ambiente virtual para instalar os pacotes usados neste livro. Se
você usar Python sem o gerenciador de conforma, poderá usar a biblioteca para criar um
novo ambiente virtual. Por exemplo, você pode criar e ativar o ambiente virtual por meio
dos dois comandos a seguir:venv
python3 -m venv /Users/sebastian/Desktop/pyml-book

source /Users/sebastian/Desktop/pyml-book/bin/activate
CopyExplain
Observe que você precisa ativar o ambiente virtual toda vez que abrir um novo terminal ou
PowerShell. Você pode encontrar mais informações sobre
em https://docs.python.org/3/library/venv.html. venv

Se você estiver usando o Anaconda com o gerenciador de pacotes conda, você pode criar e
ativar um ambiente virtual da seguinte maneira:

conda create -n pyml python=3.9

conda activate pyml


CopyExplain

Resumo
Neste capítulo, exploramos o aprendizado de máquina em um nível muito alto e
nos familiarizamos com o panorama geral e os principais conceitos que vamos
explorar nos próximos capítulos com mais detalhes. Aprendemos que a
aprendizagem supervisionada é composta por dois subcampos importantes:
classificação e regressão. Enquanto os modelos de classificação nos permitem
categorizar objetos em classes conhecidas, podemos usar a análise de regressão
para prever os resultados contínuos das variáveis-alvo. O aprendizado não
supervisionado não só oferece técnicas úteis para descobrir estruturas em dados
não rotulados, mas também pode ser útil para compactação de dados em etapas
de pré-processamento de recursos.

Analisamos brevemente o roteiro típico para aplicar o aprendizado de máquina a


tarefas problemáticas, que usaremos como base para discussões mais profundas
e exemplos práticos nos capítulos seguintes. Finalmente, configuramos nosso
ambiente Python e instalamos e atualizamos os pacotes necessários para nos
prepararmos para ver o aprendizado de máquina em ação.

Mais adiante neste livro, além do aprendizado de máquina em si, apresentaremos


diferentes técnicas para pré-processar um conjunto de dados, o que ajudará você
a obter o melhor desempenho de diferentes algoritmos de aprendizado de
máquina. Embora abordemos algoritmos de classificação extensivamente ao longo
do livro, também exploraremos diferentes técnicas para análise de regressão e
agrupamento.

Temos uma jornada emocionante pela frente, cobrindo muitas técnicas poderosas
no vasto campo do aprendizado de máquina. No entanto, abordaremos o
aprendizado de máquina um passo de cada vez, construindo nosso conhecimento
gradualmente ao longo dos capítulos deste livro. No capítulo seguinte,
começaremos essa jornada implementando um dos primeiros algoritmos de
aprendizado de máquina para classificação, que nos preparará para o Capítulo
3, Um Tour de Classificadores de Aprendizado de Máquina Usando o Scikit-Learn,
onde abordaremos algoritmos de aprendizado de máquina mais avançados
usando a biblioteca de aprendizado de máquina de código aberto scikit-learn.

Junte-se ao espaço Discord do nosso livro


Junte-se à nossa comunidade do Discord para conhecer pessoas que pensam
como você e aprender ao lado de mais de 2000 membros em:

https://packt.link/MLwPyTorch
reinamento de algoritmos simples de
aprendizado de máquina para
classificação
Neste capítulo, faremos uso de dois dos primeiros algoritmos de aprendizado de
máquina descritos algoritmicamente para classificação: o perceptron e os
neurônios lineares adaptativos. Começaremos implementando um perceptron
passo a passo em Python e treinando-o para classificar diferentes espécies de
flores no conjunto de dados Iris. Isso nos ajudará a entender o conceito de
algoritmos de aprendizado de máquina para classificação e como eles podem ser
implementados de forma eficiente em Python.

Discutir os fundamentos da otimização usando neurônios lineares adaptativos


estabelecerá as bases para o uso de classificadores mais sofisticados por meio da
biblioteca de aprendizado de máquina scikit-learn no Capítulo 3, A Tour of
Machine Learning Classifiers Using Scikit-Learn.

Os tópicos que abordaremos neste capítulo são os seguintes:

 Construindo uma compreensão dos algoritmos de aprendizado de máquina

 Usando pandas, NumPy e Matplotlib para ler, processar e visualizar dados

 Implementando classificadores lineares para problemas de 2 classes em


Python

Neurônios artificiais – um breve


vislumbre da história inicial do
aprendizado de máquina
Antes de discutirmos o perceptron e algoritmos relacionados em mais detalhes,
vamos fazer um breve tour pelos primórdios do aprendizado de máquina.
Tentando entender como o cérebro biológico funciona para projetar
uma inteligência artificial (IA), Warren McCulloch e Walter Pitts publicaram o
primeiro conceito de uma célula cerebral simplificada, o chamado neurônio
McCulloch-Pitts (MCP), em 1943 (A Logical Calculus of the Ideas Immanent in
Nervous Activity de W. S. McCulloch e W. Pitts, Bulletin of Mathematical
Biophysics , 5(4): 115-133, 1943).

Os neurônios biológicos são células nervosas interconectadas no cérebro que


estão envolvidas no processamento e transmissão de sinais químicos e elétricos,
o que é ilustrado na Figura 2.1:

Figura 2.1: Um neurônio processando sinais químicos e elétricos

McCulloch e Pitts descreveram tal célula nervosa como uma porta lógica simples
com saídas binárias; Vários sinais chegam aos dendritos, eles são então
integrados ao corpo celular e, se o sinal acumulado exceder um determinado
limiar, um sinal de saída é gerado que será transmitido pelo axônio.

Apenas alguns anos depois, Frank Rosenblatt publicou o primeiro conceito da


regra de aprendizagem do perceptron baseado no modelo de neurônio MCP (The
Perceptron: A Perceiving and Recognition Automaton de F. Rosenblatt, Cornell
Aeronautical Laboratory, 1957). Com sua regra de perceptron, Rosenblatt propôs
um algoritmo que aprenderia automaticamente os coeficientes de peso ideais que
seriam multiplicados com os recursos de entrada, a fim de tomar a decisão de se
um neurônio dispara (transmite um sinal) ou não. No contexto da aprendizagem
supervisionada e classificação, tal algoritmo poderia então ser usado para prever
se um novo ponto de dados pertence a uma classe ou outra.
A definição formal de um neurônio artificial
Mais formalmente, podemos colocar a ideia por trás dos neurônios artificiais no
contexto de uma tarefa de classificação binária com duas classes: 0 e 1. Podemos
então definir uma função de decisão, , que toma uma combinação linear de certos

valores de entrada, x, e um vetor de peso correspondente, w,  onde z é


a chamada entrada líquida z = w1x1 + w2x2 + ... + wmxm:

Agora, se a entrada líquida de um determinado exemplo, x(eu), é maior que um

limite definido, ,  prevemos a classe 1 e a classe 0 caso contrário. No algoritmo

perpetron, a função de decisão, ,  é uma variante de uma função


de passo unitário:

Para simplificar a implementação de código mais tarde, podemos modificar essa

configuração por meio de algumas etapas. Primeiro, movemos o limiar, ,  para o


lado esquerdo da equação:
Em segundo lugar, definimos uma chamada unidade de

viés como   e a tornamos parte da entrada líquida:

z = w1x1 + ... + wmxm + b = wTx + b

Em terceiro lugar, dada a introdução da unidade de viés e a redefinição da entrada


líquida z acima, podemos redefinir a função de decisão da seguinte forma:

Noções básicas de álgebra linear: transposição de produto e matriz de


pontos

Nas seções seguintes, frequentemente faremos uso de notações básicas da


álgebra linear. Por exemplo, vamos abreviar a soma dos produtos dos valores
em x e w usando um produto de ponto vetorial, enquanto o sobrescrito T significa
transpose, que é uma operação que transforma um vetor de coluna em um vetor
de linha e vice-versa. Por exemplo, suponha que temos os dois vetores de coluna
a seguir:

Then, we can write the transpose of vector a as aT = [a1 a2 a3] and write the dot


product as
Furthermore, the transpose operation can also be applied to matrices to reflect it
over its diagonal, for example:

Please note that the transpose operation is strictly only defined for matrices;
however, in the context of machine learning, we refer to n × 1 or 1 × m matrices
when we use the term “vector.”

In this book, we will only use very basic concepts from linear algebra; however, if
you need a quick refresher, please take a look at Zico Kolter’s excellent Linear
Algebra Review and Reference, which is freely available
at http://www.cs.cmu.edu/~zkolter/course/linalg/linalg_notes.pdf.

Figure 2.2 illustrates how the net input z = wTx + b is squashed into a binary output
(0 or 1) by the decision function of the perceptron (left subfigure) and how it can be
used to discriminate between two classes separable by a linear decision boundary
(right subfigure):
Figura 2.2: Uma função de limiar que produz um limite de decisão linear para um
problema de classificação binária

A regra de aprendizagem do perceptron


Toda a ideia por trás do neurônio MCP e do modelo de perceptron limiar de
Rosenblatt é usar uma abordagem reducionista para imitar como um único
neurônio no cérebro funciona: ou dispara ou não dispara. Assim, a regra clássica
do perceptron de Rosenblatt é bastante simples, e o algoritmo do perceptron pode
ser resumido pelas seguintes etapas:

1. Inicialize os pesos e a unidade de polarização para 0 ou pequenos números


aleatórios
2. Para cada exemplo de treinamento, x(eu):

a. Calcular o valor de saída, 


b. Atualizar os pesos e a unidade de polarização

Aqui, o valor de saída é o rótulo de classe previsto pela função de etapa unitária
que definimos anteriormente, e a atualização simultânea da unidade de viés e
cada peso, wj, no vetor peso, w, pode ser escrito mais formalmente como:
Os valores de atualização ("deltas") são calculados da seguinte maneira:

Note que, ao contrário da unidade de viés, cada peso, wj, corresponde a um


recurso, xj, no conjunto de dados, que está envolvido na determinação do valor de

atualização,  , definido acima. Além disso, é a taxa de

aprendizagem (tipicamente uma constante entre 0,0 e 1,0 ), y(eu) é

o verdadeiro rótulo de classe do i-ésimo exemplo de treinamento e   é o


rótulo de classe previsto. É importante notar que a unidade de viés e todos os
pesos no vetor de peso estão sendo atualizados simultaneamente, o que significa
que não recalculamos o rótulo previsto, antes que a unidade de viés e todos os

pesos sejam atualizados por meio dos respectivos valores de atualização, 

 e  . Concretamente, para um conjunto de dados bidimensional,


escreveríamos a atualização como:
Antes de implementarmos a regra perceptron em Python, vamos passar por um
experimento mental simples para ilustrar o quão lindamente simples essa regra de
aprendizado realmente é. Nos dois cenários em que o perceptron prevê o rótulo
da classe corretamente, a unidade de polarização e os pesos permanecem
inalterados, já que os valores de atualização são 0:

(1) 

(2) 

No entanto, no caso de uma previsão errada, os pesos estão sendo empurrados


para a direção da classe alvo positiva ou negativa:

(3) 

(4) 

Para entender melhor o valor do recurso como um fator multiplicativo, vamos a

outro exemplo simples,  onde:


Vamos supor isso   e classificamos erroneamente este
exemplo como classe 0. Neste caso, aumentaríamos o peso correspondente em
2,5 no total, de modo que a entrada líquida, , seria mais positiva na próxima vez
que encontrarmos este exemplo e,

portanto,  seria mais provável que


estivesse acima do limiar da função de passo unitário para classificar o exemplo
como classe 1:

A atualização de peso, ,  é proporcional ao valor de  . Por exemplo,


se tivermos outro exemplo, , que é classificado incorretamente como classe

0,  vamos empurrar o limite de decisão em uma extensão ainda


maior para classificar este exemplo corretamente da próxima vez:

É importante notar que a convergência do perceptron só é garantida se as duas


classes forem linearmente separáveis, o que significa que as duas classes podem
ser perfeitamente separadas por um limite de decisão linear. (Os leitores
interessados podem encontrar a prova de convergência em minhas notas de
aula: https://sebastianraschka.com/pdf/lecture-notes/stat453ss21/L03_perceptron_
slides.pdf). A figura 2.3 mostra exemplos visuais de cenários linearmente
separáveis e linearmente inseparáveis:
Figura 2.3: Exemplos de classes separáveis linearmente e não linearmente
separáveis

Se as duas classes não puderem ser separadas por um limite de decisão linear,
podemos definir um número máximo de passagens sobre o conjunto de dados de
treinamento (épocas) e/ou um limite para o número de erros de classificação
tolerados — o perceptron nunca pararia de atualizar os pesos de outra forma.
Mais adiante neste capítulo, abordaremos o algoritmo Adaline que produz limites
de decisão lineares e converge mesmo que as classes não sejam perfeitamente
separáveis. No Capítulo 3, aprenderemos sobre algoritmos que podem produzir
limites de decisão não lineares.

Baixando o código de exemplo

Se você comprou este livro diretamente da Packt, você pode baixar os arquivos de
código de exemplo de sua conta em http://www.packtpub.com. Se você comprou
este livro em outro lugar, você pode baixar todos os exemplos de código e
conjuntos de dados diretamente de https://github.com/rasbt/machine-learning-
book.

Agora, antes de entrarmos na implementação na próxima seção, o que você


acabou de aprender pode ser resumido em um diagrama simples que ilustra o
conceito geral do perceptron:
Figura 2.4: Os pesos e o viés do modelo são atualizados com base na função de
erro

O diagrama anterior ilustra como o perceptron recebe as entradas de um exemplo


(x) e as combina com a unidade de polarização (b) e pesos (w) para calcular a
entrada líquida. A entrada líquida é então passada para a função threshold, que
gera uma saída binária de 0 ou 1 — o rótulo de classe previsto do exemplo.
Durante a fase de aprendizagem, essa saída é usada para calcular o erro da
previsão e atualizar os pesos e a unidade de viés.

Implementando um algoritmo de
aprendizagem perceptron em Python
Na seção anterior, aprendemos como funciona a regra perceptron de Rosenblatt;
vamos agora implementá-lo em Python e aplicá-lo ao conjunto de dados Iris que
introduzimos no Capítulo 1, Dando aos computadores a capacidade de aprender
com os dados.

Uma API perceptron orientada a objetos


Tomaremos uma abordagem orientada a objetos para definir a interface
perceptron como uma classe Python, o que nos permitirá inicializar novos objetos
que podem aprender com dados por meio de um método e fazer previsões por
meio de um método separado. Como uma convenção, acrescentamos um
sublinhado () a atributos que não são criados na inicialização do objeto, mas
fazemos isso chamando os outros métodos do objeto, por
exemplo, .Perceptronfitpredict_self.w_

Recursos adicionais para a pilha de computação científica do Python

Se você ainda não está familiarizado com as bibliotecas científicas do Python ou


precisa de uma atualização, consulte os seguintes recursos:

 NumPy: https://sebastianraschka.com/blog/2020/numpy-intro.html
 Pandas: https://pandas.pydata.org/pandas-docs/stable/user_guide/10min.html
 Matplotlib: https://matplotlib.org/stable/tutorials/introductory/usage.html

A seguir está a implementação de um perceptron em Python:

import numpy as np

class Perceptron:

"""Perceptron classifier.

Parameters

------------

eta : float

Learning rate (between 0.0 and 1.0)

n_iter : int

Passes over the training dataset.

random_state : int

Random number generator seed for random weight

initialization.
Attributes

-----------

w_ : 1d-array

Weights after fitting.

b_ : Scalar

Bias unit after fitting.

errors_ : list

Number of misclassifications (updates) in each epoch.

"""

def __init__(self, eta=0.01, n_iter=50, random_state=1):

self.eta = eta

self.n_iter = n_iter

self.random_state = random_state

def fit(self, X, y):

"""Fit training data.

Parameters

----------

X : {array-like}, shape = [n_examples, n_features]

Training vectors, where n_examples is the number of

examples and n_features is the number of features.

y : array-like, shape = [n_examples]

Target values.

Returns

-------
self : object

"""

rgen = np.random.RandomState(self.random_state)

self.w_ = rgen.normal(loc=0.0, scale=0.01,

size=X.shape[1])

self.b_ = np.float_(0.)

self.errors_ = []

for _ in range(self.n_iter):

errors = 0

for xi, target in zip(X, y):

update = self.eta * (target - self.predict(xi))

self.w_ += update * xi

self.b_ += update

errors += int(update != 0.0)

self.errors_.append(errors)

return self

def net_input(self, X):

"""Calculate net input"""

return np.dot(X, self.w_) + self.b_

def predict(self, X):

"""Return class label after unit step"""

return np.where(self.net_input(X) >= 0.0, 1, 0)


CopyExplain
Usando essa implementação de perceptron, agora podemos inicializar novos

objetos com uma determinada taxa de aprendizado, (), e o número de épocas, (


passa por cima do conjunto de dados de treinamento). Perceptronetan_iter

Através do método, inicializamos o viés para um valor inicial 0 e os pesos em um

vetor,  onde m representa o número de dimensões (características) no


conjunto de dados.fitself.b_self.w_

Observe que o vetor de peso inicial contém pequenos números aleatórios


extraídos de uma distribuição normal com um desvio padrão de 0,01 via , onde é
um gerador de números aleatórios NumPy que semeamos com uma semente
aleatória especificada pelo usuário para que possamos reproduzir resultados
anteriores, se desejado.rgen.normal(loc=0.0, scale=0.01, size=1 + X.shape[1])rgen

Tecnicamente, poderíamos inicializar os pesos a zero (na verdade, isso é feito no

algoritmo perceptron original). No entanto, se fizéssemos isso, a taxa de   


aprendizagem () não teria efeito sobre o limite da decisão. Se todos os pesos
forem inicializados como zero, o parâmetro de taxa de aprendizado, , afetará
apenas a escala do vetor de peso, não a direção. Se você estiver familiarizado
com trigonometria, considere um vetor, v 1 =[1 2 3], onde o ângulo entre v 1 e um
vetor, v 2 = 0,5 × v1, seria exatamente zero, como demonstrado pelo seguinte
trecho de código:etaeta

>>> v1 = np.array([1, 2, 3])

>>> v2 = 0.5 * v1

>>> np.arccos(v1.dot(v2) / (np.linalg.norm(v1) *

... np.linalg.norm(v2)))

0.0
CopyExplain

Aqui, é o cosseno inverso trigonométrico, e é uma função que calcula o


comprimento de um vetor. (Nossa decisão de desenhar os números aleatórios de
uma distribuição normal aleatória — por exemplo, em vez de uma distribuição
uniforme — e usar um desvio padrão de foi arbitrária; lembre-se, estamos apenas
interessados em pequenos valores aleatórios para evitar as propriedades de
vetores todos zero, como discutido anteriormente.) np.arccosnp.linalg.norm0.01

Como um exercício opcional depois de ler este capítulo, você pode alterar e
executar o código de treinamento do perceptron apresentado na próxima seção
com valores diferentes para . Você observará que o limite da decisão não
muda.self.w_ = rgen.normal(loc=0.0, scale=0.01, size=X.shape[1])self.w_ =
np.zeros(X.shape[1])eta

Indexação de matriz NumPy

A indexação NumPy para matrizes unidimensionais funciona de forma semelhante


às listas Python usando a notação entre colchetes (). Para matrizes
bidimensionais, o primeiro indexador refere-se ao número da linha e o segundo
indexador ao número da coluna. Por exemplo, usaríamos para selecionar a
terceira linha e a quarta coluna de uma matriz bidimensional, . []X[2, 3]X

Depois que os pesos foram inicializados, o método faz um loop sobre todos os
exemplos individuais no conjunto de dados de treinamento e atualiza os pesos de
acordo com a regra de aprendizado de perceptron que discutimos na seção
anterior.fit

Os rótulos de classe são previstos pelo método, que é chamado no método


durante o treinamento para obter o rótulo de classe para a atualização de peso;
mas também pode ser usado para prever os rótulos de classe de novos dados
depois de termos ajustado nosso modelo. Além disso, também coletamos o
número de erros de classificação durante cada época da lista para que possamos
posteriormente analisar o desempenho de nosso perceptivo durante o
treinamento. A função que é usada no método simplesmente calcula o produto de
ponto vetorial, wpredictfitpredictself.errors_np.dotnet_input Tx + b.

Vetorização: Substituindo loops por código vetorizado

Em vez de usar o NumPy para calcular o produto de ponto vetorial entre duas
matrizes, e , via ou , também poderíamos realizar o cálculo em Python puro via .
No entanto, a vantagem de usar o NumPy sobre estruturas de loop Python
clássicas é que suas operações aritméticas são vetorizadas. Vetorização significa
que uma operação aritmética elementar é aplicada automaticamente a todos os
elementos em uma matriz. Ao formular nossas operações aritméticas como uma
sequência de instruções em uma matriz, em vez de executar um conjunto de
operações para cada elemento de cada vez, podemos fazer melhor uso de nossas
modernas arquiteturas de unidade de processamento
central (CPU) com suporte a instrução única, dados múltiplos (SIMD). Além
disso, o NumPy usa bibliotecas de álgebra linear altamente otimizadas, como
Basic Linear Algebra Subprograms (BLAS) e Linear Algebra
Package (LAPACK), que foram escritas em C ou Fortran. Por fim, o NumPy
também nos permite escrever nosso código de forma mais compacta e intuitiva
usando os conceitos básicos de álgebra linear, como produtos de pontos vetoriais
e matriciais.aba.dot(b)np.dot(a, b)sum([i * j for i, j in zip(a, b)])for

Treinando um modelo perceptron no conjunto de


dados Iris
Para testar nossa implementação de perceptron, restringiremos as seguintes
análises e exemplos no restante deste capítulo a duas variáveis de recurso
(dimensões). Embora a regra do perceptron não se restrinja a duas dimensões,
considerar apenas duas características, comprimento sépalo e comprimento da
pétala, permitirá visualizar as regiões de decisão do modelo treinado em um
gráfico de dispersão para fins de aprendizagem.

Observe que também só consideraremos duas classes de flores, setosa e


versicolor, do conjunto de dados Iris por razões práticas – lembre-se, o perceptron
é um classificador binário. No entanto, o algoritmo de perceptron pode ser
estendido para a classificação de várias classes — por exemplo, a técnica de um
contra todos (OvA).

O método OvA para classificação multiclasse

OvA, que às vezes também é chamado de um-versus-rest (OvR), é uma técnica


que nos permite estender qualquer classificador binário para problemas
multiclasse. Usando OvA, podemos treinar um classificador por classe, onde a
classe particular é tratada como a classe positiva e os exemplos de todas as
outras classes são considerados classes negativas. Se fôssemos classificar uma
nova instância de dados sem rótulo, usaríamos nossos n classificadores, onde n é
o número de rótulos de classe, e atribuiríamos o rótulo de classe com a maior
confiança à instância específica que queremos classificar. No caso do perceptron,
usaríamos OvA para escolher o rótulo de classe que está associado ao maior
valor absoluto líquido de entrada.

Primeiro, usaremos a biblioteca para carregar o conjunto de dados Iris diretamente


do Repositório de Aprendizado de Máquina UCI em um objeto e imprimir as
últimas cinco linhas por meio do método para verificar se os dados foram
carregados corretamente:pandasDataFrametail

>>> import os

>>> import pandas as pd

>>> s = 'https://archive.ics.uci.edu/ml/'\

... 'machine-learning-databases/iris/iris.data'

>>> print('From URL:', s)

From URL: https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data

>>> df = pd.read_csv(s,

... header=None,

... encoding='utf-8')

>>> df.tail()
CopyExplain

Depois de executar o código anterior, devemos ver a seguinte saída, que mostra
as últimas cinco linhas do conjunto de dados Iris:
Figura 2.5: As últimas cinco linhas do conjunto de dados Iris

Carregando o conjunto de dados Iris

Você pode encontrar uma cópia do conjunto de dados Iris (e todos os outros
conjuntos de dados usados neste livro) no pacote de códigos deste livro, que você
pode usar se estiver trabalhando offline ou se o servidor UCI
no https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data estiver
temporariamente indisponível. Por exemplo, para carregar o conjunto de dados Iris
de um diretório local, você pode substituir essa linha,

df = pd.read_csv(

'https://archive.ics.uci.edu/ml/'

'machine-learning-databases/iris/iris.data',

header=None, encoding='utf-8')
CopyExplain

com o seguinte:

df = pd.read_csv(
'your/local/path/to/iris.data',

header=None, encoding='utf-8')
CopyExplain

Em seguida, extraímos os primeiros 100 rótulos de classe que correspondem


às 50 flores Iris-setosa e 50 Iris-versicolor e convertemos os rótulos de classe nos
dois rótulos de classe inteira, (versicolor) e (setosa), que atribuímos a um vetor,
onde o método de um pandas produz a representação NumPy
correspondente.10yvaluesDataFrame

Da mesma forma, extraímos a primeira coluna de feição (comprimento da sépala)


e a terceira coluna de feição (comprimento da pétala) desses 100 exemplos de
treinamento e os atribuímos a uma matriz de feição, que podemos visualizar
através de um gráfico de dispersão bidimensional: X

>>> import matplotlib.pyplot as plt

>>> import numpy as np

>>> # select setosa and versicolor

>>> y = df.iloc[0:100, 4].values

>>> y = np.where(y == 'Iris-setosa', 0, 1)

>>> # extract sepal length and petal length

>>> X = df.iloc[0:100, [0, 2]].values

>>> # plot data

>>> plt.scatter(X[:50, 0], X[:50, 1],

... color='red', marker='o', label='Setosa')

>>> plt.scatter(X[50:100, 0], X[50:100, 1],

... color='blue', marker='s', label='Versicolor')

>>> plt.xlabel('Sepal length [cm]')

>>> plt.ylabel('Petal length [cm]')

>>> plt.legend(loc='upper left')

>>> plt.show()
CopyExplain

Depois de executar o exemplo de código anterior, devemos ver o seguinte gráfico


de dispersão:

Figura 2.6: Gráfico de dispersão das flores setosa e versicolor por comprimento
sépala e pétala

A figura 2.6 mostra a distribuição dos exemplos de flores no conjunto de dados Iris


ao longo dos dois eixos de feição: comprimento da pétala e comprimento da
sépala (medido em centímetros). Neste subespaço de feição bidimensional,
podemos ver que um limite de decisão linear deve ser suficiente para separar as
flores setosas das versicolores. Assim, um classificador linear como o perceptron
deve ser capaz de classificar perfeitamente as flores neste conjunto de dados.

Agora, é hora de treinar nosso algoritmo de perceptron no subconjunto de dados


Iris que acabamos de extrair. Além disso, plotaremos o erro de classificação
incorreta para cada época para verificar se o algoritmo convergiu e encontrou um
limite de decisão que separa as duas classes de flores de Iris:
>>> ppn = Perceptron(eta=0.1, n_iter=10)

>>> ppn.fit(X, y)

>>> plt.plot(range(1, len(ppn.errors_) + 1),

... ppn.errors_, marker='o')

>>> plt.xlabel('Epochs')

>>> plt.ylabel('Number of updates')

>>> plt.show()
CopyExplain

Observe que o número de erros de classificação incorreta e o número de


atualizações é o mesmo, uma vez que os pesos e viés do perceptron são
atualizados cada vez que ele classifica incorretamente um exemplo. Depois de
executar o código anterior, devemos ver o gráfico dos erros de classificação
incorreta versus o número de épocas, como mostrado na Figura 2.7:

Figura 2.7: Gráfico dos erros de classificação incorrecta em relação ao número de


épocas
Como podemos ver na Figura 2.7, nosso perceptron convergiu após a sexta época
e agora deve ser capaz de classificar os exemplos de treinamento perfeitamente.
Vamos implementar uma pequena função de conveniência para visualizar os
limites de decisão para conjuntos de dados bidimensionais:

from matplotlib.colors import ListedColormap

def plot_decision_regions(X, y, classifier, resolution=0.02):

# setup marker generator and color map

markers = ('o', 's', '^', 'v', '<')

colors = ('red', 'blue', 'lightgreen', 'gray', 'cyan')

cmap = ListedColormap(colors[:len(np.unique(y))])

# plot the decision surface

x1_min, x1_max = X[:, 0].min() - 1, X[:, 0].max() + 1

x2_min, x2_max = X[:, 1].min() - 1, X[:, 1].max() + 1

xx1, xx2 = np.meshgrid(np.arange(x1_min, x1_max, resolution),

np.arange(x2_min, x2_max, resolution))

lab = classifier.predict(np.array([xx1.ravel(), xx2.ravel()]).T)

lab = lab.reshape(xx1.shape)

plt.contourf(xx1, xx2, lab, alpha=0.3, cmap=cmap)

plt.xlim(xx1.min(), xx1.max())

plt.ylim(xx2.min(), xx2.max())

# plot class examples

for idx, cl in enumerate(np.unique(y)):

plt.scatter(x=X[y == cl, 0],

y=X[y == cl, 1],

alpha=0.8,

c=colors[idx],
marker=markers[idx],

label=f'Class {cl}',

edgecolor='black')
CopyExplain

First, we define a number of and and create a colormap from the list of colors via .
Then, we determine the minimum and maximum values for the two features and
use those feature vectors to create a pair of grid arrays, and , via the
NumPy function. Since we trained our perceptron classifier on two feature
dimensions, we need to flatten the grid arrays and create a matrix that has the
same number of columns as the Iris training subset so that we can use the method
to predict the class labels, , of the corresponding grid
points.colorsmarkersListedColormapxx1xx2meshgridpredictlab

After reshaping the predicted class labels, , into a grid with the same dimensions
as and , we can now draw a contour plot via Matplotlib’s function, which maps the
different decision regions to different colors for each predicted class in the grid
array:labxx1xx2contourf

>>> plot_decision_regions(X, y, classifier=ppn)

>>> plt.xlabel('Sepal length [cm]')

>>> plt.ylabel('Petal length [cm]')

>>> plt.legend(loc='upper left')

>>> plt.show()
CopyExplain

After executing the preceding code example, we should now see a plot of the
decision regions, as shown in Figure 2.8:
Figure 2.8: A plot of the perceptron’s decision regions

As we can see in the plot, the perceptron learned a decision boundary that can
classify all flower examples in the Iris training subset perfectly.

Perceptron convergence

Although the perceptron classified the two Iris flower classes perfectly,


convergence is one of the biggest problems of the perceptron. Rosenblatt proved
mathematically that the perceptron learning rule converges if the two classes can
be separated by a linear hyperplane. However, if the classes cannot be separated
perfectly by such a linear decision boundary, the weights will never stop updating
unless we set a maximum number of epochs. Interested readers can find a
summary of the proof in my lecture notes
at https://sebastianraschka.com/pdf/lecture-notes/stat453ss21/L03_perceptron_slid
es.pdf.

Neurônios lineares adaptativos e a


convergência da aprendizagem
Nesta seção, vamos dar uma olhada em outro tipo de rede neural de camada
única (NN): ADAptive LInear NEuron (Adaline). Adaline foi publicado por
Bernard Widrow e seu aluno de doutorado Tedd Hoff apenas alguns anos após o
algoritmo perceptron de Rosenblatt, e pode ser considerado uma melhoria em
relação ao último (An Adaptive "Adaline" Neuron Using Chemical
"Memistors", Technical Report Number 1553-2 por B. Widrow e colegas,
Stanford Electron Labs, Stanford, CA, outubro de 1960).

O algoritmo Adaline é particularmente interessante porque ilustra os conceitos-


chave de definição e minimização de funções de perda contínua. Isso estabelece
as bases para a compreensão de outros algoritmos de aprendizado de máquina
para classificação, como regressão logística, máquinas de vetores de suporte e
redes neurais multicamadas, bem como modelos de regressão linear, que
discutiremos nos próximos capítulos.

A principal diferença entre a regra de Adaline (também conhecida como regra de


Widrow-Hoff) e o perceptron de Rosenblatt é que os pesos são atualizados com
base em uma função de ativação linear em vez de uma função de passo unitário
como no perceptron. Em Adaline, esta função de ativação linear, , é simplesmente

a função de identidade da entrada líquida,  de modo

que  .

Embora a função de ativação linear seja usada para aprender os pesos, ainda
usamos uma função de limiar para fazer a previsão final, que é semelhante à
função de passo unitário que abordamos anteriormente.

As principais diferenças entre o algoritmo perceptron e Adaline são destacadas


na Figura 2.9:
Figura 2.9: Comparação entre um perceptron e o algoritmo Adaline

Como a Figura 2.9 indica, o algoritmo Adaline compara os rótulos de classe


verdadeira com a saída de valor contínuo da função de ativação linear para
calcular o erro do modelo e atualizar os pesos. Em contraste, o perceptron
compara os rótulos de classe verdadeiros com os rótulos de classe previstos.

Minimizando funções de perda com descida de


gradiente
Um dos principais ingredientes dos algoritmos de aprendizado de máquina
supervisionados é uma função de objetivo definida que deve ser otimizada
durante o processo de aprendizagem. Esta função objetiva é muitas vezes uma
função de perda ou custo que queremos minimizar. No caso de Adaline, podemos
definir a função de perda, L, para aprender os parâmetros do modelo como o erro
quadrático médio (MSE) entre o resultado calculado e o rótulo de classe
verdadeira:

A principal vantagem desta função de ativação linear contínua, em contraste com


a função de passo unitário, é que a função de perda torna-se diferenciável. Outra
boa propriedade desta função de perda é que ela é convexa; assim, podemos usar
um algoritmo de otimização muito simples, mas poderoso, chamado gradient
descent para encontrar os pesos que minimizam nossa função de perda para
classificar os exemplos no conjunto de dados do Iris.

Como ilustrado na Figura 2.10, podemos descrever a ideia principal por trás da
descida do gradiente como descer uma colina até que um mínimo de perda local
ou global seja atingido. Em cada iteração, damos um passo na direção oposta do
gradiente, onde o tamanho do passo é determinado pelo valor da taxa de
aprendizado, bem como a inclinação do gradiente (para simplificar, a figura a
seguir visualiza isso apenas para um único peso, w):

Figura 2.10: Como funciona a descida de gradiente


Usando a descida do gradiente, agora podemos atualizar os parâmetros do
modelo dando um passo na direção oposta do gradiente, , da nossa função de

perda, L(w,  b):

As mudanças de parâmetro, e , são definidas como o gradiente  negativo

multiplicado pela taxa de aprendizagem,   :

Para calcular o gradiente da função de perda, precisamos calcular a derivada


parcial da função de perda com relação a cada peso, wj:

Da mesma forma, calculamos a derivada parcial da perda com relação ao viés


como:

Observe que o 2 no numerador acima é meramente um fator de escala constante,


e poderíamos omiti-lo sem afetar o algoritmo. Remover o fator de escala tem o
mesmo efeito que alterar a taxa de aprendizagem por um fator de 2. A caixa de
informações a seguir explica de onde esse fator de dimensionamento se origina.

Assim, podemos escrever a atualização de peso como:

Como atualizamos todos os parâmetros simultaneamente, nossa regra de


aprendizado Adaline torna-se:

A derivada do erro quadrático médio

Se você estiver familiarizado com cálculo, a derivada parcial da função de perda


de MSE em relação ao jésimo peso pode ser obtida da seguinte maneira:
A mesma abordagem pode ser usada para encontrar derivada   parcial,

exceto que   é igual


a –1 e, portanto, a última etapa simplifica

para  .

Embora a regra de aprendizagem Adaline pareça idêntica à regra perceptron,

devemos observar que   

com   é um número real e não um rótulo de


classe inteira. Além disso, a atualização de peso é calculada com base em todos
os exemplos no conjunto de dados de treinamento (em vez de atualizar os
parâmetros incrementalmente após cada exemplo de treinamento), razão pela
qual essa abordagem também é chamada de descida de gradiente em lote. Para
ser mais explícito e evitar confusão ao falar sobre conceitos relacionados
mais adiante neste capítulo e neste livro, vamos nos referir a esse processo
como descida de gradiente em lote completo.

Implementando o Adaline em Python


Como a regra do perceptron e o Adaline são muito semelhantes, vamos pegar a
implementação do perceptron que definimos anteriormente e alterar o método
para que os parâmetros de peso e viés sejam agora atualizados, minimizando a
função de perda via descida de gradiente:fit

class AdalineGD:

"""ADAptive LInear NEuron classifier.


Parameters

------------

eta : float

Learning rate (between 0.0 and 1.0)

n_iter : int

Passes over the training dataset.

random_state : int

Random number generator seed for random weight initialization.

Attributes

-----------

w_ : 1d-array

Weights after fitting.

b_ : Scalar

Bias unit after fitting.

losses_ : list

Mean squared error loss function values in each epoch.

"""

def __init__(self, eta=0.01, n_iter=50, random_state=1):

self.eta = eta

self.n_iter = n_iter

self.random_state = random_state

def fit(self, X, y):

""" Fit training data.

Parameters
----------

X : {array-like}, shape = [n_examples, n_features]

Training vectors, where n_examples

is the number of examples and

n_features is the number of features.

y : array-like, shape = [n_examples]

Target values.

Returns

-------

self : object

"""

rgen = np.random.RandomState(self.random_state)

self.w_ = rgen.normal(loc=0.0, scale=0.01,

size=X.shape[1])

self.b_ = np.float_(0.)

self.losses_ = []

for i in range(self.n_iter):

net_input = self.net_input(X)

output = self.activation(net_input)

errors = (y - output)

self.w_ += self.eta * 2.0 * X.T.dot(errors) / X.shape[0]

self.b_ += self.eta * 2.0 * errors.mean()

loss = (errors**2).mean()

self.losses_.append(loss)
return self

def net_input(self, X):

"""Calculate net input"""

return np.dot(X, self.w_) + self.b_

def activation(self, X):

"""Compute linear activation"""

return X

def predict(self, X):

"""Return class label after unit step"""

return np.where(self.activation(self.net_input(X))

>= 0.5, 1, 0)
CopyExplain

Em vez de atualizar os pesos após avaliar cada exemplo de treinamento


individual, como no perceptron, calculamos o gradiente com base em todo o
conjunto de dados de treinamento. Para a unidade de viés, isso é feito via , onde é

uma matriz contendo os valores  de derivada parcial . Da mesma forma,


atualizamos os pesos. No entanto, observe que as atualizações de peso por meio

das derivadas parciais   envolvem os valores de recurso xself.eta * 2.0 *


errors.mean()errorsj, que podemos calcular multiplicando com cada valor de
recurso para cada peso:errors

for w_j in range(self.w_.shape[0]):

self.w_[w_j] += self.eta *
(2.0 * (X[:, w_j]*errors)).mean()
CopyExplain

Para implementar a atualização de peso de forma mais eficiente sem usar um


loop, podemos usar uma multiplicação de vetor de matriz entre nossa matriz de
recursos e o vetor de erro: for

self.w_ += self.eta * 2.0 * X.T.dot(errors) / X.shape[0]


CopyExplain

Observe que o método não tem efeito sobre o código, uma vez que é
simplesmente uma função de identidade. Aqui, adicionamos a função de ativação
(calculada através do método) para ilustrar o conceito geral em relação a como a
informação flui através de um NN de camada única: características dos dados de
entrada, entrada líquida, ativação e saída. activationactivation

No próximo capítulo, aprenderemos sobre um classificador de regressão logística


que usa uma função de ativação não-identitária e não linear. Veremos que um
modelo de regressão logística está intimamente relacionado à Adaline, com a
única diferença sendo sua função de ativação e perda.

Agora, semelhante à implementação anterior do perceptron, coletamos os valores


de perda em uma lista para verificar se o algoritmo convergiu após o
treinamento.self.losses_

Multiplicação matricial

Executar uma multiplicação de matriz é semelhante ao cálculo de um produto de


ponto vetorial em que cada linha na matriz é tratada como um vetor de linha única.
Essa abordagem vetorizada representa uma notação mais compacta e resulta em
uma computação mais eficiente usando NumPy. Por exemplo:
Observe que, na equação anterior, estamos multiplicando uma matriz com um
vetor, que não está matematicamente definido. No entanto, lembre-se que usamos
a convenção de que esse vetor precedente é considerado como uma matriz 3×1.

Na prática, muitas vezes requer alguma experimentação para encontrar uma boa

taxa de aprendizagem,  para uma convergência ótima. Então, vamos escolher

duas taxas de aprendizado diferentes,   

e  , para começar e plotar as funções de perda versus o


número de épocas para ver o quão bem a implementação do Adaline aprende com
os dados de treinamento.

Hiperparâmetros

A taxa de aprendizagem,   (), bem como o número de épocas (), são os


chamados hiperparâmetros (ou parâmetros de sintonia) dos algoritmos de
aprendizagem perceptron e adaline. No Capítulo 6, Learning Best Practices for
Model Evaluation and Hyperparameter Tuning, vamos dar uma olhada em
diferentes técnicas para encontrar automaticamente os valores de diferentes
hiperparâmetros que produzem o desempenho ideal do modelo de
classificação.etan_iter

Let’s now plot the loss against the number of epochs for the two different learning
rates:

>>> fig, ax = plt.subplots(nrows=1, ncols=2, figsize=(10, 4))

>>> ada1 = AdalineGD(n_iter=15, eta=0.1).fit(X, y)

>>> ax[0].plot(range(1, len(ada1.losses_) + 1),

... np.log10(ada1.losses_), marker='o')

>>> ax[0].set_xlabel('Epochs')

>>> ax[0].set_ylabel('log(Mean squared error)')


>>> ax[0].set_title('Adaline - Learning rate 0.1')

>>> ada2 = AdalineGD(n_iter=15, eta=0.0001).fit(X, y)

>>> ax[1].plot(range(1, len(ada2.losses_) + 1),

... ada2.losses_, marker='o')

>>> ax[1].set_xlabel('Epochs')

>>> ax[1].set_ylabel('Mean squared error')

>>> ax[1].set_title('Adaline - Learning rate 0.0001')

>>> plt.show()
CopyExplain

As we can see in the resulting loss function plots, we encountered two different
types of problems. The left chart shows what could happen if we choose a learning
rate that is too large. Instead of minimizing the loss function, the MSE becomes
larger in every epoch, because we overshoot the global minimum. On the other
hand, we can see that the loss decreases on the right plot, but the chosen learning

rate,  , is so small that the algorithm would require a very


large number of epochs to converge to the global loss minimum:

Figure 2.11: Error plots for suboptimal learning rates


Figure 2.12 illustrates what might happen if we change the value of a particular
weight parameter to minimize the loss function, L. The left subfigure illustrates the
case of a well-chosen learning rate, where the loss decreases gradually, moving in
the direction of the global minimum.

The subfigure on the right, however, illustrates what happens if we choose a


learning rate that is too large—we overshoot the global minimum:

Figure 2.12: A comparison of a well-chosen learning rate and a learning rate that is
too large

Improving gradient descent through feature scaling


Many machine learning algorithms that we will encounter throughout this book
require some sort of feature scaling for optimal performance, which we will discuss
in more detail in Chapter 3, A Tour of Machine Learning Classifiers Using Scikit-
Learn, and Chapter 4, Building Good Training Datasets – Data Preprocessing.

Gradient descent is one of the many algorithms that benefit from feature scaling. In
this section, we will use a feature scaling method called standardization. This
normalization procedure helps gradient descent learning to converge more quickly;
however, it does not make the original dataset normally distributed.
Standardization shifts the mean of each feature so that it is centered at zero and
each feature has a standard deviation of 1 (unit variance). For instance, to
standardize the jth feature, we can simply subtract the sample mean,  , from

every training example and divide it by its standard deviation,  :

Here, xj is a vector consisting of the jth feature values of all training examples, n,


and this standardization technique is applied to each feature, j, in our dataset.

One of the reasons why standardization helps with gradient descent learning is that
it is easier to find a learning rate that works well for all weights (and the bias). If the
features are on vastly different scales, a learning rate that works well for updating
one weight might be too large or too small to update the other weight equally well.
Overall, using standardized features can stabilize the training such that the
optimizer has to go through fewer steps to find a good or optimal solution (the
global loss minimum). Figure 2.13 illustrates possible gradient updates with
unscaled features (left) and standardized features (right), where the concentric
circles represent the loss surface as a function of two model weights in a two-
dimensional classification problem:
Figure 2.13: A comparison of unscaled and standardized features on gradient
updates

Standardization can easily be achieved by using the built-in NumPy


methods and :meanstd

>>> X_std = np.copy(X)

>>> X_std[:,0] = (X[:,0] - X[:,0].mean()) / X[:,0].std()

>>> X_std[:,1] = (X[:,1] - X[:,1].mean()) / X[:,1].std()


CopyExplain

After standardization, we will train Adaline again and see that it now converges

after a small number of epochs using a learning rate of  :

>>> ada_gd = AdalineGD(n_iter=20, eta=0.5)

>>> ada_gd.fit(X_std, y)

>>> plot_decision_regions(X_std, y, classifier=ada_gd)

>>> plt.title('Adaline - Gradient descent')

>>> plt.xlabel('Sepal length [standardized]')

>>> plt.ylabel('Petal length [standardized]')

>>> plt.legend(loc='upper left')

>>> plt.tight_layout()

>>> plt.show()

>>> plt.plot(range(1, len(ada_gd.losses_) + 1),

... ada_gd.losses_, marker='o')

>>> plt.xlabel('Epochs')

>>> plt.ylabel('Mean squared error')

>>> plt.tight_layout()

>>> plt.show()
CopyExplain
After executing this code, we should see a figure of the decision regions, as well as
a plot of the declining loss, as shown in Figure 2.14:

Figure 2.14: Plots of Adaline’s decision regions and MSE by number of epochs

As we can see in the plots, Adaline has now converged after training on the
standardized features. However, note that the MSE remains non-zero even though
all flower examples were classified correctly.

Large-scale machine learning and stochastic


gradient descent
In the previous section, we learned how to minimize a loss function by taking a
step in the opposite direction of the loss gradient that is calculated from the whole
training dataset; this is why this approach is sometimes also referred to as full
batch gradient descent. Now imagine that we have a very large dataset with
millions of data points, which is not uncommon in many machine learning
applications. Running full batch gradient descent can be computationally quite
costly in such scenarios, since we need to reevaluate the whole training dataset
each time we take one step toward the global minimum.

A popular alternative to the batch gradient descent algorithm is stochastic


gradient descent (SGD), which is sometimes also called iterative or online
gradient descent. Instead of updating the weights based on the sum of the
accumulated errors over all training examples, x(i):
we update the parameters incrementally for each training example, for instance:

Although SGD can be considered as an approximation of gradient descent, it


typically reaches convergence much faster because of the more frequent weight
updates. Since each gradient is calculated based on a single training example, the
error surface is noisier than in gradient descent, which can also have the
advantage that SGD can escape shallow local minima more readily if we are
working with nonlinear loss functions, as we will see later in Chapter
11, Implementing a Multilayer Artificial Neural Network from Scratch. To obtain
satisfying results via SGD, it is important to present training data in a random
order; also, we want to shuffle the training dataset for every epoch to prevent
cycles.

Adjusting the learning rate during training

In SGD implementations, the fixed learning rate,  , is often replaced by an


adaptive learning rate that decreases over time, for example:

where c1 and c2 are constants. Note that SGD does not reach the global loss
minimum but an area very close to it. And using an adaptive learning rate, we can
achieve further annealing to the loss minimum.
Another advantage of SGD is that we can use it for online learning. In online
learning, our model is trained on the fly as new training data arrives. This is
especially useful if we are accumulating large amounts of data, for example,
customer data in web applications. Using online learning, the system can
immediately adapt to changes, and the training data can be discarded after
updating the model if storage space is an issue.

Mini-batch gradient descent

A compromise between full batch gradient descent and SGD is so-called mini-


batch gradient descent. Mini-batch gradient descent can be understood as
applying full batch gradient descent to smaller subsets of the training data, for
example, 32 training examples at a time. The advantage over full batch gradient
descent is that convergence is reached faster via mini-batches because of the
more frequent weight updates. Furthermore, mini-batch learning allows us to
replace the loop over the training examples in SGD with vectorized operations
leveraging concepts from linear algebra (for example, implementing a weighted
sum via a dot product), which can further improve the computational efficiency of
our learning algorithm.for

Since we already implemented the Adaline learning rule using gradient descent,
we only need to make a few adjustments to modify the learning algorithm to update
the weights via SGD. Inside the method, we will now update the weights after each
training example. Furthermore, we will implement an additional method, which
does not reinitialize the weights, for online learning. In order to check whether our
algorithm converged after training, we will calculate the loss as the average loss of
the training examples in each epoch. Furthermore, we will add an option to shuffle
the training data before each epoch to avoid repetitive cycles when we are
optimizing the loss function; via the parameter, we allow the specification of a
random seed for reproducibility:fitpartial_fitrandom_state

class AdalineSGD:

"""ADAptive LInear NEuron classifier.

Parameters
------------

eta : float

Learning rate (between 0.0 and 1.0)

n_iter : int

Passes over the training dataset.

shuffle : bool (default: True)

Shuffles training data every epoch if True to prevent

cycles.

random_state : int

Random number generator seed for random weight

initialization.

Attributes

-----------

w_ : 1d-array

Weights after fitting.

b_ : Scalar

Bias unit after fitting.

losses_ : list

Mean squared error loss function value averaged over all

training examples in each epoch.

"""

def __init__(self, eta=0.01, n_iter=10,

shuffle=True, random_state=None):

self.eta = eta
self.n_iter = n_iter

self.w_initialized = False

self.shuffle = shuffle

self.random_state = random_state

def fit(self, X, y):

""" Fit training data.

Parameters

----------

X : {array-like}, shape = [n_examples, n_features]

Training vectors, where n_examples is the number of

examples and n_features is the number of features.

y : array-like, shape = [n_examples]

Target values.

Returns

-------

self : object

"""

self._initialize_weights(X.shape[1])

self.losses_ = []

for i in range(self.n_iter):

if self.shuffle:

X, y = self._shuffle(X, y)

losses = []

for xi, target in zip(X, y):


losses.append(self._update_weights(xi, target))

avg_loss = np.mean(losses)

self.losses_.append(avg_loss)

return self

def partial_fit(self, X, y):

"""Fit training data without reinitializing the weights"""

if not self.w_initialized:

self._initialize_weights(X.shape[1])

if y.ravel().shape[0] > 1:

for xi, target in zip(X, y):

self._update_weights(xi, target)

else:

self._update_weights(X, y)

return self

def _shuffle(self, X, y):

"""Shuffle training data"""

r = self.rgen.permutation(len(y))

return X[r], y[r]

def _initialize_weights(self, m):

"""Initialize weights to small random numbers"""

self.rgen = np.random.RandomState(self.random_state)

self.w_ = self.rgen.normal(loc=0.0, scale=0.01,

size=m)

self.b_ = np.float_(0.)
self.w_initialized = True

def _update_weights(self, xi, target):

"""Apply Adaline learning rule to update the weights"""

output = self.activation(self.net_input(xi))

error = (target - output)

self.w_ += self.eta * 2.0 * xi * (error)

self.b_ += self.eta * 2.0 * error

loss = error**2

return loss

def net_input(self, X):

"""Calculate net input"""

return np.dot(X, self.w_) + self.b_

def activation(self, X):

"""Compute linear activation"""

return X

def predict(self, X):

"""Return class label after unit step"""

return np.where(self.activation(self.net_input(X))

>= 0.5, 1, 0)
CopyExplain

The method that we are now using in the classifier works as follows: via


the function in , we generate a random sequence of unique numbers in the range 0
to 100. Those numbers can then be used as indices to shuffle our feature matrix
and class label vector._shuffleAdalineSGDpermutationnp.random
We can then use the method to train the classifier and use our to plot our training
results:fitAdalineSGDplot_decision_regions

>>> ada_sgd = AdalineSGD(n_iter=15, eta=0.01, random_state=1)

>>> ada_sgd.fit(X_std, y)

>>> plot_decision_regions(X_std, y, classifier=ada_sgd)

>>> plt.title('Adaline - Stochastic gradient descent')

>>> plt.xlabel('Sepal length [standardized]')

>>> plt.ylabel('Petal length [standardized]')

>>> plt.legend(loc='upper left')

>>> plt.tight_layout()

>>> plt.show()

>>> plt.plot(range(1, len(ada_sgd.losses_) + 1), ada_sgd.losses_,

... marker='o')

>>> plt.xlabel('Epochs')

>>> plt.ylabel('Average loss')

>>> plt.tight_layout()

>>> plt.show()
CopyExplain

The two plots that we obtain from executing the preceding code example are
shown in Figure 2.15:
Figure 2.15: Decision regions and average loss plots after training an Adaline
model using SGD

Como você pode ver, a perda média cai muito rapidamente, e o limite de decisão
final após 15 épocas parece semelhante à descida de gradiente de lote Adaline.
Se quisermos atualizar nosso modelo, por exemplo, em um cenário de
aprendizado on-line com dados de streaming, podemos simplesmente chamar o
método em exemplos de treinamento individuais — por
exemplo, .partial_fitada_sgd.partial_fit(X_std[0, :], y[0])

Resumo
Neste capítulo, obtivemos uma boa compreensão dos conceitos básicos dos
classificadores lineares para aprendizagem supervisionada. Depois de
implementarmos um perceptron, vimos como podemos treinar neurônios lineares
adaptativos de forma eficiente por meio de uma implementação vetorizada de
descida de gradiente e aprendizado on-line via SGD.

Agora que vimos como implementar classificadores simples em Python, estamos


prontos para passar para o próximo capítulo, onde usaremos a biblioteca de
aprendizado de máquina Python scikit-learn para obter acesso a classificadores de
aprendizado de máquina mais avançados e poderosos, que são comumente
usados na academia, bem como na indústria.

A abordagem orientada a objetos que usamos para implementar os algoritmos


perceptron e Adaline ajudará na compreensão da API scikit-learn, que é
implementada com base nos mesmos conceitos centrais que usamos neste
capítulo: os e métodos. Com base nesses conceitos centrais, aprenderemos sobre
regressão logística para modelagem de probabilidades de classe e máquinas de
vetores de suporte para trabalhar com limites de decisão não lineares. Além disso,
introduziremos uma classe diferente de algoritmos de aprendizagem
supervisionada, algoritmos baseados em árvores, que são comumente
combinados em classificadores de conjunto robustos. fitpredict

Junte-se ao espaço Discord do nosso livro


Junte-se à nossa comunidade do Discord para conhecer pessoas que pensam
como você e aprender ao lado de mais de 2000 membros em:

https://packt.link/MLwPyTorch

Um tour pelos classificadores de


aprendizado de máquina usando o Scikit-
Learn
Neste capítulo, faremos um tour por uma seleção de algoritmos de aprendizado de
máquina populares e poderosos que são comumente usados na academia, bem
como na indústria. Ao aprender sobre as diferenças entre vários algoritmos de
aprendizagem supervisionada para classificação, também desenvolveremos uma
apreciação de seus pontos fortes e fracos individuais. Além disso, daremos
nossos primeiros passos com a biblioteca scikit-learn, que oferece uma interface
amigável e consistente para usar esses algoritmos de forma eficiente e produtiva.

Os tópicos que serão abordados ao longo deste capítulo são os seguintes:

 Uma introdução a algoritmos robustos e populares para classificação, como


regressão logística, máquinas de vetores de suporte, árvores de decisão
e vizinhos k-mais próximos
 Exemplos e explicações usando a biblioteca de aprendizado de máquina
scikit-learn, que fornece uma ampla variedade de algoritmos de
aprendizado de máquina por meio de um usuário...

Construindo bons conjuntos de dados de


treinamento – Pré-processamento de
dados
A qualidade dos dados e a quantidade de informações úteis que eles contêm são
fatores-chave que determinam o quão bem um algoritmo de aprendizado de
máquina pode aprender. Portanto, é absolutamente crítico garantir que
examinamos e pré-processamos um conjunto de dados antes de alimentá-lo para
um algoritmo de aprendizado de máquina. Neste capítulo, discutiremos as
técnicas essenciais de pré-processamento de dados que nos ajudarão a construir
bons modelos de aprendizado de máquina.

Os tópicos que abordaremos neste capítulo são os seguintes:

 Removendo e imputando valores ausentes do conjunto de dados

 Obtendo dados categóricos em forma para algoritmos de aprendizado de


máquina

 Selecionando características relevantes para a construção do modelo

Lidando com dados ausentes


Não é incomum em aplicativos do mundo real que nossos exemplos de
treinamento estejam perdendo um ou mais valores por vários motivos. Pode ter
havido um erro no processo de coleta de dados, certas medidas podem não ser
aplicáveis ou determinados campos podem ter sido simplesmente deixados em
branco em uma pesquisa, por exemplo. Normalmente, vemos valores ausentes
como espaços em branco em nossa tabela de dados ou como cadeias de
caracteres de espaço reservado, como , que significa "não é um número" ou (um
indicador comumente usado de valores desconhecidos em bancos de dados
relacionais). Infelizmente, a maioria das ferramentas computacionais é incapaz de
lidar com esses valores ausentes ou produzirá resultados imprevisíveis se
simplesmente ignorá-los. Portanto, é crucial que cuidemos desses valores
ausentes antes de prosseguirmos com análises adicionais. NaNNULL

Nesta seção, trabalharemos com várias técnicas práticas para lidar com valores
ausentes, removendo entradas de nosso conjunto de dados ou imputando valores
ausentes de outros exemplos e recursos de treinamento.

Identificando valores ausentes em dados tabulares


Antes de discutirmos várias técnicas para lidar com valores ausentes, vamos criar
um exemplo simples de um arquivo de valores separados por vírgulas (CSV)
para obter uma melhor compreensão do problema: DataFrame

>>> import pandas as pd

>>> from io import StringIO

>>> csv_data = \

... '''A,B,C,D

... 1.0,2.0,3.0,4.0

... 5.0,6.0,,8.0

... 10.0,11.0,12.0,'''

>>> # If you are using Python 2.7, you need

>>> # to convert the string to unicode:

>>> # csv_data = unicode(csv_data)

>>> df = pd.read_csv(StringIO(csv_data))

>>> df

A B C D

0 1.0 2.0 3.0 4.0

1 5.0 6.0 NaN 8.0

2 10.0 11.0 12.0 NaN


CopyExplain

Usando o código anterior, lemos dados formatados em CSV em um pandas


através da função e notamos que as duas células ausentes foram substituídas
por . A função no exemplo de código anterior foi simplesmente usada para fins de
ilustração. Isso nos permitiu ler a cadeia de caracteres atribuída a um pandas
como se fosse um arquivo CSV normal em nosso disco
rígido.DataFrameread_csvNaNStringIOcsv_dataDataFrame

Para um maior, pode ser tedioso procurar valores ausentes manualmente; nesse
caso, podemos usar o método para retornar um com valores booleanos que
indicam se uma célula contém um valor numérico () ou se os dados estão faltando
(). Usando o método, podemos retornar o número de valores ausentes por coluna
da seguinte maneira:DataFrameisnullDataFrameFalseTruesum

>>> df.isnull().sum()

A 0

B 0

C 1

D 1

dtype: int64
CopyExplain

Dessa forma, podemos contar o número de valores faltantes por coluna; Nas


subseções a seguir, veremos diferentes estratégias de como lidar com esses
dados faltantes.

Tratamento conveniente de dados com o DataFrame da pandas

Embora o scikit-learn tenha sido originalmente desenvolvido para trabalhar apenas


com arrays NumPy, às vezes pode ser mais conveniente pré-processar dados
usando pandas. Hoje em dia, a maioria das funções scikit-learn suporta objetos
como entradas, mas como a manipulação de array NumPy é mais madura na API
scikit-learn, recomenda-se usar arrays NumPy quando possível. Observe que você
sempre pode acessar a matriz NumPy subjacente de um por meio do atributo
antes de alimentá-la em um estimador scikit-
learn:DataFrameDataFrameDataFramevalues

>>> df.values

array([[ 1., 2., 3., 4.],

[ 5., 6., nan, 8.],

[ 10., 11., 12., nan]])


CopyExplain
Eliminando exemplos de treinamento ou recursos
com valores ausentes
Uma das maneiras mais fáceis de lidar com dados ausentes é simplesmente
remover os recursos correspondentes (colunas) ou exemplos de
treinamento (linhas) do conjunto de dados completamente; As linhas com valores
ausentes podem ser facilmente descartadas por meio do método: dropna

>>> df.dropna(axis=0)

A B C D

0 1.0 2.0 3.0 4.0


CopyExplain

Da mesma forma, podemos descartar colunas que tenham pelo menos uma em
qualquer linha definindo o argumento como : NaNaxis1

>>> df.dropna(axis=1)

A B

0 1.0 2.0

1 5.0 6.0

2 10.0 11.0
CopyExplain

O método suporta vários parâmetros adicionais que podem ser úteis: dropna

>>> # only drop rows where all columns are NaN

>>> # (returns the whole array here since we don't

>>> # have a row with all values NaN)

>>> df.dropna(how='all')

A B C D

0 1.0 2.0 3.0 4.0


1 5.0 6.0 NaN 8.0

2 10.0 11.0 12.0 NaN

>>> # drop rows that have fewer than 4 real values

>>> df.dropna(thresh=4)

A B C D

0 1.0 2.0 3.0 4.0

>>> # only drop rows where NaN appear in specific columns (here: 'C')

>>> df.dropna(subset=['C'])

A B C D

0 1.0 2.0 3.0 4.0

2 10.0 11.0 12.0 NaN


CopyExplain

Embora a remoção de dados ausentes pareça ser uma abordagem conveniente,


ela também vem com certas desvantagens; Por exemplo, podemos acabar
removendo muitas amostras, o que impossibilitará uma análise confiável. Ou, se
removermos muitas colunas de recursos, correremos o risco de perder
informações valiosas que nosso classificador precisa discriminar entre classes. Na
próxima seção, veremos uma das alternativas mais usadas para lidar com valores
faltantes: as técnicas de interpolação.

Imputação de valores faltantes


Muitas vezes, a remoção de exemplos de treinamento ou a queda de colunas
inteiras de recursos simplesmente não é viável, porque podemos perder muitos
dados valiosos. Nesse caso, podemos usar diferentes técnicas de interpolação
para estimar os valores faltantes dos outros exemplos de treinamento em nosso
conjunto de dados. Uma das técnicas de interpolação mais comuns é
a imputação de média, onde simplesmente substituímos o valor ausente pelo
valor médio de toda a coluna de recurso. Uma maneira conveniente de conseguir
isso é usando a classe de scikit-learn, conforme mostrado no código a
seguir:SimpleImputer
>>> from sklearn.impute import SimpleImputer

>>> import numpy as np

>>> imr = SimpleImputer(missing_values=np.nan, strategy='mean')

>>> imr = imr.fit(df.values)

>>> imputed_data = imr.transform(df.values)

>>> imputed_data

array([[ 1., 2., 3., 4.],

[ 5., 6., 7.5, 8.],

[ 10., 11., 12., 6.]])


CopyExplain

Aqui, substituímos cada valor pela média correspondente, que é calculada


separadamente para cada coluna de recurso. Outras opções para o parâmetro são
ou , onde o último substitui os valores ausentes pelos valores mais frequentes.
Isso é útil para imputar valores de feição categóricos, por exemplo, uma coluna de
recurso que armazena uma codificação de nomes de cores, como vermelho, verde
e azul. Encontraremos exemplos de tais dados mais adiante neste
capítulo.NaNstrategymedianmost_frequent

Alternativamente, uma maneira ainda mais conveniente de imputar valores


ausentes é usando o método de pandas e fornecendo um método de imputação
como argumento. Por exemplo, usando pandas, poderíamos obter a mesma
imputação média diretamente no objeto através do seguinte
comando:fillnaDataFrame

>>> df.fillna(df.mean())
CopyExplain
Figura 4.1: Substituição dos valores em falta nos dados pela média

Métodos adicionais de imputação para dados em falta

Para técnicas adicionais de imputação, incluindo a abordagem baseada em k-


vizinhos mais próximos para imputar características ausentes por vizinhos mais
próximos, recomendamos a documentação de imputação scikit-learn
em https://scikit-learn.org/stable/modules/impute.html.KNNImputer

Entendendo a API do estimador scikit-learn


Na seção anterior, usamos a classe de scikit-learn para imputar valores ausentes
em nosso conjunto de dados. A classe faz parte da chamada
API transformer no scikit-learn, que é usada para implementar classes Python
relacionadas à transformação de dados. (Observe que a API do transformador
scikit-learn não deve ser confundida com a arquitetura do transformador que é
usada no processamento de linguagem natural, que abordaremos com mais
detalhes no Capítulo 16, Transformers – Improving Natural Language Processing
with Attention Mechanisms.) Os dois métodos essenciais desses estimadores são
e . O método é usado para aprender os parâmetros dos dados de treinamento e o
método usa esses parâmetros para transformar os dados. Qualquer matriz de
dados a ser transformada precisa ter o mesmo número de recursos que a matriz
de dados usada para ajustar o
modelo.SimpleImputerSimpleImputerfittransformfittransform

A figura 4.2 ilustra como uma instância de transformador scikit-learn, ajustada nos


dados de treinamento, é usada para transformar um conjunto de dados de
treinamento, bem como um novo conjunto de dados de teste:

Figura 4.2: Usando a API scikit-learn para transformação de dados

Os classificadores que usamos no Capítulo 3, A Tour of Machine Learning


Classifiers Using Scikit-Learn, pertencem aos chamados estimadores em scikit-
learn, com uma API conceitualmente muito semelhante à API do
transformador scikit-learn. Os estimadores têm um método, mas também podem
ter um método, como você verá mais adiante neste capítulo. Como você deve se
lembrar, também usamos o método para aprender os parâmetros de um modelo
quando treinamos esses estimadores para classificação. No entanto, em tarefas
de aprendizagem supervisionada, também fornecemos os rótulos de classe para
ajustar o modelo, que podem ser usados para fazer previsões sobre novos
exemplos de dados não rotulados por meio do método, conforme ilustrado
na Figura 4.3:predicttransformfitpredict
Figura 4.3: Usando a API scikit-learn para modelos preditivos, como
classificadores

Manipulação de dados categóricos


Até agora, temos trabalhado apenas com valores numéricos. No entanto, não é
incomum que conjuntos de dados do mundo real contenham uma ou mais colunas
de recursos categóricas. Nesta seção, faremos uso de exemplos simples, mas
eficazes, para ver como lidar com esse tipo de dados em bibliotecas de
computação numérica.
Quando estamos falando de dados categóricos, temos que distinguir melhor entre
características ordinais e nominais. As características ordinais podem ser
entendidas como valores categóricos que podem ser classificados ou ordenados.
Por exemplo, o tamanho da camiseta seria uma característica ordinal, porque
podemos definir uma ordem: XL > L > M. Em contraste, as
características nominais não implicam qualquer ordem; Para continuar com o
exemplo anterior, poderíamos pensar na cor da camiseta como uma característica
nominal, já que normalmente não faz sentido dizer que, por exemplo, o vermelho é
maior que o azul.

Codificação de dados categóricos com pandas


Antes de explorarmos diferentes técnicas para lidar com esses dados categóricos,
vamos criar um novo para ilustrar o problema:DataFrame

>>> import pandas as pd

>>> df = pd.DataFrame([

... ['green', 'M', 10.1, 'class2'],

... ['red', 'L', 13.5, 'class1'],

... ['blue', 'XL', 15.3, 'class2']])

>>> df.columns = ['color', 'size', 'price', 'classlabel']

>>> df

color size price classlabel

0 green M 10.1 class2

1 red L 13.5 class1

2 blue XL 15.3 class2


CopyExplain

Como podemos ver na saída anterior, o recém-criado contém um recurso nominal


(), um recurso ordinal () e uma coluna de feição numérica (). Os rótulos de classe
(supondo que criamos um conjunto de dados para uma tarefa de aprendizado
supervisionado) são armazenados na última coluna. Os algoritmos de
aprendizagem para classificação que discutimos neste livro não usam informações
ordinais em rótulos de classe.DataFramecolorsizeprice

Mapeando recursos ordinais


Para garantir que o algoritmo de aprendizado interprete os recursos ordinais
corretamente, precisamos converter os valores de cadeia de caracteres
categóricos em inteiros. Infelizmente, não há nenhuma função conveniente que
possa derivar automaticamente a ordem correta dos rótulos do nosso recurso,
então temos que definir o mapeamento manualmente. No exemplo simples a
seguir, vamos supor que sabemos a diferença numérica entre características, por
exemplo, XL = L + 1 = M + 2:size

>>> size_mapping = {'XL': 3,

... 'L': 2,

... 'M': 1}

>>> df['size'] = df['size'].map(size_mapping)

>>> df

color size price classlabel

0 green 1 10.1 class2

1 red 2 13.5 class1

2 blue 3 15.3 class2


CopyExplain

Se quisermos transformar os valores inteiros de volta para a representação de


cadeia de caracteres original em um estágio posterior, podemos simplesmente
definir um dicionário de mapeamento reverso, que pode ser usado por meio do
método pandas na coluna de feição transformada e é semelhante ao dicionário
que usamos anteriormente. Podemos usá-lo da seguinte forma:inv_size_mapping =
{v: k for k, v in size_mapping.items()}mapsize_mapping

>>> inv_size_mapping = {v: k for k, v in size_mapping.items()}

>>> df['size'].map(inv_size_mapping)
0 M

1 L

2 XL

Name: size, dtype: object


CopyExplain

Codificação de rótulos de classe


Muitas bibliotecas de aprendizado de máquina exigem que os rótulos de classe
sejam codificados como valores inteiros. Embora a maioria dos estimadores para
classificação em scikit-learn converta rótulos de classe em inteiros internamente, é
considerada uma boa prática fornecer rótulos de classe como matrizes inteiras
para evitar falhas técnicas. Para codificar os rótulos de classe, podemos usar uma
abordagem semelhante ao mapeamento de recursos ordinais discutidos
anteriormente. Precisamos lembrar que os rótulos de classe não são ordinais, e
não importa qual número inteiro atribuímos a um rótulo de cadeia de caracteres
específico. Assim, podemos simplesmente enumerar os rótulos de classe,
começando em:0

>>> import numpy as np

>>> class_mapping = {label: idx for idx, label in

... enumerate(np.unique(df['classlabel']))}

>>> class_mapping

{'class1': 0, 'class2': 1}
CopyExplain

Em seguida, podemos usar o dicionário de mapeamento para transformar os


rótulos de classe em inteiros:

>>> df['classlabel'] = df['classlabel'].map(class_mapping)

>>> df

color size price classlabel


0 green 1 10.1 1

1 red 2 13.5 0

2 blue 3 15.3 1
CopyExplain

Podemos inverter os pares chave-valor no dicionário de mapeamento da seguinte


forma para mapear os rótulos de classe convertidos de volta para a representação
de cadeia de caracteres original:

>>> inv_class_mapping = {v: k for k, v in class_mapping.items()}

>>> df['classlabel'] = df['classlabel'].map(inv_class_mapping)

>>> df

color size price classlabel

0 green 1 10.1 class2

1 red 2 13.5 class1

2 blue 3 15.3 class2


CopyExplain

Alternativamente, há uma classe conveniente implementada diretamente no scikit-


learn para conseguir isso:LabelEncoder

>>> from sklearn.preprocessing import LabelEncoder

>>> class_le = LabelEncoder()

>>> y = class_le.fit_transform(df['classlabel'].values)

>>> y

array([1, 0, 1])
CopyExplain

Observe que o método é apenas um atalho para chamar e separadamente, e


podemos usar o método para transformar os rótulos de classe inteira de volta em
sua representação de cadeia de caracteres
original:fit_transformfittransforminverse_transform
>>> class_le.inverse_transform(y)

array(['class2', 'class1', 'class2'], dtype=object)


CopyExplain

Executando codificação one-hot em recursos


nominais
Na seção anterior Mapeando recursos ordinais, usamos uma abordagem simples
de mapeamento de dicionário para converter o recurso ordinal em inteiros. Como
os estimadores de classificação do scikit-learn tratam rótulos de classe como
dados categóricos que não implicam em nenhuma ordem (nominal), usamos o
conveniente para codificar os rótulos de cadeia de caracteres em inteiros.
Poderíamos usar uma abordagem semelhante para transformar a coluna nominal
do nosso conjunto de dados, da seguinte maneira: sizeLabelEncodercolor

>>> X = df[['color', 'size', 'price']].values

>>> color_le = LabelEncoder()

>>> X[:, 0] = color_le.fit_transform(X[:, 0])

>>> X

array([[1, 1, 10.1],

[2, 2, 13.5],

[0, 3, 15.3]], dtype=object)


CopyExplain

Depois de executar o código anterior, a primeira coluna da matriz NumPy, , agora


contém os novos valores, que são codificados da seguinte maneira: Xcolor

 blue = 0
 green = 1
 red = 2

Se pararmos neste ponto e alimentarmos a matriz para o nosso classificador,


cometeremos um dos erros mais comuns ao lidar com dados categóricos. Você
consegue identificar o problema? Embora os valores de cor não venham em
nenhuma ordem específica, modelos de classificação comuns, como os
abordados nos capítulos anteriores, agora assumirão que é maior que , e é maior
que . Embora essa suposição esteja incorreta, um classificador ainda pode
produzir resultados úteis. No entanto, esses resultados não seriam
ideais.greenblueredgreen

Uma solução alternativa comum para esse problema é usar uma técnica
chamada codificação one-hot. A ideia por trás dessa abordagem é criar um novo
recurso fictício para cada valor exclusivo na coluna de recurso nominal. Aqui,
converteríamos o recurso em três novos recursos: , e . Os valores binários podem
então ser usados para indicar o particular de um exemplo; Por exemplo, um
exemplo pode ser codificado como , , . Para realizar essa transformação,
podemos utilizar o que é implementado no módulo scikit-
learn:colorbluegreenredcolorblueblue=1green=0red=0OneHotEncoderpreprocessing

>>> from sklearn.preprocessing import OneHotEncoder

>>> X = df[['color', 'size', 'price']].values

>>> color_ohe = OneHotEncoder()

>>> color_ohe.fit_transform(X[:, 0].reshape(-1, 1)).toarray()

array([[0., 1., 0.],

[0., 0., 1.],

[1., 0., 0.]])


CopyExplain

Observe que aplicamos o a apenas uma única coluna, , para evitar modificar as
outras duas colunas na matriz também. Se quisermos transformar seletivamente
colunas em uma matriz de vários recursos, podemos usar o , que aceita uma lista
de tuplas da seguinte maneira:OneHotEncoder(X[:, 0].reshape(-1,
1))ColumnTransformer(name, transformer, column(s))

>>> from sklearn.compose import ColumnTransformer

>>> X = df[['color', 'size', 'price']].values


>>> c_transf = ColumnTransformer([

... ('onehot', OneHotEncoder(), [0]),

... ('nothing', 'passthrough', [1, 2])

... ])

>>> c_transf.fit_transform(X).astype(float)

array([[0.0, 1.0, 0.0, 1, 10.1],

[0.0, 0.0, 1.0, 2, 13.5],

[1.0, 0.0, 0.0, 3, 15.3]])


CopyExplain

No exemplo de código anterior, especificamos que queremos modificar apenas a


primeira coluna e deixar as outras duas colunas intocadas por meio do
argumento.'passthrough'

Uma maneira ainda mais conveniente de criar esses recursos fictícios por meio de
uma codificação quente é usar o método implementado em pandas. Aplicado a um
, o método converterá apenas colunas de cadeia de caracteres e deixará todas as
outras colunas inalteradas:get_dummiesDataFrameget_dummies

>>> pd.get_dummies(df[['price', 'color', 'size']])

price size color_blue color_green color_red

0 10.1 1 0 1 0

1 13.5 2 0 0 1

2 15.3 3 1 0 0
CopyExplain

Quando estamos usando conjuntos de dados de codificação one-hot, temos que


ter em mente que isso introduz a multicolinearidade, o que pode ser um problema
para certos métodos (por exemplo, métodos que exigem inversão de matriz). Se
as características são altamente correlacionadas, as matrizes são
computacionalmente difíceis de inverter, o que pode levar a estimativas
numericamente instáveis. Para reduzir a correlação entre as variáveis, podemos
simplesmente remover uma coluna de recurso da matriz codificada em um ativo.
Observe que não perdemos nenhuma informação importante removendo uma
coluna de recurso, no entanto; Por exemplo, se removermos a coluna, as
informações do recurso ainda serão preservadas, pois se observarmos e , isso
implica que a observação deve ser .color_bluecolor_green=0color_red=0blue

Se usarmos a função, podemos descartar a primeira coluna passando um


argumento para o parâmetro, como mostrado no exemplo de código a
seguir:get_dummiesTruedrop_first

>>> pd.get_dummies(df[['price', 'color', 'size']],

... drop_first=True)

price size color_green color_red

0 10.1 1 1 0

1 13.5 2 0 1

2 15.3 3 0 0
CopyExplain

Para soltar uma coluna redundante através do , precisamos definir e definir da


seguinte maneira:OneHotEncoderdrop='first'categories='auto'

>>> color_ohe = OneHotEncoder(categories='auto', drop='first')

>>> c_transf = ColumnTransformer([

... ('onehot', color_ohe, [0]),

... ('nothing', 'passthrough', [1, 2])

... ])

>>> c_transf.fit_transform(X).astype(float)

array([[ 1. , 0. , 1. , 10.1],

[ 0. , 1. , 2. , 13.5],

[ 0. , 0. , 3. , 15.3]])
CopyExplain

Esquemas de codificação adicionais para dados nominais


Embora a codificação one-hot seja a maneira mais comum de codificar variáveis
categóricas não ordenadas, existem vários métodos alternativos. Algumas dessas
técnicas podem ser úteis ao trabalhar com características categóricas que têm alta
cardinalidade (um grande número de rótulos de categoria exclusivos). Exemplos
incluem:

 Codificação binária, que produz vários recursos binários semelhantes à


codificação a quente, mas requer menos colunas de recursos, ou seja, log 2(K)
em vez de K – 1, onde K é o número de categorias únicas. Na codificação
binária, os números são primeiro convertidos em representações binárias e,
em seguida, cada posição numérica binária formará uma nova coluna de
recurso.
 Contagem ou codificação de frequência, que substitui o rótulo de cada
categoria pelo número de vezes ou frequência que ocorre no conjunto de
treinamento.

Esses métodos, bem como esquemas de codificação categórica adicionais, estão


disponíveis através da biblioteca compatível com scikit-learn: https://contrib.scikit-
learn.org/category_encoders/.category_encoders

Embora não seja garantido que esses métodos tenham um desempenho melhor
do que a codificação a quente em termos de desempenho do modelo, podemos
considerar a escolha de um esquema de codificação categórica como um
"hiperparâmetro" adicional para melhorar o desempenho do modelo.

Opcional: recursos ordinais de codificação

Se não tivermos certeza sobre as diferenças numéricas entre as categorias de


características ordinais, ou a diferença entre dois valores ordinais não estiver
definida, também podemos codificá-las usando uma codificação de limiar com
valores 0/1. Por exemplo, podemos dividir o recurso com valores , e em dois
novos recursos, e . Vamos considerar o original:sizeMLXLx > Mx > LDataFrame

>>> df = pd.DataFrame([['green', 'M', 10.1,

... 'class2'],
... ['red', 'L', 13.5,

... 'class1'],

... ['blue', 'XL', 15.3,

... 'class2']])

>>> df.columns = ['color', 'size', 'price',

... 'classlabel']

>>> df
CopyExplain

Podemos usar o método de pandas' para escrever expressões lambda


personalizadas a fim de codificar essas variáveis usando a abordagem valor-
limite:applyDataFrame

>>> df['x > M'] = df['size'].apply(

... lambda x: 1 if x in {'L', 'XL'} else 0)

>>> df['x > L'] = df['size'].apply(

... lambda x: 1 if x == 'XL' else 0)

>>> del df['size']

>>> df
CopyExplain

Particionando um conjunto de dados em


conjuntos de dados de treinamento e
teste separados
Introduzimos brevemente o conceito de particionar um conjunto de dados em
conjuntos de dados separados para treinamento e teste no Capítulo 1, Dando aos
Computadores a Capacidade de Aprender com os Dados, e no Capítulo 3, Um
Tour de Classificadores de Aprendizado de Máquina Usando o Scikit-Learn.
Lembre-se de que comparar previsões com rótulos verdadeiros no conjunto de
testes pode ser entendido como a avaliação de desempenho imparcial do nosso
modelo antes de deixá-lo solto no mundo real. Nesta seção, prepararemos um
novo conjunto de dados, o conjunto de dados do Wine. Depois de pré-
processarmos o conjunto de dados, exploraremos diferentes técnicas de seleção
de recursos para reduzir a dimensionalidade de um conjunto de dados.

O conjunto de dados do Wine é outro conjunto de dados de código aberto que


está disponível no repositório de aprendizado de máquina UCI
(https://archive.ics.uci.edu/ml/datasets/Wine); É composto por 178 exemplos de
vinho com 13 características que descrevem suas diferentes propriedades
químicas.

Obtendo o conjunto de dados do Wine

Você pode encontrar uma cópia do conjunto de dados do Wine (e todos os outros


conjuntos de dados usados neste livro) no pacote de códigos deste livro, que você
pode usar se estiver trabalhando offline ou se o conjunto de dados
no https://archive.ics.uci.edu/ml/machine-learning-databases/wine/wine.data estive
r temporariamente indisponível no servidor UCI. Por exemplo, para carregar o
conjunto de dados do Wine de um diretório local, você pode substituir esta linha:

df = pd.read_csv(

'https://archive.ics.uci.edu/ml/'

'machine-learning-databases/wine/wine.data',

header=None

)
CopyExplain

com o seguinte:

df = pd.read_csv(

'your/local/path/to/wine.data', header=None

)
CopyExplain
Usando a biblioteca de pandas, leremos diretamente no conjunto de dados de
código aberto do Wine do repositório de aprendizado de máquina UCI:

>>> df_wine = pd.read_csv('https://archive.ics.uci.edu/'

... 'ml/machine-learning-databases/'

... 'wine/wine.data', header=None)

>>> df_wine.columns = ['Class label', 'Alcohol',

... 'Malic acid', 'Ash',

... 'Alcalinity of ash', 'Magnesium',

... 'Total phenols', 'Flavanoids',

... 'Nonflavanoid phenols',

... 'Proanthocyanins',

... 'Color intensity', 'Hue',

... 'OD280/OD315 of diluted wines',

... 'Proline']

>>> print('Class labels', np.unique(df_wine['Class label']))

Class labels [1 2 3]

>>> df_wine.head()
CopyExplain

As 13 características diferentes no conjunto de dados do Wine, descrevendo as


propriedades químicas dos 178 exemplos de vinho, estão listadas na tabela a
seguir:

Figura 4.4: Uma amostra do conjunto de dados Wine


Os exemplos pertencem a uma das três classes diferentes, , e , que se referem
aos três tipos diferentes de uvas cultivadas na mesma região na Itália, mas
derivadas de cultivares de vinho diferentes, conforme descrito no resumo do
conjunto de dados
(https://archive.ics.uci.edu/ml/machine-learning-databases/wine/wine.names).123

Uma maneira conveniente de particionar aleatoriamente esse conjunto de dados


em conjuntos de dados de teste e treinamento separados é usar a função do
submódulo scikit-learn:train_test_splitmodel_selection

>>> from sklearn.model_selection import train_test_split

>>> X, y = df_wine.iloc[:, 1:].values, df_wine.iloc[:, 0].values

>>> X_train, X_test, y_train, y_test =\

... train_test_split(X, y,

... test_size=0.3,

... random_state=0,

... stratify=y)
CopyExplain

Primeiro, atribuímos a representação da matriz NumPy das colunas de recurso 1-


13 à variável e atribuímos os rótulos de classe da primeira coluna à variável . Em
seguida, usamos a função para dividir aleatoriamente e em conjuntos de dados de
treinamento e teste separados.Xytrain_test_splitXy

Ao definir , atribuímos 30 por cento dos exemplos de vinho a e , e os restantes 70


por cento dos exemplos foram atribuídos a e , respectivamente. Fornecer a matriz
de rótulo de classe como um argumento para garantir que os conjuntos de dados
de treinamento e teste tenham as mesmas proporções de classe que o conjunto
de dados original.test_size=0.3X_testy_testX_trainy_trainystratify

Escolhendo uma proporção apropriada para particionar um conjunto de


dados em conjuntos de dados de treinamento e teste

Se estamos dividindo um conjunto de dados em conjuntos de dados de


treinamento e teste, temos que ter em mente que estamos retendo informações
valiosas das quais o algoritmo de aprendizado poderia se beneficiar. Assim, não
queremos alocar muitas informações para o conjunto de testes. No entanto,
quanto menor o conjunto de testes, mais imprecisa é a estimativa do erro de
generalização. Dividir um conjunto de dados em conjuntos de dados de
treinamento e teste tem tudo a ver com equilibrar essa compensação. Na prática,
as divisões mais usadas são 60:40, 70:30 ou 80:20, dependendo do tamanho do
conjunto de dados inicial. No entanto, para grandes conjuntos de dados, divisões
90:10 ou 99:1 também são comuns e apropriadas. Por exemplo, se o conjunto de
dados contiver mais de 100.000 exemplos de treinamento, talvez seja bom reter
apenas 10.000 exemplos para teste a fim de obter uma boa estimativa do
desempenho de generalização. Mais informações e ilustrações podem ser
encontradas na seção um do meu artigo Avaliação de modelo, seleção de
modelos e seleção de algoritmos em aprendizado de máquina, que está disponível
gratuitamente em https://arxiv.org/pdf/1811.12808.pdf. Além disso, revisitaremos o
tópico da avaliação de modelos e discutiremos com mais detalhes no Capítulo
6, Aprendendo as melhores práticas para avaliação de modelos e ajuste de
hiperparâmetros.

Além disso, em vez de descartar os dados de teste alocados após o treinamento e


a avaliação do modelo, é uma prática comum treinar um classificador em todo o
conjunto de dados, pois isso pode melhorar o desempenho preditivo do modelo.
Embora essa abordagem seja geralmente recomendada, ela pode levar a um pior
desempenho de generalização se o conjunto de dados for pequeno e o conjunto
de dados de teste contiver valores atípicos, por exemplo. Além disso, depois de
reajustar o modelo em todo o conjunto de dados, não temos mais dados
independentes para avaliar seu desempenho.

Trazendo recursos para a mesma escala


O dimensionamento de recursos é uma etapa crucial em nosso pipeline de pré-
processamento que pode ser facilmente esquecida. Árvores de
decisão e florestas aleatórias são dois dos poucos algoritmos de aprendizado de
máquina em que não precisamos nos preocupar com o dimensionamento de
recursos. Esses algoritmos são invariantes em escala. No entanto, a maioria dos
algoritmos de aprendizado de máquina e otimização se comportam muito melhor
se os recursos estiverem na mesma escala, como vimos no Capítulo 2, Treinando
algoritmos simples de aprendizado de máquina para classificação, quando
implementamos o algoritmo de otimização de descida de gradiente.

A importância do dimensionamento de recursos pode ser ilustrada por um


exemplo simples. Vamos supor que temos duas características onde uma
característica é medida em uma escala de 1 a 10 e a segunda característica é
medida em uma escala de 1 a 100.000, respectivamente.

Quando pensamos na função de erro quadrado em Adaline do Capítulo 2, faz


sentido dizer que o algoritmo estará ocupado principalmente otimizando os pesos
de acordo com os erros maiores no segundo recurso. Outro exemplo é o
algoritmo k-nearest neighbors (KNN) com uma medida de distância euclidiana:
as distâncias computadas entre os exemplos serão dominadas pelo segundo eixo
de feição.

Agora, existem duas abordagens comuns para trazer diferentes recursos para a


mesma escala: normalização e padronização. Esses termos são frequentemente
usados de forma bastante vaga em diferentes campos, e o significado tem que ser
derivado do contexto. Na maioria das vezes, a normalização refere-se ao
reescalonamento dos recursos para um intervalo de [0, 1], que é um caso
especial de dimensionamento min-max. Para normalizar nossos dados,
podemos simplesmente aplicar o dimensionamento min-max a cada coluna de

recurso, onde o novo valor, , de um exemplo,  x(eu), pode ser


calculado da seguinte forma:

Aqui, x(eu) é um exemplo particular, xMin é o menor valor em uma coluna de recurso


e x.max é o maior valor.
O procedimento de escalonamento min-max é implementado no scikit-learn e
pode ser usado da seguinte maneira:

>>> from sklearn.preprocessing import MinMaxScaler

>>> mms = MinMaxScaler()

>>> X_train_norm = mms.fit_transform(X_train)

>>> X_test_norm = mms.transform(X_test)


CopyExplain

Embora a normalização via escalonamento min-max seja uma técnica comumente


usada que é útil quando precisamos de valores em um intervalo limitado, a
padronização pode ser mais prática para muitos algoritmos de aprendizado de
máquina, especialmente para algoritmos de otimização, como a descida de
gradiente. A razão é que muitos modelos lineares, como a regressão logística e a
SVM do Capítulo 3, inicializam os pesos para 0 ou pequenos valores aleatórios
próximos de 0. Usando a padronização, centralizamos as colunas de feição em
média 0 com desvio padrão 1 para que as colunas de feição tenham os mesmos
parâmetros de uma distribuição normal padrão (média zero e variância unitária), o
que facilita o aprendizado dos pesos. No entanto, enfatizaremos que a
padronização não altera a forma da distribuição e não transforma dados não
normalmente distribuídos em dados normalmente distribuídos. Além de
dimensionar os dados de forma que eles tenham média zero e variância unitária, a
padronização mantém informações úteis sobre outliers e torna o algoritmo menos
sensível a eles, em contraste com o dimensionamento min-max, que dimensiona
os dados para um intervalo limitado de valores.

O procedimento de padronização pode ser expresso pela seguinte equação:


Aqui,   é a média de amostra de uma coluna de feição específica e   é o
desvio padrão correspondente.

A tabela a seguir ilustra a diferença entre as duas técnicas de dimensionamento


de recursos comumente usadas, padronização e normalização, em um conjunto
de dados de exemplo simples que consiste em números de 0 a 5:

Entrad
Padronizado Mín-máx normalizado
a

0.0 -1.46385 0.0

1.0 -0.87831 0.2

2.0 -0.29277 0.4

3.0 0.29277 0.6

4.0 0.87831 0.8

5.0 1.46385 1.0

Tabela 4.1: Comparação entre padronização e normalização mín-máx

Você pode executar a padronização e normalização mostradas na tabela


manualmente executando os seguintes exemplos de código:

>>> ex = np.array([0, 1, 2, 3, 4, 5])

>>> print('standardized:', (ex - ex.mean()) / ex.std())

standardized: [-1.46385011 -0.87831007 -0.29277002 0.29277002

0.87831007 1.46385011]

>>> print('normalized:', (ex - ex.min()) / (ex.max() - ex.min()))

normalized: [ 0. 0.2 0.4 0.6 0.8 1. ]


CopyExplain
Semelhante à classe, scikit-learn também implementa uma classe para
padronização:MinMaxScaler

>>> from sklearn.preprocessing import StandardScaler

>>> stdsc = StandardScaler()

>>> X_train_std = stdsc.fit_transform(X_train)

>>> X_test_std = stdsc.transform(X_test)


CopyExplain

Novamente, também é importante destacar que ajustamos a classe apenas uma


vez — nos dados de treinamento — e usamos esses parâmetros para transformar
o conjunto de dados de teste ou qualquer novo ponto de dados. StandardScaler

Outros métodos mais avançados para dimensionamento de recursos estão


disponíveis no scikit-learn, como o . é especialmente útil e recomendado se
estivermos trabalhando com pequenos conjuntos de dados que contêm muitos
outliers. Da mesma forma, se o algoritmo de aprendizado de máquina aplicado a
esse conjunto de dados for propenso a sobreajuste, pode ser uma boa escolha.
Operando em cada coluna de feição independentemente, remove o valor mediano
e dimensiona o conjunto de dados de acordo com o 1º e 3º quartil do conjunto de
dados (ou seja, o 25º e 75º quantil, respectivamente) de tal forma que valores
mais extremos e outliers se tornem menos pronunciados. O leitor interessado
pode encontrar mais informações sobre na documentação oficial do scikit-learn
em https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.Robust
Scaler.html.RobustScalerRobustScalerRobustScalerRobustScalerRobustScaler

Selecionando recursos significativos


Se notarmos que um modelo tem um desempenho muito melhor em um conjunto
de dados de treinamento do que no conjunto de dados de teste, essa observação
é um forte indicador de sobreajuste. Como discutimos no Capítulo 3, A Tour of
Machine Learning Classifiers Using Scikit-Learn, overfitting significa que o modelo
se ajusta demais aos parâmetros em relação às observações específicas no
conjunto de dados de treinamento, mas não generaliza bem para novos dados;
Dizemos que o modelo tem uma variância alta. A razão para o overfitting é que
nosso modelo é muito complexo para os dados de treinamento fornecidos. As
soluções comuns para reduzir o erro de generalização são as seguintes:

 Coletar mais dados de treinamento

 Introduzir uma penalidade por complexidade via regularização

 Escolha um modelo mais simples com menos parâmetros

 Reduzir a dimensionalidade dos dados

A coleta de mais dados de treinamento muitas vezes não é aplicável. No Capítulo


6, Aprendendo as melhores práticas para avaliação de modelos e ajuste de
hiperparâmetros, aprenderemos sobre uma técnica útil para verificar se mais
dados de treinamento são úteis. Nas seções a seguir, veremos maneiras comuns
de reduzir o overfitting por regularização e redução de dimensionalidade por meio
da seleção de recursos, o que leva a modelos mais simples ao exigir menos
parâmetros a serem ajustados aos dados. Em seguida, no Capítulo
5, Compactando dados via redução de dimensionalidade, daremos uma olhada
em técnicas adicionais de extração de recursos.

Regularização L1 e L2 como penalidades contra a


complexidade do modelo
Você vai lembrar do Capítulo 3 que a regularização L2 é uma abordagem para
reduzir a complexidade de um modelo, penalizando grandes pesos individuais.
Definimos a norma L2 quadrada do nosso vetor de peso, w, da seguinte forma:

Outra abordagem para reduzir a complexidade do modelo é a regularização


L1 relacionada:
Aqui, simplesmente substituímos o quadrado dos pesos pela soma dos valores
absolutos dos pesos. Em contraste com a regularização L2, a regularização L1
geralmente produz vetores de feição esparsos, e a maioria dos pesos de feição
será zero. A esparsidade pode ser útil na prática se tivermos um conjunto de
dados de alta dimensão com muitos recursos que são irrelevantes, especialmente
nos casos em que temos dimensões mais irrelevantes do que exemplos de
treinamento. Nesse sentido, a regularização da L1 pode ser entendida como uma
técnica de seleção de características.

Uma interpretação geométrica da regularização L2


Como mencionado na seção anterior, a regularização L2 adiciona um termo de
penalidade à função de perda que efetivamente resulta em valores de peso menos
extremos em comparação com um modelo treinado com uma função de perda não
regularizada.

Para entender melhor como a regularização L1 incentiva a esparsidade, vamos


dar um passo atrás e dar uma olhada em uma interpretação geométrica da
regularização. Vamos plotar os contornos de uma função de perda convexa para
dois coeficientes de peso, w1 e w2.

Aqui, consideraremos a função de perda de erro quadrático médio (MSE) que


usamos para Adaline no Capítulo 2, que calcula as distâncias quadráticas entre os

rótulos de classe verdadeiro e previsto, y e  , calculados pela média de todos os


exemplos N no conjunto de treinamento. Como o EPM é esférico, é mais fácil
desenhar do que a função de perda da regressão logística; no entanto, os
mesmos conceitos se aplicam. Lembre-se que nosso objetivo é encontrar a
combinação de coeficientes de peso que minimizem a função de perda para os
dados de treinamento, como mostra a Figura 4.5 (o ponto no centro das elipses):

Figura 4.5: Minimizando a função de perda de erro quadrática média

Podemos pensar na regularização como a adição de um termo de penalidade à


função de perda para incentivar pesos menores; Ou seja, penalizamos os grandes
pesos. Assim, ao aumentar a força de regularização via parâmetro de

regularização,  reduzimos os pesos para zero e diminuímos a dependência do


nosso modelo em relação aos dados de treinamento. Vamos ilustrar esse conceito
na figura a seguir para o termo de penalidade L2:
Figura 4.6: Aplicação da regularização L2 à função de perda

O termo de regularização L2 quadrática é representado pela bola sombreada.


Aqui, nossos coeficientes de peso não podem exceder nosso orçamento de
regularização – a combinação dos coeficientes de peso não pode ficar fora da
área sombreada. Por outro lado, ainda queremos minimizar a função de perda.
Sob a restrição de penalidade, nosso melhor esforço é escolher o ponto onde a
bola L2 se cruza com os contornos da função de perda não penalizada. Quanto
maior o valor do parâmetro de regularização, mais rápido cresce a perda

penalizada,  o que leva a uma bola L2 mais estreita. Por exemplo, se


aumentarmos o parâmetro de regularização em direção ao infinito, os coeficientes
de peso se tornarão efetivamente zero, denotados pelo centro da bola L2. Para
resumir a mensagem principal do exemplo, nosso objetivo é minimizar a soma da
perda não penalizada mais o termo de penalidade, o que pode ser entendido
como adicionar viés e preferir um modelo mais simples para reduzir a variância na
ausência de dados de treinamento suficientes para se ajustar ao modelo.
Soluções esparsas com regularização L1
Agora, vamos discutir a regularização L1 e a esparsidade. O conceito principal por
trás da regularização L1 é semelhante ao que discutimos na seção anterior. No
entanto, como a penalidade L1 é a soma dos coeficientes de peso absoluto
(lembre-se que o termo L2 é quadrático), podemos representá-la como um
orçamento em forma de diamante, como mostra a Figura 4.7:

Figura 4.7: Aplicação da regularização L1 à função de perda

Na figura anterior, podemos ver que o contorno da função de perda toca o


diamante L1 em w1 = 0. Como os contornos de um sistema regularizado L1 são
nítidos, é mais provável que o ótimo – isto é, a interseção entre as elipses da
função de perda e o limite do diamante L1 – esteja localizado nos eixos, o que
favorece a esparsidade.

L1 regularização e esparsidade
Os detalhes matemáticos de por que a regularização L1 pode levar a soluções
esparsas estão além do escopo deste livro. Se você estiver interessado, uma
excelente explicação da regularização L2 versus L1 pode ser encontrada
na Seção 3.4, The Elements of Statistical Learning de Trevor Hastie, Robert
Tibshirani e Jerome Friedman, Springer Science+Business Media, 2009.

Para modelos regularizados em scikit-learn que suportam a regularização L1,


podemos simplesmente definir o parâmetro para obter uma solução
esparsa:penalty'l1'

>>> from sklearn.linear_model import LogisticRegression

>>> LogisticRegression(penalty='l1',

... solver='liblinear',

... multi_class='ovr')
CopyExplain

Observe que também precisamos selecionar um algoritmo de otimização diferente


(por exemplo, ), já que atualmente não suporta otimização de perda regularizada
L1. Aplicada aos dados padronizados do Wine, a regressão logística regularizada
L1 produziria a seguinte solução esparsa: solver='liblinear''lbfgs'

>>> lr = LogisticRegression(penalty='l1',

... C=1.0,

... solver='liblinear',

... multi_class='ovr')

>>> # Note that C=1.0 is the default. You can increase

>>> # or decrease it to make the regularization effect

>>> # stronger or weaker, respectively.

>>> lr.fit(X_train_std, y_train)

>>> print('Training accuracy:', lr.score(X_train_std, y_train))

Training accuracy: 1.0

>>> print('Test accuracy:', lr.score(X_test_std, y_test))


Test accuracy: 1.0
CopyExplain

As precisões de treinamento e teste (ambas 100%) indicam que nosso modelo faz
um trabalho perfeito em ambos os conjuntos de dados. Quando acessamos os
termos de interceptação por meio do atributo, podemos ver que a matriz retorna
três valores:lr.intercept_

>>> lr.intercept_

array([-1.26317363, -1.21537306, -2.37111954])


CopyExplain

Como ajustamos o objeto em um conjunto de dados multiclasse por meio da


abordagem um-versus-rest (OvR), a primeira interceptação pertence ao modelo
que se ajusta à classe 1 versus classes 2 e 3, o segundo valor é a interceptação
do modelo que se ajusta à classe 2 versus classes 1 e 3, e o terceiro valor é a
interceptação do modelo que se ajusta à classe 3 versus classes 1 e
2:LogisticRegression

>>> lr.coef_

array([[ 1.24647953, 0.18050894, 0.74540443, -1.16301108,

0. ,0. , 1.16243821, 0. ,

0. , 0. , 0. , 0.55620267,

2.50890638],

[-1.53919461, -0.38562247, -0.99565934, 0.36390047,

-0.05892612, 0. , 0.66710883, 0. ,

0. , -1.9318798 , 1.23775092, 0. ,

-2.23280039],

[ 0.13557571, 0.16848763, 0.35710712, 0. ,

0. , 0. , -2.43804744, 0. ,

0. , 1.56388787, -0.81881015, -0.49217022,

0. ]])
CopyExplain

A matriz de peso que acessamos por meio do atributo contém três linhas de
coeficientes de peso, um vetor de peso para cada classe. Cada linha consiste em
13 pesos, onde cada peso é multiplicado pela respectiva característica no conjunto
de dados do Wine de 13 dimensões para calcular a entrada líquida: lr.coef_

Acessando os parâmetros de unidade de viés e peso de estimadores scikit-


learn

No scikit-learn, corresponde à unidade de viés e corresponde aos


valores wintercept_coef_j.

Como resultado da regularização L1, que, como mencionado, serve como um


método para seleção de recursos, acabamos de treinar um modelo que é robusto
para as características potencialmente irrelevantes neste conjunto de dados.
Estritamente falando, porém, os vetores de peso do exemplo anterior não são
necessariamente esparsos porque contêm mais entradas diferentes de zero do
que zero. No entanto, poderíamos impor a esparsidade (mais entradas zero)
aumentando ainda mais a força de regularização, ou seja, escolhendo valores
mais baixos para o parâmetro.C

No último exemplo sobre regularização deste capítulo, vamos variar a força da


regularização e traçar o caminho da regularização – os coeficientes de peso das
diferentes características para diferentes forças de regularização:

>>> import matplotlib.pyplot as plt

>>> fig = plt.figure()

>>> ax = plt.subplot(111)

>>> colors = ['blue', 'green', 'red', 'cyan',

... 'magenta', 'yellow', 'black',


... 'pink', 'lightgreen', 'lightblue',

... 'gray', 'indigo', 'orange']

>>> weights, params = [], []

>>> for c in np.arange(-4., 6.):

... lr = LogisticRegression(penalty='l1', C=10.**c,

... solver='liblinear',

... multi_class='ovr', random_state=0)

... lr.fit(X_train_std, y_train)

... weights.append(lr.coef_[1])

... params.append(10**c)

>>> weights = np.array(weights)

>>> for column, color in zip(range(weights.shape[1]), colors):

... plt.plot(params, weights[:, column],

... label=df_wine.columns[column + 1],

... color=color)

>>> plt.axhline(0, color='black', linestyle='--', linewidth=3)

>>> plt.xlim([10**(-5), 10**5])

>>> plt.ylabel('Weight coefficient')

>>> plt.xlabel('C (inverse regularization strength)')

>>> plt.xscale('log')

>>> plt.legend(loc='upper left')

>>> ax.legend(loc='upper center',

... bbox_to_anchor=(1.38, 1.03),

... ncol=1, fancybox=True)

>>> plt.show()
CopyExplain
A parcela resultante nos fornece mais informações sobre o comportamento da
regularização L1. Como podemos ver, todos os pesos de feição serão zero se
penalizarmos o modelo com um forte parâmetro de regularização (C < 0,01); C é o

inverso do parâmetro de regularização,  :

Figura 4.8: O impacto do valor da força de regularização hiperparâmetro C

Algoritmos de seleção de recursos sequenciais


Uma maneira alternativa de reduzir a complexidade do modelo e evitar o
sobreajuste é a redução da dimensionalidade via seleção de recursos, o que é
especialmente útil para modelos não regularizados. Existem duas categorias
principais de técnicas de redução de dimensionalidade: seleção de características
e extração de características. Através da seleção de recursos, selecionamos um
subconjunto dos recursos originais, enquanto na extração de
recursos, derivamos informações do conjunto de recursos para construir um novo
subespaço de recursos.

Nesta seção, vamos dar uma olhada em uma família clássica de algoritmos de
seleção de recursos. No próximo capítulo, Capítulo 5, Compactando dados via
redução de dimensionalidade, aprenderemos sobre diferentes técnicas de
extração de recursos para compactar um conjunto de dados em um subespaço de
feição de dimensão inferior.
Algoritmos de seleção de feição sequencial são uma família de algoritmos de
busca gananciosos que são usados para reduzir um espaço de feição d-
dimensional inicial para um subespaço de feição k-dimensional onde k<d. A
motivação por trás dos algoritmos de seleção de recursos é selecionar
automaticamente um subconjunto de recursos que são mais relevantes para o
problema, para melhorar a eficiência computacional ou reduzir o erro de
generalização do modelo removendo recursos irrelevantes ou ruído, o que pode
ser útil para algoritmos que não suportam regularização.

Um algoritmo clássico de seleção de características sequenciais é a seleção


sequencial para trás (SBS), que visa reduzir a dimensionalidade do subespaço
de feição inicial com um mínimo decaimento no desempenho do classificador para
melhorar a eficiência computacional. Em certos casos, o SBS pode até melhorar o
poder preditivo do modelo se um modelo sofrer de overfitting.

Algoritmos de busca gananciosos

Algoritmos gananciosos fazem escolhas localmente ótimas em cada estágio de


um problema de busca combinatória e geralmente produzem uma solução
subótima para o problema, em contraste com algoritmos de busca exaustivos, que
avaliam todas as combinações possíveis e têm a garantia de encontrar a solução
ideal. No entanto, na prática, uma busca exaustiva é muitas vezes
computacionalmente inviável, enquanto algoritmos gananciosos permitem uma
solução menos complexa e computacionalmente mais eficiente.

A ideia por trás do algoritmo do SBS é bastante simples: o SBS remove


sequencialmente os recursos do subconjunto completo de recursos até que o novo
subespaço de recursos contenha o número desejado de recursos. Para determinar
qual recurso deve ser removido em cada estágio, precisamos definir a função de
critério, J, que queremos minimizar.

O critério calculado pela função critério pode ser simplesmente a diferença no


desempenho do classificador antes e depois da remoção de uma determinada
característica. Então, o recurso a ser removido em cada estágio pode ser
simplesmente definido como o recurso que maximiza esse critério; Ou, em termos
mais simples, em cada estágio eliminamos o recurso que causa a menor perda de
desempenho após a remoção. Com base na definição anterior de SBS, podemos
delinear o algoritmo em quatro passos simples:

1. Inicialize o algoritmo com k = d, onde d é a dimensionalidade do espaço de


feição completo, Xd.
2. Determine o recurso, x–, que maximiza o critério: x– = argmax J(Xk – x),

onde  .
3. Remova o recurso, x , do conjunto de recursos: Xk–1 = Xk – x–;  k = k – 1.

4. Terminar se k for igual ao número de recursos desejados; caso contrário, vá


para a etapa 2.

Um recurso sobre algoritmos de recursos sequenciais

Você pode encontrar uma avaliação detalhada de vários algoritmos de


recursos sequenciais em Comparative Study of Techniques for Large-Scale
Feature Selection por F. Ferri, P. Pudil, M. Hatef e J. Kittler, páginas 403-413,
1994.

Para praticar nossas habilidades de codificação e capacidade de implementar


nossos próprios algoritmos, vamos em frente e implementá-lo em Python do zero:

from sklearn.base import clone

from itertools import combinations

import numpy as np

from sklearn.metrics import accuracy_score

from sklearn.model_selection import train_test_split

class SBS:

def __init__(self, estimator, k_features,

scoring=accuracy_score,

test_size=0.25, random_state=1):

self.scoring = scoring

self.estimator = clone(estimator)
self.k_features = k_features

self.test_size = test_size

self.random_state = random_state

def fit(self, X, y):

X_train, X_test, y_train, y_test = \

train_test_split(X, y, test_size=self.test_size,

random_state=self.random_state)

dim = X_train.shape[1]

self.indices_ = tuple(range(dim))

self.subsets_ = [self.indices_]

score = self._calc_score(X_train, y_train,

X_test, y_test, self.indices_)

self.scores_ = [score]

while dim > self.k_features:

scores = []

subsets = []

for p in combinations(self.indices_, r=dim - 1):

score = self._calc_score(X_train, y_train,

X_test, y_test, p)

scores.append(score)

subsets.append(p)

best = np.argmax(scores)

self.indices_ = subsets[best]

self.subsets_.append(self.indices_)
dim -= 1

self.scores_.append(scores[best])

self.k_score_ = self.scores_[-1]

return self

def transform(self, X):

return X[:, self.indices_]

def _calc_score(self, X_train, y_train, X_test, y_test, indices):

self.estimator.fit(X_train[:, indices], y_train)

y_pred = self.estimator.predict(X_test[:, indices])

score = self.scoring(y_test, y_pred)

return score
CopyExplain

Na implementação anterior, definimos o parâmetro para especificar o número


desejado de recursos que queremos retornar. Por padrão, usamos o scikit-learn
para avaliar o desempenho de um modelo (um estimador para classificação) nos
subconjuntos de recursos.k_featuresaccuracy_score

Dentro do loop do método, os subconjuntos de recursos criados pela função são


avaliados e reduzidos até que o subconjunto de recursos tenha a
dimensionalidade desejada. Em cada iteração, a pontuação de precisão do melhor
subconjunto é coletada em uma lista, , com base no conjunto de dados de teste
criado internamente, . Utilizaremos esses escores posteriormente para avaliar os
resultados. Os índices de coluna do subconjunto de recursos finais são atribuídos
ao , que podemos usar por meio do método para retornar uma nova matriz de
dados com as colunas de feição selecionadas. Observe que, em vez de calcular o
critério explicitamente dentro do método, simplesmente removemos o recurso que
não está contido no subconjunto de recursos de melhor
desempenho.whilefititertools.combinationself.scores_X_testself.indices_transfo
rmfit

Agora, vamos ver nossa implementação do SBS em ação usando o classificador


KNN do scikit-learn:

>>> import matplotlib.pyplot as plt

>>> from sklearn.neighbors import KNeighborsClassifier

>>> knn = KNeighborsClassifier(n_neighbors=5)

>>> sbs = SBS(knn, k_features=1)

>>> sbs.fit(X_train_std, y_train)


CopyExplain

Embora nossa implementação do SBS já divida o conjunto de dados em um


conjunto de dados de teste e treinamento dentro da função, ainda alimentamos o
conjunto de dados de treinamento, , para o algoritmo. O método SBS criará novos
subconjuntos de treinamento para teste (validação) e treinamento, razão pela qual
esse conjunto de testes também é chamado de conjunto de dados de validação.
Essa abordagem é necessária para evitar que nosso conjunto de testes original se
torne parte dos dados de treinamento. fitX_trainfit

Lembre-se de que nosso algoritmo SBS coleta as pontuações do melhor


subconjunto de recursos em cada estágio, então vamos passar para a parte mais
empolgante de nossa implementação e plotar a precisão de classificação do
classificador KNN que foi calculada no conjunto de dados de validação. O código é
o seguinte:

>>> k_feat = [len(k) for k in sbs.subsets_]

>>> plt.plot(k_feat, sbs.scores_, marker='o')

>>> plt.ylim([0.7, 1.02])

>>> plt.ylabel('Accuracy')

>>> plt.xlabel('Number of features')

>>> plt.grid()
>>> plt.tight_layout()

>>> plt.show()
CopyExplain

Como podemos ver na Figura 4.9, a precisão do classificador KNN melhorou no


conjunto de dados de validação à medida que reduzimos o número de recursos, o
que provavelmente se deve a uma diminuição na maldição da
dimensionalidade que discutimos no contexto do algoritmo KNN no Capítulo 3.
Além disso, podemos ver no gráfico a seguir que o classificador atingiu 100% de
precisão para k = {3, 7, 8, 9, 10, 11, 12}:

Figura 4.9: Impacto do número de funcionalidades na precisão do modelo

Para satisfazer nossa própria curiosidade, vamos ver como é o menor subconjunto
de recursos (k=3), que produziu um desempenho tão bom no conjunto de dados
de validação:

>>> k3 = list(sbs.subsets_[10])

>>> print(df_wine.columns[1:][k3])
Index(['Alcohol', 'Malic acid', 'OD280/OD315 of diluted wines'], dtype='object')
CopyExplain

Usando o código anterior, obtivemos os índices de coluna do subconjunto de três


características da 11ª posição no atributo e retornamos os nomes de feição
correspondentes do índice de coluna do pandas Wine . sbs.subsets_DataFrame

Em seguida, vamos avaliar o desempenho do classificador KNN no conjunto de


dados de teste original:

>>> knn.fit(X_train_std, y_train)

>>> print('Training accuracy:', knn.score(X_train_std, y_train))

Training accuracy: 0.967741935484

>>> print('Test accuracy:', knn.score(X_test_std, y_test))

Test accuracy: 0.962962962963


CopyExplain

Na seção de código anterior, usamos o conjunto completo de recursos e


obtivemos aproximadamente 97% de precisão no conjunto de dados de
treinamento e aproximadamente 96% de precisão no conjunto de dados de teste,
o que indica que nosso modelo já generaliza bem para novos dados. Agora,
vamos usar o subconjunto de três recursos selecionado e ver o desempenho do
KNN:

>>> knn.fit(X_train_std[:, k3], y_train)

>>> print('Training accuracy:',

... knn.score(X_train_std[:, k3], y_train))

Training accuracy: 0.951612903226

>>> print('Test accuracy:',

... knn.score(X_test_std[:, k3], y_test))

Test accuracy: 0.925925925926


CopyExplain
Ao usar menos de um quarto dos recursos originais no conjunto de dados do
Wine, a precisão da previsão no conjunto de dados de teste diminuiu ligeiramente.
Isso pode indicar que essas três características não fornecem informações menos
discriminatórias do que o conjunto de dados original. No entanto, também temos
que ter em mente que o conjunto de dados do Wine é um conjunto de dados
pequeno e é muito suscetível à aleatoriedade, ou seja, a maneira como dividimos
o conjunto de dados em subconjuntos de treinamento e teste e como dividimos o
conjunto de dados de treinamento em um subconjunto de treinamento e validação.

Embora não tenhamos aumentado o desempenho do modelo KNN reduzindo o


número de recursos, reduzimos o tamanho do conjunto de dados, o que pode ser
útil em aplicativos do mundo real que podem envolver etapas caras de coleta de
dados. Além disso, ao reduzir substancialmente o número de recursos, obtemos
modelos mais simples, que são mais fáceis de interpretar.

Algoritmos de seleção de recursos no scikit-learn

Você pode encontrar implementações de vários tipos diferentes de seleção de


recursos sequenciais relacionados ao SBS simples que implementamos
anteriormente no pacote Python
em http://rasbt.github.io/mlxtend/user_guide/feature_selection/SequentialFeatureS
elector/. Embora nossa implementação venha com muitos sinos e assobios,
colaboramos com a equipe scikit-learn para implementar uma versão simplificada
e fácil de usar, que fez parte da recente versão v0.24. O uso e o comportamento
são muito semelhantes ao código que implementamos neste capítulo. Se você
quiser saber mais, consulte a documentação
em https://scikit-learn.org/stable/modules/generated/sklearn.feature_selection.Seq
uentialFeatureSelector.html.mlxtendmlxtendSBS

Há muitos outros algoritmos de seleção de recursos disponíveis via scikit-learn.


Isso inclui eliminação recursiva com base em pesos de recursos, métodos
baseados em árvore para selecionar feições por importância e testes estatísticos
univariados. Uma discussão abrangente dos diferentes métodos de seleção de
recursos está além do escopo deste livro, mas um bom resumo com exemplos
ilustrativos pode ser encontrado
em http://scikit-learn.org/stable/modules/feature_selection.html.
Avaliando a importância dos recursos
com florestas aleatórias
Nas seções anteriores, você aprendeu como usar a regularização L1 para zerar
recursos irrelevantes via regressão logística e como usar o algoritmo SBS para
seleção de recursos e aplicá-lo a um algoritmo KNN. Outra abordagem útil para
selecionar recursos relevantes de um conjunto de dados é usar uma floresta
aleatória, uma técnica de conjunto que foi introduzida no Capítulo 3. Usando uma
floresta aleatória, podemos medir a importância do recurso como a diminuição
média de impurezas calculada a partir de todas as árvores de decisão na floresta,
sem fazer suposições sobre se nossos dados são linearmente separáveis ou não.
Convenientemente, a implementação de floresta aleatória no scikit-learn já coleta
os valores de importância do recurso para que possamos acessá-los por meio do
atributo depois de ajustar um . Ao executar o código a seguir, agora treinaremos
uma floresta de 500 árvores no conjunto de dados do Wine e classificaremos os
13 recursos por suas respectivas medidas de importância — lembre-se de nossa
discussão no Capítulo 3 que não precisamos usar recursos padronizados ou
normalizados em modelos baseados
em árvores:feature_importances_RandomForestClassifier

>>> from sklearn.ensemble import RandomForestClassifier

>>> feat_labels = df_wine.columns[1:]

>>> forest = RandomForestClassifier(n_estimators=500,

... random_state=1)

>>> forest.fit(X_train, y_train)

>>> importances = forest.feature_importances_

>>> indices = np.argsort(importances)[::-1]

>>> for f in range(X_train.shape[1]):

... print("%2d) %-*s %f" % (f + 1, 30,

... feat_labels[indices[f]],
... importances[indices[f]]))

>>> plt.title('Feature importance')

>>> plt.bar(range(X_train.shape[1]),

... importances[indices],

... align='center')

>>> plt.xticks(range(X_train.shape[1]),

... feat_labels[indices], rotation=90)

>>> plt.xlim([-1, X_train.shape[1]])

>>> plt.tight_layout()

>>> plt.show()

1) Proline 0.185453

2) Flavanoids 0.174751

3) Color intensity 0.143920

4) OD280/OD315 of diluted wines 0.136162

5) Alcohol 0.118529

6) Hue 0.058739

7) Total phenols 0.050872

8) Magnesium 0.031357

9) Malic acid 0.025648

10) Proanthocyanins 0.025570

11) Alcalinity of ash 0.022366

12) Nonflavanoid phenols 0.013354

13) Ash 0.013279


CopyExplain

Depois de executar o código, criamos um gráfico que classifica os diferentes


recursos no conjunto de dados do Wine por sua importância relativa; Observe que
os valores de importância do recurso são normalizados para que eles somem até
1,0:

Figura 4.10: Importância do conjunto de dados do Wine baseado em florestas


aleatórias

Podemos concluir que os níveis de prolina e flavonoides, a intensidade da cor, a


difração OD280/OD315 e a concentração de álcool do vinho são as características
mais discriminativas no conjunto de dados com base na diminuição média das
impurezas nas 500 árvores de decisão. Curiosamente, duas das características
mais bem classificadas no gráfico também estão na seleção de subconjuntos de
três características do algoritmo SBS que implementamos na seção anterior
(concentração de álcool e OD280/OD315 de vinhos diluídos).

No entanto, no que diz respeito à interpretabilidade, a técnica da floresta aleatória


vem com uma vantagem importante que vale a pena mencionar. Se dois ou mais
recursos estiverem altamente correlacionados, um recurso pode ser classificado
muito bem, enquanto as informações sobre o(s) outro(s) recurso(s) podem não ser
totalmente capturadas. Por outro lado, não precisamos nos preocupar com esse
problema se estivermos meramente interessados no desempenho preditivo de um
modelo em vez da interpretação de valores de importância de recursos.

Para concluir esta seção sobre valores de importância de recursos e florestas


aleatórias, vale a pena mencionar que o scikit-learn também implementa um
objeto que seleciona recursos com base em um limite especificado pelo usuário
após o ajuste do modelo, o que é útil se quisermos usar o como seletor de
recursos e etapa intermediária em um objeto scikit-learn, o que nos permite
conectar diferentes etapas de pré-processamento com um estimador, como você
verá no Capítulo 6, Aprendendo as melhores práticas para avaliação de modelos
e ajuste de hiperparâmetros. Por exemplo, podemos definir o para reduzir o
conjunto de dados para os cinco recursos mais importantes usando o seguinte
código:SelectFromModelRandomForestClassifierPipelinethreshold0.1

>>> from sklearn.feature_selection import SelectFromModel

>>> sfm = SelectFromModel(forest, threshold=0.1, prefit=True)

>>> X_selected = sfm.transform(X_train)

>>> print('Number of features that meet this threshold',

... 'criterion:', X_selected.shape[1])

Number of features that meet this threshold criterion: 5

>>> for f in range(X_selected.shape[1]):

... print("%2d) %-*s %f" % (f + 1, 30,

... feat_labels[indices[f]],

... importances[indices[f]]))

1) Proline 0.185453

2) Flavanoids 0.174751

3) Color intensity 0.143920

4) OD280/OD315 of diluted wines 0.136162

5) Alcohol 0.118529
CopyExplain
Resumo
Começamos este capítulo analisando técnicas úteis para garantir que lidamos
corretamente com os dados ausentes. Antes de alimentarmos dados para um
algoritmo de aprendizado de máquina, também temos que nos certificar de
codificar variáveis categóricas corretamente e, neste capítulo, vimos como
podemos mapear valores de feição ordinais e nominais para representações
inteiras.

Além disso, discutimos brevemente a regularização L1, que pode nos ajudar a
evitar o overfitting reduzindo a complexidade de um modelo. Como uma
abordagem alternativa para remover recursos irrelevantes, usamos um algoritmo
de seleção de recursos sequencial para selecionar recursos significativos de um
conjunto de dados.

No próximo capítulo, você aprenderá sobre outra abordagem útil para a redução
de dimensionalidade: a extração de recursos. Ele nos permite compactar recursos
em um subespaço de dimensão inferior, em vez de remover recursos inteiramente
como na seleção de recursos.

Junte-se ao espaço Discord do nosso livro


Junte-se à nossa comunidade do Discord para conhecer pessoas que pensam
como você e aprender ao lado de mais de 2000 membros em:

https://packt.link/MLwPyTorch

Compactando dados por meio da


redução de dimensionalidade
No Capítulo 4, Criando bons conjuntos de dados de treinamento – Pré-
processamento de dados, você aprendeu sobre as diferentes abordagens para
reduzir a dimensionalidade de um conjunto de dados usando diferentes técnicas
de seleção de recursos. Uma abordagem alternativa para a seleção de recursos
para redução de dimensionalidade é a extração de recursos. Neste capítulo,
você aprenderá sobre duas técnicas fundamentais que o ajudarão a resumir o
conteúdo de informações de um conjunto de dados, transformando-o em um novo
subespaço de recurso de menor dimensionalidade do que o original. A
compactação de dados é um tópico importante no aprendizado de máquina e nos
ajuda a armazenar e analisar as quantidades crescentes de dados que são
produzidos e coletados na era moderna da tecnologia.

Neste capítulo, abordaremos os seguintes tópicos:

 Análise de componentes principais para compactação de dados não


supervisionada

 Análise discriminante linear como técnica supervisionada de redução de


dimensionalidade para maximizar a separabilidade de classes

 Uma breve visão geral das técnicas de redução de dimensionalidade não


linear e incorporação de vizinhos estocásticos distribuídos em t para
visualização de dados

Redução de dimensionalidade não


supervisionada via análise de
componentes principais
Semelhante à seleção de recursos, podemos usar diferentes técnicas de extração
de recursos para reduzir o número de recursos em um conjunto de dados. A
diferença entre a seleção de recursos e a extração de recursos é que, embora
mantenhamos os recursos originais quando usamos algoritmos de seleção de
recursos, como seleção sequencial para trás, usamos a extração de recursos
para transformar ou projetar os dados em um novo espaço de recurso.

No contexto da redução de dimensionalidade, a extração de recursos pode ser


entendida como uma abordagem para a compactação de dados com o objetivo de
manter a maioria das informações relevantes. Na prática, a extração de
recursos não é usada apenas para melhorar o espaço de armazenamento ou a
eficiência computacional do algoritmo de aprendizado, mas também pode
melhorar o desempenho preditivo, reduzindo a maldição da dimensionalidade —
especialmente se estivermos trabalhando com modelos não regularizados.

As principais etapas da análise de componentes


principais
Nesta seção, discutiremos a análise de componentes principais (ACP), uma
técnica de transformação linear não supervisionada que é amplamente utilizada
em diferentes campos, mais proeminentemente para extração de características e
redução de dimensionalidade. Outras aplicações populares da PCA incluem a
análise exploratória de dados e a denoização de sinais na negociação do mercado
de ações, e a análise de dados do genoma e níveis de expressão gênica no
campo da bioinformática.

A ACP nos ajuda a identificar padrões nos dados com base na correlação entre as
características. Em poucas palavras, a PCA visa encontrar as direções de máxima
variância em dados de alta dimensão e projeta os dados em um novo subespaço
com dimensões iguais ou menores do que o original. Os eixos ortogonais
(componentes principais) do novo subespaço podem ser interpretados como as
direções de variância máxima, dada a restrição de que os novos eixos de feição
são ortogonais entre si, como ilustrado na Figura 5.1:
Figura 5.1: Usando a ACP para encontrar as direções de variância máxima em um
conjunto de dados

Na figura 5.1, x1 e x2 são os eixos de recursos originais, e PC 1 e PC 2 são os


componentes principais.

Se usarmos PCA para redução de dimensionalidade, construiremos uma matriz de


transformação d×k-dimensional, W, que nos permite mapear um vetor das
características do exemplo de treinamento, x, em um novo subespaço de feição k-
dimensional que tem menos dimensões do que o espaço de feição d-
dimensional original. Por exemplo, o processo é o seguinte. Suponha que
tenhamos um vetor de recurso, x:
que é então transformada por uma matriz de

transformação,  :

xW = z

resultando no vetor de saída:

Como resultado da transformação dos dados d-dimensionais originais neste


novo subespaço k-dimensional (tipicamente k << d), o primeiro componente
principal terá a maior variância possível. Todos os componentes principais
consequentes terão a maior variância, dada a restrição de que esses
componentes não estão correlacionados (ortogonais) com os outros componentes
principais — mesmo que os recursos de entrada estejam correlacionados, os
componentes principais resultantes serão mutuamente ortogonais (não
correlacionados). Observe que as direções da PCA são altamente sensíveis ao
dimensionamento de dados, e precisamos padronizar os recursos antes da PCA
se os recursos foram medidos em escalas diferentes e queremos atribuir igual
importância a todos os recursos.

Antes de examinar o algoritmo PCA para redução de dimensionalidade em mais


detalhes, vamos resumir a abordagem em alguns passos simples:

1. Padronize o conjunto de dados d-dimensional.


2. Construa a matriz de covariância.
3. Decompor a matriz de covariância em seus autovetores e autovalores.
4. Classifique os autovalores por ordem decrescente para classificar os
autovetores correspondentes.
5. Selecione k autovetores, que correspondem aos maiores autovalores k,

onde k é a dimensionalidade do novo subespaço de feição ( ).


6. Construa uma matriz de projeção, W, a partir dos autovetores k "superiores".
7. Transforme o conjunto de dados de entrada d-dimensional, X, usando a
matriz de projeção, W, para obter o novo subespaço de feição k-dimensional.

Nas seções a seguir, faremos um PCA passo a passo usando Python como um
exercício de aprendizado. Então, veremos como realizar uma ACP de forma mais
conveniente usando scikit-learn.

Autodecomposição: Decompor uma Matriz em Autovetores e Autovalores

A autodecomposição, a fatoração de uma matriz quadrada


nos chamados autovalores e autovetores, está no centro do procedimento de
ACP descrito nesta seção.

A matriz de covariância é um caso especial de uma matriz quadrada: é uma matriz


simétrica, o que significa que a matriz é igual à sua transposição, A = AT.

Quando decompomos tal matriz simétrica, os autovalores são números reais (em
vez de complexos), e os autovetores são ortogonais (perpendiculares) uns aos
outros. Além disso, autovalores e autovetores vêm em pares. Se decompormos
uma matriz de covariância em seus autovetores e autovalores, os autovetores
associados ao autovalor mais alto correspondem à direção da variância máxima
no conjunto de dados. Aqui, essa "direção" é uma transformação linear das
colunas de feição do conjunto de dados.

Embora uma discussão mais detalhada de autovalores e autovetores esteja além


do escopo deste livro, um tratamento relativamente completo com indicações para
recursos adicionais pode ser encontrado na Wikipédia
em https://en.wikipedia.org/wiki/Eigenvalues_and_eigenvectors.

Extraindo os componentes principais passo a


passo
Nesta subseção, abordaremos os quatro primeiros passos de um PCA:

1. Padronizando os dados
2. Construindo a matriz de covariância
3. Obtenção dos autovalores e autovetores da matriz de covariância
4. Ordenar os autovalores por ordem decrescente para classificar os
autovetores

Primeiro, começaremos carregando o conjunto de dados do Wine com o qual


trabalhamos no Capítulo 4, Building Good Training Datasets – Data
Preprocessing:

>>> import pandas as pd

>>> df_wine = pd.read_csv(

... 'https://archive.ics.uci.edu/ml/'

... 'machine-learning-databases/wine/wine.data',

... header=None

... )
CopyExplain

Obtendo o conjunto de dados do Wine

Você pode encontrar uma cópia do conjunto de dados do Wine (e todos os outros
conjuntos de dados usados neste livro) no pacote de códigos deste livro, que você
pode usar se estiver trabalhando offline ou se o servidor UCI
no https://archive.ics.uci.edu/ml/machine-learning-databases/wine/wine.data estive
r temporariamente indisponível. Por exemplo, para carregar o conjunto de dados
do Wine de um diretório local, você pode substituir as seguintes linhas:

df = pd.read_csv(

'https://archive.ics.uci.edu/ml/'

'machine-learning-databases/wine/wine.data',

header=None
)
CopyExplain

com estes:

df = pd.read_csv(

'your/local/path/to/wine.data',

header=None

)
CopyExplain

Em seguida, processaremos os dados do Wine em conjuntos de dados de


treinamento e teste separados — usando 70% e 30% dos dados, respectivamente
— e os padronizaremos para variação de unidade:

>>> from sklearn.model_selection import train_test_split

>>> X, y = df_wine.iloc[:, 1:].values, df_wine.iloc[:, 0].values

>>> X_train, X_test, y_train, y_test = \

... train_test_split(X, y, test_size=0.3,

... stratify=y,

... random_state=0)

>>> # standardize the features

>>> from sklearn.preprocessing import StandardScaler

>>> sc = StandardScaler()

>>> X_train_std = sc.fit_transform(X_train)

>>> X_test_std = sc.transform(X_test)


CopyExplain

After completing the mandatory preprocessing by executing the preceding code,


let’s advance to the second step: constructing the covariance matrix. The
symmetric d×d-dimensional covariance matrix, where d is the number of
dimensions in the dataset, stores the pairwise covariances between the different
features. For example, the covariance between two features, xj and xk, on the
population level can be calculated via the following equation:

Aqui, e são as médias de exemplo dos recursos j e   k,   respectivamente.


Observe que as médias da amostra são zero se padronizarmos o conjunto de
dados. Uma covariância positiva entre duas características indica que as
características aumentam ou diminuem juntas, enquanto uma covariância negativa
indica que as características variam em direções opostas. Por exemplo, a matriz
de covariância de três características pode então ser escrita da seguinte forma

(note que é a letra maiúscula grega sigma, que   não deve ser confundida com o
símbolo de soma):

Os autovetores da matriz de covariância representam os componentes principais


(as direções da variância máxima), enquanto os autovalores correspondentes
definirão sua magnitude. No caso do conjunto de dados Wine, obteríamos 13
autovetores e autovalores da matriz de covariância de 13×13 dimensões.

Agora, para nossa terceira etapa, vamos obter os autopares da matriz de


covariância. Se você fez uma aula de álgebra linear, você pode ter aprendido que
um autovetor, v, satisfaz a seguinte condição:
Aqui,   está um escalar: o autovalor. Como o cálculo manual de autovetores e
autovalores é uma tarefa um tanto tediosa e elaborada, usaremos a função de
NumPy para obter os autopares da matriz de covariância de Wine: linalg.eig

>>> import numpy as np

>>> cov_mat = np.cov(X_train_std.T)

>>> eigen_vals, eigen_vecs = np.linalg.eig(cov_mat)

>>> print('\nEigenvalues \n', eigen_vals)

Eigenvalues

[ 4.84274532 2.41602459 1.54845825 0.96120438 0.84166161

0.6620634 0.51828472 0.34650377 0.3131368 0.10754642

0.21357215 0.15362835 0.1808613 ]


CopyExplain

Usando a função, calculamos a matriz de covariância do conjunto de dados de


treinamento padronizado. Usando a função, realizamos a autodecomposição, que
resultou em um vetor () composto por 13 autovalores e os autovetores
correspondentes armazenados como colunas em uma matriz de 13×13 dimensões
().numpy.covlinalg.eigeigen_valseigen_vecs

Autodecomposição em NumPy

A função foi projetada para operar em matrizes quadradas simétricas e não


simétricas. No entanto, você pode achar que ele retorna autovalores complexos
em certos casos.numpy.linalg.eig

Uma função relacionada, , foi implementada para decompor matrizes hermetianas,


que é uma abordagem numericamente mais estável para trabalhar com matrizes
simétricas como a matriz de covariância; sempre retorna autovalores
reais.numpy.linalg.eighnumpy.linalg.eigh
Variância total e explicada
Como queremos reduzir a dimensionalidade do nosso conjunto de dados
compactando-o em um novo subespaço de recurso, selecionamos apenas o
subconjunto dos autovetores (componentes principais) que contém a maior parte
das informações (variância). Os autovalores definem a magnitude dos
autovetores, então temos que ordenar os autovalores por magnitude decrescente;
Estamos interessados nos autovetores K superiores com base nos valores de
seus autovalores correspondentes. Mas antes de coletarmos esses autovetores
k mais informativos, vamos plotar as razões de variância
explicadas dos autovalores. A razão de variância explicada de um autovalor, , é

simplesmente a fração de um autovalor,  e a soma total dos autovalores:

Usando a função NumPy, podemos então calcular a soma cumulativa das


variâncias explicadas, que então plotaremos através da função de
Matplotlib:cumsumstep

>>> tot = sum(eigen_vals)

>>> var_exp = [(i / tot) for i in

... sorted(eigen_vals, reverse=True)]

>>> cum_var_exp = np.cumsum(var_exp)

>>> import matplotlib.pyplot as plt

>>> plt.bar(range(1,14), var_exp, align='center',

... label='Individual explained variance')

>>> plt.step(range(1,14), cum_var_exp, where='mid',

... label='Cumulative explained variance')


>>> plt.ylabel('Explained variance ratio')

>>> plt.xlabel('Principal component index')

>>> plt.legend(loc='best')

>>> plt.tight_layout()

>>> plt.show()
CopyExplain

O gráfico resultante indica que o primeiro componente principal sozinho responde


por aproximadamente 40% da variância.

Além disso, podemos ver que os dois primeiros componentes principais


combinados explicam quase 60% da variância no conjunto de dados:

Figura 5.2: Proporção da variância total captada pelos componentes principais

Embora o gráfico de variância explicado nos lembre dos valores de importância de


recurso que calculamos no Capítulo 4, Building Good Training Datasets – Data
Preprocessing, via florestas aleatórias, devemos nos lembrar que o PCA é um
método não supervisionado, o que significa que as informações sobre os rótulos
de classe são ignoradas. Enquanto uma floresta aleatória usa as informações de
associação de classe para calcular as impurezas do nó, a variância mede a
dispersão dos valores ao longo de um eixo de recurso.

Transformação de recursos
Agora que decompomos com sucesso a matriz de covariância em autopares,
vamos prosseguir com as três últimas etapas para transformar o conjunto de
dados do Wine nos novos eixos do componente principal. As etapas restantes que
abordaremos nesta seção são as seguintes:

1. Selecione k autovetores, que correspondem aos maiores autovalores k,

onde k é a dimensionalidade do novo subespaço de feição ( ).


2. Construa uma matriz de projeção, W, a partir dos autovetores k "superiores".
3. Transforme o conjunto de dados de entrada d-dimensional, X, usando a
matriz de projeção, W, para obter o novo subespaço de feição k-dimensional.

Ou, em termos menos técnicos, classificaremos os autopares por ordem


decrescente dos autovalores, construiremos uma matriz de projeção a partir dos
autovetores selecionados e usaremos a matriz de projeção para transformar os
dados no subespaço de dimensão inferior.

Começamos classificando os autopares por ordem decrescente dos autovalores:

>>> # Make a list of (eigenvalue, eigenvector) tuples

>>> eigen_pairs = [(np.abs(eigen_vals[i]), eigen_vecs[:, i])

... for i in range(len(eigen_vals))]

>>> # Sort the (eigenvalue, eigenvector) tuples from high to low

>>> eigen_pairs.sort(key=lambda k: k[0], reverse=True)


CopyExplain

Em seguida, coletamos os dois autovetores que correspondem aos dois maiores


autovalores, para capturar cerca de 60% da variância nesse conjunto de dados.
Note que dois autovetores foram escolhidos para fins de ilustração, uma vez que
vamos plotar os dados por meio de um gráfico de dispersão bidimensional mais
adiante nesta subseção. Na prática, o número de componentes principais deve ser
determinado por um tradeoff entre a eficiência computacional e o desempenho do
classificador:

>>> w = np.hstack((eigen_pairs[0][1][:, np.newaxis],

... eigen_pairs[1][1][:, np.newaxis]))

>>> print('Matrix W:\n', w)

Matrix W:

[[-0.13724218 0.50303478]

[ 0.24724326 0.16487119]

[-0.02545159 0.24456476]

[ 0.20694508 -0.11352904]

[-0.15436582 0.28974518]

[-0.39376952 0.05080104]

[-0.41735106 -0.02287338]

[ 0.30572896 0.09048885]

[-0.30668347 0.00835233]

[ 0.07554066 0.54977581]

[-0.32613263 -0.20716433]

[-0.36861022 -0.24902536]

[-0.29669651 0.38022942]]
CopyExplain

Ao executar o código anterior, criamos uma matriz de projeção 13×2


dimensões, W, a partir dos dois autovetores superiores.

Projeções espelhadas
Dependendo de quais versões do NumPy e LAPACK você está usando, você
pode obter a matriz, W, com seus sinais invertidos. Por favor, note que isso não é

um problema; Se V é um autovetor de uma matriz, ,  temos:

Aqui, v é o autovetor, e –v também é um autovetor, que podemos mostrar da


seguinte forma. Usando álgebra básica, podemos multiplicar ambos os lados da

equação por um escalar,  :

Como a multiplicação matricial é associativa para a multiplicação escalar,


podemos então reorganizá-la para o seguinte:

Agora, podemos ver que   é um autovetor com o mesmo autovalor, ,  para

ambos   e  . Assim, tanto v quanto –v são


autovetores.

Usando a matriz de projeção, agora podemos transformar um exemplo, x


(representado como um vetor de linha de 13 dimensões), no subespaço PCA (os
componentes principais um e dois) obtendo x′, agora um vetor de exemplo
bidimensional consistindo de dois novos recursos:

x′ = xW

>>> X_train_std[0].dot(w)
array([ 2.38299011, 0.45458499])
CopyExplain

Da mesma forma, podemos transformar todo o conjunto de dados de treinamento


de 124×13 dimensões nos dois componentes principais calculando o produto de
ponto matricial:

X′ = XW

>>> X_train_pca = X_train_std.dot(w)


CopyExplain

Por fim, vamos visualizar o conjunto de dados de treinamento do Wine


transformado, agora armazenado como uma matriz 124×2 dimensões, em um
gráfico de dispersão bidimensional:

>>> colors = ['r', 'b', 'g']

>>> markers = ['o', 's', '^']

>>> for l, c, m in zip(np.unique(y_train), colors, markers):

... plt.scatter(X_train_pca[y_train==l, 0],

... X_train_pca[y_train==l, 1],

... c=c, label=f'Class {l}', marker=m)

>>> plt.xlabel('PC 1')

>>> plt.ylabel('PC 2')

>>> plt.legend(loc='lower left')

>>> plt.tight_layout()

>>> plt.show()
CopyExplain

As we can see in Figure 5.3, the data is more spread along the first principal
component (x axis) than the second principal component (y axis), which is
consistent with the explained variance ratio plot that we created in the previous
subsection. However, we can tell that a linear classifier will likely be able to
separate the classes well:
Figure 5.3: Data records from the Wine dataset projected onto a 2D feature space
via PCA

Although we encoded the class label information for the purpose of illustration in
the preceding scatterplot, we have to keep in mind that PCA is an unsupervised
technique that doesn’t use any class label information.

Principal component analysis in scikit-learn


Although the verbose approach in the previous subsection helped us to follow the
inner workings of PCA, we will now discuss how to use the class implemented in
scikit-learn.PCA

The class is another one of scikit-learn’s transformer classes, with which we first fit
the model using the training data before we transform both the training data and
the test dataset using the same model parameters. Now, let’s use the class from
scikit-learn on the Wine training dataset, classify the transformed examples via
logistic regression, and visualize the decision regions via the function that we
defined in Chapter 2, Training Simple Machine Learning Algorithms for
Classification:PCAPCAplot_decision_regions
from matplotlib.colors import ListedColormap

def plot_decision_regions(X, y, classifier, test_idx=None, resolution=0.02):

# setup marker generator and color map

markers = ('o', 's', '^', 'v', '<')

colors = ('red', 'blue', 'lightgreen', 'gray', 'cyan')

cmap = ListedColormap(colors[:len(np.unique(y))])

# plot the decision surface

x1_min, x1_max = X[:, 0].min() - 1, X[:, 0].max() + 1

x2_min, x2_max = X[:, 1].min() - 1, X[:, 1].max() + 1

xx1, xx2 = np.meshgrid(np.arange(x1_min, x1_max, resolution),

np.arange(x2_min, x2_max, resolution))

lab = classifier.predict(np.array([xx1.ravel(), xx2.ravel()]).T)

lab = lab.reshape(xx1.shape)

plt.contourf(xx1, xx2, lab, alpha=0.3, cmap=cmap)

plt.xlim(xx1.min(), xx1.max())

plt.ylim(xx2.min(), xx2.max())

# plot class examples

for idx, cl in enumerate(np.unique(y)):

plt.scatter(x=X[y == cl, 0],

y=X[y == cl, 1],

alpha=0.8,

c=colors[idx],

marker=markers[idx],

label=f'Class {cl}',

edgecolor='black')
CopyExplain
For your convenience, you can place the preceding code into a separate code file
in your current working directory, for example, , and import it into your current
Python session:plot_decision_regionsplot_decision_regions_script.py

>>> from sklearn.linear_model import LogisticRegression

>>> from sklearn.decomposition import PCA

>>> # initializing the PCA transformer and

>>> # logistic regression estimator:

>>> pca = PCA(n_components=2)

>>> lr = LogisticRegression(multi_class='ovr',

... random_state=1,

... solver='lbfgs')

>>> # dimensionality reduction:

>>> X_train_pca = pca.fit_transform(X_train_std)

>>> X_test_pca = pca.transform(X_test_std)

>>> # fitting the logistic regression model on the reduced dataset:

>>> lr.fit(X_train_pca, y_train)

>>> plot_decision_regions(X_train_pca, y_train, classifier=lr)

>>> plt.xlabel('PC 1')

>>> plt.ylabel('PC 2')

>>> plt.legend(loc='lower left')

>>> plt.tight_layout()

>>> plt.show()
CopyExplain

By executing this code, we should now see the decision regions for the training
data reduced to two principal component axes:
Figure 5.4: Training examples and logistic regression decision regions after using
scikit-learn’s PCA for dimensionality reduction

When we compare the PCA projections via scikit-learn with our own PCA
implementation, we might see that the resulting plots are mirror images of each
other. Note that this is not due to an error in either of those two implementations;
the reason for this difference is that, depending on the eigensolver, eigenvectors
can have either negative or positive signs.

Not that it matters, but we could simply revert the mirror image by multiplying the
data by –1 if we wanted to; note that eigenvectors are typically scaled to unit length
1. For the sake of completeness, let’s plot the decision regions of the logistic
regression on the transformed test dataset to see if it can separate the classes
well:

>>> plot_decision_regions(X_test_pca, y_test, classifier=lr)

>>> plt.xlabel('PC 1')

>>> plt.ylabel('PC 2')

>>> plt.legend(loc='lower left')


>>> plt.tight_layout()

>>> plt.show()
CopyExplain

After we plot the decision regions for the test dataset by executing the preceding
code, we can see that logistic regression performs quite well on this small two-
dimensional feature subspace and only misclassifies a few examples in the test
dataset:

Figure 5.5: Test datapoints with logistic regression decision regions in the PCA-
based feature space

If we are interested in the explained variance ratios of the different principal


components, we can simply initialize the class with the parameter set to , so all
principal components are kept and the explained variance ratio can then be
accessed via the attribute:PCAn_componentsNoneexplained_variance_ratio_

>>> pca = PCA(n_components=None)

>>> X_train_pca = pca.fit_transform(X_train_std)


>>> pca.explained_variance_ratio_

array([ 0.36951469, 0.18434927, 0.11815159, 0.07334252,

0.06422108, 0.05051724, 0.03954654, 0.02643918,

0.02389319, 0.01629614, 0.01380021, 0.01172226,

0.00820609])
CopyExplain

Note that we set when we initialized the class so that it will return all principal
components in a sorted order, instead of performing a dimensionality
reduction.n_components=NonePCA

Assessing feature contributions


In this section, we will take a brief look at how we can assess the contributions of
the original features to the principal components. As we learned, via PCA, we
create principal components that represent linear combinations of the features.
Sometimes, we are interested to know about how much each original feature
contributes to a given principal component. These contributions are often
called loadings.

The factor loadings can be computed by scaling the eigenvectors by the square
root of the eigenvalues. The resulting values can then be interpreted as the
correlation between the original features and the principal component. To illustrate
this, let us plot the loadings for the first principal component.

First, we compute the 13×13-dimensional loadings matrix by multiplying the


eigenvectors by the square root of the eigenvalues:

>>> loadings = eigen_vecs * np.sqrt(eigen_vals)


CopyExplain

Then, we plot the loadings for the first principal component, , which is the first
column in this matrix:loadings[:, 0]

>>> fig, ax = plt.subplots()


>>> ax.bar(range(13), loadings[:, 0], align='center')

>>> ax.set_ylabel('Loadings for PC 1')

>>> ax.set_xticks(range(13))

>>> ax.set_xticklabels(df_wine.columns[1:], rotation=90)

>>> plt.ylim([-1, 1])

>>> plt.tight_layout()

>>> plt.show()
CopyExplain

In Figure 5.6, we can see that, for example, Alcohol has a negative correlation


with the first principal component (approximately –0.3), whereas Malic acid has a
positive correlation (approximately 0.54). Note that a value of 1 describes a perfect
positive correlation whereas a value of –1 corresponds to a perfect negative
correlation:

Figure 5.6: Feature correlations with the first principal component


In the preceding code example, we compute the factor loadings for our own PCA
implementation. We can obtain the loadings from a fitted scikit-learn PCA object in
a similar manner, where represents the eigenvectors and represents the
eigenvalues:pca.components_pca.explained_variance_

>>> sklearn_loadings = pca.components_.T * np.sqrt(pca.explained_variance_)


CopyExplain

To compare the scikit-learn PCA loadings with those we created previously, let us
create a similar bar plot:

>>> fig, ax = plt.subplots()

>>> ax.bar(range(13), sklearn_loadings[:, 0], align='center')

>>> ax.set_ylabel('Loadings for PC 1')

>>> ax.set_xticks(range(13))

>>> ax.set_xticklabels(df_wine.columns[1:], rotation=90)

>>> plt.ylim([-1, 1])

>>> plt.tight_layout()

>>> plt.show()
CopyExplain

As we can see, the bar plots look the same:


Figure 5.7: Feature correlations to the first principal component using scikit-learn

Depois de explorar a ACP como uma técnica de extração de características não


supervisionada, a próxima seção introduzirá a análise discriminante linear
(LDA), que é uma técnica de transformação linear que leva em consideração as
informações do rótulo de classe.

Compressão supervisionada de dados


via análise discriminante linear
A LDA pode ser usada como uma técnica de extração de características para
aumentar a eficiência computacional e reduzir o grau de overfitting devido à
maldição da dimensionalidade em modelos não regularizados. O conceito geral
por trás do LDA é muito semelhante ao PCA, mas enquanto o PCA tenta encontrar
os eixos de componentes ortogonais de variância máxima em um conjunto de
dados, o objetivo no LDA é encontrar o subespaço de recurso que otimiza a
separabilidade de classes. Nas seções a seguir, discutiremos as semelhanças
entre LDA e PCA em mais detalhes e percorreremos a abordagem LDA passo a
passo.

Análise de componentes principais versus análise


discriminante linear
Tanto a ACP quanto a LDA são técnicas de transformação linear que podem
ser usadas para reduzir o número de dimensões em um conjunto de dados; o
primeiro é um algoritmo não supervisionado, enquanto o segundo é
supervisionado. Assim, pode-se pensar que a ADL é uma técnica de extração de
características superior para tarefas de classificação em comparação com a ACP.
No entanto, A.M. Martinez relatou que o pré-processamento via PCA tende a
resultar em melhores resultados de classificação em uma tarefa de
reconhecimento de imagem em certos casos, por exemplo, se cada classe
consistir de apenas um pequeno número de exemplos (PCA Versus LDA por A. M.
Martinez e A. C. Kak, IEEE Transactions on Pattern Analysis and Machine
Intelligence, 23(2): 228-233, 2001).

Pescador LDA

LDA às vezes também é chamado de LDA de Fisher. Ronald A. Fisher


inicialmente formulou o Discriminante Linear de Fisher para problemas de
classificação de duas classes em 1936 (The Use of Multiple Measurements in
Taxonomic Problems, R. A. Fisher, Annals of Eugenics, 7(2): 179-188, 1936). Em
1948, o discriminante linear de Fisher foi generalizado para problemas multiclasse
por C. Radhakrishna Rao sob a suposição de covariâncias de classe iguais e
classes normalmente distribuídas, que agora chamamos de LDA (The Utilization
of Multiple Measurements in Problems of Biological Classification by C. R.
Rao, Journal of the Royal Statistical Society. Série B (Metodológica), 10(2): 159-
203, 1948).

A figura 5.8 resume o conceito de LDA para um problema de duas classes.


Exemplos da classe 1 são mostrados como círculos, e exemplos da classe 2 são
mostrados como cruzes:
Figura 5.8: O conceito de LDA para um problema de duas classes

Um discriminante linear, como mostrado no eixo x (DL 1), separaria bem as duas


classes normais distribuídas. Embora o discriminante linear exemplar mostrado no
eixo y (LD 2) capture grande parte da variância no conjunto de dados, ele falharia
como um bom discriminante linear, uma vez que não captura nenhuma das
informações discriminatórias de classe.

Uma suposição na LDA é que os dados são normalmente distribuídos. Além disso,
assumimos que as classes têm matrizes de covariância idênticas e que os
exemplos de treinamento são estatisticamente independentes uns dos outros. No
entanto, mesmo que uma, ou mais, dessas suposições sejam (ligeiramente)
violadas, a LDA para redução de dimensionalidade ainda pode funcionar
razoavelmente bem (Pattern Classification 2nd Edition por R. O. Duda, P. E.
Hart e D. G. Stork, New York, 2001).
O funcionamento interno da análise discriminante
linear
Antes de nos aprofundarmos na implementação de código, vamos resumir
brevemente as principais etapas necessárias para executar o LDA:

1. Padronize o conjunto de dados d-dimensional (d é o número de recursos).


2. Para cada classe, calcule o vetor médio d-dimensional.
3. Construir a matriz de dispersão entre classes, SBe a matriz de dispersão
dentro da classe, SW.
4. Calcular os autovetores e os autovalores correspondentes da

matriz,  .
5. Classifique os autovalores por ordem decrescente para classificar os
autovetores correspondentes.
6. Escolha os autovetores k que correspondem aos maiores autovalores k para
construir uma matriz de transformação d×k-dimensional, W; Os autovetores
são as colunas desta matriz.
7. Projete os exemplos no novo subespaço de recurso usando a matriz de
transformação, W.

Como podemos ver, a LDA é bastante semelhante à PCA no sentido de que


estamos decompondo matrizes em autovalores e autovetores, que formarão o
novo espaço de feição de menor dimensão. No entanto, como mencionado
anteriormente, a LDA leva em consideração as informações do rótulo de classe,
que são representadas na forma dos vetores médios computados na etapa 2. Nas
seções a seguir, discutiremos essas sete etapas com mais detalhes,
acompanhadas de implementações de código ilustrativas.

Calculando as matrizes de dispersão


Como já padronizamos as características do conjunto de dados do Wine na seção
PCA no início deste capítulo, podemos pular a primeira etapa e prosseguir com o
cálculo dos vetores médios, que usaremos para construir a matriz de dispersão
dentro da classe e a matriz de dispersão entre classes, respectivamente. Cada

vetor médio, meu, armazena o valor médio do recurso, ,  com relação aos


exemplos da classe i:

Isso resulta em três vetores médios:

Esses vetores médios podem ser calculados pelo seguinte código, onde
calculamos um vetor médio para cada um dos três rótulos:

>>> np.set_printoptions(precision=4)

>>> mean_vecs = []

>>> for label in range(1,4):

... mean_vecs.append(np.mean(

... X_train_std[y_train==label], axis=0))

... print(f'MV {label}: {mean_vecs[label - 1]}\n')

MV 1: [ 0.9066 -0.3497 0.3201 -0.7189 0.5056 0.8807 0.9589 -0.5516

0.5416 0.2338 0.5897 0.6563 1.2075]


MV 2: [-0.8749 -0.2848 -0.3735 0.3157 -0.3848 -0.0433 0.0635 -0.0946

0.0703 -0.8286 0.3144 0.3608 -0.7253]

MV 3: [ 0.1992 0.866 0.1682 0.4148 -0.0451 -1.0286 -1.2876 0.8287

-0.7795 0.9649 -1.209 -1.3622 -0.4013]


CopyExplain

Usando os vetores médios, agora podemos calcular a matriz de dispersão dentro


da classe, SW:

Isso é calculado somando-se as matrizes de dispersão individuais, Seu, de cada


classe individual i:

>>> d = 13 # number of features

>>> S_W = np.zeros((d, d))

>>> for label, mv in zip(range(1, 4), mean_vecs):

... class_scatter = np.zeros((d, d))

... for row in X_train_std[y_train == label]:

... row, mv = row.reshape(d, 1), mv.reshape(d, 1)

... class_scatter += (row - mv).dot((row - mv).T)

... S_W += class_scatter


>>> print('Within-class scatter matrix: '

... f'{S_W.shape[0]}x{S_W.shape[1]}')

Within-class scatter matrix: 13x13


CopyExplain

A suposição que estamos fazendo quando estamos computando as matrizes de


dispersão é que os rótulos de classe no conjunto de dados de treinamento são
uniformemente distribuídos. No entanto, se imprimirmos o número de etiquetas de
classe, veremos que essa suposição é violada:

>>> print('Class label distribution:',

... np.bincount(y_train)[1:])

Class label distribution: [41 50 33]


CopyExplain

Assim, queremos dimensionar as matrizes de dispersão individuais, Seu, antes de


resumi-los como a matriz de dispersão, SW. Quando dividimos as matrizes de
dispersão pelo número de exemplos de classe, neu, podemos ver que calcular a

matriz de dispersão é de fato o mesmo que calcular a matriz de covariância, 


—a matriz de covariância é uma versão normalizada da matriz de dispersão:

O código para calcular a matriz de dispersão dimensionada dentro da classe é o


seguinte:

>>> d = 13 # number of features

>>> S_W = np.zeros((d, d))

>>> for label,mv in zip(range(1, 4), mean_vecs):


... class_scatter = np.cov(X_train_std[y_train==label].T)

... S_W += class_scatter

>>> print('Scaled within-class scatter matrix: '

... f'{S_W.shape[0]}x{S_W.shape[1]}')

Scaled within-class scatter matrix: 13x13


CopyExplain

Depois de calcularmos a matriz de dispersão dentro da classe (ou matriz de


covariância) dimensionada, podemos passar para a próxima etapa e calcular a
matriz de dispersão entre classes SB:

Aqui, m é a média geral que é calculada, incluindo exemplos de todas as


classes c:

>>> mean_overall = np.mean(X_train_std, axis=0)

>>> mean_overall = mean_overall.reshape(d, 1)

>>> d = 13 # number of features

>>> S_B = np.zeros((d, d))

>>> for i, mean_vec in enumerate(mean_vecs):

... n = X_train_std[y_train == i + 1, :].shape[0]

... mean_vec = mean_vec.reshape(d, 1) # make column vector

... S_B += n * (mean_vec - mean_overall).dot(

... (mean_vec - mean_overall).T)

>>> print('Between-class scatter matrix: '

... f'{S_B.shape[0]}x{S_B.shape[1]}')
Between-class scatter matrix: 13x13
CopyExplain

Selecionando discriminantes lineares para o novo


subespaço de recurso
Os demais passos da LDA são semelhantes aos passos da ACP. No entanto, em
vez de realizar a autodecomposição na matriz de covariância, resolvemos o

problema de autovalor generalizado da matriz,  :

>>> eigen_vals, eigen_vecs =\

... np.linalg.eig(np.linalg.inv(S_W).dot(S_B))
CopyExplain

Depois de calcularmos os autopares, podemos classificar os autovalores em


ordem decrescente:

>>> eigen_pairs = [(np.abs(eigen_vals[i]), eigen_vecs[:,i])

... for i in range(len(eigen_vals))]

>>> eigen_pairs = sorted(eigen_pairs,

... key=lambda k: k[0], reverse=True)

>>> print('Eigenvalues in descending order:\n')

>>> for eigen_val in eigen_pairs:

... print(eigen_val[0])

Eigenvalues in descending order:

349.617808906

172.76152219

3.78531345125e-14

2.11739844822e-14

1.51646188942e-14
1.51646188942e-14

1.35795671405e-14

1.35795671405e-14

7.58776037165e-15

5.90603998447e-15

5.90603998447e-15

2.25644197857e-15

0.0
CopyExplain

No LDA, o número de discriminantes lineares é no máximo c – 1, onde c é o


número de rótulos de classe, já que a matriz de dispersão intermediária, SB, é a
soma de matrizes c com rank um ou menos. Podemos realmente ver que temos
apenas dois autovalores diferentes de zero (os autovalores 3-13 não são
exatamente zero, mas isso é devido à aritmética de ponto flutuante em NumPy.)

Colinearidade

Note que no raro caso de colinearidade perfeita (todos os pontos de exemplo


alinhados caem em uma linha reta), a matriz de covariância teria classificação um,
o que resultaria em apenas um autovetor com um autovalor diferente de zero.

Para medir o quanto da informação discriminatória de classe é capturada pelos


discriminantes lineares (autovetores), vamos plotar os discriminantes lineares por
autovalores decrescentes, semelhante ao gráfico de variância explicado que
criamos na seção ACP. Para simplificar, chamaremos o conteúdo de
discriminação de informações discriminatórias de classe:

>>> tot = sum(eigen_vals.real)

>>> discr = [(i / tot) for i in sorted(eigen_vals.real,

... reverse=True)]

>>> cum_discr = np.cumsum(discr)

>>> plt.bar(range(1, 14), discr, align='center',


... label='Individual discriminability')

>>> plt.step(range(1, 14), cum_discr, where='mid',

... label='Cumulative discriminability')

>>> plt.ylabel('"Discriminability" ratio')

>>> plt.xlabel('Linear Discriminants')

>>> plt.ylim([-0.1, 1.1])

>>> plt.legend(loc='best')

>>> plt.tight_layout()

>>> plt.show()
CopyExplain

Como podemos ver na Figura 5.9, os dois primeiros discriminantes lineares


capturam 100% das informações úteis no conjunto de dados de treinamento do
Wine:

Figura 5.9: Os dois principais discriminantes capturam 100% das informações


úteis
Vamos agora empilhar as duas colunas de autovetor mais discriminativas para
criar a matriz de transformação, W:

>>> w = np.hstack((eigen_pairs[0][1][:, np.newaxis].real,

... eigen_pairs[1][1][:, np.newaxis].real))

>>> print('Matrix W:\n', w)

Matrix W:

[[-0.1481 -0.4092]

[ 0.0908 -0.1577]

[-0.0168 -0.3537]

[ 0.1484 0.3223]

[-0.0163 -0.0817]

[ 0.1913 0.0842]

[-0.7338 0.2823]

[-0.075 -0.0102]

[ 0.0018 0.0907]

[ 0.294 -0.2152]

[-0.0328 0.2747]

[-0.3547 -0.0124]

[-0.3915 -0.5958]]
CopyExplain

Projetando exemplos no novo espaço de recursos


Usando a matriz de transformação W que criamos na subseção anterior, agora
podemos transformar o conjunto de dados de treinamento multiplicando as
matrizes:

X′ = XW
>>> X_train_lda = X_train_std.dot(w)

>>> colors = ['r', 'b', 'g']

>>> markers = ['o', 's', '^']

>>> for l, c, m in zip(np.unique(y_train), colors, markers):

... plt.scatter(X_train_lda[y_train==l, 0],

... X_train_lda[y_train==l, 1] * (-1),

... c=c, label= f'Class {l}', marker=m)

>>> plt.xlabel('LD 1')

>>> plt.ylabel('LD 2')

>>> plt.legend(loc='lower right')

>>> plt.tight_layout()

>>> plt.show()
CopyExplain

Como podemos ver na Figura 5.10, as três classes de Wine agora são
perfeitamente separáveis linearmente no novo subespaço de recursos:
Figura 5.10: Classes de vinho perfeitamente separáveis após projecção dos dados
nos dois primeiros discriminantes

LDA via scikit-learn


Essa implementação passo a passo foi um bom exercício para entender o
funcionamento interno da LDA e entender as diferenças entre LDA e PCA. Agora,
vejamos a aula implementada no scikit-learn:LDA

>>> # the following import statement is one line

>>> from sklearn.discriminant_analysis import LinearDiscriminantAnalysis as LDA

>>> lda = LDA(n_components=2)

>>> X_train_lda = lda.fit_transform(X_train_std, y_train)


CopyExplain

A seguir, vejamos como o classificador de regressão logística lida com o conjunto


de dados de treinamento de dimensões inferiores após a transformação da LDA:

>>> lr = LogisticRegression(multi_class='ovr', random_state=1,

... solver='lbfgs')

>>> lr = lr.fit(X_train_lda, y_train)

>>> plot_decision_regions(X_train_lda, y_train, classifier=lr)

>>> plt.xlabel('LD 1')

>>> plt.ylabel('LD 2')

>>> plt.legend(loc='lower left')

>>> plt.tight_layout()

>>> plt.show()
CopyExplain

Observando a Figura 5.11, podemos observar que o modelo de regressão


logística classifica erroneamente um dos exemplos da classe 2:
Figura 5.11: O modelo de regressão logística classifica erroneamente uma das
classes

Ao diminuir a força da regularização, provavelmente poderíamos mudar os limites


de decisão para que o modelo de regressão logística classifique todos os
exemplos no conjunto de dados de treinamento corretamente. No entanto, e mais
importante, vamos dar uma olhada nos resultados no conjunto de dados de teste:

>>> X_test_lda = lda.transform(X_test_std)

>>> plot_decision_regions(X_test_lda, y_test, classifier=lr)

>>> plt.xlabel('LD 1')

>>> plt.ylabel('LD 2')

>>> plt.legend(loc='lower left')

>>> plt.tight_layout()

>>> plt.show()
CopyExplain
Como podemos ver na Figura 5.12, o classificador de regressão logística é capaz
de obter uma pontuação de precisão perfeita para classificar os exemplos no
conjunto de dados do teste usando apenas um subespaço de feição
bidimensional, em vez dos 13 recursos originais do Wine:

Figura 5.12: O modelo de regressão logística funciona perfeitamente nos dados do


teste

Dimensionalidade não linear, redução e


visualização
Na seção anterior, abordamos técnicas de transformação linear, como PCA e
LDA, para extração de características. Nesta seção, discutiremos por que
considerar técnicas de redução de dimensionalidade não linear pode valer a pena.

Uma técnica de redução de dimensionalidade não linear que é particularmente


digna de destaque é a incorporação de vizinhos estocásticos t-distribuídos (t-
SNE), uma vez que é frequentemente usada na literatura para visualizar conjuntos
de dados de alta dimensão em duas ou três dimensões. Veremos como podemos
aplicar o t-SNE para plotar imagens de imagens manuscritas em um espaço de
feição 2-dimensional.

Por que considerar a redução da dimensionalidade


não linear?
Muitos algoritmos de aprendizado de máquina fazem suposições sobre a
separabilidade linear dos dados de entrada. Você aprendeu que o perceptron
requer até mesmo dados de treinamento perfeitamente separáveis para convergir.
Outros algoritmos que cobrimos até agora assumem que a falta de separabilidade
linear perfeita é devido ao ruído: Adaline, regressão logística e o SVM (padrão),
para citar apenas alguns.

No entanto, se estivermos lidando com problemas não lineares, que podemos


encontrar com bastante frequência em aplicações do mundo real, técnicas de
transformação linear para redução de dimensionalidade, como PCA e LDA, podem
não ser a melhor escolha:

Figura 5.13: Diferença entre problemas lineares e não lineares

A biblioteca scikit-learn implementa uma seleção de técnicas avançadas para


redução de dimensionalidade não linear que estão além do escopo deste livro. O
leitor interessado pode encontrar uma bela visão geral das implementações atuais
no scikit-learn, complementada por exemplos ilustrativos, em http://scikit-
learn.org/stable/modules/manifold.html.

O desenvolvimento e aplicação de técnicas de redução de dimensionalidade não


linear também é muitas vezes referido como aprendizagem múltipla, onde uma
variedade refere-se a um espaço topológico de dimensão inferior embutido em um
espaço de alta dimensão. Os algoritmos para aprendizagem múltipla têm que
capturar a estrutura complicada dos dados para projetá-los em um espaço de
dimensões inferiores onde a relação entre os pontos de dados é preservada.

Um exemplo clássico de aprendizado múltiplo é o rolo suíço tridimensional


ilustrado na Figura 3.5:

Figura 5.14: Rolo suíço tridimensional projectado num espaço bidimensional


inferior

Embora a redução de dimensionalidade não linear e os algoritmos de


aprendizagem múltipla sejam muito poderosos, devemos notar que essas técnicas
são notoriamente difíceis de usar e, com escolhas de hiperparâmetros não ideais,
podem causar mais danos do que benefícios. A razão por trás dessa dificuldade é
que muitas vezes estamos trabalhando com conjuntos de dados de alta dimensão
que não podemos visualizar prontamente e onde a estrutura não é óbvia (ao
contrário do exemplo de rolo suíço na Figura 5.14). Além disso, a menos que
projetemos o conjunto de dados em duas ou três dimensões (o que muitas vezes
não é suficiente para capturar relacionamentos mais complicados), é difícil ou
mesmo impossível avaliar a qualidade dos resultados. Assim, muitas pessoas
ainda dependem de técnicas mais simples, como PCA e LDA, para redução da
dimensionalidade.

Visualizando dados por meio da incorporação de


vizinhos estocásticos distribuídos em t
Depois de introduzir a redução de dimensionalidade não linear e discutir alguns de
seus desafios, vamos dar uma olhada em um exemplo prático envolvendo t-SNE,
que é frequentemente usado para visualizar conjuntos de dados complexos em
duas ou três dimensões.

Em poucas palavras, t-SNE está modelando pontos de dados com base em suas
distâncias pareadas no espaço de feição de alta dimensão (original). Em seguida,
ele encontra uma distribuição de probabilidade de distâncias pareadas no novo
espaço de dimensões inferiores que é próxima à distribuição de probabilidade de
distâncias pareadas no espaço original. Ou, em outras palavras, o t-SNE aprende
a incorporar pontos de dados em um espaço de dimensões inferiores, de modo
que as distâncias pareadas no espaço original sejam preservadas. Você pode
encontrar mais detalhes sobre esse método no artigo de pesquisa
original Visualizing data using t-SNE de Maaten e Hinton, Journal of Machine
Learning Research, 2018
(https://www.jmlr.org/papers/volume9/vandermaaten08a/vandermaaten08a.pdf).
No entanto, como o título do artigo de pesquisa sugere, o t-SNE é uma técnica
destinada a fins de visualização, pois requer todo o conjunto de dados para a
projeção. Como ele projeta os pontos diretamente (ao contrário do PCA, não
envolve uma matriz de projeção), não podemos aplicar o t-SNE a novos pontos de
dados.
O código a seguir mostra uma demonstração rápida de como o t-SNE pode ser
aplicado a um conjunto de dados de 64 dimensões. Primeiro, carregamos o
conjunto de dados Dígitos do scikit-learn, que consiste em dígitos manuscritos de
baixa resolução (os números de 0 a 9):

>>> from sklearn.datasets import load_digits

>>> digits = load_digits()


CopyExplain

Os dígitos são 8×8 imagens em tons de cinza. O código a seguir plota as quatro
primeiras imagens no conjunto de dados, que consiste em 1.797 imagens no total:

>>> fig, ax = plt.subplots(1, 4)

>>> for i in range(4):

>>> ax[i].imshow(digits.images[i], cmap='Greys')

>>> plt.show()
CopyExplain

Como podemos ver na Figura 5.15, as imagens são de resolução relativamente


baixa, 8×8 pixels (ou seja, 64 pixels por imagem):

Figura 5.15: Imagens de baixa resolução de dígitos manuscritos

Observe que o atributo nos permite acessar uma versão tabular desse conjunto de
dados onde os exemplos são representados pelas linhas e as colunas
correspondem aos pixels:digits.data

>>> digits.data.shape
(1797, 64)
CopyExplain

Em seguida, vamos atribuir os recursos (pixels) a uma nova variável e os rótulos a


outra nova variável:X_digitsy_digits

>>> y_digits = digits.target

>>> X_digits = digits.data


CopyExplain

Em seguida, importamos a classe t-SNE do scikit-learn e ajustamos um novo


objeto. Usando , realizamos o ajuste t-SNE e a transformação de dados em uma
única etapa:tsnefit_transform

>>> from sklearn.manifold import TSNE

>>> tsne = TSNE(n_components=2, init='pca',

... random_state=123)

>>> X_digits_tsne = tsne.fit_transform(X_digits)


CopyExplain

Usando esse código, projetamos o conjunto de dados de 64 dimensões em um


espaço de 2 dimensões. Especificamos , que inicializa a incorporação t-SNE
usando PCA, como é recomendado no artigo de pesquisa A inicialização é crítica
para preservar a estrutura global de dados em t-SNE e
UMAP por Kobak e Linderman, Nature Biotechnology Volume 39, páginas 156-
157, 2021 (https://www.nature.com/articles/s41587-020-00809-z).init='pca'

Observe que o t-SNE inclui hiperparâmetros adicionais, como a perplexidade e a


taxa de aprendizado (muitas vezes chamados de épsilon), que omitimos no
exemplo (usamos os valores padrão scikit-learn). Na prática, recomendamos que
você explore esses parâmetros também. Mais informações sobre esses
parâmetros e seus efeitos sobre os resultados podem ser encontradas no
excelente artigo How to Use t-SNE
Effective de Wattenberg, Viegas e Johnson, Distill, 2016
(https://distill.pub/2016/misread-tsne/).
Finalmente, vamos visualizar as incorporações 2D t-SNE usando o seguinte
código:

>>> import matplotlib.patheffects as PathEffects

>>> def plot_projection(x, colors):

... f = plt.figure(figsize=(8, 8))

... ax = plt.subplot(aspect='equal')

... for i in range(10):

... plt.scatter(x[colors == i, 0],

... x[colors == i, 1])

... for i in range(10):

... xtext, ytext = np.median(x[colors == i, :], axis=0)

... txt = ax.text(xtext, ytext, str(i), fontsize=24)

... txt.set_path_effects([

... PathEffects.Stroke(linewidth=5, foreground="w"),

... PathEffects.Normal()])

>>> plot_projection(X_digits_tsne, y_digits)

>>> plt.show()
CopyExplain

Como PCA, t-SNE é um método não supervisionado e, no código anterior, usamos


os rótulos de classe (0-9) apenas para fins de visualização por meio do argumento
de cor de funções. Matplotlib's são usados para fins visuais, de modo que o rótulo
de classe é exibido no centro (via ) de pontos de dados pertencentes a cada
respectivo dígito. O gráfico resultante é o seguinte:y_digitsPathEffectsnp.median
Figure 5.16: A visualization of how t-SNE embeds the handwritten digits in a 2D
feature space

As we can see, t-SNE is able to separate the different digits (classes) nicely,
although not perfectly. It might be possible to achieve better separation by tuning
the hyperparameters. However, a certain degree of class mixing might be
unavoidable due to illegible handwriting. For instance, by inspecting individual
images, we might find that certain instances of the number 3 indeed look like the
number 9, and so forth.

Uniform manifold approximation and projection

Outra técnica de visualização popular é a aproximação e projeção de


variedades uniformes (UMAP). Embora o UMAP possa produzir resultados
igualmente bons como o t-SNE (por exemplo, veja o artigo de Kobak e Linderman
mencionado anteriormente), ele é tipicamente mais rápido e também pode ser
usado para projetar novos dados, o que o torna mais atraente como uma técnica
de redução de dimensionalidade em um contexto de aprendizado de máquina,
semelhante ao PCA. Os leitores interessados podem encontrar mais informações
sobre o UMAP no artigo original: UMAP: Uniform manifold approximation and
projection for dimension reduction por McInnes,
Healy e Melville, 2018 (https://arxiv.org/abs/1802.03426). Uma implementação
compatível com scikit-learn do UMAP pode ser encontrada em https://umap-
learn.readthedocs.io.

Resumo
Neste capítulo, você aprendeu sobre duas técnicas fundamentais de redução de
dimensionalidade para extração de recursos: PCA e LDA. Usando PCA,
projetamos dados em um subespaço de dimensões inferiores para maximizar a
variância ao longo dos eixos de feição ortogonais, ignorando os rótulos de classe.
A LDA, em contraste com a ACP, é uma técnica para redução de
dimensionalidade supervisionada, o que significa que ela considera as
informações de classe no conjunto de dados de treinamento para tentar maximizar
a separabilidade de classe em um espaço de feição linear. Por fim, você também
aprendeu sobre o t-SNE, que é uma técnica de extração de recursos não linear
que pode ser usada para visualizar dados em duas ou três dimensões.

Equipado com PCA e LDA como técnicas fundamentais de pré-processamento de


dados, você agora está bem preparado para aprender sobre as melhores práticas
para incorporar eficientemente diferentes técnicas de pré-processamento e avaliar
o desempenho de diferentes modelos no próximo capítulo.

Aprendendo práticas recomendadas para


avaliação de modelos e ajuste de
hiperparâmetros
Nos capítulos anteriores, aprendemos sobre os algoritmos essenciais de
aprendizado de máquina para classificação e como colocar nossos dados em
forma antes de alimentá-los nesses algoritmos. Agora, é hora de aprender sobre
as melhores práticas de construção de bons modelos de aprendizado de máquina,
ajustando os algoritmos e avaliando o desempenho dos modelos. Neste capítulo,
aprenderemos como fazer o seguinte:

 Avaliar o desempenho de modelos de aprendizado de máquina

 Diagnosticar os problemas comuns dos algoritmos de aprendizado de


máquina

 Ajuste os modelos de aprendizado de máquina

 Avaliar modelos preditivos usando diferentes métricas de desempenho

Simplificando fluxos de trabalho com


pipelines
Quando aplicamos diferentes técnicas de pré-processamento nos capítulos
anteriores, como padronização para dimensionamento de recursos no
Capítulo 4, Building Good Training Datasets – Data Preprocessing, ou análise de
componentes principais para compactação de dados no Capítulo 5, Compressing
Data via Dimensionality Reduction, você aprendeu que temos que reutilizar os
parâmetros que foram obtidos durante o ajuste dos dados de treinamento para
dimensionar e compactar quaisquer novos dados, como os exemplos no conjunto
de dados de teste separado. Nesta seção, você aprenderá sobre uma ferramenta
extremamente útil, a aula de scikit-learn. Ele nos permite ajustar um modelo,
incluindo um número arbitrário de etapas de transformação, e aplicá-lo para fazer
previsões sobre novos dados.Pipeline

Carregando o conjunto de dados do Câncer de


Mama Wisconsin
Neste capítulo, trabalharemos com o conjunto de dados Breast Cancer Wisconsin,
que contém 569 exemplos de células tumorais malignas e benignas. As duas
primeiras colunas do conjunto de dados armazenam os números de ID exclusivos
dos exemplos e os diagnósticos correspondentes ( = maligno, = benigno),
respectivamente. As colunas 3-32 contêm 30 características de valor real que
foram computadas a partir de imagens digitalizadas dos núcleos celulares, que
podem ser usadas para construir um modelo para prever se um tumor é benigno
ou maligno. O conjunto de dados do Breast Cancer Wisconsin foi depositado no
UCI Machine Learning Repository, e informações mais detalhadas sobre esse
conjunto de dados podem ser encontradas
em https://archive.ics.uci.edu/ml/datasets/Breast+Cancer+Wisconsin+
(Diagnostic).MB

Obtendo o conjunto de dados do Câncer de Mama Wisconsin

Você pode encontrar uma cópia do conjunto de dados (e todos os outros


conjuntos de dados usados neste livro) no pacote de códigos deste livro, que você
pode usar se estiver trabalhando offline ou se o servidor UCI
em https://archive.ics.uci.edu/ml/machine-learning-databases/breast-cancer-
wisconsin/wdbc.data estiver temporariamente indisponível. Por exemplo, para
carregar o conjunto de dados de um diretório local, você pode substituir as
seguintes linhas:

df = pd.read_csv(

'https://archive.ics.uci.edu/ml/'

'machine-learning-databases'

'/breast-cancer-wisconsin/wdbc.data',

header=None

)
CopyExplain

com estes:

df = pd.read_csv(

'your/local/path/to/wdbc.data',

header=None

)
CopyExplain

Nesta seção, vamos ler o conjunto de dados e dividi-lo em conjuntos de dados de


treinamento e teste em três etapas simples:

1. Começaremos lendo o conjunto de dados diretamente do site da UCI usando


pandas:
2. >>> import pandas as pd

3. >>> df = pd.read_csv('https://archive.ics.uci.edu/ml/'

4. ... 'machine-learning-databases'

5. ... '/breast-cancer-wisconsin/wdbc.data',

6. ... header=None)
CopyExplain
7. Em seguida, atribuiremos os 30 recursos a uma matriz NumPy, . Usando um
objeto, transformaremos os rótulos de classe de sua representação de cadeia
de caracteres original ( e ) em inteiros: XLabelEncoder'M''B'
8. >>> from sklearn.preprocessing import LabelEncoder

9. >>> X = df.loc[:, 2:].values

10. >>> y = df.loc[:, 1].values

11. >>> le = LabelEncoder()

12. >>> y = le.fit_transform(y)

13. >>> le.classes_

14. array(['B', 'M'], dtype=object)


CopyExplain
15. Após a codificação dos rótulos de classe (diagnóstico) em uma matriz, , os
tumores malignos passam a ser representados como classe , e os tumores
benignos são representados como classe , respectivamente. Podemos
verificar esse mapeamento chamando o método do ajuste em dois rótulos de
classe fictícia: y10transformLabelEncoder
16. >>> le.transform(['M', 'B'])

17. array([1, 0])


CopyExplain
18. Antes de construirmos nosso primeiro pipeline de modelo na subseção a
seguir, vamos dividir o conjunto de dados em um conjunto de dados de
treinamento separado (80% dos dados) e um conjunto de dados de teste
separado (20% dos dados):
19. >>> from sklearn.model_selection import train_test_split

20. >>> X_train, X_test, y_train, y_test = \

21. ... train_test_split(X, y,

22. ... test_size=0.20,

23. ... stratify=y,

24. ... random_state=1)


CopyExplain

Combinando transformadores e estimadores em


uma tubulação
No capítulo anterior, você aprendeu que muitos algoritmos de aprendizado exigem
recursos de entrada na mesma escala para um desempenho ideal. Como as
características no conjunto de dados do Breast Cancer Wisconsin são medidas em
várias escalas diferentes, padronizaremos as colunas no conjunto de dados do
Breast Cancer Wisconsin antes de alimentá-las para um classificador linear, como
a regressão logística. Além disso, vamos supor que queremos compactar nossos
dados das 30 dimensões iniciais em um subespaço bidimensional inferior
via análise de componentes principais (PCA), uma técnica de extração de
recursos para redução de dimensionalidade que foi introduzida no Capítulo 5.

Em vez de passar pelas etapas de ajuste de modelo e transformação de dados


para os conjuntos de dados de treinamento e teste separadamente, podemos
encadear os objetos , e em um pipeline:StandardScalerPCALogisticRegression

>>> from sklearn.preprocessing import StandardScaler

>>> from sklearn.decomposition import PCA

>>> from sklearn.linear_model import LogisticRegression


>>> from sklearn.pipeline import make_pipeline

>>> pipe_lr = make_pipeline(StandardScaler(),

... PCA(n_components=2),

... LogisticRegression())

>>> pipe_lr.fit(X_train, y_train)

>>> y_pred = pipe_lr.predict(X_test)

>>> test_acc = pipe_lr.score(X_test, y_test)

>>> print(f'Test accuracy: {test_acc:.3f}')

Test accuracy: 0.956


CopyExplain

A função toma um número arbitrário de transformadores scikit-learn (objetos que


suportam os métodos e como entrada), seguido por um estimador scikit-learn que
implementa os métodos e . Em nosso exemplo de código anterior, fornecemos
dois transformadores scikit-learn, e , e um estimador como entradas para a
função, que constrói um objeto scikit-learn a partir desses
objetos.make_pipelinefittransformfitpredictStandardScalerPCALogisticRegressionma
ke_pipelinePipeline

Podemos pensar em um scikit-learn como um meta-estimador ou invólucro em


torno desses transformadores e estimadores individuais. Se chamarmos o método
de , os dados serão passados por uma série de transformadores via e chamadas
nessas etapas intermediárias até chegar ao objeto estimador (o elemento final em
uma tubulação). O estimador será então ajustado aos dados de treinamento
transformados.PipelinefitPipelinefittransform

Quando executamos o método no pipeline no exemplo de código anterior, primeiro


executamos e chamamos os dados de treinamento. Em segundo lugar, os dados
de treinamento transformados foram passados para o próximo objeto no pipeline, .
Semelhante à etapa anterior, também executou e escalou os dados de entrada e
passou para o elemento final do pipeline, o
estimador.fitpipe_lrStandardScalerfittransformPCAPCAfittransform
Finalmente, o estimador foi ajustado aos dados de treinamento após sofrer
transformações via e . Mais uma vez, devemos notar que não há limite para o
número de etapas intermediárias em um pipeline; No entanto, se quisermos usar o
pipeline para tarefas de previsão, o último elemento do pipeline deve ser um
estimador.LogisticRegressionStandardScalerPCA

Semelhante à chamada de um pipeline, os pipelines também implementam um


método se a última etapa do pipeline for um estimador. Se alimentarmos um
conjunto de dados para a chamada de uma instância de objeto, os dados
passarão pelas etapas intermediárias por meio de chamadas. Na etapa final, o
objeto estimador retornará uma previsão sobre os dados
transformados.fitpredictpredictPipelinetransform

Os pipelines da biblioteca scikit-learn são ferramentas de wrapper imensamente


úteis que usaremos com frequência ao longo do resto deste livro. Para ter certeza
de que você tem uma boa compreensão de como o objeto funciona, dê uma
olhada na Figura 6.1, que resume nossa discussão dos parágrafos
anteriores:Pipeline
Figura 6.1: O funcionamento interno do objeto Pipeline

Usando validação cruzada k-fold para


avaliar o desempenho do modelo
Nesta seção, você aprenderá sobre as técnicas comuns de validação cruzada
e validação cruzada k-fold, que podem nos ajudar a obter estimativas confiáveis
do desempenho de generalização do modelo, ou seja, quão bem o modelo se
comporta em dados não vistos.

O método de retenção
Uma abordagem clássica e popular para estimar o desempenho de generalização
de modelos de aprendizado de máquina é o método holdout. Usando o método
holdout, dividimos nosso conjunto de dados inicial em conjuntos de dados de
treinamento e teste separados — o primeiro é usado para treinamento de modelo
e o segundo é usado para estimar seu desempenho de generalização. No entanto,
em aplicativos típicos de aprendizado de máquina, também estamos interessados
em ajustar e comparar diferentes configurações de parâmetros para melhorar
ainda mais o desempenho para fazer previsões em dados não vistos. Esse
processo é chamado de seleção de modelo, com o nome se referindo a um
determinado problema de classificação para o qual queremos selecionar os
valores ótimos dos parâmetros de ajuste (também chamados
de hiperparâmetros). No entanto, se reutilizarmos o mesmo conjunto de dados
de teste repetidamente durante a seleção do modelo, ele se tornará parte de
nossos dados de treinamento e, portanto, o modelo terá maior probabilidade de
superajustar. Apesar desse problema, muitas pessoas ainda usam o conjunto de
dados de teste para seleção de modelo, o que não é uma boa prática de
aprendizado de máquina.

Uma maneira melhor de usar o método de retenção para seleção de modelo é


separar os dados em três partes: um conjunto de dados de treinamento, um
conjunto de dados de validação e um conjunto de dados de teste. O conjunto de
dados de treinamento é usado para ajustar os diferentes modelos, e o
desempenho no conjunto de dados de validação é usado para a seleção do
modelo. A vantagem de ter um conjunto de dados de teste que o modelo não viu
antes durante as etapas de treinamento e seleção do modelo é que podemos
obter uma estimativa menos enviesada de sua capacidade de generalizar para
novos dados. A figura 6.2 ilustra o conceito de validação cruzada de holdout, onde
usamos um conjunto de dados de validação para avaliar repetidamente o
desempenho do modelo após o treinamento usando diferentes valores de
hiperparâmetros. Uma vez satisfeitos com o ajuste dos valores de
hiperparâmetros, estimamos o desempenho de generalização do modelo no
conjunto de dados de teste:

Figura 6.2: Como usar conjuntos de dados de treinamento, validação e teste

Uma desvantagem do método de retenção é que a estimativa de desempenho


pode ser muito sensível à forma como particionamos o conjunto de dados de
treinamento nos subconjuntos de treinamento e validação; A estimativa variará
para diferentes exemplos dos dados. Na próxima subseção, examinaremos uma
técnica mais robusta para estimativa de desempenho, a validação cruzada k-fold,
onde repetimos o método de retenção k vezes em subconjuntos k dos
dados de treinamento.

Validação cruzada K-fold


Na validação cruzada de k-fold, dividimos aleatoriamente o conjunto de dados de
treinamento em k dobras sem substituição. Aqui, k – 1 dobras, as
chamadas dobras de treinamento, são usadas para o treinamento modelo, e uma
dobra, a chamada dobra de teste, é usada para avaliação de desempenho. Este
procedimento é repetido k vezes para que se obtenham modelos k e estimativas
de desempenho.

Amostragem com e sem substituição

Analisamos um exemplo para ilustrar a amostragem com e sem substituição


no Capítulo 3. Se você não leu esse capítulo ou deseja uma atualização, consulte
a caixa de informações intitulada Amostragem com e sem substituição na
seção Combinando várias árvores de decisão por meio de florestas aleatórias.

Em seguida, calculamos o desempenho médio dos modelos com base nas


diferentes dobras de teste independentes para obter uma estimativa de
desempenho menos sensível ao subparticionamento dos dados de treinamento
em comparação com o método de holdout. Normalmente, usamos validação
cruzada k-fold para ajuste do modelo, ou seja, encontrar os valores ótimos de
hiperparâmetros que produzem um desempenho de generalização satisfatório,
que é estimado a partir da avaliação do desempenho do modelo nas dobras de
teste.

Uma vez encontrados valores satisfatórios de hiperparâmetros, podemos retreinar


o modelo no conjunto completo de dados de treinamento e obter uma estimativa
de desempenho final usando o conjunto de dados de teste independente. A lógica
por trás do ajuste de um modelo a todo o conjunto de dados de treinamento após
a validação cruzada k-fold é que, primeiro, estamos tipicamente interessados em
um único modelo final (versus k modelos individuais) e, segundo, fornecer mais
exemplos de treinamento para um algoritmo de aprendizado geralmente resulta
em um modelo mais preciso e robusto.
Como a validação cruzada k-fold é uma técnica de reamostragem sem
substituição, a vantagem dessa abordagem é que, em cada iteração, cada
exemplo será usado exatamente uma vez, e as dobras de treinamento e teste são
disjuntas. Além disso, todas as dobras de teste são disjuntas; ou seja, não há
sobreposição entre as dobras de teste. A figura 6.3 resume o conceito por trás da
validação cruzada k-fold com k = 10. O conjunto de dados de treinamento é
dividido em 10 dobras, e durante as 10 iterações, 9 dobras são usadas para
treinamento, e 1 dobra será usada como o conjunto de dados de teste para
avaliação do modelo.

Além disso, os desempenhos estimados, Eeu (por exemplo, precisão ou erro de


classificação), para cada dobra são então usados para calcular o desempenho
médio estimado, E, do modelo:

Figura 6.3: Como funciona a validação cruzada k-fold

Em resumo, a validação cruzada k-fold faz melhor uso do conjunto de dados do


que o método holdout com um conjunto de validação, uma vez que na validação
cruzada k-fold todos os pontos de dados estão sendo usados para avaliação.

Um bom valor padrão para k na validação cruzada k-fold é 10, como mostram


evidências empíricas. Por exemplo, experimentos de Ron Kohavi em vários
conjuntos de dados do mundo real sugerem que a validação cruzada de 10 vezes
oferece o melhor tradeoff entre viés e variância (A Study of Cross-Validation and
Bootstrap for Accuracy Estimation and Model Selection by Kohavi,
Ron, International Joint Conference on Artificial Intelligence (IJCAI), 14 (12): 1137-
43, 1995, https://www.ijcai.org/Proceedings/95-2/Papers/016.pdf).

No entanto, se estivermos trabalhando com conjuntos de treinamento


relativamente pequenos, pode ser útil aumentar o número de dobras. Se
aumentarmos o valor de k, mais dados de treinamento serão usados em cada
iteração, o que resulta em um viés pessimista menor para estimar o desempenho
de generalização pela média das estimativas individuais do modelo. No entanto,
grandes valores de k também aumentarão o tempo de execução do algoritmo de
validação cruzada e as estimativas de rendimento com maior variância, uma vez
que as dobras de treinamento serão mais semelhantes entre si. Por outro lado, se
estivermos trabalhando com grandes conjuntos de dados, podemos escolher um
valor menor para k, por exemplo, k = 5, e ainda obter uma estimativa precisa do
desempenho médio do modelo, reduzindo o custo computacional de reajuste e
avaliação do modelo nas diferentes dobras.

Validação cruzada de saída única

Um caso especial de validação cruzada k-fold é o método de validação cruzada


leave-one-out (LOOCV). No LOOCV, definimos o número de dobras igual ao
número de exemplos de treinamento (k = n) para que apenas um exemplo de
treinamento seja usado para teste durante cada iteração, que é uma abordagem
recomendada para trabalhar com conjuntos de dados muito pequenos.

Uma pequena melhora em relação à abordagem de validação cruzada padrão k-


fold é a validação cruzada estratificada k-fold, que pode produzir melhores
estimativas de viés e variância, especialmente em casos de proporções de classe
desiguais, o que também foi mostrado no mesmo estudo de Ron Kohavi
referenciado anteriormente nesta seção. Na validação cruzada estratificada, as
proporções do rótulo de classe são preservadas em cada dobra para garantir que
cada dobra seja representativa das proporções de classe no conjunto de dados de
treinamento, o que ilustraremos usando o iterador em scikit-learn: StratifiedKFold

>>> import numpy as np


>>> from sklearn.model_selection import StratifiedKFold

>>> kfold = StratifiedKFold(n_splits=10).split(X_train, y_train)

>>> scores = []

>>> for k, (train, test) in enumerate(kfold):

... pipe_lr.fit(X_train[train], y_train[train])

... score = pipe_lr.score(X_train[test], y_train[test])

... scores.append(score)

... print(f'Fold: {k+1:02d}, '

... f'Class distr.: {np.bincount(y_train[train])}, '

... f'Acc.: {score:.3f}')

Fold: 01, Class distr.: [256 153], Acc.: 0.935

Fold: 02, Class distr.: [256 153], Acc.: 0.935

Fold: 03, Class distr.: [256 153], Acc.: 0.957

Fold: 04, Class distr.: [256 153], Acc.: 0.957

Fold: 05, Class distr.: [256 153], Acc.: 0.935

Fold: 06, Class distr.: [257 153], Acc.: 0.956

Fold: 07, Class distr.: [257 153], Acc.: 0.978

Fold: 08, Class distr.: [257 153], Acc.: 0.933

Fold: 09, Class distr.: [257 153], Acc.: 0.956

Fold: 10, Class distr.: [257 153], Acc.: 0.956

>>> mean_acc = np.mean(scores)

>>> std_acc = np.std(scores)

>>> print(f'\nCV accuracy: {mean_acc:.3f} +/- {std_acc:.3f}')

CV accuracy: 0.950 +/- 0.014


CopyExplain

Primeiro, inicializamos o iterador do módulo com os rótulos de classe no conjunto


de dados de treinamento e especificamos o número de dobras por meio do
parâmetro. Quando usamos o iterador para percorrer as dobras, usamos os
índices retornados para ajustar o pipeline de regressão logística que configuramos
no início deste capítulo. Usando o pipeline, garantimos que os exemplos fossem
dimensionados corretamente (por exemplo, padronizados) em cada iteração. Em
seguida, utilizamos os índices para calcular o escore de acurácia do modelo, que
coletamos na lista para calcular a acurácia média e o desvio padrão da
estimativa.StratifiedKFoldsklearn.model_selectiony_trainn_splitskfoldktrainpipe_
lrtestscores

Embora o exemplo de código anterior tenha sido útil para ilustrar como a validação
cruzada k-fold funciona, o scikit-learn também implementa um pontuador de
validação cruzada k-fold, o que nos permite avaliar nosso modelo usando
validação cruzada k-fold estratificada de forma menos detalhada:

>>> from sklearn.model_selection import cross_val_score

>>> scores = cross_val_score(estimator=pipe_lr,

... X=X_train,

... y=y_train,

... cv=10,

... n_jobs=1)

>>> print(f'CV accuracy scores: {scores}')

CV accuracy scores: [ 0.93478261 0.93478261 0.95652174

0.95652174 0.93478261 0.95555556

0.97777778 0.93333333 0.95555556

0.95555556]

>>> print(f'CV accuracy: {np.mean(scores):.3f} '

... f'+/- {np.std(scores):.3f}')

CV accuracy: 0.950 +/- 0.014


CopyExplain

Uma característica extremamente útil da abordagem é que podemos distribuir a


avaliação das diferentes dobras em várias unidades centrais de
processamento (CPUs) em nossa máquina. Se definirmos o parâmetro como ,
apenas uma CPU será usada para avaliar os desempenhos, assim como em
nosso exemplo anterior. No entanto, ao definir , poderíamos distribuir as 10
rodadas de validação cruzada para duas CPUs (se disponíveis em nossa
máquina), e ao definir , podemos usar todas as CPUs disponíveis em nossa
máquina para fazer a computação em
paralelo.cross_val_scoren_jobs1StratifiedKFoldn_jobs=2n_jobs=-1

Estimando o desempenho de generalização

Observe que uma discussão detalhada de como a variância do desempenho de


generalização é estimada na validação cruzada está além do escopo deste livro,
mas você pode consultar um artigo abrangente sobre avaliação de modelos e
validação cruzada (Avaliação de Modelos, Seleção de Modelos e Seleção de
Algoritmos em Aprendizado de Máquina de S. Raschka), que compartilhamos
em https://arxiv.org/abs/1811.12808. Este artigo também discute técnicas
alternativas de validação cruzada, como os métodos de validação cruzada de
bootstrap .632 e .632+.

Além disso, você pode encontrar uma discussão detalhada em um excelente


artigo de M. Markatou e outros (Analysis of Variance of Cross-validation
Estimators of the Generalization Error de M. Markatou, H. Tian, S. Biswas e G. M.
Hripcsak, Journal of Machine Learning Research, 6: 1127-1168, 2005), que está
disponível em https://www.jmlr.org/papers/v6/markatou05a.html.

Depuração de algoritmos com curvas de


aprendizado e validação
Nesta seção, vamos dar uma olhada em duas ferramentas de diagnóstico muito
simples, mas poderosas, que podem nos ajudar a melhorar o desempenho de um
algoritmo de aprendizagem: curvas de aprendizado e curvas de validação. Nas
próximas subseções, discutiremos como podemos usar curvas de
aprendizado para diagnosticar se um algoritmo de aprendizagem tem um
problema com overfitting (alta variância) ou underfitting (alto viés). Além disso,
daremos uma olhada nas curvas de validação, que podem nos ajudar a abordar os
problemas comuns dos algoritmos de aprendizagem.

Diagnosticando problemas de viés e variância com


curvas de aprendizado
Se um modelo for muito complexo para um determinado conjunto de dados de
treinamento — por exemplo, pense em uma árvore de decisão muito profunda — o
modelo tende a sobreajustar os dados de treinamento e não generaliza bem para
dados invisíveis. Muitas vezes, pode ajudar a coletar mais exemplos de
treinamento para reduzir o grau de overfitting.

No entanto, na prática, muitas vezes pode ser muito caro ou simplesmente inviável
coletar mais dados. Ao plotar as precisões de treinamento e validação do modelo
como funções do tamanho do conjunto de dados de treinamento, podemos
detectar facilmente se o modelo sofre de alta variância ou alto viés, e se a coleta
de mais dados poderia ajudar a resolver esse problema.

Mas antes de discutirmos como traçar curvas de aprendizado no scikit-learn,


vamos discutir esses dois problemas comuns de modelo caminhando pela
ilustração a seguir:
Figura 6.4: Problemas comuns do modelo

O gráfico no canto superior esquerdo mostra um modelo com viés alto. Esse
modelo tem baixa precisão de treinamento e validação cruzada, o que indica que
ele se ajusta aos dados de treinamento. Maneiras comuns de resolver esse
problema são aumentar o número de parâmetros do modelo, por exemplo,
coletando ou construindo recursos adicionais, ou diminuindo o grau de
regularização, por exemplo, em máquina de vetor de suporte (SVM)
ou classificadores de regressão logística.

O gráfico no canto superior direito mostra um modelo que sofre de alta variância, o
que é indicado pela grande lacuna entre a precisão do treinamento e da validação
cruzada. Para resolver esse problema de overfitting, podemos coletar mais dados
de treinamento, reduzir a complexidade do modelo ou aumentar o parâmetro de
regularização, por exemplo.

Para modelos não regularizados, também pode ajudar a diminuir o número de


recursos via seleção de recursos (Capítulo 4) ou extração de recursos (Capítulo 5)
para diminuir o grau de sobreajuste. Embora a coleta de mais dados de
treinamento geralmente tenda a diminuir a chance de overfitting, nem sempre
pode ajudar, por exemplo, se os dados de treinamento forem extremamente
barulhentos ou o modelo já estiver muito próximo do ideal.

Na próxima subseção, veremos como resolver esses problemas de modelo


usando curvas de validação, mas vamos primeiro ver como podemos usar a
função de curva de aprendizado do scikit-learn para avaliar o modelo:

>>> import matplotlib.pyplot as plt

>>> from sklearn.model_selection import learning_curve

>>> pipe_lr = make_pipeline(StandardScaler(),

... LogisticRegression(penalty='l2',

... max_iter=10000))

>>> train_sizes, train_scores, test_scores =\

... learning_curve(estimator=pipe_lr,

... X=X_train,

... y=y_train,

... train_sizes=np.linspace(

... 0.1, 1.0, 10),

... cv=10,

... n_jobs=1)

>>> train_mean = np.mean(train_scores, axis=1)

>>> train_std = np.std(train_scores, axis=1)

>>> test_mean = np.mean(test_scores, axis=1)

>>> test_std = np.std(test_scores, axis=1)


>>> plt.plot(train_sizes, train_mean,

... color='blue', marker='o',

... markersize=5, label='Training accuracy')

>>> plt.fill_between(train_sizes,

... train_mean + train_std,

... train_mean - train_std,

... alpha=0.15, color='blue')

>>> plt.plot(train_sizes, test_mean,

... color='green', linestyle='--',

... marker='s', markersize=5,

... label='Validation accuracy')

>>> plt.fill_between(train_sizes,

... test_mean + test_std,

... test_mean - test_std,

... alpha=0.15, color='green')

>>> plt.grid()

>>> plt.xlabel('Number of training examples')

>>> plt.ylabel('Accuracy')

>>> plt.legend(loc='lower right')

>>> plt.ylim([0.8, 1.03])

>>> plt.show()
CopyExplain

Observe que passamos como um argumento adicional ao instanciar o objeto (que


usa 1.000 iterações como padrão) para evitar problemas de convergência para os
tamanhos de conjunto de dados menores ou valores de parâmetro de
regularização extremos (abordados na próxima seção). Depois de executarmos
com sucesso o código anterior, obteremos o seguinte gráfico de curva
de aprendizado:max_iter=10000LogisticRegression
Figura 6.5: Uma curva de aprendizagem que mostra a precisão do conjunto de
dados de treinamento e validação pelo número de exemplos de treinamento

Através do parâmetro na função, podemos controlar o número absoluto ou relativo


de exemplos de treinamento que são usados para gerar as curvas de
aprendizado. Aqui, definimos o uso de 10 intervalos relativos espaçados
uniformemente para os tamanhos do conjunto de dados de treinamento. Por
padrão, a função usa validação cruzada k-fold estratificada para calcular a
precisão de validação cruzada de um classificador, e definimos k = 10 através do
parâmetro para validação cruzada estratificada de 10
vezes.train_sizeslearning_curvetrain_sizes=np.linspace(0.1, 1.0,
10)learning_curvecv

Em seguida, nós simplesmente calculamos as acurácias médias dos escores de


treinamento e teste cruzados retornados para os diferentes tamanhos do conjunto
de dados de treinamento, que plotamos usando a função de Matplotlib. Além
disso, foi adicionado o desvio padrão da precisão média ao gráfico usando a
função para indicar a variância da estimativa.plotfill_between
Como podemos ver no gráfico da curva de aprendizado anterior, nosso modelo
tem um desempenho muito bom nos conjuntos de dados de treinamento e
validação se tiver visto mais de 250 exemplos durante o treinamento. Também
podemos ver que a precisão do treinamento aumenta para conjuntos de dados de
treinamento com menos de 250 exemplos, e a lacuna entre a validação e a
precisão do treinamento aumenta — um indicador de um grau crescente de
sobreajuste.

Abordando o over- e underfitting com curvas de


validação
As curvas de validação são uma ferramenta útil para melhorar o desempenho de
um modelo, abordando questões como overfitting ou underfitting. As curvas de
validação estão relacionadas às curvas de aprendizado, mas ao invés de plotar as
acurácias de treinamento e teste como função do tamanho da amostra, variamos
os valores dos parâmetros do modelo, por exemplo, o parâmetro de regularização
inversa, , em regressão logística. C

Vamos em frente e ver como criamos curvas de validação via scikit-learn:

>>> from sklearn.model_selection import validation_curve

>>> param_range = [0.001, 0.01, 0.1, 1.0, 10.0, 100.0]

>>> train_scores, test_scores = validation_curve(

... estimator=pipe_lr,

... X=X_train,

... y=y_train,

... param_name='logisticregression__C',

... param_range=param_range,

... cv=10)

>>> train_mean = np.mean(train_scores, axis=1)

>>> train_std = np.std(train_scores, axis=1)


>>> test_mean = np.mean(test_scores, axis=1)

>>> test_std = np.std(test_scores, axis=1)

>>> plt.plot(param_range, train_mean,

... color='blue', marker='o',

... markersize=5, label='Training accuracy')

>>> plt.fill_between(param_range, train_mean + train_std,

... train_mean - train_std, alpha=0.15,

... color='blue')

>>> plt.plot(param_range, test_mean,

... color='green', linestyle='--',

... marker='s', markersize=5,

... label='Validation accuracy')

>>> plt.fill_between(param_range,

... test_mean + test_std,

... test_mean - test_std,

... alpha=0.15, color='green')

>>> plt.grid()

>>> plt.xscale('log')

>>> plt.legend(loc='lower right')

>>> plt.xlabel('Parameter C')

>>> plt.ylabel('Accuracy')

>>> plt.ylim([0.8, 1.0])

>>> plt.show()
CopyExplain

Utilizando o código anterior, obteve-se o gráfico da curva de validação para o


parâmetro :C
Figure 6.6: A validation curve plot for the SVM hyperparameter C

Similar to the function, the function uses stratified k-fold cross-validation by


default to estimate the performance of the classifier. Inside the function, we
specified the parameter that we wanted to evaluate. In this case, it is , the inverse
regularization parameter of the classifier, which we wrote as to access the object
inside the scikit-learn pipeline for a specified value range that we set via
the parameter. Similar to the learning curve example in the previous section, we
plotted the average training and cross-validation accuracies and the corresponding
standard
deviations.learning_curvevalidation_curvevalidation_curveCLogisticRegression'log
isticregression__C'LogisticRegressionparam_range

Although the differences in the accuracy for varying values of are subtle, we can
see that the model slightly underfits the data when we increase the regularization
strength (small values of ). However, for large values of , it means lowering the
strength of regularization, so the model tends to slightly overfit the data. In this
case, the sweet spot appears to be between 0.1 and 1.0 of the value.CCCC
Ajustando modelos de aprendizado de
máquina por meio de pesquisa em grade
No aprendizado de máquina, temos dois tipos de parâmetros: aqueles que são
aprendidos a partir dos dados de treinamento, por exemplo, os pesos na
regressão logística, e os parâmetros de um algoritmo de aprendizado que são
otimizados separadamente. Estes últimos são os parâmetros de sintonia (ou
hiperparâmetros) de um modelo, por exemplo, o parâmetro de regularização em
regressão logística ou o parâmetro de profundidade máxima de uma árvore de
decisão.

Na seção anterior, usamos curvas de validação para melhorar o desempenho de


um modelo ajustando um de seus hiperparâmetros. Nesta seção, vamos dar uma
olhada em uma técnica popular de otimização de hiperparâmetros
chamada pesquisa em grade, que pode ajudar ainda mais a melhorar o
desempenho de um modelo encontrando a combinação ideal de valores de
hiperparâmetros.

Ajustando hiperparâmetros por meio de pesquisa


em grade
A abordagem de busca em grade é bastante simples: é um paradigma de
pesquisa exaustiva de força bruta onde especificamos uma lista de valores para
diferentes hiperparâmetros, e o computador avalia o desempenho do modelo para
cada combinação para obter a combinação ideal de valores desse conjunto:

>>> from sklearn.model_selection import GridSearchCV

>>> from sklearn.svm import SVC

>>> pipe_svc = make_pipeline(StandardScaler(),

... SVC(random_state=1))

>>> param_range = [0.0001, 0.001, 0.01, 0.1,

... 1.0, 10.0, 100.0, 1000.0]


>>> param_grid = [{'svc__C': param_range,

... 'svc__kernel': ['linear']},

... {'svc__C': param_range,

... 'svc__gamma': param_range,

... 'svc__kernel': ['rbf']}]

>>> gs = GridSearchCV(estimator=pipe_svc,

... param_grid=param_grid,

... scoring='accuracy',

... cv=10,

... refit=True,

... n_jobs=-1)

>>> gs = gs.fit(X_train, y_train)

>>> print(gs.best_score_)

0.9846153846153847

>>> print(gs.best_params_)

{'svc__C': 100.0, 'svc__gamma': 0.001, 'svc__kernel': 'rbf'}


CopyExplain

Usando o código anterior, inicializamos um objeto do módulo para treinar e ajustar


um pipeline SVM. Definimos o parâmetro de para uma lista de dicionários para
especificar os parâmetros que gostaríamos de ajustar. Para a MVS linear, avaliou-
se apenas o parâmetro de regularização inversa, ; para o kernel SVM da função
base radial (RBF), ajustamos os parâmetros e . Observe que o parâmetro é
específico para SVMs do
kernel.GridSearchCVsklearn.model_selectionparam_gridGridSearchCVCsvc__Csvc__gamm
asvc__gamma

GridSearchCV usa validação cruzada k-fold para comparar modelos treinados com


diferentes configurações de hiperparâmetros. Por meio da configuração, ele
realizará validação cruzada de 10 vezes e calculará a precisão média (via ) nessas
10 vezes para avaliar o desempenho do modelo. Nós configuramos para que
possamos usar todos os nossos núcleos de processamento para acelerar a
pesquisa de grade, ajustando os modelos às diferentes dobras em paralelo, mas
se sua máquina tiver problemas com essa configuração, você pode alterar essa
configuração para processamento único.cv=10scoring='accuracy'n_jobs=-
1GridSearchCVn_jobs=None

Após utilizarmos os dados de treinamento para realizar a busca na grade,


obtivemos a pontuação do modelo com melhor desempenho por meio do atributo
e observamos seus parâmetros, que podem ser acessados por meio do atributo.
Neste caso em particular, o modelo SVM do kernel RBF com produziu a melhor
precisão de validação cruzada k-fold: 98,5 por cento. best_score_best_params_svc__C
= 100.0

Finalmente, usamos o conjunto de dados de teste independente para estimar o


desempenho do modelo melhor selecionado, que está disponível por meio do
atributo do objeto:best_estimator_GridSearchCV

>>> clf = gs.best_estimator_

>>> clf.fit(X_train, y_train)

>>> print(f'Test accuracy: {clf.score(X_test, y_test):.3f}')

Test accuracy: 0.974


CopyExplain

Por favor, note que não é necessário ajustar um modelo com as melhores
configurações () no conjunto de treinamento manualmente após a conclusão da
pesquisa de grade. A classe tem um parâmetro, que irá refazer o conjunto de
treinamento inteiro automaticamente se definirmos
(padrão).gs.best_estimator_clf.fit(X_train,
y_train)GridSearchCVrefitgs.best_estimator_refit=True

Explorando configurações de hiperparâmetros


mais amplamente com pesquisa aleatória
Como a pesquisa em grade é uma pesquisa exaustiva, é garantido encontrar a
configuração de hiperparâmetro ideal se ela estiver contida na grade de
parâmetros especificada pelo usuário. No entanto, especificar grandes grades de
hiperparâmetros torna a pesquisa em grade muito cara na prática.
Uma abordagem alternativa para amostragem de diferentes combinações de
parâmetros é a busca aleatória. Na busca aleatória, desenhamos configurações
de hiperparâmetros aleatoriamente a partir de distribuições (ou conjuntos
discretos). Em contraste com a pesquisa em grade, a pesquisa aleatória não faz
uma pesquisa exaustiva sobre o espaço de hiperparâmetros. Ainda assim, nos
permite explorar uma gama mais ampla de configurações de valor de
hiperparâmetros de uma maneira mais econômica e econômica. Esse conceito é
ilustrado na Figura 6.7, que mostra uma grade fixa de nove configurações de
hiperparâmetros sendo pesquisadas por meio de pesquisa em grade e pesquisa
aleatória:

Figura 6.7: Comparação da pesquisa em grade e da busca aleatória para


amostragem de nove configurações diferentes de hiperparâmetros cada

A principal conclusão é que, embora a pesquisa em grade explore apenas opções


discretas especificadas pelo usuário, ela pode perder boas configurações de
hiperparâmetros se o espaço de pesquisa for muito escasso. Os leitores
interessados podem encontrar detalhes adicionais sobre a pesquisa aleatória,
juntamente com estudos empíricos, no seguinte artigo: Random Search for Hyper-
Parameter Optimization por J. Bergstra, Y. Bengio, Journal of Machine Learning
Research, pp. 281-305,
2012, https://www.jmlr.org/papers/volume13/bergstra12a/bergstra12a.
Vejamos como podemos usar a pesquisa aleatória para ajustar um SVM. Scikit-
learn implementa uma classe, que é análoga à que usamos na subseção anterior.
A principal diferença é que podemos especificar distribuições como parte de nossa
grade de parâmetros e especificar o número total de configurações de
hiperparâmetros a serem avaliadas. Por exemplo, vamos considerar o intervalo de
hiperparâmetros que usamos para vários hiperparâmetros ao ajustar o SVM no
exemplo de pesquisa de grade na seção anterior: RandomizedSearchCVGridSearchCV

>>> import scipy.stats

>>> param_range = [0.0001, 0.001, 0.01, 0.1,

... 1.0, 10.0, 100.0, 1000.0]


CopyExplain

Observe que, embora possa aceitar listas discretas semelhantes de valores como
entradas para a grade de parâmetros, o que é útil ao considerar hiperparâmetros
categóricos, seu principal poder reside no fato de que podemos substituir essas
listas por distribuições para amostragem. Assim, por exemplo, podemos substituir
a lista anterior pela seguinte distribuição da SciPy: RandomizedSearchCV

>>> param_range = scipy.stats.loguniform(0.0001, 1000.0)


CopyExplain

Por exemplo, o uso de uma distribuição loguniforme em vez de uma distribuição


uniforme regular garantirá que, em um número suficientemente grande de ensaios,
o mesmo número de amostras será extraído do intervalo [0,0001, 0,001] como, por
exemplo, o intervalo [10,0, 100,0]. Para verificar seu comportamento, podemos
extrair 10 amostras aleatórias dessa distribuição através do método, como
mostrado aqui:rvs(10)

>>> np.random.seed(1)

>>> param_range.rvs(10)

array([8.30145146e-02, 1.10222804e+01, 1.00184520e-04, 1.30715777e-02,

1.06485687e-03, 4.42965766e-04, 2.01289666e-03, 2.62376594e-02,

5.98924832e-02, 5.91176467e-01])
CopyExplain
Especificando distribuições

RandomizedSearchCV suporta distribuições arbitrárias, desde que possamos obter


amostras delas chamando o método. Uma lista de todas as distribuições
atualmente disponíveis via pode ser encontrada
aqui: https://docs.scipy.org/doc/scipy/reference/stats.html#probability-
distributions.rvs()scipy.stats

Vamos agora ver o em ação e ajustar um SVM como fizemos na seção


anterior:RandomizedSearchCVGridSearchCV

>>> from sklearn.model_selection import RandomizedSearchCV

>>> pipe_svc = make_pipeline(StandardScaler(),

... SVC(random_state=1))

>>> param_grid = [{'svc__C': param_range,

... 'svc__kernel': ['linear']},

... {'svc__C': param_range,

... 'svc__gamma': param_range,

... 'svc__kernel': ['rbf']}]

>>> rs = RandomizedSearchCV(estimator=pipe_svc,

... param_distributions=param_grid,

... scoring='accuracy',

... refit=True,

... n_iter=20,

... cv=10,

... random_state=1,

... n_jobs=-1)

>>> rs = rs.fit(X_train, y_train)

>>> print(rs.best_score_)

0.9670531400966184
>>> print(rs.best_params_)

{'svc__C': 0.05971247755848464, 'svc__kernel': 'linear'}


CopyExplain

Com base neste exemplo de código, podemos ver que o uso é muito semelhante
ao , exceto que poderíamos usar distribuições para especificar intervalos de
parâmetros e especificar o número de iterações — 20 iterações —
definindo .GridSearchCVn_iter=20

Pesquisa de hiperparâmetros mais eficiente em


termos de recursos com redução sucessiva pela
metade
Levando a ideia de busca aleatória um passo adiante, o scikit-learn implementa
uma variante sucessiva de halving, , que torna mais eficiente encontrar
configurações de hiperparâmetros adequadas. O halving sucessivo, dado um
grande conjunto de configurações candidatas, lança sucessivamente fora
configurações de hiperparâmetros pouco promissoras até que reste apenas uma
configuração. Podemos resumir o procedimento através dos seguintes
passos:HalvingRandomSearchCV

1. Desenhe um grande conjunto de configurações candidatas por amostragem


aleatória
2. Treinar os modelos com recursos limitados, por exemplo, um pequeno
subconjunto dos dados de treinamento (em vez de usar todo o conjunto de
treinamento)
3. Descarte os 50% mais baixos com base no desempenho preditivo
4. Volte para a etapa 2 com uma quantidade maior de recursos disponíveis

As etapas são repetidas até que reste apenas uma configuração de


hiperparâmetro. Observe que também há uma implementação de halving
sucessiva para a variante de pesquisa de grade chamada , onde todas as
configurações de hiperparâmetro especificadas são usadas na etapa 1 em vez de
amostras aleatórias.HalvingGridSearchCV
No scikit-learn 1.0, ainda é experimental, e é por isso que temos que habilitá-lo
primeiro:HalvingRandomSearchCV

>>> from sklearn.experimental import enable_halving_search_cv


CopyExplain

(O código acima pode não funcionar ou ser suportado em versões futuras.)

Após habilitar o suporte experimental, podemos utilizar a busca aleatória com


halving sucessivo, como mostrado a seguir:

>>> from sklearn.model_selection import HalvingRandomSearchCV

>>> hs = HalvingRandomSearchCV(pipe_svc,

... param_distributions=param_grid,

... n_candidates='exhaust',

... resource='n_samples',

... factor=1.5,

... random_state=1,

... n_jobs=-1)
CopyExplain

A configuração (padrão) especifica que consideramos o tamanho do conjunto de


treinamento como o recurso que variamos entre as rodadas. Através do
parâmetro, podemos determinar quantos candidatos são eliminados em cada
rodada. Por exemplo, definir elimina metade dos candidatos, e definir significa que
apenas 100%/1,5 ≈ 66% dos candidatos chegam ao próximo turno. Em vez de
escolher um número fixo de iterações como no , definimos (padrão), que
amostrará o número de configurações de hiperparâmetros de modo que o número
máximo de recursos (aqui: exemplos de treinamento) seja usado na última
rodada.resource='n_samples'factorfactor=2factor=1.5RandomizedSearchCVn_candidat
es='exhaust'

Podemos então realizar a pesquisa semelhante a:RandomizedSearchCV


>>> hs = hs.fit(X_train, y_train)

>>> print(hs.best_score_)

0.9617647058823529

>>> print(hs.best_params_)

{'svc__C': 4.934834261073341, 'svc__kernel': 'linear'}

>>> clf = hs.best_estimator_

>>> print(f'Test accuracy: {hs.score(X_test, y_test):.3f}')

Test accuracy: 0.982


CopyExplain

Se compararmos os resultados de e das duas subseções anteriores com o modelo


do , podemos ver que o último produz um modelo que tem um desempenho
ligeiramente melhor no conjunto de teste (98,2% de precisão contra
97,4%).GridSearchCVRandomizedSearchCVHalvingRandomSearchCV

Ajuste de hiperparâmetros com hyperopt

Outra biblioteca popular para otimização de hiperparâmetros é a hyperopt


(https://github.com/hyperopt/hyperopt), que implementa vários métodos diferentes
para otimização de hiperparâmetros, incluindo pesquisa aleatória e o
método Tree-structured Parzen Estimators (TPE). O TPE é um método de
otimização bayesiana baseado em um modelo probabilístico que é continuamente
atualizado com base em avaliações de hiperparâmetros anteriores e nos escores
de desempenho associados, em vez de considerar essas avaliações como
eventos independentes. Você pode descobrir mais sobre TPE em Algoritmos para
otimização de hiperparâmetros. Bergstra J, Bardenet R, Bengio Y, Kegl
B. NeurIPS 2011. pp. 2546-
2554, https://dl.acm.org/doi/10.5555/2986459.2986743.

Embora o hyperopt forneça uma interface de uso geral para otimização de


hiperparâmetros, há também um pacote específico do scikit-learn chamado
hyperopt-sklearn para conveniência
adicional: https://github.com/hyperopt/hyperopt-sklearn.
Seleção de algoritmos com validação cruzada
aninhada
O uso de validação cruzada k-fold em combinação com pesquisa em grade ou
pesquisa aleatória é uma abordagem útil para ajustar o desempenho de um
modelo de aprendizado de máquina variando seus valores de hiperparâmetro,
como vimos nas subseções anteriores. Se quisermos selecionar entre diferentes
algoritmos de aprendizado de máquina, no entanto, outra abordagem
recomendada é a validação cruzada aninhada. Em um belo estudo sobre o viés
na estimação de erros, Sudhir Varma e Richard Simon concluíram que o
verdadeiro erro da estimativa é quase imparcial em relação ao conjunto de dados
do teste quando a validação cruzada aninhada é usada (Viés na Estimação de
Erros ao Usar a Validação Cruzada para Seleção de Modelos por S. Varma e R.
Simon, BMC Bioinformatics, 7(1): 91,
2006, https://bmcbioinformatics.biomedcentral.com/articles/10.1186/1471-2105-7-
91).

Na validação cruzada aninhada, temos um loop externo de validação cruzada k-


fold para dividir os dados em dobras de treinamento e teste, e um loop interno é
usado para selecionar o modelo usando validação cruzada k-fold na dobra de
treinamento. Após a seleção do modelo, a dobra de teste é então usada para
avaliar o desempenho do modelo. A figura 6.8 explica o conceito de validação
cruzada aninhada com apenas cinco dobras externas e duas internas, o que pode
ser útil para grandes conjuntos de dados onde o desempenho computacional é
importante; Esse tipo específico de validação cruzada aninhada também é
conhecido como validação cruzada 5×2:
Figura 6.8: O conceito de validação cruzada aninhada

No scikit-learn, podemos realizar validação cruzada aninhada com pesquisa em


grade da seguinte maneira:

>>> param_range = [0.0001, 0.001, 0.01, 0.1,

... 1.0, 10.0, 100.0, 1000.0]

>>> param_grid = [{'svc__C': param_range,

... 'svc__kernel': ['linear']},

... {'svc__C': param_range,

... 'svc__gamma': param_range,

... 'svc__kernel': ['rbf']}]


>>> gs = GridSearchCV(estimator=pipe_svc,

... param_grid=param_grid,

... scoring='accuracy',

... cv=2)

>>> scores = cross_val_score(gs, X_train, y_train,

... scoring='accuracy', cv=5)

>>> print(f'CV accuracy: {np.mean(scores):.3f} '

... f'+/- {np.std(scores):.3f}')

CV accuracy: 0.974 +/- 0.015


CopyExplain

A precisão média de validação cruzada retornada nos dá uma boa estimativa do


que esperar se ajustarmos os hiperparâmetros de um modelo e usá-lo em dados
não vistos.

Por exemplo, podemos usar a abordagem de validação cruzada aninhada para


comparar um modelo SVM a um classificador de árvore de decisão simples; Para
simplificar, vamos apenas ajustar seu parâmetro de profundidade:

>>> from sklearn.tree import DecisionTreeClassifier

>>> gs = GridSearchCV(

... estimator=DecisionTreeClassifier(random_state=0),

... param_grid=[{'max_depth': [1, 2, 3, 4, 5, 6, 7, None]}],

... scoring='accuracy',

... cv=2

... )

>>> scores = cross_val_score(gs, X_train, y_train,

... scoring='accuracy', cv=5)

>>> print(f'CV accuracy: {np.mean(scores):.3f} '

... f'+/- {np.std(scores):.3f}')


CV accuracy: 0.934 +/- 0.016
CopyExplain

Como podemos ver, o desempenho de validação cruzada aninhada do modelo


SVM (97,4%) é notavelmente melhor do que o desempenho da árvore de decisão
(93,4%) e, portanto, esperamos que seja a melhor escolha para classificar novos
dados provenientes da mesma população que esse conjunto de dados específico.

Analisando diferentes métricas de


avaliação de desempenho
Nas seções e capítulos anteriores, avaliamos diferentes modelos de aprendizado
de máquina usando a precisão de previsão, que é uma métrica útil para quantificar
o desempenho de um modelo em geral. No entanto, existem várias
outras métricas de desempenho que podem ser usadas para medir a relevância
de um modelo, como precisão, recordação, o escore F1 e o coeficiente de
correlação de Matthews (CCM).

Lendo uma matriz de confusão


Antes de entrarmos nos detalhes de diferentes métricas de pontuação, vamos dar
uma olhada em uma matriz de confusão, uma matriz que estabelece o
desempenho de um algoritmo de aprendizado.

Uma matriz de confusão é simplesmente uma matriz quadrada que relata as


contagens das previsões de verdadeiro positivo (TP), verdadeiro negativo
(TN), falso positivo (FP) e falso negativo (FN) de um classificador, como
mostrado na Figura 6.9:
Figura 6.9: A matriz de confusão

Embora essas métricas possam ser facilmente calculadas manualmente


comparando os rótulos de classe reais e previstos, o scikit-learn fornece uma
função conveniente que podemos usar, da seguinte maneira: confusion_matrix

>>> from sklearn.metrics import confusion_matrix

>>> pipe_svc.fit(X_train, y_train)

>>> y_pred = pipe_svc.predict(X_test)

>>> confmat = confusion_matrix(y_true=y_test, y_pred=y_pred)

>>> print(confmat)
[[71 1]

[ 2 40]]
CopyExplain

A matriz retornada após a execução do código nos fornece informações sobre os


diferentes tipos de erro que o classificador cometeu no conjunto de dados de
teste. Podemos mapear essas informações para a ilustração da matriz de
confusão na Figura 6.9 usando a função de Matplotlib:matshow

>>> fig, ax = plt.subplots(figsize=(2.5, 2.5))

>>> ax.matshow(confmat, cmap=plt.cm.Blues, alpha=0.3)

>>> for i in range(confmat.shape[0]):

... for j in range(confmat.shape[1]):

... ax.text(x=j, y=i, s=confmat[i, j],

... va='center', ha='center')

>>> ax.xaxis.set_ticks_position('bottom')

>>> plt.xlabel('Predicted label')

>>> plt.ylabel('True label')

>>> plt.show()
CopyExplain

Agora, o seguinte gráfico de matriz de confusão, com os rótulos adicionados, deve


tornar os resultados um pouco mais fáceis de interpretar:
Figura 6.10: Uma matriz de confusão para os nossos dados

Assumindo que a classe (maligna) é a classe positiva neste exemplo, nosso


modelo classificou corretamente 71 dos exemplos que pertencem à classe (TN) e
40 exemplos que pertencem à classe (TP), respectivamente. No entanto, nosso
modelo também classificou incorretamente dois exemplos de classe como classe
(FN), e previu que um exemplo é maligno, embora seja um tumor benigno (FP).
Na próxima subseção, aprenderemos como podemos usar essas informações
para calcular várias métricas de erro.10110
Otimizando a precisão e a recuperação de um
modelo de classificação
Tanto o erro de previsão (ERR) quanto a precisão (ACC) fornecem informações
gerais sobre quantos exemplos são classificados incorretamente. O erro pode
ser entendido como a soma de todas as previsões falsas dividido pelo número de
previsões totais, e a precisão é calculada como a soma das previsões corretas
dividido pelo número total de previsões, respectivamente:

A precisão da previsão pode então ser calculada diretamente a partir do erro:

A taxa de verdadeiro positivo (TPR) e a taxa de falso positivo (FPR) são métricas


de desempenho que são especialmente úteis para problemas de classe
desbalanceados:
No diagnóstico de tumores, por exemplo, nos preocupamos mais com a detecção
de tumores malignos para auxiliar o paciente no tratamento adequado. No entanto,
também é importante diminuir o número de tumores benignos classificados
incorretamente como malignos (FP) para não preocupar desnecessariamente os
pacientes. Em contraste com o FPR, o TPR fornece informações úteis sobre a
fração de exemplos positivos (ou relevantes) que foram corretamente identificados
do conjunto total de positivos (P).

As métricas de desempenho de precisão (PRE) e recall (REC) estão


relacionadas a essas taxas de TP e TN e, de fato, REC é sinônimo de TPR:

Em outras palavras, o recall quantifica quantos dos registros relevantes (os


positivos) são capturados como tal (os verdadeiros positivos). A precisão
quantifica quantos dos registros previstos como relevantes (a soma de verdadeiros
e falsos positivos) são realmente relevantes (verdadeiros positivos):

Revisitando o exemplo de detecção de tumor maligno, otimizar para a


recuperação ajuda a minimizar a chance de não detectar um tumor maligno. No
entanto, isso tem o custo de prever tumores malignos em pacientes, embora os
pacientes sejam saudáveis (um número elevado de PFs). Se otimizarmos para
precisão, por outro lado, enfatizamos a correção se prevemos que um paciente
tem um tumor maligno. No entanto, isso vem ao custo de faltar tumores malignos
com mais frequência (um número elevado de NFs).

Para equilibrar os pontos positivos e negativos da otimização do PRE e do REC,


utiliza-se a média harmônica de PRE e REC, o chamado escore F1:
Leitura adicional sobre precisão e recall

Se você está interessado em uma discussão mais aprofundada das diferentes


métricas de desempenho, como precisão e recall, leia o relatório técnico de David
M. W. Powers Evaluation: From Precision, Recall and F-Factor to ROC,
Informedness, Markedness & Correlation, que está disponível gratuitamente
em https://arxiv.org/abs/2010.16061.

Por fim, uma medida que resume uma matriz de confusão é o CCM, que é
especialmente popular em contextos de pesquisa biológica. O CCM é calculado da
seguinte forma:

Em contraste com PRE, REC e a pontuação F1, o CCM varia entre –1 e 1, e leva
em conta todos os elementos de uma matriz de confusão – por exemplo, a
pontuação F1 não envolve a TN. Embora os valores do CCM sejam mais difíceis
de interpretar do que o escore F1, ele é considerado uma métrica superior, como
descrito no artigo a seguir: As vantagens do coeficiente de correlação de
Matthews (CCM) sobre o escore F1 e a acurácia na avaliação da classificação
binária por D. Chicco e G. Jurman, BMC Genomics. pp. 281-305,
2012, https://bmcgenomics.biomedcentral.com/articles/10.1186/s12864-019-6413-
7.

Essas métricas de pontuação são todas implementadas no scikit-learn e podem


ser importadas do módulo, conforme mostrado no trecho a seguir: sklearn.metrics

>>> from sklearn.metrics import precision_score

>>> from sklearn.metrics import recall_score, f1_score


>>> from sklearn.metrics import matthews_corrcoef

>>> pre_val = precision_score(y_true=y_test, y_pred=y_pred)

>>> print(f'Precision: {pre_val:.3f}')

Precision: 0.976

>>> rec_val = recall_score(y_true=y_test, y_pred=y_pred)

>>> print(f'Recall: {rec_val:.3f}')

Recall: 0.952

>>> f1_val = f1_score(y_true=y_test, y_pred=y_pred)

>>> print(f'F1: {f1_val:.3f}')

F1: 0.964

>>> mcc_val = matthews_corrcoef(y_true=y_test, y_pred=y_pred)

>>> print(f'MCC: {mcc_val:.3f}')

MCC: 0.943
CopyExplain

Além disso, podemos usar uma métrica de pontuação diferente da precisão no


parâmetro de pontuação. Uma lista completa dos diferentes valores que são
aceitos pelo parâmetro de pontuação pode ser encontrada em http://scikit-
learn.org/stable/modules/model_evaluation.html.GridSearchCV

Lembre-se que a classe positiva em scikit-learn é a classe que é rotulada como


classe. Se quisermos especificar um rótulo positivo diferente, podemos construir
nosso próprio pontuador por meio da função, que podemos fornecer diretamente
como um argumento para o parâmetro em (neste exemplo, usando o como
métrica):1make_scorerscoringGridSearchCVf1_score

>>> from sklearn.metrics import make_scorer

>>> c_gamma_range = [0.01, 0.1, 1.0, 10.0]

>>> param_grid = [{'svc__C': c_gamma_range,

... 'svc__kernel': ['linear']},

... {'svc__C': c_gamma_range,


... 'svc__gamma': c_gamma_range,

... 'svc__kernel': ['rbf']}]

>>> scorer = make_scorer(f1_score, pos_label=0)

>>> gs = GridSearchCV(estimator=pipe_svc,

... param_grid=param_grid,

... scoring=scorer,

... cv=10)

>>> gs = gs.fit(X_train, y_train)

>>> print(gs.best_score_)

0.986202145696

>>> print(gs.best_params_)

{'svc__C': 10.0, 'svc__gamma': 0.01, 'svc__kernel': 'rbf'}


CopyExplain

Plotando uma característica de operação do


receptor
Os gráficos ROC (receiver operating characteristic) são ferramentas úteis para
selecionar modelos para classificação com base em seu desempenho em relação
ao FPR e TPR, que são calculados mudando o limiar de decisão do classificador.
A diagonal de um gráfico ROC pode ser interpretada como adivinhação aleatória,
e os modelos de classificação que caem abaixo da diagonal são considerados
piores do que a adivinhação aleatória. Um classificador perfeito cairia no canto
superior esquerdo do gráfico com um TPR de 1 e um FPR de 0. Com base na
curva ROC, pode-se então calcular a chamada área sob a curva ROC (ROC
AUC) para caracterizar o desempenho de um modelo de classificação.

Semelhante às curvas ROC, podemos calcular curvas de precisão de


evocação para diferentes limiares de probabilidade de um classificador. Uma
função para plotar essas curvas de recordação de precisão também é
implementada no scikit-learn e é documentada em
http://scikit-learn.org/stable/modules/generated/sklearn.metrics.precision_recall_cu
rve.html.

Executando o exemplo de código a seguir, traçaremos uma curva ROC de um


classificador que usa apenas dois recursos do conjunto de dados Breast Cancer
Wisconsin para prever se um tumor é benigno ou maligno. Embora vamos usar o
mesmo pipeline de regressão logística que definimos anteriormente, desta vez
estamos usando apenas dois recursos. Isso é para tornar a tarefa de classificação
mais desafiadora para o classificador, retendo informações úteis contidas nas
outras características, de modo que a curva ROC resultante se torne visualmente
mais interessante. Por razões semelhantes, também estamos reduzindo o número
de dobras no validador para três. O código é o seguinte: StratifiedKFold

>>> from sklearn.metrics import roc_curve, auc

>>> from numpy import interp

>>> pipe_lr = make_pipeline(

... StandardScaler(),

... PCA(n_components=2),

... LogisticRegression(penalty='l2', random_state=1,

... solver='lbfgs', C=100.0)

... )

>>> X_train2 = X_train[:, [4, 14]]

>>> cv = list(StratifiedKFold(n_splits=3).split(X_train, y_train))

>>> fig = plt.figure(figsize=(7, 5))

>>> mean_tpr = 0.0

>>> mean_fpr = np.linspace(0, 1, 100)

>>> all_tpr = []

>>> for i, (train, test) in enumerate(cv):

... probas = pipe_lr.fit(

... X_train2[train],
... y_train[train]

... ).predict_proba(X_train2[test])

... fpr, tpr, thresholds = roc_curve(y_train[test],

... probas[:, 1],

... pos_label=1)

... mean_tpr += interp(mean_fpr, fpr, tpr)

... mean_tpr[0] = 0.0

... roc_auc = auc(fpr, tpr)

... plt.plot(fpr,

... tpr,

... label=f'ROC fold {i+1} (area = {roc_auc:.2f})')

>>> plt.plot([0, 1],

... [0, 1],

... linestyle='--',

... color=(0.6, 0.6, 0.6),

... label='Random guessing (area=0.5)')

>>> mean_tpr /= len(cv)

>>> mean_tpr[-1] = 1.0

>>> mean_auc = auc(mean_fpr, mean_tpr)

>>> plt.plot(mean_fpr, mean_tpr, 'k--',

... label=f'Mean ROC (area = {mean_auc:.2f})', lw=2)

>>> plt.plot([0, 0, 1],

... [0, 1, 1],

... linestyle=':',

... color='black',

... label='Perfect performance (area=1.0)')


>>> plt.xlim([-0.05, 1.05])

>>> plt.ylim([-0.05, 1.05])

>>> plt.xlabel('False positive rate')

>>> plt.ylabel('True positive rate')

>>> plt.legend(loc='lower right')

>>> plt.show()
CopyExplain

In the preceding code example, we used the already familiar class from scikit-learn


and calculated the ROC performance of the classifier in our pipeline using
the function from the module separately for each iteration. Furthermore, we
interpolated the average ROC curve from the three folds via the function that we
imported from NumPy and calculated the area under the curve via the function.
The resulting ROC curve indicates that there is a certain degree of variance
between the different folds, and the average ROC AUC (0.76) falls between a
perfect score (1.0) and random guessing
(0.5):StratifiedKFoldLogisticRegressionpipe_lrroc_curvesklearn.metricsinterpauc
Figure 6.11: The ROC plot

Note that if we are just interested in the ROC AUC score, we could also directly
import the function from the submodule, which can be used similarly to the other
scoring functions (for example, ) that were introduced in the previous
sections.roc_auc_scoresklearn.metricsprecision_score

Reporting the performance of a classifier as the ROC AUC can yield further
insights into a classifier’s performance with respect to imbalanced samples.
However, while the accuracy score can be interpreted as a single cutoff point on a
ROC curve, A. P. Bradley showed that the ROC AUC and accuracy metrics mostly
agree with each other: The Use of the Area Under the ROC Curve in the
Evaluation of Machine Learning Algorithms by A. P. Bradley, Pattern Recognition,
30(7): 1145-1159,
1997, https://reader.elsevier.com/reader/sd/pii/S0031320396001422.

Scoring metrics for multiclass classification


The scoring metrics that we’ve discussed so far are specific to binary classification
systems. However, scikit-learn also implements macro and micro averaging
methods to extend those scoring metrics to multiclass problems via one-vs.-
all (OvA) classification. The micro-average is calculated from the individual TPs,
TNs, FPs, and FNs of the system. For example, the micro-average of the precision
score in a k-class system can be calculated as follows:

The macro-average is simply calculated as the average scores of the different


systems:

Micro-averaging is useful if we want to weight each instance or prediction equally,


whereas macro-averaging weights all classes equally to evaluate the overall
performance of a classifier with regard to the most frequent class labels.

If we are using binary performance metrics to evaluate multiclass classification


models in scikit-learn, a normalized or weighted variant of the macro-average is
used by default. The weighted macro-average is calculated by weighting the score
of each class label by the number of true instances when calculating the average.
The weighted macro-average is useful if we are dealing with class imbalances, that
is, different numbers of instances for each label.

While the weighted macro-average is the default for multiclass problems in scikit-
learn, we can specify the averaging method via the parameter inside the different
scoring functions that we import from the module, for example,
the or functions:averagesklearn.metricsprecision_scoremake_scorer

>>> pre_scorer = make_scorer(score_func=precision_score,


... pos_label=1,

... greater_is_better=True,

... average='micro')
CopyExplain

Dealing with class imbalance


We’ve mentioned class imbalances several times throughout this chapter, and yet
we haven’t actually discussed how to deal with such scenarios appropriately if they
occur. Class imbalance is a quite common problem when working with real-world
data—examples from one class or multiple classes are over-represented in a
dataset. We can think of several domains where this may occur, such as spam
filtering, fraud detection, or screening for diseases.

Imagine that the Breast Cancer Wisconsin dataset that we’ve been working with in
this chapter consisted of 90 percent healthy patients. In this case, we could
achieve 90 percent accuracy on the test dataset by just predicting the majority
class (benign tumor) for all examples, without the help of a supervised machine
learning algorithm. Thus, training a model on such a dataset that achieves
approximately 90 percent test accuracy would mean our model hasn’t learned
anything useful from the features provided in this dataset.

In this section, we will briefly go over some of the techniques that could help with
imbalanced datasets. But before we discuss different methods to approach this
problem, let’s create an imbalanced dataset from our dataset, which originally
consisted of 357 benign tumors (class ) and 212 malignant tumors (class ):01

>>> X_imb = np.vstack((X[y == 0], X[y == 1][:40]))

>>> y_imb = np.hstack((y[y == 0], y[y == 1][:40]))


CopyExplain

In this code snippet, we took all 357 benign tumor examples and stacked them with
the first 40 malignant examples to create a stark class imbalance. If we were to
compute the accuracy of a model that always predicts the majority class (benign,
class ), we would achieve a prediction accuracy of approximately 90 percent: 0
>>> y_pred = np.zeros(y_imb.shape[0])

>>> np.mean(y_pred == y_imb) * 100

89.92443324937027
CopyExplain

Thus, when we fit classifiers on such datasets, it would make sense to focus on
other metrics than accuracy when comparing different models, such as precision,
recall, the ROC curve—whatever we care most about in our application. For
instance, our priority might be to identify the majority of patients with malignant
cancer to recommend an additional screening, so recall should be our metric of
choice. In spam filtering, where we don’t want to label emails as spam if the system
is not very certain, precision might be a more appropriate metric.

Aside from evaluating machine learning models, class imbalance influences a


learning algorithm during model fitting itself. Since machine learning algorithms
typically optimize a reward or loss function that is computed as a sum over the
training examples that it sees during fitting, the decision rule is likely going to be
biased toward the majority class.

In other words, the algorithm implicitly learns a model that optimizes the predictions
based on the most abundant class in the dataset to minimize the loss or maximize
the reward during training.

One way to deal with imbalanced class proportions during model fitting is to assign
a larger penalty to wrong predictions on the minority class. Via scikit-learn,
adjusting such a penalty is as convenient as setting the parameter to , which is
implemented for most classifiers.class_weightclass_weight='balanced'

Other popular strategies for dealing with class imbalance include upsampling the
minority class, downsampling the majority class, and the generation of synthetic
training examples. Unfortunately, there’s no universally best solution or technique
that works best across different problem domains. Thus, in practice, it is
recommended to try out different strategies on a given problem, evaluate the
results, and choose the technique that seems most appropriate.
The scikit-learn library implements a simple function that can help with the
upsampling of the minority class by drawing new samples from the dataset with
replacement. The following code will take the minority class from our imbalanced
Breast Cancer Wisconsin dataset (here, class ) and repeatedly draw new samples
from it until it contains the same number of examples as class label :resample10

>>> from sklearn.utils import resample

>>> print('Number of class 1 examples before:',

... X_imb[y_imb == 1].shape[0])

Number of class 1 examples before: 40

>>> X_upsampled, y_upsampled = resample(

... X_imb[y_imb == 1],

... y_imb[y_imb == 1],

... replace=True,

... n_samples=X_imb[y_imb == 0].shape[0],

... random_state=123)

>>> print('Number of class 1 examples after:',

... X_upsampled.shape[0])

Number of class 1 examples after: 357


CopyExplain

After resampling, we can then stack the original class samples with the upsampled
class subset to obtain a balanced dataset as follows:01

>>> X_bal = np.vstack((X[y == 0], X_upsampled))

>>> y_bal = np.hstack((y[y == 0], y_upsampled))


CopyExplain

Consequently, a majority vote prediction rule would only achieve 50 percent


accuracy:

>>> y_pred = np.zeros(y_bal.shape[0])


>>> np.mean(y_pred == y_bal) * 100

50
CopyExplain

Similarly, we could downsample the majority class by removing training examples


from the dataset. To perform downsampling using the function, we could simply
swap the class label with class in the previous code example and vice
versa.resample10

Generating new training data to address class imbalance

Another technique for dealing with class imbalance is the generation of synthetic
training examples, which is beyond the scope of this book. Probably the most
widely used algorithm for synthetic training data generation is Synthetic Minority
Over-sampling Technique (SMOTE), and you can learn more about this
technique in the original research article by Nitesh Chawla and others: SMOTE:
Synthetic Minority Over-sampling Technique, Journal of Artificial Intelligence
Research, 16: 321-357, 2002, which is available
at https://www.jair.org/index.php/jair/article/view/10302. It is also highly
recommended to check out imbalanced-learn, a Python library that is entirely
focused on imbalanced datasets, including an implementation of SMOTE. You can
learn more about imbalanced-learn
at https://github.com/scikit-learn-contrib/imbalanced-learn.

Resumo
No início deste capítulo, discutimos como encadear diferentes técnicas de
transformação e classificadores em pipelines de modelos convenientes que nos
ajudam a treinar e avaliar modelos de aprendizado de máquina de forma mais
eficiente. Em seguida, usamos esses pipelines para realizar a validação cruzada
k-fold, uma das técnicas essenciais para a seleção e avaliação do modelo.
Usando validação cruzada k-fold, plotamos curvas de aprendizagem e validação
para diagnosticar problemas comuns de algoritmos de aprendizagem, como
overfitting e underfitting.
Usando pesquisa em grade, pesquisa aleatória e halving sucessivo, ajustamos
ainda mais nosso modelo. Em seguida, usamos matrizes de confusão e várias
métricas de desempenho para avaliar e otimizar o desempenho de um modelo
para tarefas de problemas específicos. Finalmente, concluímos este capítulo
discutindo diferentes métodos para lidar com dados desequilibrados, que é um
problema comum em muitas aplicações do mundo real. Agora, você deve estar
bem equipado com as técnicas essenciais para construir modelos de aprendizado
de máquina supervisionados para classificação com sucesso.

No próximo capítulo, veremos os métodos de conjunto: métodos que nos


permitem combinar vários modelos e algoritmos de classificação para aumentar
ainda mais o desempenho preditivo de um sistema de aprendizado de máquina.

Combinando diferentes modelos para


aprendizagem em conjunto
No capítulo anterior, nos concentramos nas melhores práticas para ajuste e
avaliação de diferentes modelos de classificação. Neste capítulo, vamos nos
basear nessas técnicas e explorar diferentes métodos para construir um conjunto
de classificadores que muitas vezes podem ter um desempenho preditivo melhor
do que qualquer um de seus membros individuais. Vamos aprender a fazer o
seguinte:

 Faça previsões com base na votação por maioria

 Use o ensacamento para reduzir o sobreajuste desenhando combinações


aleatórias do conjunto de dados de treinamento com repetição

 Aplique o impulsionamento para criar modelos poderosos a partir de alunos


fracos que aprendem com seus erros

Aprendendo com conjuntos


O objetivo dos métodos de conjunto é combinar diferentes classificadores em
um meta-classificador que tenha melhor desempenho de generalização do que
cada classificador individual sozinho. Por exemplo, supondo que coletamos
previsões de 10 especialistas, os métodos de conjunto nos permitiriam combinar
estrategicamente essas previsões pelos 10 especialistas para chegar a uma
previsão mais precisa e robusta do que as previsões de cada especialista
individual. Como você verá mais adiante neste capítulo, existem várias
abordagens diferentes para criar um conjunto de classificadores. Esta seção
apresentará uma explicação básica de como os conjuntos funcionam e por que
eles são tipicamente reconhecidos por produzir um bom desempenho de
generalização.

Neste capítulo, vamos nos concentrar nos métodos de conjunto mais populares
que usam o princípio do voto majoritário. A votação por maioria significa
simplesmente que selecionamos o rótulo de classe que foi previsto pela maioria
dos classificadores, ou seja, recebeu mais de 50% dos votos. Estritamente
falando, o termo "voto da maioria" refere-se apenas a configurações de classe
binária. No entanto, é fácil generalizar o princípio do voto majoritário para
ambientes multiclasse, o que é conhecido como voto plural. (No Reino Unido, as
pessoas distinguem entre voto majoritário e plural através dos termos "maioria
absoluta" e "relativa", respectivamente.)

Aqui, selecionamos o rótulo da classe que recebeu mais votos (a modalidade). A


figura 7.1 ilustra o conceito de votação por maioria e pluralidade para um conjunto
de 10 classificadores, onde cada símbolo único (triângulo, quadrado e círculo)
representa um rótulo de classe único:

Figura 7.1: Os diferentes conceitos de votação

Usando o conjunto de dados de treinamento, começamos treinando m diferentes


classificadores (C1, ..., Cm). Dependendo da técnica, o conjunto pode ser
construído a partir de diferentes algoritmos de classificação, por exemplo, árvores
de decisão, máquinas de vetores de suporte, classificadores de regressão
logística e assim por diante. Alternativamente, também podemos usar o mesmo
algoritmo de classificação de base, ajustando diferentes subconjuntos do conjunto
de dados de treinamento. Um exemplo proeminente dessa abordagem é o
algoritmo de floresta aleatória combinando diferentes classificadores de árvore de
decisão, que abordamos no Capítulo 3, A Tour of Machine Learning Classifiers
Using Scikit-Learn. A figura 7.2 ilustra o conceito de uma abordagem geral de
conjunto utilizando a votação por maioria:

Figura 7.2: Uma abordagem geral do conjunto

Para prever um rótulo de classe por meio de votação por maioria simples ou
pluralidade, podemos combinar os rótulos de classe previstos de cada
classificador individual, Cje selecione o rótulo da classe,  , que recebeu o maior
número de votos:

(Em estatísticas, o modo é o evento ou resultado mais frequente em um conjunto.


Por exemplo, mode{1, 2, 1, 1, 2, 4, 5, 4} = 1.)

Por exemplo, em uma tarefa de classificação binária em que class1 = –1 e class2


= +1, podemos escrever a previsão de voto da maioria da seguinte maneira:

Para ilustrar por que os métodos de conjunto podem funcionar melhor do que
classificadores individuais sozinhos, vamos aplicar alguns conceitos de
combinatória. Para o exemplo a seguir, assumiremos que todos os
classificadores n-base para uma tarefa de classificação binária têm uma taxa de

erro igual,  . Além disso, assumiremos que os classificadores são


independentes e as taxas de erro não estão correlacionadas. Sob esses
pressupostos, podemos simplesmente expressar a probabilidade de erro de um
conjunto de classificadores de base como uma função de massa de probabilidade
de uma distribuição binomial:
Aqui,   está o coeficiente binomial n escolher k. Em outras palavras,
calculamos a probabilidade de que a previsão do conjunto esteja errada. Agora,
vamos dar uma olhada em um exemplo mais concreto de 11 classificadores de
base (n = 11), onde cada classificador tem uma taxa de erro de 0,25 (

):

O coeficiente binomial

O coeficiente binomial refere-se ao número de maneiras pelas quais podemos


escolher subconjuntos de k elementos não ordenados de um conjunto de
tamanho n; Assim, muitas vezes é chamado de "n escolher K". Uma vez que a
ordem não importa aqui, o coeficiente binomial também é às vezes referido
como combinação ou número combinatório, e em sua forma não abreviada, é
escrito da seguinte forma:

Aqui, o símbolo (!) significa fatorial – por exemplo, 3! = 3×2×1 = 6.

Como você pode ver, a taxa de erro do conjunto (0,034) é muito menor do que a
taxa de erro de cada classificador individual (0,25) se todas as suposições forem
atendidas. Note que, nesta ilustração simplificada, uma divisão 50-50 por um
número par de classificadores, n, é tratada como um erro, enquanto isso é
verdade apenas metade do tempo. Para comparar tal classificador de conjunto
idealista com um classificador base em uma faixa de diferentes taxas de erro de
base, vamos implementar a função de massa de probabilidade em Python:
>>> from scipy.special import comb

>>> import math

>>> def ensemble_error(n_classifier, error):

... k_start = int(math.ceil(n_classifier / 2.))

... probs = [comb(n_classifier, k) *

... error**k *

... (1-error)**(n_classifier - k)

... for k in range(k_start, n_classifier + 1)]

... return sum(probs)

>>> ensemble_error(n_classifier=11, error=0.25)

0.03432750701904297
CopyExplain

Depois de implementarmos a função, podemos calcular as taxas de erro do


conjunto para uma gama de diferentes erros básicos de 0,0 a 1,0 para visualizar a
relação entre os erros do conjunto e da base em um gráfico de
linhas:ensemble_error

>>> import numpy as np

>>> import matplotlib.pyplot as plt

>>> error_range = np.arange(0.0, 1.01, 0.01)

>>> ens_errors = [ensemble_error(n_classifier=11, error=error)

... for error in error_range]

>>> plt.plot(error_range, ens_errors,

... label='Ensemble error',

... linewidth=2)

>>> plt.plot(error_range, error_range,

... linestyle='--', label='Base error',

... linewidth=2)
>>> plt.xlabel('Base error')

>>> plt.ylabel('Base/Ensemble error')

>>> plt.legend(loc='upper left')

>>> plt.grid(alpha=0.5)

>>> plt.show()
CopyExplain

Como você pode ver no gráfico resultante, a probabilidade de erro de um conjunto


é sempre melhor do que o erro de um classificador de base individual, desde que
os classificadores de base tenham um desempenho melhor do que a adivinhação

aleatória ( ).

Observe que o eixo y representa o erro base (linha pontilhada), bem como o erro
de conjunto (linha contínua):

Figura 7.3: Gráfico do erro do conjunto versus o erro base


Combinação de classificadores via voto
majoritário
Após a breve introdução ao aprendizado de conjunto na seção anterior, vamos
começar com um exercício de aquecimento e implementar um classificador de
conjunto simples para votação majoritária em Python.

Voto plural

Embora o algoritmo de votação majoritária que discutiremos nesta seção também


generalize para configurações multiclasse via voto plural, o termo "votação por
maioria" será usado por simplicidade, como é frequentemente o caso na literatura.

Implementação de um classificador de votos por


maioria simples
O algoritmo que vamos implementar nesta seção nos permitirá combinar
diferentes algoritmos de classificação associados a pesos individuais para
confiança. Nosso objetivo é construir um metaclassificador mais forte que equilibre
os pontos fracos dos classificadores individuais em um conjunto de dados
específico. Em termos matemáticos mais precisos, podemos escrever o voto da
maioria ponderada da seguinte forma:

Aqui, wj é um peso associado a um classificador de base, Cj;   é o rótulo de

classe previsto do conjunto; A é o conjunto de rótulos de classe exclusivos;   


(do grego chi) é a função característica ou função indicadora, que retorna 1 se a
classe prevista do jésimo classificador corresponder a i (Cj(x) = i). Para pesos
iguais, podemos simplificar esta equação e escrevê-la da seguinte forma:

Para entender melhor o conceito de ponderação, vamos agora dar uma olhada em
um exemplo mais concreto. Vamos supor que temos um conjunto de três
classificadores de base, , e queremos prever o rótulo de classe, , de um dado

exemplo,  x.
Dois dos três classificadores de base predizem o rótulo de classe 0 e um, C3,
prevê que o exemplo pertence à classe 1. Se ponderarmos igualmente as
previsões de cada classificador base, o voto da maioria prevê que o exemplo
pertence à classe 0:

Agora, vamos atribuir um peso de 0,6 a C3, e vamos pesar C1 e C2 por um


coeficiente de 0,2:
Mais simplesmente, já que 3×0,2 = 0,6, podemos dizer que a previsão feita
por C3 tem três vezes mais peso do que as previsões de C1 ou C2, que podemos
escrever da seguinte forma:

Para traduzir o conceito do voto da maioria ponderada em código Python,


podemos usar as funções e conveniências do NumPy, onde se conta o número de
ocorrências de cada rótulo de classe. Em seguida, a função retorna a posição de
índice da contagem mais alta, correspondente ao rótulo de classe majoritária (isso
pressupõe que os rótulos de classe comecem em 0): argmaxbincountbincountargmax

>>> import numpy as np

>>> np.argmax(np.bincount([0, 0, 1],

... weights=[0.2, 0.2, 0.6]))

1
CopyExplain

Como você vai se lembrar da discussão sobre regressão logística no Capítulo 3,


certos classificadores no scikit-learn também podem retornar a probabilidade de
um rótulo de classe previsto através do método. Usar as probabilidades de classe
previstas em vez dos rótulos de classe para votação por maioria pode ser útil se
os classificadores em nosso conjunto estiverem bem calibrados. A versão
modificada do voto da maioria para prever rótulos de classe a partir de
probabilidades pode ser escrita da seguinte forma: predict_proba

Aqui, pIj é a probabilidade prevista do jésimo classificador para o rótulo de classe i.


Para continuar com nosso exemplo anterior, vamos supor que temos um problema

de classificação binária com rótulos   de classe e um conjunto

de três classificadores,  . Vamos supor que os


classificadores Cj Retorne as seguintes probabilidades de associação de classe
para um exemplo específico, X:

Usando os mesmos pesos anteriores (0,2, 0,2 e 0,6), podemos então calcular as
probabilidades de classe individuais da seguinte maneira:

Para implementar o voto por maioria ponderada com base em probabilidades de


classe, podemos novamente fazer uso do NumPy, usando e : np.averagenp.argmax

>>> ex = np.array([[0.9, 0.1],

... [0.8, 0.2],

... [0.4, 0.6]])

>>> p = np.average(ex, axis=0, weights=[0.2, 0.2, 0.6])

>>> p

array([0.58, 0.42])

>>> np.argmax(p)

0
CopyExplain
Juntando tudo, vamos agora implementar em Python: MajorityVoteClassifier

from sklearn.base import BaseEstimator

from sklearn.base import ClassifierMixin

from sklearn.preprocessing import LabelEncoder

from sklearn.base import clone

from sklearn.pipeline import _name_estimators

import numpy as np

import operator

class MajorityVoteClassifier(BaseEstimator, ClassifierMixin):

def __init__(self, classifiers, vote='classlabel', weights=None):

self.classifiers = classifiers

self.named_classifiers = {

key: value for key,

value in _name_estimators(classifiers)

self.vote = vote

self.weights = weights

def fit(self, X, y):

if self.vote not in ('probability', 'classlabel'):

raise ValueError(f"vote must be 'probability' "

f"or 'classlabel'"

f"; got (vote={self.vote})")

if self.weights and

len(self.weights) != len(self.classifiers):

raise ValueError(f'Number of classifiers and'


f' weights must be equal'

f'; got {len(self.weights)} weights,'

f' {len(self.classifiers)} classifiers')

# Use LabelEncoder to ensure class labels start

# with 0, which is important for np.argmax

# call in self.predict

self.lablenc_ = LabelEncoder()

self.lablenc_.fit(y)

self.classes_ = self.lablenc_.classes_

self.classifiers_ = []

for clf in self.classifiers:

fitted_clf = clone(clf).fit(X,

self.lablenc_.transform(y))

self.classifiers_.append(fitted_clf)

return self
CopyExplain

We’ve added a lot of comments to the code to explain the individual parts.
However, before we implement the remaining methods, let’s take a quick break
and discuss some of the code that may look confusing at first. We used
the and parent classes to get some base functionality for free, including
the and methods to set and return the classifier’s parameters, as well as
the method to calculate the prediction
accuracy.BaseEstimatorClassifierMixinget_paramsset_paramsscore

Next, we will add the method to predict the class label via a majority vote based on
the class labels if we initialize a new object with . Alternatively, we will be able to
initialize the ensemble classifier with to predict the class label based on the class
membership probabilities. Furthermore, we will also add a method to return the
averaged probabilities, which is useful when computing the receiver operating
characteristic area under the curve (ROC
AUC):predictMajorityVoteClassifiervote='classlabel'vote='probability'predict_pr
oba

def predict(self, X):

if self.vote == 'probability':

maj_vote = np.argmax(self.predict_proba(X), axis=1)

else: # 'classlabel' vote

# Collect results from clf.predict calls

predictions = np.asarray([

clf.predict(X) for clf in self.classifiers_

]).T

maj_vote = np.apply_along_axis(

lambda x: np.argmax(

np.bincount(x, weights=self.weights)

),

axis=1, arr=predictions

maj_vote = self.lablenc_.inverse_transform(maj_vote)

return maj_vote

def predict_proba(self, X):

probas = np.asarray([clf.predict_proba(X)

for clf in self.classifiers_])

avg_proba = np.average(probas, axis=0,

weights=self.weights)

return avg_proba
def get_params(self, deep=True):

if not deep:

return super().get_params(deep=False)

else:

out = self.named_classifiers.copy()

for name, step in self.named_classifiers.items():

for key, value in step.get_params(

deep=True).items():

out[f'{name}__{key}'] = value

return out
CopyExplain

Also, note that we defined our own modified version of the method to use
the function to access the parameters of individual classifiers in the ensemble; this
may look a little bit complicated at first, but it will make perfect sense when we use
grid search for hyperparameter tuning in later sections. get_params_name_estimators

VotingClassifier in scikit-learn

Although the implementation is very useful for demonstration purposes, we


implemented a more sophisticated version of this majority vote classifier in scikit-
learn based on the implementation in the first edition of this book. The ensemble
classifier is available as in scikit-learn version 0.17 and newer. You can find out
more
about at https://scikit-learn.org/stable/modules/generated/sklearn.ensemble.Voting
Classifier.htmlMajorityVoteClassifiersklearn.ensemble.VotingClassifierVotingClass
ifier

Using the majority voting principle to make


predictions
Now it is time to put the that we implemented in the previous section into action.
But first, let’s prepare a dataset that we can test it on. Since we are already familiar
with techniques to load datasets from CSV files, we will take a shortcut and load
the Iris dataset from scikit-learn’s module. Furthermore, we will only select two
features, sepal width and petal length, to make the classification task more
challenging for illustration purposes. Although our generalizes to multiclass
problems, we will only classify flower examples from the and classes, with which
we will compute the ROC AUC later. The code is as
follows:MajorityVoteClassifierdatasetsMajorityVoteClassifierIris-versicolorIris-
virginica

>>> from sklearn import datasets

>>> from sklearn.model_selection import train_test_split

>>> from sklearn.preprocessing import StandardScaler

>>> from sklearn.preprocessing import LabelEncoder

>>> iris = datasets.load_iris()

>>> X, y = iris.data[50:, [1, 2]], iris.target[50:]

>>> le = LabelEncoder()

>>> y = le.fit_transform(y)
CopyExplain

Class membership probabilities from decision trees

Note that scikit-learn uses the method (if applicable) to compute the ROC AUC


score. In Chapter 3, we saw how the class probabilities are computed in logistic
regression models. In decision trees, the probabilities are calculated from a
frequency vector that is created for each node at training time. The vector collects
the frequency values of each class label computed from the class label distribution
at that node. Then, the frequencies are normalized so that they sum up to 1.
Similarly, the class labels of the k-nearest neighbors are aggregated to return the
normalized class label frequencies in the k-nearest neighbors algorithm. Although
the normalized probabilities returned by both the decision tree and k-nearest
neighbors classifier may look similar to the probabilities obtained from a logistic
regression model, we have to be aware that they are actually not derived from
probability mass functions.predict_proba
Next, we will split the Iris examples into 50 percent training and 50 percent test
data:

>>> X_train, X_test, y_train, y_test =\

... train_test_split(X, y,

... test_size=0.5,

... random_state=1,

... stratify=y)
CopyExplain

Using the training dataset, we now will train three different classifiers:

 Logistic regression classifier

 Decision tree classifier

 k-nearest neighbors classifier

We will then evaluate the model performance of each classifier via 10-fold cross-
validation on the training dataset before we combine them into an ensemble
classifier:

>>> from sklearn.model_selection import cross_val_score

>>> from sklearn.linear_model import LogisticRegression

>>> from sklearn.tree import DecisionTreeClassifier

>>> from sklearn.neighbors import KNeighborsClassifier

>>> from sklearn.pipeline import Pipeline

>>> import numpy as np

>>> clf1 = LogisticRegression(penalty='l2',

... C=0.001,

... solver='lbfgs',

... random_state=1)

>>> clf2 = DecisionTreeClassifier(max_depth=1,


... criterion='entropy',

... random_state=0)

>>> clf3 = KNeighborsClassifier(n_neighbors=1,

... p=2,

... metric='minkowski')

>>> pipe1 = Pipeline([['sc', StandardScaler()],

... ['clf', clf1]])

>>> pipe3 = Pipeline([['sc', StandardScaler()],

... ['clf', clf3]])

>>> clf_labels = ['Logistic regression', 'Decision tree', 'KNN']

>>> print('10-fold cross validation:\n')

>>> for clf, label in zip([pipe1, clf2, pipe3], clf_labels):

... scores = cross_val_score(estimator=clf,

... X=X_train,

... y=y_train,

... cv=10,

... scoring='roc_auc')

... print(f'ROC AUC: {scores.mean():.2f} '

... f'(+/- {scores.std():.2f}) [{label}]')


CopyExplain

A saída que recebemos, como mostrado no trecho a seguir, mostra que os


desempenhos preditivos dos classificadores individuais são quase iguais:

10-fold cross validation:

ROC AUC: 0.92 (+/- 0.15) [Logistic regression]

ROC AUC: 0.87 (+/- 0.18) [Decision tree]

ROC AUC: 0.85 (+/- 0.13) [KNN]


CopyExplain
Você pode estar se perguntando por que treinamos a regressão logística e
o classificador de vizinhos k-mais próximos como parte de um pipeline. A razão
por trás disso é que, como discutido no Capítulo 3, tanto a regressão logística
quanto os algoritmos de vizinhos k-mais próximos (usando a métrica de distância
euclidiana) não são invariantes em escala, em contraste com as árvores de
decisão. Embora as feições da íris sejam todas medidas na mesma escala (cm), é
um bom hábito trabalhar com características padronizadas.

Agora, vamos passar para a parte mais emocionante e combinar os


classificadores individuais para votação de regra de maioria em
nosso :MajorityVoteClassifier

>>> mv_clf = MajorityVoteClassifier(

... classifiers=[pipe1, clf2, pipe3]

... )

>>> clf_labels += ['Majority voting']

>>> all_clf = [pipe1, clf2, pipe3, mv_clf]

>>> for clf, label in zip(all_clf, clf_labels):

... scores = cross_val_score(estimator=clf,

... X=X_train,

... y=y_train,

... cv=10,

... scoring='roc_auc')

... print(f'ROC AUC: {scores.mean():.2f} '

... f'(+/- {scores.std():.2f}) [{label}]')

ROC AUC: 0.92 (+/- 0.15) [Logistic regression]

ROC AUC: 0.87 (+/- 0.18) [Decision tree]

ROC AUC: 0.85 (+/- 0.13) [KNN]

ROC AUC: 0.98 (+/- 0.05) [Majority voting]


CopyExplain
Como você pode ver, o desempenho de melhorou em relação aos classificadores
individuais na avaliação de validação cruzada de 10
vezes.MajorityVotingClassifier

Avaliação e ajuste do classificador de conjunto


Nesta seção, vamos calcular as curvas ROC a partir do conjunto de dados de
teste para verificar se generaliza bem com dados invisíveis. Devemos lembrar que
o conjunto de dados de teste não deve ser usado para seleção de modelo; Seu
objetivo é meramente relatar uma estimativa imparcial do desempenho de
generalização de um sistema classificador:MajorityVoteClassifier

>>> from sklearn.metrics import roc_curve

>>> from sklearn.metrics import auc

>>> colors = ['black', 'orange', 'blue', 'green']

>>> linestyles = [':', '--', '-.', '-']

>>> for clf, label, clr, ls \

... in zip(all_clf, clf_labels, colors, linestyles):

... # assuming the label of the positive class is 1

... y_pred = clf.fit(X_train,

... y_train).predict_proba(X_test)[:, 1]

... fpr, tpr, thresholds = roc_curve(y_true=y_test,

... y_score=y_pred)

... roc_auc = auc(x=fpr, y=tpr)

... plt.plot(fpr, tpr,

... color=clr,

... linestyle=ls,

... label=f'{label} (auc = {roc_auc:.2f})')

>>> plt.legend(loc='lower right')

>>> plt.plot([0, 1], [0, 1],


... linestyle='--',

... color='gray',

... linewidth=2)

>>> plt.xlim([-0.1, 1.1])

>>> plt.ylim([-0.1, 1.1])

>>> plt.grid(alpha=0.5)

>>> plt.xlabel('False positive rate (FPR)')

>>> plt.ylabel('True positive rate (TPR)')

>>> plt.show()
CopyExplain

Como você pode ver no ROC resultante, o classificador de conjunto também tem
um bom desempenho no conjunto de dados de teste (ROC AUC = 0,95). No
entanto, você pode ver que o classificador de regressão logística tem
um desempenho semelhante no mesmo conjunto de dados, o que
provavelmente se deve à alta variância (neste caso, a sensibilidade de como
dividimos o conjunto de dados), dado o pequeno tamanho do conjunto de dados:
Figura 7.4: A curva ROC para os diferentes classificadores

Como selecionamos apenas dois recursos para os exemplos de classificação,


seria interessante ver como realmente é a região de decisão do classificador de
conjunto.

Embora não seja necessário padronizar os recursos de treinamento antes do


ajuste do modelo, porque nossa regressão logística e pipelines de vizinhos k-mais
próximos cuidarão automaticamente disso, padronizaremos o conjunto de dados
de treinamento para que as regiões de decisão da árvore de decisão estejam na
mesma escala para fins visuais. O código é o seguinte:

>>> sc = StandardScaler()

>>> X_train_std = sc.fit_transform(X_train)

>>> from itertools import product

>>> x_min = X_train_std[:, 0].min() - 1

>>> x_max = X_train_std[:, 0].max() + 1

>>> y_min = X_train_std[:, 1].min() - 1

>>>

>>> y_max = X_train_std[:, 1].max() + 1

>>> xx, yy = np.meshgrid(np.arange(x_min, x_max, 0.1),

... np.arange(y_min, y_max, 0.1))

>>> f, axarr = plt.subplots(nrows=2, ncols=2,

... sharex='col',

... sharey='row',

... figsize=(7, 5))

>>> for idx, clf, tt in zip(product([0, 1], [0, 1]),

... all_clf, clf_labels):

... clf.fit(X_train_std, y_train)

... Z = clf.predict(np.c_[xx.ravel(), yy.ravel()])


... Z = Z.reshape(xx.shape)

... axarr[idx[0], idx[1]].contourf(xx, yy, Z, alpha=0.3)

... axarr[idx[0], idx[1]].scatter(X_train_std[y_train==0, 0],

... X_train_std[y_train==0, 1],

... c='blue',

... marker='^',

... s=50)

... axarr[idx[0], idx[1]].scatter(X_train_std[y_train==1, 0],

... X_train_std[y_train==1, 1],

... c='green',

... marker='o',

... s=50)

... axarr[idx[0], idx[1]].set_title(tt)

>>> plt.text(-3.5, -5.,

... s='Sepal width [standardized]',

... ha='center', va='center', fontsize=12)

>>> plt.text(-12.5, 4.5,

... s='Petal length [standardized]',

... ha='center', va='center',

... fontsize=12, rotation=90)

>>> plt.show()
CopyExplain

Curiosamente, mas também como esperado, as regiões de decisão


do classificador de conjunto parecem ser um híbrido das regiões de decisão dos
classificadores individuais. À primeira vista, o limite de decisão do voto da maioria
se parece muito com a decisão do toco da árvore de decisão, que é ortogonal ao
eixo y para a largura da sépala ≥ 1.
No entanto, você também pode notar a não-linearidade do classificador vizinho k-
mais próximo misturado em:

Figura 7.5: Limites de decisão para os diferentes classificadores

Antes de ajustarmos os parâmetros do classificador individual para a classificação


de conjunto, vamos chamar o método para ter uma ideia básica de como podemos
acessar os parâmetros individuais dentro de um objeto: get_paramsGridSearchCV

>>> mv_clf.get_params()

{'decisiontreeclassifier':

DecisionTreeClassifier(class_weight=None, criterion='entropy',

max_depth=1, max_features=None,

max_leaf_nodes=None, min_samples_leaf=1,

min_samples_split=2,

min_weight_fraction_leaf=0.0,
random_state=0, splitter='best'),

'decisiontreeclassifier__class_weight': None,

'decisiontreeclassifier__criterion': 'entropy',

[...]

'decisiontreeclassifier__random_state': 0,

'decisiontreeclassifier__splitter': 'best',

'pipeline-1':

Pipeline(steps=[('sc', StandardScaler(copy=True, with_mean=True,

with_std=True)),

('clf', LogisticRegression(C=0.001,

class_weight=None,

dual=False,

fit_intercept=True,

intercept_scaling=1,

max_iter=100,

multi_class='ovr',

penalty='l2',

random_state=0,

solver='liblinear',

tol=0.0001,

verbose=0))]),

'pipeline-1__clf':

LogisticRegression(C=0.001, class_weight=None, dual=False,

fit_intercept=True, intercept_scaling=1,

max_iter=100, multi_class='ovr',

penalty='l2', random_state=0,
solver='liblinear', tol=0.0001, verbose=0),

'pipeline-1__clf__C': 0.001,

'pipeline-1__clf__class_weight': None,

'pipeline-1__clf__dual': False,

[...]

'pipeline-1__sc__with_std': True,

'pipeline-2':

Pipeline(steps=[('sc', StandardScaler(copy=True, with_mean=True,

with_std=True)),

('clf', KNeighborsClassifier(algorithm='auto',

leaf_size=30,

metric='minkowski',

metric_params=None,

n_neighbors=1,

p=2,

weights='uniform'))]),

'pipeline-2__clf':

KNeighborsClassifier(algorithm='auto', leaf_size=30,

metric='minkowski', metric_params=None,

n_neighbors=1, p=2, weights='uniform'),

'pipeline-2__clf__algorithm': 'auto',

[...]

'pipeline-2__sc__with_std': True}
CopyExplain

Based on the values returned by the method, we now know how to access the
individual classifier’s attributes. Let’s now tune the inverse regularization
parameter, , of the logistic regression classifier and the decision tree depth via a
grid search for demonstration purposes:get_paramsC

>>> from sklearn.model_selection import GridSearchCV

>>> params = {'decisiontreeclassifier__max_depth': [1, 2],

... 'pipeline-1__clf__C': [0.001, 0.1, 100.0]}

>>> grid = GridSearchCV(estimator=mv_clf,

... param_grid=params,

... cv=10,

... scoring='roc_auc')

>>> grid.fit(X_train, y_train)


CopyExplain

After the grid search has completed, we can print the different hyperparameter


value combinations and the average ROC AUC scores computed via 10-fold cross-
validation as follows:

>>> for r, _ in enumerate(grid.cv_results_['mean_test_score']):

... mean_score = grid.cv_results_['mean_test_score'][r]

... std_dev = grid.cv_results_['std_test_score'][r]

... params = grid.cv_results_['params'][r]

... print(f'{mean_score:.3f} +/- {std_dev:.2f} {params}')

0.983 +/- 0.05 {'decisiontreeclassifier__max_depth': 1,

'pipeline-1__clf__C': 0.001}

0.983 +/- 0.05 {'decisiontreeclassifier__max_depth': 1,

'pipeline-1__clf__C': 0.1}

0.967 +/- 0.10 {'decisiontreeclassifier__max_depth': 1,

'pipeline-1__clf__C': 100.0}

0.983 +/- 0.05 {'decisiontreeclassifier__max_depth': 2,


'pipeline-1__clf__C': 0.001}

0.983 +/- 0.05 {'decisiontreeclassifier__max_depth': 2,

'pipeline-1__clf__C': 0.1}

0.967 +/- 0.10 {'decisiontreeclassifier__max_depth': 2,

'pipeline-1__clf__C': 100.0}

>>> print(f'Best parameters: {grid.best_params_}')

Best parameters: {'decisiontreeclassifier__max_depth': 1,

'pipeline-1__clf__C': 0.001}

>>> print(f'ROC AUC : {grid.best_score_:.2f}')

ROC AUC: 0.98


CopyExplain

Como você pode ver, obtemos os melhores resultados de validação cruzada


quando escolhemos uma menor resistência de regularização (), enquanto a
profundidade da árvore parece não afetar o desempenho, sugerindo que um toco
de decisão é suficiente para separar os dados. Para lembrar que é uma má prática
usar o conjunto de dados de teste mais de uma vez para avaliação do modelo,
não vamos estimar o desempenho de generalização dos hiperparâmetros
ajustados nesta seção. Passaremos rapidamente para uma abordagem alternativa
para o aprendizado em conjunto: o ensacamento.C=0.001

Construindo conjuntos usando empilhamento

A abordagem de votação por maioria que implementamos nesta seção não deve


ser confundida com empilhamento. O algoritmo de empilhamento pode ser
entendido como um conjunto de dois níveis, onde o primeiro nível consiste de
classificadores individuais que alimentam suas previsões para o segundo nível,
onde outro classificador (tipicamente regressão logística) é ajustado às previsões
do classificador de nível um para fazer as previsões finais. Para obter mais
informações sobre empilhamento, consulte os seguintes recursos:
 O algoritmo de empilhamento foi descrito em mais detalhes por David H.
Wolpert em Stacked generalization, Neural Networks, 5(2):241–259, 1992
(https://www.sciencedirect.com/science/article/pii/S0893608005800231).
 Os leitores interessados podem encontrar nosso tutorial em vídeo sobre
empilhamento no YouTube em https://www.youtube.com/watch?
v=8T2emza6g80.
 Uma versão compatível com scikit-learn de um classificador de empilhamento
está disponível em
mlxtend: http://rasbt.github.io/mlxtend/user_guide/classifier/StackingCVClassifi
er/.
 Além disso, um foi recentemente adicionado ao scikit-learn (disponível na
versão 0.22 e mais recente); Para obter mais informações, consulte a
documentação
em https://scikit-learn.org/stable/modules/generated/sklearn.ensemble.Stackin
gClassifier.html.StackingClassifier

Ensacamento – construção de um
conjunto de classificadores a partir de
amostras de bootstrap
O ensacamento é uma técnica de aprendizagem de conjunto que está
intimamente relacionada com a que implementamos na seção anterior. No
entanto, em vez de usar o mesmo conjunto de dados de treinamento para ajustar
os classificadores individuais no conjunto, extraímos amostras de bootstrap
(amostras aleatórias com substituição) do conjunto de dados de treinamento
inicial, e é por isso que o ensacamento também é conhecido como agregação de
bootstrap.MajorityVoteClassifier

O conceito de ensacamento é resumido na figura 7.6:


Figura 7.6: O conceito de ensacamento

Nas subseções a seguir, vamos trabalhar com um exemplo simples de


ensacamento à mão e usar scikit-learn para classificar exemplos de vinhos.

Ensacamento em poucas palavras


Para fornecer um exemplo mais concreto de como funciona a agregação bootstrap
de um classificador de ensacamento, vamos considerar o exemplo mostrado
na Figura 7.7. Aqui, temos sete instâncias de treinamento diferentes (denotadas
como índices de 1 a 7) que são amostradas aleatoriamente com reposição em
cada rodada de ensacamento. Cada amostra de bootstrap é então usada para
ajustar um classificador, Cj, que é mais tipicamente uma árvore de decisão não
podada:
Figura 7.7: Um exemplo de ensacamento

Como você pode ver na Figura 7.7, cada classificador recebe um subconjunto
aleatório de exemplos do conjunto de dados de treinamento. Denotamos essas
amostras aleatórias obtidas via ensacamento como Ensacamento rodada
1, Ensacamento rodada 2, e assim por diante. Cada subconjunto contém uma
determinada parte de duplicatas e alguns dos exemplos originais não aparecem
em um conjunto de dados reamostrado devido à amostragem com substituição.
Uma vez que os classificadores individuais estejam aptos às amostras de
bootstrap, as previsões são combinadas usando a votação por maioria.

Observe que o ensacamento também está relacionado ao classificador de


florestas aleatórias que introduzimos no Capítulo 3. Na verdade, florestas
aleatórias são um caso especial de ensacamento onde também usamos
subconjuntos de recursos aleatórios ao ajustar as árvores de decisão individuais.

Conjuntos de modelos usando ensacamento

O ensacamento foi proposto pela primeira vez por Leo Breiman em um relatório


técnico em 1994; Ele também mostrou que o ensacamento pode melhorar a
precisão de modelos instáveis e diminuir o grau de sobreajuste. É altamente
recomendável que você leia sobre sua pesquisa em Preditores de ensacamento
por L. Breiman, Machine Learning, 24(2):123–140, 1996, que está disponível
gratuitamente on-line, para aprender mais detalhes sobre ensacamento.

Aplicando ensacamento para classificar exemplos


no conjunto de dados do Wine
Para ver o ensacamento em ação, vamos criar um problema de classificação mais
complexo usando o conjunto de dados do Wine que foi introduzido no Capítulo
4, Building Good Training Datasets – Data Preprocessing. Aqui, consideraremos
apenas as classes de Vinho 2 e 3, e selecionaremos duas características –
e:AlcoholOD280/OD315 of diluted wines

>>> import pandas as pd

>>> df_wine = pd.read_csv('https://archive.ics.uci.edu/ml/'

... 'machine-learning-databases/'

... 'wine/wine.data',

... header=None)

>>> df_wine.columns = ['Class label', 'Alcohol',

... 'Malic acid', 'Ash',

... 'Alcalinity of ash',

... 'Magnesium', 'Total phenols',

... 'Flavanoids', 'Nonflavanoid phenols',

... 'Proanthocyanins',

... 'Color intensity', 'Hue',

... 'OD280/OD315 of diluted wines',

... 'Proline']

>>> # drop 1 class

>>> df_wine = df_wine[df_wine['Class label'] != 1]

>>> y = df_wine['Class label'].values

>>> X = df_wine[['Alcohol',

... 'OD280/OD315 of diluted wines']].values


CopyExplain

Em seguida, codificaremos os rótulos de classe em formato binário e dividiremos o


conjunto de dados em conjuntos de dados de treinamento de 80% e conjuntos de
dados de teste de 20%:
>>> from sklearn.preprocessing import LabelEncoder

>>> from sklearn.model_selection import train_test_split

>>> le = LabelEncoder()

>>> y = le.fit_transform(y)

>>> X_train, X_test, y_train, y_test =\

... train_test_split(X, y,

... test_size=0.2,

... random_state=1,

... stratify=y)
CopyExplain

Obtendo o conjunto de dados do Wine

Você pode encontrar uma cópia do conjunto de dados do Wine (e todos os outros
conjuntos de dados usados neste livro) no pacote de códigos deste livro, que você
pode usar se estiver trabalhando offline ou se o servidor UCI
no https://archive.ics.uci.edu/ml/machine-learning-databases/wine/wine.data estive
r temporariamente indisponível. Por exemplo, para carregar o conjunto de dados
do Wine de um diretório local, siga as seguintes linhas:

df = pd.read_csv('https://archive.ics.uci.edu/ml/'

'machine-learning-databases'

'/wine/wine.data',

header=None)
CopyExplain

e substitua-os por estes:

df = pd.read_csv('your/local/path/to/wine.data',

header=None)
CopyExplain
Um algoritmo já está implementado no scikit-learn, que podemos importar do
submódulo. Aqui, usaremos uma árvore de decisão não podada como
classificador base e criaremos um conjunto de 500 árvores de decisão que se
encaixam em diferentes amostras de bootstrap do conjunto de dados de
treinamento:BaggingClassifierensemble

>>> from sklearn.ensemble import BaggingClassifier

>>> tree = DecisionTreeClassifier(criterion='entropy',

... random_state=1,

... max_depth=None)

>>> bag = BaggingClassifier(base_estimator=tree,

... n_estimators=500,

... max_samples=1.0,

... max_features=1.0,

... bootstrap=True,

... bootstrap_features=False,

... n_jobs=1,

... random_state=1)
CopyExplain

Em seguida, calcularemos o escore de precisão da previsão nos conjuntos de


dados de treinamento e teste para comparar o desempenho do classificador de
ensacamento com o desempenho de uma única árvore de decisão não podada:

>>> from sklearn.metrics import accuracy_score

>>> tree = tree.fit(X_train, y_train)

>>> y_train_pred = tree.predict(X_train)

>>> y_test_pred = tree.predict(X_test)

>>> tree_train = accuracy_score(y_train, y_train_pred)

>>> tree_test = accuracy_score(y_test, y_test_pred)


>>> print(f'Decision tree train/test accuracies '

... f'{tree_train:.3f}/{tree_test:.3f}')

Decision tree train/test accuracies 1.000/0.833


CopyExplain

Com base nos valores de precisão que imprimimos aqui, a árvore de decisão não
podada prevê todos os rótulos de classe dos exemplos de treinamento
corretamente; No entanto, a precisão de teste substancialmente menor indica alta
variância (overfitting) do modelo:

>>> bag = bag.fit(X_train, y_train)

>>> y_train_pred = bag.predict(X_train)

>>> y_test_pred = bag.predict(X_test)

>>> bag_train = accuracy_score(y_train, y_train_pred)

>>> bag_test = accuracy_score(y_test, y_test_pred)

>>> print(f'Bagging train/test accuracies '

... f'{bag_train:.3f}/{bag_test:.3f}')

Bagging train/test accuracies 1.000/0.917


CopyExplain

Embora as precisões de treinamento da árvore de decisão e do classificador de


ensacamento sejam semelhantes no conjunto de dados de treinamento (ambos
100%), podemos ver que o classificador de ensacamento tem um desempenho de
generalização ligeiramente melhor, conforme estimado no conjunto de dados de
teste. Em seguida, vamos comparar as regiões de decisão entre a árvore de
decisão e o classificador de ensacamento:

>>> x_min = X_train[:, 0].min() - 1

>>> x_max = X_train[:, 0].max() + 1

>>> y_min = X_train[:, 1].min() - 1

>>> y_max = X_train[:, 1].max() + 1

>>> xx, yy = np.meshgrid(np.arange(x_min, x_max, 0.1),


... np.arange(y_min, y_max, 0.1))

>>> f, axarr = plt.subplots(nrows=1, ncols=2,

... sharex='col',

... sharey='row',

... figsize=(8, 3))

>>> for idx, clf, tt in zip([0, 1],

... [tree, bag],

... ['Decision tree', 'Bagging']):

... clf.fit(X_train, y_train)

...

... Z = clf.predict(np.c_[xx.ravel(), yy.ravel()])

... Z = Z.reshape(xx.shape)

... axarr[idx].contourf(xx, yy, Z, alpha=0.3)

... axarr[idx].scatter(X_train[y_train==0, 0],

... X_train[y_train==0, 1],

... c='blue', marker='^')

... axarr[idx].scatter(X_train[y_train==1, 0],

... X_train[y_train==1, 1],

... c='green', marker='o')

... axarr[idx].set_title(tt)

>>> axarr[0].set_ylabel('OD280/OD315 of diluted wines', fontsize=12)

>>> plt.tight_layout()

>>> plt.text(0, -0.2,

... s='Alcohol',

... ha='center',

... va='center',
... fontsize=12,

... transform=axarr[1].transAxes)

>>> plt.show()
CopyExplain

As we can see in the resulting plot, the piece-wise linear decision boundary of the
three-node deep decision tree looks smoother in the bagging ensemble:

Figure 7.8: The piece-wise linear decision boundary of a decision tree versus
bagging

We only looked at a very simple bagging example in this section. In practice, more
complex classification tasks and a dataset’s high dimensionality can easily lead to
overfitting in single decision trees, and this is where the bagging algorithm can
really play to its strengths. Finally, we must note that the bagging algorithm can be
an effective approach to reducing the variance of a model. However, bagging is
ineffective in reducing model bias, that is, models that are too simple to capture the
trends in the data well. This is why we want to perform bagging on an ensemble of
classifiers with low bias, for example, unpruned decision trees.

Alavancando alunos fracos por meio do


aumento adaptativo
Nesta última seção sobre métodos de conjunto, discutiremos o boosting, com
foco especial em sua implementação mais comum: Adaptive
Boosting (AdaBoost).

Reconhecimento AdaBoost

A ideia original por trás do AdaBoost foi formulada por Robert E. Schapire em


1990 em The Strength of Weak Learnability, Machine Learning, 5(2): 197-227,
1990, URL: http://rob.schapire.net/papers/strengthofweak.pdf. Depois que Robert
Schapire e Yoav Freund apresentaram o algoritmo AdaBoost nos Anais da
Décima Terceira Conferência Internacional (ICML 1996), o AdaBoost tornou-se um
dos métodos de conjunto mais utilizados nos anos seguintes (Experimentos com
um Novo Algoritmo de Impulsionamento, de Y. Freund, R. E. Schapire, e
outros, ICML, volume 96, 148-156, 1996). Em 2003, Freund e Schapire receberam
o Prêmio Gödel por seu trabalho inovador, que é um prêmio de prestígio para as
publicações mais destacadas no campo da ciência da computação.

No boosting, o conjunto consiste em classificadores de base muito simples,


também muitas vezes referidos como alunos fracos, que muitas vezes têm
apenas uma ligeira vantagem de desempenho sobre adivinhações aleatórias – um
exemplo típico de um aluno fraco é um toco de árvore de decisão. O conceito-
chave por trás do impulsionamento é focar em exemplos de treinamento que são
difíceis de classificar, ou seja, permitir que os alunos fracos aprendam
posteriormente com exemplos de treinamento mal classificados para melhorar o
desempenho do conjunto.

As subseções a seguir apresentarão o procedimento algorítmico por trás do


conceito geral de impulsionamento e AdaBoost. Por fim, usaremos scikit-learn
para um exemplo prático de classificação.

Como funciona o impulsionamento adaptativo


Em contraste com o ensacamento, a formulação inicial do algoritmo de
impulsionamento usa subconjuntos aleatórios de exemplos de treinamento
extraídos do conjunto de dados de treinamento sem substituição; O procedimento
de reforço original pode ser resumido nas seguintes quatro etapas principais:
1. Desenhar um subconjunto aleatório (amostra) de exemplos de
treinamento, d1, sem substituição do conjunto de dados de treinamento, D,
para treinar um aluno fraco, C1.
2. Desenhar um segundo subconjunto de treinamento aleatório, d2, sem
substituir o conjunto de dados de treinamento e adicionar 50% dos exemplos
que foram anteriormente classificados incorretamente para treinar um aluno
fraco, C2.
3. Encontre os exemplos de treinamento, d3, no conjunto de dados de
treinamento, D, que C1 e C2 discordo, para treinar um terceiro aluno fraco, C3.
4. Combine os alunos fracos C1, C2e C3 por maioria de votos.

Conforme discutido por Leo Breiman (Bias, variance, and arcing classifiers, 1996),
o aumento pode levar a uma diminuição do viés, bem como da variância em
comparação com os modelos de ensacamento. Na prática, no entanto, algoritmos
de impulsionamento como o AdaBoost também são conhecidos por sua alta
variância, ou seja, a tendência de sobreajustar os dados de treinamento (Uma
melhoria do AdaBoost para evitar o overfitting por G. Raetsch, T. Onoda e K.
R. Mueller. Anais da Conferência Internacional sobre Processamento de
Informações Neurais, CiteSeer, 1998).

Em contraste com o procedimento de impulsionamento original descrito aqui, o


AdaBoost usa o conjunto de dados de treinamento completo para treinar os alunos
fracos, onde os exemplos de treinamento são reponderados em cada iteração
para construir um classificador forte que aprende com os erros dos alunos fracos
anteriores no conjunto.

Antes de nos aprofundarmos nos detalhes específicos do algoritmo AdaBoost,


vamos dar uma olhada na Figura 7.9 para entender melhor o conceito básico por
trás do AdaBoost:
Figura 7.9: O conceito de AdaBoost para melhorar os alunos fracos

Para percorrer a ilustração do AdaBoost passo a passo, começaremos com a


subfigura 1, que representa um conjunto de dados de treinamento para
classificação binária onde todos os exemplos de treinamento recebem pesos
iguais. Com base nesse conjunto de dados de treinamento, treinamos um toco de
decisão (mostrado como uma linha tracejada) que tenta classificar os exemplos
das duas classes (triângulos e círculos), bem como possivelmente minimizar a
função de perda (ou o escore de impureza no caso especial de conjuntos de
árvores de decisão).

Para a próxima rodada (subfigura 2), atribuímos um peso maior aos dois exemplos
anteriormente classificados erroneamente (círculos). Além disso, diminuímos o
peso dos exemplos corretamente classificados. A próxima decisão agora será
mais focada nos exemplos de treinamento que têm os maiores pesos – os
exemplos de treinamento que supostamente são difíceis de classificar.

O aluno fraco mostrado na subfigura 2 classifica erroneamente três exemplos


diferentes da classe circular, aos quais é atribuído um peso maior, como mostrado
na subfigura 3.

Supondo que nosso conjunto AdaBoost consiste apenas em três rodadas de


reforço, combinamos os três alunos fracos treinados em diferentes subconjuntos
de treinamento reponderados por uma maioria ponderada de votos, como
mostrado na subfigura 4.

Agora que temos uma melhor compreensão do conceito básico do AdaBoost,


vamos dar uma olhada mais detalhada no algoritmo usando pseudo código. Para
maior clareza, denotaremos a multiplicação elementar pelo símbolo da cruz (×) e o
produto-ponto entre dois vetores por um símbolo de ponto (⋅):

1. Defina o vetor de peso, w, para pesos uniformes, onde 


.
2. Para j, em m, faça o seguinte:
a. Treinar um aluno fraco ponderado: Cj = trem(X, y, w).

b. Prever rótulos de classe:  .


c. Calcule a taxa de erro

ponderada:  .

d. Calcule o coeficiente:  .
e. Atualize os
pesos: 

.
f. Normalize os pesos para somar 1:  .
3. Calcule a previsão
final: 

Observe que a expressão   na etapa 2c refere-se a um vetor


binário que consiste em 1s e 0s, onde um 1 é atribuído se a previsão estiver
incorreta e 0 for atribuído de outra forma.

Embora o algoritmo AdaBoost pareça ser bastante simples, vamos percorrer um


exemplo mais concreto usando um conjunto de dados de treinamento que consiste
em 10 exemplos de treinamento, conforme ilustrado na Figura 7.10:

Figura 7.10: Executando 10 exemplos de treinamento por meio do algoritmo


AdaBoost

A primeira coluna da tabela mostra os índices dos exemplos de treinamento de 1 a


10. Na segunda coluna, você pode ver os valores de recurso das amostras
individuais, supondo que esse seja um conjunto de dados unidimensional. A
terceira coluna mostra o rótulo de classe verdadeiro, yeu, para cada amostra de

treinamento, xeu, onde  . Os pesos iniciais são


mostrados na quarta coluna; Inicializamos os pesos uniformemente (atribuindo o
mesmo valor constante) e os normalizamos para somar 1. No caso do conjunto de
dados de treinamento de 10 amostras, atribuímos 0,1 a cada peso, weu, no vetor

peso, w. Os rótulos de classe previstos, , são mostrados na quinta coluna, 

assumindo que nosso critério de divisão é  . A última coluna da


tabela mostra os pesos atualizados com base nas regras de atualização que
definimos no pseudocódigo.

Como o cálculo das atualizações de peso pode parecer um pouco complicado no


início, agora seguiremos o passo a passo do cálculo. Começaremos calculando a

taxa de erro ponderada,   (), conforme descrito na etapa 2c:epsilon

>>> y = np.array([1, 1, 1, -1, -1, -1, 1, 1, 1, -1])

>>> yhat = np.array([1, 1, 1, -1, -1, -1, -1, -1, -1, -1])

>>> correct = (y == yhat)

>>> weights = np.full(10, 0.1)

>>> print(weights)

[0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1]

>>> epsilon = np.mean(~correct)

>>> print(epsilon)

0.3
CopyExplain

Observe que é uma matriz booleana que consiste em e valores onde indica que
uma previsão está correta. Via , invertemos a matriz de tal forma que calcula a
proporção de previsões incorretas ( conta como o valor 1 e como 0), ou seja, o
erro de classificação.correctTrueFalseTrue~correctnp.mean(~correct)TrueFalse

Em seguida, calcularemos o coeficiente, mostrado na etapa 2d, que

posteriormente será usado na etapa 2e para atualizar os pesos,  bem como


para os pesos na previsão de voto majoritário (etapa 3):

>>> alpha_j = 0.5 * np.log((1-epsilon) / epsilon)

>>> print(alpha_j)

0.42364893019360184
CopyExplain

Depois de calcularmos o coeficiente,   (), agora podemos atualizar o vetor de


peso usando a seguinte equação:alpha_j

Aqui,   há uma multiplicação elementar entre os vetores dos rótulos


de classe predito e verdadeiro, respectivamente. Assim, se uma previsão, , estiver

correta, terá um sinal positivo para que diminuamos oi-ésimo peso, 

 já   que é um número positivo também:

>>> update_if_correct = 0.1 * np.exp(-alpha_j * 1 * 1)

>>> print(update_if_correct)

0.06546536707079771
CopyExplain
Da mesma forma, aumentaremos o i-ésimo peso se   o rótulo for previsto
incorretamente, assim:

>>> update_if_wrong_1 = 0.1 * np.exp(-alpha_j * 1 * -1)

>>> print(update_if_wrong_1)

0.1527525231651947
CopyExplain

Alternativamente, é assim:

>>> update_if_wrong_2 = 0.1 * np.exp(-alpha_j * -1 * 1)

>>> print(update_if_wrong_2)

0.1527525231651947
CopyExplain

Podemos usar esses valores para atualizar os pesos da seguinte maneira:

>>> weights = np.where(correct == 1,

... update_if_correct,

... update_if_wrong_1)

>>> print(weights)

array([0.06546537, 0.06546537, 0.06546537, 0.06546537, 0.06546537,

0.06546537, 0.15275252, 0.15275252, 0.15275252, 0.06546537])


CopyExplain

O código acima atribuiu o valor a todas as previsões corretas e o valor a todas as


previsões erradas. Omitimos o uso por simplicidade, já que é semelhante ao
mesmo
tempo.update_if_correctupdate_if_wrong_1update_if_wrong_2update_if_wrong_1

Depois de atualizarmos cada peso no vetor de peso, normalizamos os pesos para


que eles somem até 1 (passo 2f):
No código, podemos fazer isso da seguinte maneira:

>>> normalized_weights = weights / np.sum(weights)

>>> print(normalized_weights)

[0.07142857 0.07142857 0.07142857 0.07142857 0.07142857 0.07142857

0.16666667 0.16666667 0.16666667 0.07142857]


CopyExplain

Assim, cada peso que corresponda a um exemplo corretamente classificado será


reduzido do valor inicial de 0,1 para 0,0714 para a próxima rodada de reforço. Da
mesma forma, os pesos dos exemplos classificados incorretamente aumentarão
de 0,1 para 0,1667.

Aplicando o AdaBoost usando scikit-learn


A subseção anterior introduziu o AdaBoost em poucas palavras. Pulando para a
parte mais prática, vamos agora treinar um classificador de conjunto AdaBoost via
scikit-learn. Usaremos o mesmo subconjunto Wine que usamos na seção anterior
para treinar o metaclassificador de ensacamento.

Através do atributo, treinaremos os 500 tocos de árvores de


decisão:base_estimatorAdaBoostClassifier

>>> from sklearn.ensemble import AdaBoostClassifier

>>> tree = DecisionTreeClassifier(criterion='entropy',

... random_state=1,

... max_depth=1)

>>> ada = AdaBoostClassifier(base_estimator=tree,

... n_estimators=500,
... learning_rate=0.1,

... random_state=1)

>>> tree = tree.fit(X_train, y_train)

>>> y_train_pred = tree.predict(X_train)

>>> y_test_pred = tree.predict(X_test)

>>> tree_train = accuracy_score(y_train, y_train_pred)

>>> tree_test = accuracy_score(y_test, y_test_pred)

>>> print(f'Decision tree train/test accuracies '

... f'{tree_train:.3f}/{tree_test:.3f}')

Decision tree train/test accuracies 0.916/0.875


CopyExplain

Como você pode ver, o toco da árvore de decisão parece não ajustar os dados de
treinamento em contraste com a árvore de decisão não podada que vimos na
seção anterior:

>>> ada = ada.fit(X_train, y_train)

>>> y_train_pred = ada.predict(X_train)

>>> y_test_pred = ada.predict(X_test)

>>> ada_train = accuracy_score(y_train, y_train_pred)

>>> ada_test = accuracy_score(y_test, y_test_pred)

>>> print(f'AdaBoost train/test accuracies '

... f'{ada_train:.3f}/{ada_test:.3f}')

AdaBoost train/test accuracies 1.000/0.917


CopyExplain

Aqui, você pode ver que o modelo AdaBoost prevê todos os rótulos de classe do
conjunto de dados de treinamento corretamente e também mostra um
desempenho de conjunto de dados de teste ligeiramente melhorado em
comparação com o toco da árvore de decisão. No entanto, você também pode ver
que introduzimos variância adicional com nossa tentativa de reduzir o viés do
modelo — uma lacuna maior entre o treinamento e o desempenho no teste.

Embora tenhamos usado outro exemplo simples para fins de demonstração,


podemos ver que o desempenho do classificador AdaBoost é ligeiramente
melhorado em comparação com o coto de decisão e alcançou pontuações de
precisão muito semelhantes às do classificador de ensacamento que treinamos na
seção anterior. No entanto, devemos observar que é considerada má prática
selecionar um modelo com base no uso repetido do conjunto de dados de teste. A
estimativa do desempenho da generalização pode ser excessivamente otimista, o
que discutimos em mais detalhes no Capítulo 6, Aprendendo Melhores Práticas
para Avaliação de Modelos e Ajuste de Hiperparâmetros.

Por fim, vamos conferir como são as regiões de decisão:

>>> x_min = X_train[:, 0].min() - 1

>>> x_max = X_train[:, 0].max() + 1

>>> y_min = X_train[:, 1].min() - 1

>>> y_max = X_train[:, 1].max() + 1

>>> xx, yy = np.meshgrid(np.arange(x_min, x_max, 0.1),

... np.arange(y_min, y_max, 0.1))

>>> f, axarr = plt.subplots(1, 2,

... sharex='col',

... sharey='row',

... figsize=(8, 3))

>>> for idx, clf, tt in zip([0, 1],

... [tree, ada],

... ['Decision tree', 'AdaBoost']):

... clf.fit(X_train, y_train)

... Z = clf.predict(np.c_[xx.ravel(), yy.ravel()])

... Z = Z.reshape(xx.shape)
... axarr[idx].contourf(xx, yy, Z, alpha=0.3)

... axarr[idx].scatter(X_train[y_train==0, 0],

... X_train[y_train==0, 1],

... c='blue',

... marker='^')

... axarr[idx].scatter(X_train[y_train==1, 0],

... X_train[y_train==1, 1],

... c='green',

... marker='o')

... axarr[idx].set_title(tt)

... axarr[0].set_ylabel('OD280/OD315 of diluted wines', fontsize=12)

>>> plt.tight_layout()

>>> plt.text(0, -0.2,

... s='Alcohol',

... ha='center',

... va='center',

... fontsize=12,

... transform=axarr[1].transAxes)

>>> plt.show()
CopyExplain

Ao observar as regiões de decisão, você pode ver que o limite de decisão do


modelo AdaBoost é substancialmente mais complexo do que o limite de decisão
do toco de decisão. Além disso, observe que o modelo AdaBoost separa o espaço
de recursos de forma muito semelhante ao classificador de ensacamento que
treinamos na seção anterior:
Figura 7.11: Os limites de decisão da árvore de decisão versus AdaBoost

Como considerações finais sobre as técnicas de conjunto, vale a pena notar que o
aprendizado de conjunto aumenta a complexidade computacional em comparação
com classificadores individuais. Na prática, precisamos pensar cuidadosamente se
queremos pagar o preço do aumento dos custos computacionais por uma melhoria
muitas vezes relativamente modesta no desempenho preditivo.

Um exemplo frequentemente citado dessa troca é o famoso prêmio Netflix de US$


1 milhão, que foi ganho usando técnicas de conjunto. Os detalhes sobre o
algoritmo foram publicados em The BigChaos Solution to the Netflix Grand
Prize por A. Toescher, M. Jahrer e R. M. Bell, documentação do Prêmio Netflix,
2009, que está disponível
em http://www.stat.osu.edu/~dmsl/GrandPrize2009_BPC_BigChaos.pdf. A equipe
vencedora recebeu o grande prêmio de US$ 1 milhão; no entanto, a Netflix nunca
implementou seu modelo devido à sua complexidade, o que o tornou inviável para
um aplicativo do mundo real:

"Avaliamos alguns dos novos métodos offline, mas os ganhos adicionais de


precisão que medimos não pareciam justificar o esforço de engenharia necessário
para trazê-los para um ambiente de produção."

http://techblog.netflix.com/2012/04/netflix-recommendations-beyond-5-stars.html
Aumento de gradiente – treinamento de
um conjunto baseado em gradientes de
perda
O gradiente de aumento é outra variante do conceito de impulsionamento
introduzido na seção anterior, ou seja, treinar sucessivamente alunos fracos para
criar um conjunto forte. O aumento de gradiente é um tópico extremamente
importante porque forma a base de algoritmos populares de aprendizado de
máquina, como o XGBoost, que é conhecido por vencer competições do Kaggle.

O algoritmo de aumento de gradiente pode parecer um pouco assustador no


início. Assim, nas subseções a seguir, abordaremos isso passo a passo,
começando com uma visão geral. Em seguida, veremos como o aumento de
gradiente é usado para classificação e percorreremos um exemplo. Finalmente,
depois de introduzirmos os conceitos fundamentais de aumento de gradiente,
daremos uma breve olhada em implementações populares, como o XGBoost, e
veremos como podemos usar o aumento de gradiente na prática.

Comparando o AdaBoost com o aumento de


gradiente
Fundamentalmente, o aumento de gradiente é muito semelhante ao AdaBoost,
que discutimos anteriormente neste capítulo. O AdaBoost treina tocos de árvore
de decisão com base em erros do toco de árvore de decisão anterior. Em
particular, os erros são usados para calcular os pesos das amostras em cada
rodada, bem como para calcular um peso do classificador para cada toco de
árvore de decisão ao combinar os tocos individuais em um conjunto. Paramos de
treinar quando um número máximo de iterações (tocos de árvore de decisão) é
atingido. Como o AdaBoost, o aumento de gradiente se ajusta às árvores de
decisão de forma iterativa usando erros de previsão. No entanto, as árvores que
aumentam o gradiente são geralmente mais profundas do que os tocos de árvores
de decisão e têm tipicamente uma profundidade máxima de 3 a 6 (ou um número
máximo de 8 a 64 nós de folhas). Além disso, em contraste com o AdaBoost, o
aumento de gradiente não usa os erros de previsão para atribuir pesos de
amostra; eles são usados diretamente para formar a variável de destino para
ajustar a próxima árvore. Além disso, em vez de ter um termo de ponderação
individual para cada árvore, como no AdaBoost, o aumento de gradiente usa uma
taxa de aprendizado global que é a mesma para cada árvore.

Como você pode ver, o AdaBoost e o gradient boosting compartilham várias


semelhanças, mas diferem em certos aspectos-chave. Na subseção a seguir,
vamos esboçar o esboço geral do algoritmo de aumento de gradiente.

Delineando o algoritmo geral de aumento de


gradiente
Nesta seção, veremos o aumento de gradiente para classificação. Para simplificar,
veremos um exemplo de classificação binária. Os leitores interessados podem
encontrar a generalização para o cenário multiclasse com perda logística
na Seção 4.6. Regressão logística multiclasse e classificação do artigo original de
aumento de gradiente escrito por Friedman em 2001, Greedy function
approximation: A gradient boosting
machine, https://projecteuclid.org/journals/annals-of-statistics/volume-29/issue-5/
Greedy-function-approximation-A-gradient-boostingmachine/10.1214/aos/
1013203451.full.

Aumento de gradiente para regressão

Observe que o procedimento por trás do aumento de gradiente é um pouco mais


complicado do que o AdaBoost. Omitimos um exemplo de regressão mais simples,
que foi dado no artigo de Friedman, por brevidade, mas os leitores interessados
são encorajados a considerar também meu tutorial complementar em vídeo sobre
aumento de gradiente para regressão, que está disponível
em: https://www.youtube.com/watch?v=zblsrxc7XpM.

Em essência, o aumento de gradiente cria uma série de árvores, onde cada árvore
se encaixa no erro — a diferença entre o rótulo e o valor previsto — da árvore
anterior. Em cada rodada, o conjunto de árvores melhora, pois estamos
empurrando cada árvore mais na direção certa por meio de pequenas
atualizações. Essas atualizações são baseadas em um gradiente de perda, que é
como o aumento de gradiente recebeu seu nome.

As etapas a seguir apresentarão o algoritmo geral por trás do aumento de


gradiente. Depois de ilustrar as etapas principais, vamos mergulhar em algumas
de suas partes com mais detalhes e percorrer um exemplo prático nas próximas
subseções.

1. Inicialize um modelo para retornar um valor de previsão constante. Para isso,


usamos um nó raiz de árvore de decisão; ou seja, uma árvore de decisão com

um único nó de folha. Denotamos o valor retornado pela árvore como  ,e


encontramos esse valor minimizando uma função de perda
diferenciável L que definiremos mais adiante:

Aqui, n refere-se aos n exemplos de treinamento em nosso conjunto de


dados.

2. Para cada árvore m = 1, ..., M, onde M é um número total de árvores


especificado pelo usuário, realizamos os seguintes cálculos descritos
nas etapas 2a a 2d abaixo:

a. Calcular a diferença entre um valor   previsto e


o rótulo de classe yeu. Esse valor às vezes é chamado de pseudo-
resposta ou pseudo-residual. Mais formalmente, podemos escrever
este pseudo-residual como o gradiente negativo da função de perda
em relação aos valores previstos:
Note que na notação acima F(x) está a previsão da árvore anterior, Fm–
1(x). Assim, na primeira rodada, isso se refere ao valor constante da

árvore (nó de folha única) do passo 1.

b. Ajustar uma árvore aos pseudo-resíduos rIm. Usamos a


notação RJM para denotar o j = 1 ... Jm nós foliares da árvore resultante
na iteração m.
c. Para cada nó de folha RJM, calculamos o seguinte valor de saída:

Na próxima subseção, vamos nos aprofundar em como isso   é


calculado, minimizando a função de perda. Neste ponto, já podemos
notar que os nós da folha RJM pode conter mais de um exemplo de
treinamento, daí a somatória.

d. Atualize o modelo adicionando os valores   de saída à árvore


anterior:

No entanto, em vez de adicionar os valores preditos completos da

árvore atual à árvore    anterior, dimensionamos   


por uma taxa  de aprendizado, que normalmente é um pequeno
valor entre 0,01 e 1. Em outras palavras, atualizamos o modelo de
forma incremental, dando pequenos passos, o que ajuda a evitar o
overfitting.

Agora, depois de olhar para a estrutura geral do aumento de gradiente, vamos


adotar essas mecânicas para olhar para o aumento de gradiente para
classificação.

Explicando o algoritmo de aumento de gradiente


para classificação
Nesta subseção, abordaremos os detalhes para implementar o algoritmo de
aumento de gradiente para classificação binária. Neste contexto, usaremos a
função de perda logística que introduzimos para regressão logística no Capítulo
3, A Tour of Machine Learning Classifiers Using Scikit-Learn. Para um único
exemplo de treinamento, podemos especificar a perda logística da seguinte
maneira:

No Capítulo 3, também introduzimos o log(odds):

Por razões que farão sentido mais tarde, usaremos esses log(odds) para
reescrever a função logística da seguinte forma (omitindo etapas intermediárias
aqui):
Agora, podemos definir a derivada parcial da função de perda com relação a

esses log(odds),  . A derivada desta função de perda em relação ao log(odds)


é:

Depois de especificar essas definições matemáticas, vamos agora revisitar as


etapas gerais de aumento de gradiente 1 a 2d da seção anterior e reformulá-las
para esse cenário de classificação binária.

1. Crie um nó raiz que minimize a perda logística. Acontece que a perda é

minimizada se o nó raiz retornar o log(odds),  .


2. Para cada árvore m = 1, ..., M, onde M é um número especificado pelo
usuário de árvores totais, realizamos os seguintes cálculos descritos
nas etapas 2a a 2d:
a. Convertemos o log(odds) em probabilidade usando a função logística
familiar que usamos na regressão logística (no Capítulo 3):

Em seguida, calculamos o pseudo-residual, que é a derivada parcial


negativa da perda em relação ao log(odds), que acaba sendo a
diferença entre o rótulo da classe e a probabilidade prevista:
b. Encaixe uma nova árvore aos pseudo-resíduos.

c. Para cada nó de folha RJM, calcula um valor   que minimiza a


função de perda logística. Isso inclui uma etapa de resumo para lidar
com nós de folha que contêm vários exemplos de treinamento:

Ignorando detalhes matemáticos intermediários, isso resulta no


seguinte:

Observe que a soma aqui é apenas sobre os exemplos no nó


correspondente ao nó folha RJM e não o conjunto completo de
treinamento.

d. Atualize o modelo adicionando o valor gama da etapa 2c com a

taxa  de aprendizado:

Log de saída (probabilidades) vs probabilidades


Por que as árvores retornam valores de log (odds) e não probabilidades? Isso
porque não podemos simplesmente somar valores de probabilidade e chegar a um
resultado significativo. (Então, tecnicamente falando, o aumento de gradiente para
classificação usa árvores de regressão.)

Nesta seção, adotamos o algoritmo geral de aumento de gradiente e o


especificamos para classificação binária, por exemplo, substituindo a função de
perda genérica pela perda logística e os valores previstos pelo log(odds). No
entanto, muitas das etapas individuais ainda podem parecer muito abstratas e, na
próxima seção, aplicaremos essas etapas a um exemplo concreto.

Ilustrando o aumento de gradiente para


classificação
As duas subseções anteriores abordaram os detalhes matemáticos condensados
do algoritmo de aumento de gradiente para classificação binária. Para tornar
esses conceitos mais claros, vamos aplicá-lo a um pequeno exemplo de
brinquedo, ou seja, um conjunto de dados de treinamento dos três exemplos a
seguir mostrados na Figura 7.12:

Figura 7.12: Conjunto de dados do brinquedo para explicar o aumento do


gradiente

Vamos começar com a etapa 1, construindo o nó raiz e computando o log(odds), e


a etapa 2a, convertendo o log(odds) em probabilidades de associação de classe e
computando os pseudo-resíduos. Observe que, com base no que aprendemos
no Capítulo 3, as chances podem ser computadas como o número de acertos
dividido pelo número de fracassos. Aqui, consideramos o rótulo 1 como sucesso e
o rótulo 0 como fracasso, então as chances são calculadas como: odds = 2/1.
Realizando as etapas 1 e 2a, obtemos os seguintes resultados mostrados
na Figura 7.13:

Figura 7.13: Resultados da primeira ronda de aplicação dos passos 1 e 2a

Em seguida, na etapa 2b, encaixamos uma nova árvore no pseudo-resíduo r. Em

seguida, na etapa 2c, calculamos os valores de saída,  , para esta árvore como
mostrado na Figura 7.14:
Figura 7.14: Uma ilustração das etapas 2b e 2c, que ajusta uma árvore aos
resíduos e calcula os valores de saída para cada nó de folha

(Observe que limitamos artificialmente a árvore a ter apenas dois nós de folha, o
que ajuda a ilustrar o que acontece se um nó de folha contiver mais de um
exemplo.)

Em seguida, na etapa final 2d, atualizamos o modelo anterior e o modelo atual.


Assumindo uma taxa de aprendizagem de , a previsão resultante para o primeiro

exemplo de  treinamento é mostrada na Figura 7.15:


Figura 7.15: A actualização do modelo anterior apresentada no contexto do
primeiro exemplo de formação

Agora que concluímos as etapas 2a a 2d da primeira rodada , m = 1, podemos


prosseguir para executar as etapas 2a a 2d para a segunda rodada, m = 2. Na
segunda rodada, usamos o log(odds) retornado pelo modelo atualizado, por

exemplo, ,  como entrada para a etapa 2A. Os


novos valores que obtemos na segunda rodada são mostrados na Figura 7.16:
Figura 7.16: Valores da segunda ronda junto aos valores da primeira ronda

Já podemos ver que as probabilidades previstas são maiores para a classe


positiva e menores para a classe negativa. Consequentemente, os resíduos
também estão ficando menores. Observe que o processo das etapas 2a a 2d é
repetido até que tenhamos árvores M ajustadas ou os resíduos sejam menores do
que um valor limite especificado pelo usuário. Então, uma vez que o algoritmo de
aumento de gradiente tenha sido concluído, podemos usá-lo para prever os
rótulos de classe limitando os valores de probabilidade do modelo final, FM(x) em
0,5, como a regressão logística no Capítulo 3. No entanto, em contraste com a
regressão logística, o aumento do gradiente consiste em múltiplas árvores e
produz limites de decisão não lineares. Na próxima seção, veremos como o
aumento de gradiente parece estar em ação.

Usando XGBoost
Depois de cobrir os detalhes detalhados por trás do aumento de gradiente, vamos
finalmente ver como podemos usar implementações de código de aumento de
gradiente.

No scikit-learn, o aumento de gradiente é implementado como (veja https://scikit-


learn.org/stable/modules/generated/sklearn.ensemble.GradientBoostingClassifier.h
tml para obter mais detalhes). É importante notar que o aumento de gradiente é
um processo sequencial que pode ser lento para treinar. No entanto, nos últimos
anos, uma implementação mais popular de aumento de gradiente surgiu, ou seja,
XGBoost.sklearn.ensemble.GradientBoostingClassifier

XGBoost propôs vários truques e aproximações que aceleram substancialmente o


processo de treinamento. Daí, o nome XGBoost, que significa aumento de
gradiente extremo. Além disso, essas aproximações e truques resultam em
desempenhos preditivos muito bons. Na verdade, o XGBoost ganhou
popularidade, pois tem sido a solução vencedora para muitas competições do
Kaggle.

Ao lado do XGBoost, há também outras implementações populares de aumento


de gradiente, por exemplo, LightGBM e CatBoost. Inspirado no LightGBM, o scikit-
learn agora também implementa um , que é mais eficiente do que o classificador
de aumento de gradiente original
(). HistGradientBoostingClassifierGradientBoostingClassifier

Você pode encontrar mais detalhes sobre esses métodos através dos recursos


abaixo:

 XGBoost: https://xgboost.readthedocs.io/en/stable/
 LightGBM: https://lightgbm.readthedocs.io/en/latest/
 CatBoost: https://catboost.ai
 HistGradientBoostingClassifier: https://scikit-learn.org/stable/modules/
generated/sklearn.ensemble.HistGradientBoostingClassifier.html

No entanto, como o XGBoost ainda está entre as implementações de aumento de


gradiente mais populares, veremos como podemos usá-lo na prática. Primeiro,
precisamos instalá-lo, por exemplo, através de: pip

pip install xgboost


CopyExplain

Instalando o XGBoost

Para este capítulo, usamos o XGBoost versão 1.5.0, que pode ser instalado
através de:

pip install XGBoost==1.5.0


CopyExplain

Você pode encontrar mais informações sobre os detalhes da instalação


em https://xgboost.readthedocs.io/en/stable/install.html

Felizmente, o XGBoot segue a API scikit-learn. Então, usá-lo é relativamente


simples:XGBClassifier

>>> import xgboost as xgb

>>> model = xgb.XGBClassifier(n_estimators=1000, learning_rate=0.01,


... max_depth=4, random_state=1,

... use_label_encoder=False)

>>> gbm = model.fit(X_train, y_train)

>>> y_train_pred = gbm.predict(X_train)

>>> y_test_pred = gbm.predict(X_test)

>>> gbm_train = accuracy_score(y_train, y_train_pred)

>>> gbm_test = accuracy_score(y_test, y_test_pred)

>>> print(f'XGboost train/test accuracies '

... f'{gbm_train:.3f}/{gbm_test:.3f}')

XGboost train/test accuracies 0.968/0.917


CopyExplain

Aqui, encaixamos o classificador de aumento de gradiente com 1.000 árvores


(rodadas) e uma taxa de aprendizado de 0,01. Normalmente, recomenda-se uma
taxa de aprendizagem entre 0,01 e 0,1. No entanto, lembre-se de que a taxa de
aprendizado é usada para dimensionar as previsões das rodadas individuais.
Assim, intuitivamente, quanto menor a taxa de aprendizado, mais estimadores são
necessários para obter previsões precisas.

Em seguida, temos as árvores de decisão individuais, que definimos como 4.


Como ainda estamos impulsionando alunos fracos, um valor entre 2 e 6 é
razoável, mas valores maiores também podem funcionar bem, dependendo do
conjunto de dados.max_depth

Finalmente, desabilita uma mensagem de aviso que informa aos usuários que o
XGBoost não está mais convertendo rótulos por padrão e espera que os usuários
forneçam rótulos em um formato inteiro começando com o rótulo 0. (Não há com o
que se preocupar aqui, já que temos seguido esse formato ao longo deste
livro.)use_label_encoder=False

Há muito mais configurações disponíveis, e uma discussão detalhada está fora do


escopo deste livro. No entanto, os leitores interessados podem encontrar mais
detalhes na documentação original
em https://xgboost.readthedocs.io/en/latest/python/python_api.html#xgboost.XGB
Classifier.

Resumo
Neste capítulo, analisamos algumas das técnicas mais populares e amplamente
utilizadas para o aprendizado em conjunto. Os métodos Ensemble combinam
diferentes modelos de classificação para anular suas fraquezas individuais, o que
geralmente resulta em modelos estáveis e de bom desempenho que são muito
atraentes para aplicações industriais, bem como competições de aprendizado de
máquina.

No início deste capítulo, implementamos em Python, o que nos permite combinar


diferentes algoritmos para classificação. Em seguida, analisamos o ensacamento,
uma técnica útil para reduzir a variância de um modelo, desenhando amostras
aleatórias de bootstrap do conjunto de dados de treinamento e combinando os
classificadores treinados individualmente por meio de votação majoritária. Por fim,
aprendemos sobre o boost na forma de AdaBoost e gradient boosting, que são
algoritmos baseados no treinamento de alunos fracos que, posteriormente,
aprendem com os erros.MajorityVoteClassifier

Ao longo dos capítulos anteriores, aprendemos muito sobre diferentes algoritmos


de aprendizagem, ajuste e técnicas de avaliação. No próximo capítulo, veremos
uma aplicação específica do aprendizado de máquina, a análise de sentimento,
que se tornou um tópico interessante na era da internet e das mídias sociais.

Junte-se ao espaço Discord do nosso livro


Junte-se à nossa comunidade do Discord para conhecer pessoas que pensam
como você e aprender ao lado de mais de 2000 membros em:

https://packt.link/MLwPyTorch

Aplicando Machine Learning à Análise de


Sentimento
Na era moderna da internet e das mídias sociais, as opiniões, avaliações e
recomendações das pessoas se tornaram um recurso valioso para a ciência
política e as empresas. Graças às tecnologias modernas, agora somos capazes
de coletar e analisar esses dados de forma mais eficiente. Neste capítulo, vamos
nos aprofundar em um subcampo do processamento de linguagem natural (NLP)
chamado análise de sentimento e aprender a usar algoritmos de aprendizado de
máquina para classificar documentos com base em seu sentimento: a atitude do
escritor. Em particular, vamos trabalhar com um conjunto de dados de 50.000
críticas de filmes do Internet Movie Database (IMDb) e construir um preditor que
possa distinguir entre críticas positivas e negativas.

Os tópicos que abordaremos neste capítulo incluem o seguinte:

 Limpeza e preparação de dados de texto

 Criando vetores de recursos a partir de documentos de texto

 Treinando um modelo de aprendizado de máquina para classificar críticas


positivas e negativas de filmes

 Trabalhando com grandes conjuntos de dados de texto usando aprendizado


fora do núcleo

 Inferindo tópicos de coleções de documentos para categorização

Preparando os dados de revisão de filme


do IMDb para processamento de texto
Como mencionado, a análise de sentimento, às vezes também chamada
de mineração de opinião, é uma subdisciplina popular do campo mais amplo da
PNL; preocupa-se em analisar o sentimento dos documentos. Uma tarefa popular
na análise de sentimentos é a classificação de documentos com base nas
opiniões ou emoções expressas dos autores em relação a um determinado tópico.

Neste capítulo, trabalharemos com um grande conjunto de dados de resenhas de


filmes do IMDb que foram coletados por Andrew Maas e outros (Learning Word
Vectors for Sentiment Analysis por A. L. Maas, R. E. Daly, P. T. Pham, D.
Huang, A. Y. Ng e C. Potts, Proceedings of the 49th Annual Meeting of the
Association for Computational Linguistics: Human Language Technologies,
páginas 142–150, Portland, Oregon, EUA, Association for
Computational Linguistics, junho de 2011). O conjunto de dados de revisão de
filmes consiste em 50.000 críticas de filmes polares que são rotuladas como
positivas ou negativas; aqui, positivo significa que um filme foi classificado com
mais de seis estrelas no IMDb, e negativo significa que um filme foi classificado
com menos de cinco estrelas no IMDb. Nas seções a seguir, baixaremos o
conjunto de dados, o pré-processaremos em um formato utilizável para
ferramentas de aprendizado de máquina e extrairemos informações significativas
de um subconjunto dessas resenhas de filmes para criar um modelo de
aprendizado de máquina que possa prever se um determinado revisor gostou ou
não de um filme.

Obtendo o conjunto de dados de revisão de filme


Um arquivo compactado do conjunto de dados de revisão de filmes (84,1 MB)
pode ser baixado de http://ai.stanford.edu/~amaas/data/sentiment/ como um
arquivo tarball compactado com gzip:

 Se você estiver trabalhando com Linux ou macOS, poderá abrir uma nova


janela de terminal, no diretório de download e executar para descompactar o
conjunto de dados.cdtar -zxf aclImdb_v1.tar.gz
 Se você estiver trabalhando com o Windows, poderá baixar um arquivador
gratuito, como o 7-Zip (http://www.7-zip.org), para extrair os arquivos do
arquivo de download.
 Como alternativa, você pode descompactar o arquivo tarball compactado com
gzip diretamente em Python da seguinte maneira:
 >>> import tarfile

 >>> with tarfile.open('aclImdb_v1.tar.gz', 'r:gz') as tar:

 ... tar.extractall()
CopyExplain
Pré-processamento do conjunto de dados do filme
em um formato mais conveniente
Tendo extraído com sucesso o conjunto de dados, agora vamos montar os
documentos de texto individuais do arquivo de download descompactado em um
único arquivo CSV. Na seção de código a seguir, estaremos lendo as resenhas de
filmes em um objeto pandas, que pode levar até 10 minutos em um computador
desktop padrão.DataFrame

Para visualizar o progresso e o tempo estimado até a conclusão, utilizaremos


o pacote Python Progress
Indicator (PyPrind, https://pypi.python.org/pypi/PyPrind/), que foi desenvolvido há
vários anos para tais fins. O PyPrind pode ser instalado executando o
comando:pip install pyprind

>>> import pyprind

>>> import pandas as pd

>>> import os

>>> import sys

>>> # change the 'basepath' to the directory of the

>>> # unzipped movie dataset

>>> basepath = 'aclImdb'

>>>

>>> labels = {'pos': 1, 'neg': 0}

>>> pbar = pyprind.ProgBar(50000, stream=sys.stdout)

>>> df = pd.DataFrame()

>>> for s in ('test', 'train'):

... for l in ('pos', 'neg'):

... path = os.path.join(basepath, s, l)

... for file in sorted(os.listdir(path)):


... with open(os.path.join(path, file),

... 'r', encoding='utf-8') as infile:

... txt = infile.read()

... df = df.append([[txt, labels[l]]],

... ignore_index=True)

... pbar.update()

>>> df.columns = ['review', 'sentiment']

0% 100%

[##############################] | ETA: 00:00:00

Total time elapsed: 00:00:25


CopyExplain

No código anterior, inicializamos primeiro um novo objeto de barra de progresso, ,


com 50.000 iterações, que era o número de documentos que iríamos ler. Usando
os loops aninhados, iteramos sobre os subdiretórios e no diretório principal e
lemos os arquivos de texto individuais dos e subdiretórios que eventualmente
anexamos aos pandas, juntamente com um rótulo de classe inteira (1 = positivo e
0 = negativo).pbarfortraintestaclImdbposnegdfDataFrame

Como os rótulos de classe no conjunto de dados montado são classificados, agora


vamos embaralhar o uso da função do submódulo — isso será útil para dividir o
conjunto de dados em conjuntos de dados de treinamento e teste em seções
posteriores, quando transmitiremos os dados de nossa unidade local
diretamente.DataFramepermutationnp.random

Para nossa própria conveniência, também armazenaremos o conjunto de dados


de revisão de filmes montado e embaralhado como um arquivo CSV:

>>> import numpy as np

>>> np.random.seed(0)

>>> df = df.reindex(np.random.permutation(df.index))

>>> df.to_csv('movie_data.csv', index=False, encoding='utf-8')


CopyExplain

Como vamos usar esse conjunto de dados mais adiante neste capítulo, vamos
confirmar rapidamente que salvamos com sucesso os dados no formato correto
lendo no CSV e imprimindo um trecho dos três primeiros exemplos:

>>> df = pd.read_csv('movie_data.csv', encoding='utf-8')

>>> # the following column renaming is necessary on some computers:

>>> df = df.rename(columns={"0": "review", "1": "sentiment"})

>>> df.head(3)
CopyExplain

Se você estiver executando os exemplos de código em um bloco de anotações do


Jupyter, agora deverá ver os três primeiros exemplos do conjunto de dados,
conforme mostrado na Figura 8.1:

Figura 8.1: As três primeiras linhas do conjunto de dados de revisão de filmes

Como uma verificação de sanidade, antes de prosseguirmos para a próxima


seção, vamos nos certificar de que o contém todas as 50.000 linhas: DataFrame

>>> df.shape

(50000, 2)
CopyExplain
Apresentando o modelo de saco de
palavras
Você deve se lembrar do Capítulo 4, Building Good Training Datasets – Data
Preprocessing, que temos que converter dados categóricos, como texto ou
palavras, em uma forma numérica antes de podermos passá-los para um
algoritmo de aprendizado de máquina. Nesta seção, apresentaremos o modelo
de saco de palavras, que nos permite representar texto como vetores de feição
numérica. A ideia por trás do saco de palavras é bastante simples e pode ser
resumida da seguinte forma:

1. Criamos um vocabulário de tokens exclusivos, por exemplo, palavras, a partir


de todo o conjunto de documentos.
2. Construímos um vetor de recurso a partir de cada documento que contém as
contagens da frequência com que cada palavra ocorre no documento
específico.

Como as palavras únicas em cada documento representam apenas um pequeno


subconjunto de todas as palavras no vocabulário do saco de palavras, os vetores
de recursos consistirão principalmente de zeros, e é por isso que os chamamos
de esparsos. Não se preocupe se isso soar muito abstrato; Nas subseções a
seguir, vamos percorrer o processo de criação de um modelo simples de saco de
palavras passo a passo.

Transformando palavras em vetores de feição


Para construir um modelo de saco de palavras baseado na contagem de
palavras nos respectivos documentos, podemos usar a classe implementada no
scikit-learn. Como você verá na seção de código a seguir, pega uma matriz de
dados de texto, que podem ser documentos ou frases, e constrói o modelo de
saco de palavras para nós:CountVectorizerCountVectorizer

>>> import numpy as np


>>> from sklearn.feature_extraction.text import CountVectorizer

>>> count = CountVectorizer()

>>> docs = np.array(['The sun is shining',

... 'The weather is sweet',

... 'The sun is shining, the weather is sweet,'

... 'and one and one is two'])

>>> bag = count.fit_transform(docs)


CopyExplain

Ao denominar o método no , construímos o vocabulário do modelo de bolsa de


palavras e transformamos as três frases a seguir em vetores de feição
esparsa:fit_transformCountVectorizer

 'The sun is shining'


 'The weather is sweet'
 'The sun is shining, the weather is sweet, and one and one is two'

Agora, vamos imprimir o conteúdo do vocabulário para obter uma melhor


compreensão dos conceitos subjacentes:

>>> print(count.vocabulary_)

{'and': 0,

'two': 7,

'shining': 3,

'one': 2,

'sun': 4,

'weather': 8,

'the': 6,

'sweet': 5,

'is': 1}
CopyExplain
Como você pode ver ao executar o comando anterior, o vocabulário é armazenado
em um dicionário Python que mapeia as palavras exclusivas para índices inteiros.
Em seguida, vamos imprimir os vetores de recursos que acabamos de criar:

>>> print(bag.toarray())

[[0 1 0 1 1 0 1 0 0]

[0 1 0 0 0 1 1 0 1]

[2 3 2 1 1 1 2 1 1]]
CopyExplain

Cada posição de índice nos vetores de feição mostrados aqui corresponde aos
valores inteiros que são armazenados como itens de dicionário no vocabulário.
Por exemplo, o primeiro recurso na posição do índice se assemelha à contagem
da palavra , que ocorre apenas no último documento, e a palavra , na posição do
índice (o segundo recurso nos vetores do documento), ocorre em todas as três
frases. Esses valores nos vetores de feição também são
chamados de frequências de termo bruto: tf(t, d) — o número de vezes que um
termo, t, ocorre em um documento, d. Deve-se notar que, no modelo de saco de
palavras, a ordem da palavra ou do termo em uma frase ou documento não
importa. A ordem em que o termo frequências aparece no vetor de feição é
derivada dos índices de vocabulário, que geralmente são atribuídos em ordem
alfabética.CountVectorizer0'and''is'1

Modelos de N-grama

A sequência de itens no modelo de saco de palavras que acabamos de criar


também é chamada de modelo de 1 grama ou unigrama — cada item ou token no
vocabulário representa uma única palavra. Mais geralmente, as sequências
contíguas de itens na PNL — palavras, letras ou símbolos — também são
chamadas de n-gramas. A escolha do número, n, no modelo de n-
gramas depende da aplicação particular; por exemplo, um estudo de Ioannis
Kanaris e outros revelou que n-gramas de tamanho 3 e 4 produzem bons
desempenhos na filtragem antisspam de mensagens de e-mail (Words versus
character n-grams for antisspam filtering by Ioannis Kanaris, Konstantinos
Kanaris, Ioannis Houvardas and Efstathios Stamatatos, International Journal on
Artificial Intelligence Tools, World Scientific Publishing Company, 16(06): 1047-
1067, 2007).

Para resumir o conceito de representação de n-gramas, as representações de 1


grama e 2 gramas de nosso primeiro documento, "o sol está brilhando", seriam
construídas da seguinte forma:

 1 grama: "o", "sol", "é", "brilhando"

 2 gramas: "o sol", "o sol está", "está brilhando"

A aula em scikit-learn nos permite usar diferentes modelos de n-gramas através


de seu parâmetro. Enquanto uma representação de 1 grama é usada por padrão,
podemos alternar para uma representação de 2 gramas inicializando uma nova
instância com .CountVectorizerngram_rangeCountVectorizerngram_range=(2,2)

Avaliando a relevância de palavras por meio de


frequência de documento inversa de frequência de
termo
Quando estamos analisando dados de texto, geralmente encontramos palavras
que ocorrem em vários documentos de ambas as classes. Essas palavras que
ocorrem com frequência normalmente não contêm informações úteis ou
discriminatórias. Nesta subseção, você aprenderá sobre uma técnica útil chamada
frequência de documento inversa de frequência de frequência (tf-idf), que pode
ser usada para reduzir o peso dessas palavras que ocorrem com frequência nos
vetores de recurso. O tf-idf pode ser definido como o produto do termo frequência
e da frequência inversa do documento:

tf-idf(t, d) = tf(t, d) × idf(t, d)

Aqui, tf(t, d) é o termo frequência que introduzimos na seção anterior, e idf(t, d) é a


frequência inversa do documento, que pode ser calculada da seguinte forma:
Aqui, nd é o número total de documentos, e df(d, t) é o número de documentos, d,
que contêm o termo t. Observe que adicionar a constante 1 ao denominador é
opcional e serve ao propósito de atribuir um valor diferente de zero a termos que
ocorrem em nenhum dos exemplos de treinamento; O log é usado para garantir
que as baixas frequências de documentos não recebam muito peso.

A biblioteca scikit-learn implementa ainda outro transformador, a classe, que toma


as frequências do termo bruto da classe como entrada e as transforma em tf-
idfs:TfidfTransformerCountVectorizer

>>> from sklearn.feature_extraction.text import TfidfTransformer

>>> tfidf = TfidfTransformer(use_idf=True,

... norm='l2',

... smooth_idf=True)

>>> np.set_printoptions(precision=2)

>>> print(tfidf.fit_transform(count.fit_transform(docs))

... .toarray())

[[ 0. 0.43 0. 0.56 0.56 0. 0.43 0. 0. ]

[ 0. 0.43 0. 0. 0. 0.56 0.43 0. 0.56]

[ 0.5 0.45 0.5 0.19 0.19 0.19 0.3 0.25 0.19]]


CopyExplain

Como você viu na subseção anterior, a palavra teve a maior frequência de termos
no terceiro documento, sendo a palavra que mais ocorreu. No entanto, depois de
transformar o mesmo vetor de recurso em tf-idfs, a palavra agora é associada a
um tf-idf relativamente pequeno (0,45) no terceiro documento, uma vez que
também está presente no primeiro e segundo documento e, portanto, é improvável
que contenha qualquer informação discriminatória útil. 'is''is'
No entanto, se tivéssemos calculado manualmente o tf-idfs dos termos individuais
em nossos vetores de recursos, teríamos notado que calcula o tf-idfs de forma
ligeiramente diferente em comparação com as equações padrão do livro didático
que definimos anteriormente. A equação para a frequência inversa do documento
implementada no scikit-learn é calculada da seguinte forma: TfidfTransformer

Da mesma forma, o tf-idf calculado no scikit-learn se desvia ligeiramente da


equação padrão que definimos anteriormente:

tf-idf(t, d) = tf(t, d) × (idf(t, d) + 1)

Observe que o "+1" na equação idf anterior é devido à configuração no exemplo


de código anterior, que é útil para atribuir peso zero (ou seja, idf(t, d) = log(1) = 0)
a termos que ocorrem em todos os documentos.smooth_idf=True

Embora também seja mais típico normalizar as frequências de termo bruto antes


de calcular o tf-idfs, a classe normaliza o tf-idfs diretamente. Por padrão (), scikit-
learn's aplica a normalização L2, que retorna um vetor de comprimento 1 dividindo
um vetor de feição não normalizado, v, por sua norma
L2:TfidfTransformernorm='l2'TfidfTransformer

Para ter certeza de que entendemos como funciona, vamos percorrer um exemplo
e calcular o tf-idf da palavra no terceiro documento. A palavra tem uma frequência
de termo de 3 (tf = 3) no terceiro documento, e a frequência de documento deste
termo é 3, uma vez que o termo ocorre em todos os três documentos (df = 3).
Assim, podemos calcular a frequência inversa do documento da seguinte
forma:TfidfTransformer'is''is''is'
Agora, para calcular o tf-idf, basta adicionar 1 à frequência inversa do documento
e multiplicá-la pelo termo frequência:

Se repetirmos esse cálculo para todos os termos no terceiro documento,


obteremos os seguintes vetores tf-idf: . No entanto, observe que os valores neste
vetor de recurso são diferentes dos valores que obtivemos dos que usamos
anteriormente. A etapa final que nos falta neste cálculo tf-idf é a normalização L2,
que pode ser aplicada da seguinte forma:[3.39, 3.0, 3.39, 1.29, 1.29, 1.29, 2.0,
1.69, 1.29]TfidfTransformer

Como você pode ver, os resultados agora correspondem aos resultados


retornados pelo scikit-learn's , e como agora você entende como os tf-idfs são
calculados, vamos prosseguir para a próxima seção e aplicar esses conceitos ao
conjunto de dados de revisão de filmes. TfidfTransformer

Limpando dados de texto


Nas subseções anteriores, aprendemos sobre o modelo bag-of-words, frequências
de termos e tf-idfs. No entanto, o primeiro passo importante — antes de criarmos
nosso modelo de saco de palavras — é limpar os dados de texto, retirando-os de
todos os caracteres indesejados.
Para ilustrar por que isso é importante, vamos exibir os últimos 50 caracteres do
primeiro documento no conjunto de dados de revisão de filme reembaralhado:

>>> df.loc[0, 'review'][-50:]

'is seven.<br /><br />Title (Brazil): Not Available'


CopyExplain

Como você pode ver aqui, o texto contém marcação HTML, bem como pontuação
e outros caracteres que não sejam letras. Embora a marcação HTML não
contenha muitas semânticas úteis, os sinais de pontuação podem representar
informações úteis e adicionais em determinados contextos de PNL. No entanto,
para simplificar, agora removeremos todos os sinais de pontuação, exceto os
caracteres de emoticon, como :), uma vez que esses são certamente úteis para a
análise de sentimento.

Para realizar essa tarefa, usaremos a biblioteca de expressão regular (regex) do


Python, como mostrado aqui:re

>>> import re

>>> def preprocessor(text):

... text = re.sub('<[^>]*>', '', text)

... emoticons = re.findall('(?::|;|=)(?:-)?(?:\)|\(|D|P)',

... text)

... text = (re.sub('[\W]+', ' ', text.lower()) +

... ' '.join(emoticons).replace('-', ''))

... return text


CopyExplain

Através do primeiro regex, , na seção de código anterior, tentamos remover toda a


marcação HTML das resenhas de filmes. Embora muitos programadores
geralmente desaconselham o uso de regex para analisar HTML, esse regex deve
ser suficiente para limpar esse conjunto de dados específico. Como estamos
interessados apenas em remover a marcação HTML e não planejamos usar a
marcação HTML ainda mais, usar regex para fazer o trabalho deve ser aceitável.
No entanto, se você preferir usar ferramentas sofisticadas para remover a
marcação HTML do texto, você pode dar uma olhada no módulo analisador
HTML do Python, que é descrito
em https://docs.python.org/3/library/html.parser.html. Depois de removermos a
marcação HTML, usamos um regex um pouco mais complexo para encontrar
emoticons, que armazenamos temporariamente como emoticons. Em seguida,
removemos todos os caracteres não-palavras do texto através do regex e
convertemos o texto em caracteres minúsculos.<[^>]*>[\W]+

Lidando com maiúsculas

No contexto desta análise, assumimos que a capitalização de uma palavra — por


exemplo, se ela aparece no início de uma frase — não contém informações
semanticamente relevantes. No entanto, observe que há exceções; Por exemplo,
removemos a notação de nomes próprios. Mas, novamente, no contexto desta
análise, é uma suposição simplificadora que o caso da carta não contém
informações relevantes para a análise de sentimento.

Eventualmente, adicionamos os emoticons armazenados temporariamente ao final


da cadeia de caracteres do documento processado. Além disso, removemos o
caractere do nariz (- em :-)) dos emoticons para consistência.

Expressões regulares

Embora as expressões regulares ofereçam uma abordagem eficiente e


conveniente para procurar caracteres em uma cadeia de caracteres, elas também
vêm com uma curva de aprendizado íngreme. Infelizmente, uma discussão
aprofundada de expressões regulares está além do escopo deste livro. No
entanto, você pode encontrar um ótimo tutorial no portal do Google Developers
em https://developers.google.com/edu/python/regular-expressions ou você pode
conferir a documentação oficial do módulo Python
em https://docs.python.org/3.9/library/re.html.re

Embora a adição dos caracteres de emoticon ao final das cadeias de caracteres


de documento limpas possa não parecer a abordagem mais elegante, devemos
observar que a ordem das palavras não importa em nosso modelo de saco de
palavras se nosso vocabulário consiste em apenas tokens de uma palavra. Mas
antes de falarmos mais sobre a divisão de documentos em termos, palavras ou
tokens individuais, vamos confirmar se nossa função funciona
corretamente:preprocessor

>>> preprocessor(df.loc[0, 'review'][-50:])

'is seven title brazil not available'

>>> preprocessor("</a>This :) is :( a test :-)!")

'this is a test :) :( :)'


CopyExplain

Por fim, já que faremos uso dos dados de texto limpos repetidamente durante as


próximas seções, vamos agora aplicar nossa função a todas as críticas de filmes
em nosso :preprocessorDataFrame

>>> df['review'] = df['review'].apply(preprocessor)


CopyExplain

Processando documentos em tokens


Depois de preparar com sucesso o conjunto de dados de revisão de filmes,
agora precisamos pensar em como dividir os corpora de texto em elementos
individuais. Uma maneira de tokenizar documentos é dividi-los em palavras
individuais, dividindo os documentos limpos em seus caracteres de espaço em
branco:

>>> def tokenizer(text):

... return text.split()

>>> tokenizer('runners like running and thus they run')

['runners', 'like', 'running', 'and', 'thus', 'they', 'run']


CopyExplain

No contexto da tokenização, outra técnica útil é o word stemming, que é o


processo de transformar uma palavra em sua forma raiz. Ele nos permite mapear
palavras relacionadas para o mesmo caule. O algoritmo de hasteamento original
foi desenvolvido por Martin F. Porter em 1979 e, portanto, é conhecido
como algoritmo de haste de Porter (Um algoritmo para remoção de
sufixos por Martin F. Porter, Program: Electronic Library and Information Systems,
14(3): 130–137, 1980). O Natural Language Toolkit (NLTK, http://www.nltk.org)
para Python implementa o algoritmo de derivação Porter, que usaremos na seção
de código a seguir. Para instalar o NLTK, você pode simplesmente executar
ou .conda install nltkpip install nltk

Livro online NLTK

Embora a NLTK não seja o foco deste capítulo, eu recomendo que você visite o
site da NLTK, bem como leia o livro oficial da NLTK, que está disponível
gratuitamente em http://www.nltk.org/book/, se você estiver interessado em
aplicações mais avançadas em PNL.

O código a seguir mostra como usar o algoritmo de derivação Porter:

>>> from nltk.stem.porter import PorterStemmer

>>> porter = PorterStemmer()

>>> def tokenizer_porter(text):

... return [porter.stem(word) for word in text.split()]

>>> tokenizer_porter('runners like running and thus they run')

['runner', 'like', 'run', 'and', 'thu', 'they', 'run']


CopyExplain

Usando o do pacote, modificamos nossa função para reduzir as palavras à sua


forma raiz, o que foi ilustrado pelo exemplo anterior simples, onde a palavra
foi derivada para sua forma raiz .PorterStemmernltktokenizer'running''run'

Algoritmos de derivação

O algoritmo de derivação de Porter é provavelmente o algoritmo de derivação


mais antigo e mais simples. Outros algoritmos populares de derivação incluem o
mais recente Snowball stemmer (Porter2 ou stemmer inglês) e o Lancaster
stemmer (Paice/Husk stemmer). Embora os troncos Bola de Neve e Lancaster
sejam mais rápidos que o hasteador Porter original, o haste Lancaster também é
notório por ser mais agressivo que o haste Porter, o que significa que produzirá
palavras mais curtas e obscuras. Esses algoritmos alternativos de derivação
também estão disponíveis através do pacote NLTK
(http://www.nltk.org/api/nltk.stem.html).

Enquanto o stemming pode criar palavras não-reais, como (de ), como mostrado
no exemplo anterior, uma técnica chamada lemmatização visa obter as formas
canônicas (gramaticalmente corretas) de palavras individuais – os
chamados lemas. No entanto, a lemmatização é computacionalmente mais difícil e
cara em comparação com a derivação e, na prática, tem sido observado que a
derivação e a lemmatização têm pouco impacto no desempenho da classificação
de texto (Influence of Word Normalization on Text Classification, de Michal
Toman, Roman Tesar e Karel Jezek, Proceedings of InSciT, páginas 354-358,
2006).'thu''thus'

Antes de entrarmos na próxima seção, onde treinaremos um modelo de


aprendizado de máquina usando o modelo de saco de palavras, vamos falar
brevemente sobre outro tópico útil chamado parar remoção de palavras.
Palavras de parada são simplesmente aquelas palavras que são extremamente
comuns em todos os tipos de textos e provavelmente não contêm (ou apenas um
pouco) de informação útil que pode ser usada para distinguir entre diferentes
classes de documentos. Exemplos de palavras de parada são é, e, tem e gosta. A
remoção de palavras stop pode ser útil se estivermos trabalhando com
frequências de termos brutos ou normalizados em vez de tf-idfs, que já reduzem o
peso das palavras que ocorrem com frequência.

Para remover palavras de parada das resenhas de filmes, usaremos o conjunto de


127 palavras de parada em inglês que está disponível na biblioteca NLTK, que
pode ser obtido chamando a função:nltk.download

>>> import nltk

>>> nltk.download('stopwords')
CopyExplain

Depois de baixarmos o conjunto de palavras stop, podemos carregar e aplicar o


conjunto de palavras stop em inglês da seguinte maneira:
>>> from nltk.corpus import stopwords

>>> stop = stopwords.words('english')

>>> [w for w in tokenizer_porter('a runner likes'

... ' running and runs a lot')

... if w not in stop]

['runner', 'like', 'run', 'run', 'lot']


CopyExplain

Treinamento de um modelo de regressão


logística para classificação de
documentos
Nesta seção, treinaremos um modelo de regressão logística para classificar
as críticas de filmes em positivas e negativas com base no modelo bag-of-words.
Primeiro, vamos dividir os documentos de texto limpo em 25.000 documentos para
treinamento e 25.000 documentos para teste: DataFrame

>>> X_train = df.loc[:25000, 'review'].values

>>> y_train = df.loc[:25000, 'sentiment'].values

>>> X_test = df.loc[25000:, 'review'].values

>>> y_test = df.loc[25000:, 'sentiment'].values


CopyExplain

Em seguida, usaremos um objeto para encontrar o conjunto ótimo de parâmetros


para nosso modelo de regressão logística usando validação cruzada estratificada
de 5 vezes:GridSearchCV

>>> from sklearn.model_selection import GridSearchCV

>>> from sklearn.pipeline import Pipeline

>>> from sklearn.linear_model import LogisticRegression


>>> from sklearn.feature_extraction.text import TfidfVectorizer

>>> tfidf = TfidfVectorizer(strip_accents=None,

... lowercase=False,

... preprocessor=None)

>>> small_param_grid = [

... {

... 'vect__ngram_range': [(1, 1)],

... 'vect__stop_words': [None],

... 'vect__tokenizer': [tokenizer, tokenizer_porter],

... 'clf__penalty': ['l2'],

... 'clf__C': [1.0, 10.0]

... },

... {

... 'vect__ngram_range': [(1, 1)],

... 'vect__stop_words': [stop, None],

... 'vect__tokenizer': [tokenizer],

... 'vect__use_idf':[False],

... 'vect__norm':[None],

... 'clf__penalty': ['l2'],

... 'clf__C': [1.0, 10.0]

... },

... ]

>>> lr_tfidf = Pipeline([

... ('vect', tfidf),

... ('clf', LogisticRegression(solver='liblinear'))

... ])
>>> gs_lr_tfidf = GridSearchCV(lr_tfidf, small_param_grid,

... scoring='accuracy', cv=5,

... verbose=2, n_jobs=1)

>>> gs_lr_tfidf.fit(X_train, y_train)


CopyExplain

Observe que para o classificador de regressão logística, estamos usando o


solucionador LIBLINEAR, pois ele pode ter um desempenho melhor do que a
opção padrão () para conjuntos de dados relativamente grandes.'lbfgs'

Multiprocessamento através do parâmetro n_jobs

Observe que é altamente recomendável configurar (em vez de , como no exemplo


de código anterior) para utilizar todos os núcleos disponíveis em sua máquina e
acelerar a pesquisa em grade. No entanto, alguns usuários do Windows relataram
problemas ao executar o código anterior com a configuração relacionada à
demarcação de funções e para multiprocessamento no Windows. Outra solução
seria substituir essas duas funções, , por . No entanto, observe que a substituição
pelo simples não suportaria a derivação. n_jobs=-1n_jobs=1n_jobs=-
1tokenizertokenizer_porter[tokenizer, tokenizer_porter][str.split]str.split

Quando inicializamos o objeto e sua grade de parâmetros usando o código


anterior, nos restringimos a um número limitado de combinações de parâmetros,
uma vez que o número de vetores de recursos, bem como o grande vocabulário,
pode tornar a pesquisa de grade computacionalmente bastante cara. Usando um
computador desktop padrão, nossa pesquisa de grade pode levar de 5 a 10
minutos para ser concluída.GridSearchCV

No exemplo de código anterior, substituímos e da subseção anterior por , que


combina com o . Nosso consistia em dois dicionários de parâmetros. No primeiro
dicionário, usamos com suas configurações padrão (, e ) para calcular o tf-idfs; No
segundo dicionário, definimos esses parâmetros como , e para treinar um modelo
baseado em frequências de termos brutos. Além disso, para o classificador de
regressão logística propriamente dito, treinamos modelos usando a regularização
L2 por meio do parâmetro penalidade e comparamos diferentes forças de
regularização definindo uma faixa de valores para o parâmetro de regularização
inversa. Como um exercício opcional, você também é encorajado a adicionar a
regularização L1 à grade de parâmetros alterando
para .CountVectorizerTfidfTransformerTfidfVectorizerCountVectorizerTfidfTransfor
merparam_gridTfidfVectorizeruse_idf=Truesmooth_idf=Truenorm='l2'use_idf=Falsesmo
oth_idf=Falsenorm=NoneC'clf__penalty': ['l2']'clf__penalty': ['l2', 'l1']

Depois que a pesquisa de grade terminar, podemos imprimir o melhor conjunto de


parâmetros:

>>> print(f'Best parameter set: {gs_lr_tfidf.best_params_}')

Best parameter set: {'clf__C': 10.0, 'clf__penalty': 'l2', 'vect__ngram_range': (1, 1),

'vect__stop_words': None, 'vect__tokenizer': <function tokenizer at 0x169932dc0>}


CopyExplain

Como você pode ver na saída anterior, obtivemos os melhores resultados de


pesquisa de grade usando o regular sem haste de Porter, biblioteca de palavras
sem parada e tf-idfs em combinação com um classificador de regressão logística
que usa L2-regularização com a força de regularização de . tokenizerC10.0

Usando o melhor modelo dessa pesquisa em grade, vamos imprimir as


pontuações médias de precisão de validação cruzada de 5 vezes no conjunto de
dados de treinamento e a precisão da classificação no conjunto de dados de teste:

>>> print(f'CV Accuracy: {gs_lr_tfidf.best_score_:.3f}')

CV Accuracy: 0.897

>>> clf = gs_lr_tfidf.best_estimator_

>>> print(f'Test Accuracy: {clf.score(X_test, y_test):.3f}')

Test Accuracy: 0.899


CopyExplain

Os resultados revelam que nosso modelo de aprendizado de máquina pode prever


se uma crítica de filme é positiva ou negativa com 90% de precisão.

O ingênuo classificador Bayes


Um classificador ainda muito popular para classificação de texto é o classificador
Bayes ingênuo, que ganhou popularidade em aplicações de filtragem de spam de
e-mail. Os classificadores Bayes ingênuos são fáceis de implementar,
computacionalmente eficientes e tendem a ter um desempenho particularmente
bom em conjuntos de dados relativamente pequenos em comparação com outros
algoritmos. Embora não discutamos classificadores Bayes ingênuos neste livro, o
leitor interessado pode encontrar um artigo sobre classificação de texto Bayes
ingênuo que está disponível gratuitamente no arXiv (Naive Bayes and Text
Classification I – Introduction and Theory by S. Raschka, Computing Research
Repository (CoRR), abs/1410.5329, 2014, http://arxiv.org/pdf/1410.5329v3.pdf).
Diferentes versões dos classificadores naïve Bayes referenciados neste artigo são
implementadas no scikit-learn. Você pode encontrar uma página de visão geral
com links para as respectivas classes de código
aqui: https://scikit-learn.org/stable/modules/naive_bayes.html.

Trabalhando com dados maiores –


algoritmos on-line e aprendizado fora do
núcleo
Se você executou os exemplos de código na seção anterior, deve ter notado que
pode ser computacionalmente muito caro construir os vetores de recurso para o
conjunto de dados de revisão de 50.000 filmes durante uma pesquisa em grade.
Em muitos aplicativos do mundo real, não é incomum trabalhar com conjuntos de
dados ainda maiores que podem exceder a memória do nosso computador.

Como nem todos têm acesso a recursos de supercomputadores, agora


aplicaremos uma técnica chamada aprendizado fora do núcleo, que nos permite
trabalhar com conjuntos de dados tão grandes, ajustando o classificador
incrementalmente em lotes menores de um conjunto de dados.

Classificação de texto com redes neurais recorrentes

No Capítulo 15, Modelando dados sequenciais usando redes neurais recorrentes,


revisitaremos esse conjunto de dados e treinaremos um classificador baseado em
aprendizado profundo (uma rede neural recorrente) para classificar as avaliações
no conjunto de dados de revisão de filmes do IMDb. Este classificador baseado
em redes neurais segue o mesmo princípio out-of-core usando o algoritmo de
otimização de descida de gradiente estocástico, mas não requer a construção de
um modelo de saco de palavras.

No Capítulo 2, Training Simple Machine Learning Algorithms for Classification, o


conceito de descida de gradiente estocástico foi introduzido; É um algoritmo de
otimização que atualiza os pesos do modelo usando um exemplo de cada vez.
Nesta seção, faremos uso da função de in scikit-learn para transmitir os
documentos diretamente de nossa unidade local e treinar um modelo de regressão
logística usando pequenos minilotes de documentos. partial_fitSGDClassifier

Primeiro, definiremos uma função que limpa os dados de texto não processados
do arquivo que construímos no início deste capítulo e os separa em tokens de
palavras enquanto removemos palavras de parada: tokenizermovie_data.csv

>>> import numpy as np

>>> import re

>>> from nltk.corpus import stopwords

>>> stop = stopwords.words('english')

>>> def tokenizer(text):

... text = re.sub('<[^>]*>', '', text)

... emoticons = re.findall('(?::|;|=)(?:-)?(?:\)|\(|D|P)',

... text)

... text = re.sub('[\W]+', ' ', text.lower()) \

... + ' '.join(emoticons).replace('-', '')

... tokenized = [w for w in text.split() if w not in stop]

... return tokenized


CopyExplain

Em seguida, definiremos uma função geradora, , que lê e retorna um documento


de cada vez:stream_docs
>>> def stream_docs(path):

... with open(path, 'r', encoding='utf-8') as csv:

... next(csv) # skip header

... for line in csv:

... text, label = line[:-3], int(line[-2])

... yield text, label


CopyExplain

Para verificar se nossa função funciona corretamente, vamos ler no primeiro


documento do arquivo, que deve retornar uma tupla que consiste no texto de
revisão, bem como o rótulo de classe correspondente: stream_docsmovie_data.csv

>>> next(stream_docs(path='movie_data.csv'))

('"In 1974, the teenager Martha Moxley ... ',1)


CopyExplain

Agora definiremos uma função, , que pegará um fluxo de documentos da função e


retornará um número específico de documentos especificados pelo
parâmetro:get_minibatchstream_docssize

>>> def get_minibatch(doc_stream, size):

... docs, y = [], []

... try:

... for _ in range(size):

... text, label = next(doc_stream)

... docs.append(text)

... y.append(label)

... except StopIteration:

... return None, None

... return docs, y


CopyExplain
Infelizmente, não podemos usar para aprendizagem fora do núcleo, uma vez que
requer manter o vocabulário completo na memória. Além disso, precisa manter
todos os vetores de recursos do conjunto de dados de treinamento na memória
para calcular as frequências inversas do documento. No entanto, outro vetorizador
útil para processamento de texto implementado no scikit-learn é . é independente
de dados e faz uso do truque de hash através da função de 32 bits de Austin
Appleby (você pode encontrar mais informações sobre MurmurHash
em https://en.wikipedia.org/wiki/MurmurHash):CountVectorizerTfidfVectorizerHashi
ngVectorizerHashingVectorizerMurmurHash3

>>> from sklearn.feature_extraction.text import HashingVectorizer

>>> from sklearn.linear_model import SGDClassifier

>>> vect = HashingVectorizer(decode_error='ignore',

... n_features=2**21,

... preprocessor=None,

... tokenizer=tokenizer)

>>> clf = SGDClassifier(loss='log', random_state=1)

>>> doc_stream = stream_docs(path='movie_data.csv')


CopyExplain

Usando o código anterior, inicializamos com nossa função e definimos o número


de recursos como . Além disso, reinicializamos um classificador de regressão
logística definindo o parâmetro de para . Observe que, ao escolher um grande
número de recursos no , reduzimos a chance de causar colisões de hash, mas
também aumentamos o número de coeficientes em nosso modelo de regressão
logística.HashingVectorizertokenizer2**21lossSGDClassifier'log'HashingVectorizer

Agora vem a parte realmente interessante — tendo configurado todas as funções


complementares, podemos iniciar o aprendizado fora do núcleo usando o seguinte
código:

>>> import pyprind

>>> pbar = pyprind.ProgBar(45)


>>> classes = np.array([0, 1])

>>> for _ in range(45):

... X_train, y_train = get_minibatch(doc_stream, size=1000)

... if not X_train:

... break

... X_train = vect.transform(X_train)

... clf.partial_fit(X_train, y_train, classes=classes)

... pbar.update()

0% 100%

[##############################] | ETA: 00:00:00

Total time elapsed: 00:00:21


CopyExplain

Novamente, fizemos uso do pacote PyPrind para estimar o progresso do nosso


algoritmo de aprendizagem. Inicializamos o objeto da barra de progresso com 45
iterações e, no loop a seguir, iteramos mais de 45 minilotes de documentos onde
cada minilote consiste em 1.000 documentos. Concluído o processo de
aprendizagem incremental, utilizaremos os últimos 5.000 documentos para avaliar
o desempenho do nosso modelo:for

>>> X_test, y_test = get_minibatch(doc_stream, size=5000)

>>> X_test = vect.transform(X_test)

>>> print(f'Accuracy: {clf.score(X_test, y_test):.3f}')

Accuracy: 0.868
CopyExplain

Erro NoneType

Observe que, se você encontrar um erro, você pode ter executado o código duas
vezes. Através do loop anterior, temos 45 iterações onde buscamos 1.000
documentos cada. Assim, restam exatamente 5.000 documentos para teste, que
atribuímos por meio de:NoneTypeX_test, y_test = get_minibatch(...)
>>> X_test, y_test = get_minibatch(doc_stream, size=5000)
CopyExplain

Se executarmos esse código duas vezes, então não há documentos suficientes no


gerador, e retorna . Portanto, se você encontrar o erro, você tem que começar no
código anterior novamente.X_testNoneNoneTypestream_docs(...)

Como você pode ver, a precisão do modelo é de aproximadamente 87%, um


pouco abaixo da precisão que alcançamos na seção anterior usando a pesquisa
de grade para ajuste de hiperparâmetros. No entanto, o aprendizado fora do
núcleo é muito eficiente em termos de memória e levou menos de um minuto para
ser concluído.

Finalmente, podemos usar os últimos 5.000 documentos para atualizar nosso


modelo:

>>> clf = clf.partial_fit(X_test, y_test)


CopyExplain

O modelo word2vec

Uma alternativa mais moderna ao modelo de saco de palavras é o word2vec, um


algoritmo que o Google lançou em 2013 (Efficient Estimation of Word
Representations in Vector Space de T. Mikolov, K. Chen, G. Corrado e J.
Dean, https://arxiv.org/abs/1301.3781).

O algoritmo word2vec é um algoritmo de aprendizagem não supervisionado


baseado em redes neurais que tenta aprender automaticamente a relação entre as
palavras. A ideia por trás do word2vec é colocar palavras que têm significados
semelhantes em agrupamentos semelhantes e, por meio de espaçamento vetorial
inteligente, o modelo pode reproduzir certas palavras usando matemática vetorial
simples, por exemplo, rei – homem + mulher = rainha.

A implementação C original com links úteis para os documentos relevantes e


implementações alternativas pode ser encontrada
em https://code.google.com/p/word2vec/.
Modelagem de tópicos com alocação
latente de Dirichlet
A modelagem de tópicos descreve a ampla tarefa de atribuir tópicos a
documentos de texto sem rótulo. Por exemplo, uma aplicação típica é a
categorização de documentos em um grande corpus de texto de artigos de jornal.
Em aplicações de modelagem de tópicos, pretendemos atribuir rótulos de
categoria a esses artigos, por exemplo, esportes, finanças, notícias mundiais,
política e notícias locais. Assim, no contexto das amplas categorias de
aprendizado de máquina que discutimos no Capítulo 1, Dando aos computadores
a capacidade de aprender com dados, podemos considerar a modelagem de
tópicos como uma tarefa de clustering, uma subcategoria de aprendizagem não
supervisionada.

Nesta seção, discutiremos uma técnica popular para modelagem de tópicos


chamada alocação latente de Dirichlet (LDA). No entanto, observe que, embora
a alocação latente de Dirichlet seja frequentemente abreviada como LDA, ela não
deve ser confundida com análise discriminante linear, uma técnica supervisionada
de redução de dimensionalidade que foi introduzida no Capítulo 5, Compactando
dados via redução de dimensionalidade.

Decompondo documentos de texto com LDA


Uma vez que a matemática por trás da LDA é bastante envolvida e requer
conhecimento da inferência bayesiana, abordaremos este tópico da perspectiva de
um praticante e interpretaremos a LDA usando termos leigos. No entanto, o leitor
interessado pode ler mais sobre a LDA no seguinte artigo de pesquisa: Latent
Dirichlet Allocation, de David M. Blei, Andrew Y. Ng e Michael I. Jordan, Journal of
Machine Learning Research 3, páginas: 993-1022, janeiro de 2003, p.
https://www.jmlr.org/papers/volume3/blei03a/blei03a.pdf.

A LDA é um modelo probabilístico generativo que tenta encontrar grupos de


palavras que aparecem frequentemente juntas em diferentes documentos. Essas
palavras que aparecem com frequência representam nossos tópicos, assumindo
que cada documento é uma mistura de palavras diferentes. A entrada para uma
LDA é o modelo de saco de palavras que discutimos anteriormente neste capítulo.

Dada uma matriz de saco de palavras como entrada, a LDA a decompõe em duas
novas matrizes:

 Uma matriz de documento para tópico

 Uma matriz palavra-a-tópico

A LDA decompõe a matriz bolsa-de-palavras de tal forma que, se multiplicarmos


essas duas matrizes juntas, conseguiremos reproduzir a matriz, a matriz bolsa-de-
palavras, com o menor erro possível. Na prática, interessam-nos aqueles temas
que a LDA encontrou na matriz do saco de palavras. A única desvantagem pode
ser que devemos definir o número de tópicos de antemão — o número de tópicos
é um hiperparâmetro da LDA que precisa ser especificado manualmente.

LDA com scikit-learn


Nesta subseção, usaremos a classe implementada no scikit-learn para decompor
o conjunto de dados de revisão de filmes e categorizá-lo em diferentes tópicos. No
exemplo a seguir, restringiremos a análise a 10 tópicos diferentes, mas os leitores
são encorajados a experimentar os hiperparâmetros do algoritmo para explorar
melhor os tópicos que podem ser encontrados neste conjunto de
dados.LatentDirichletAllocation

Primeiro, vamos carregar o conjunto de dados em um pandas usando o arquivo


local das críticas de filmes que criamos no início deste
capítulo:DataFramemovie_data.csv

>>> import pandas as pd

>>> df = pd.read_csv('movie_data.csv', encoding='utf-8')

>>> # the following is necessary on some computers:

>>> df = df.rename(columns={"0": "review", "1": "sentiment"})


CopyExplain
Em seguida, vamos usar o já conhecido para criar a matriz de saco de palavras
como entrada para o LDA.CountVectorizer

Por conveniência, usaremos a biblioteca de palavras em inglês integrada do scikit-


learn por meio de:stop_words='english'

>>> from sklearn.feature_extraction.text import CountVectorizer

>>> count = CountVectorizer(stop_words='english',

... max_df=.1,

... max_features=5000)

>>> X = count.fit_transform(df['review'].values)
CopyExplain

Observe que definimos a frequência máxima de palavras do documento a ser


considerada como 10% () para excluir palavras que ocorrem com muita frequência
em documentos. A lógica por trás da remoção de palavras que ocorrem com
frequência é que essas podem ser palavras comuns que aparecem em todos os
documentos que, portanto, são menos propensas a serem associadas a uma
categoria de tópico específica de um determinado documento. Além disso,
limitamos o número de palavras a serem consideradas às 5.000 palavras mais
frequentes (), para limitar a dimensionalidade desse conjunto de dados e melhorar
a inferência realizada pela ADL. No entanto, ambos e são valores hiperparâmetros
escolhidos arbitrariamente, e os leitores são encorajados a sintonizá-los ao
comparar os resultados.max_df=.1max_features=5000max_df=.1max_features=5000

O exemplo de código a seguir demonstra como ajustar um estimador à matriz de


saco de palavras e inferir os 10 tópicos diferentes dos documentos (observe que o
ajuste do modelo pode levar até 5 minutos ou mais em um laptop ou computador
desktop padrão):LatentDirichletAllocation

>>> from sklearn.decomposition import LatentDirichletAllocation

>>> lda = LatentDirichletAllocation(n_components=10,

... random_state=123,

... learning_method='batch')
>>> X_topics = lda.fit_transform(X)
CopyExplain

Ao definir , permitimos que o estimador faça sua estimativa com base em todos os
dados de treinamento disponíveis (a matriz de saco de palavras) em uma iteração,
que é mais lenta do que o método de aprendizagem alternativo, mas pode levar a
resultados mais precisos (a configuração é análoga à aprendizagem on-line ou em
minilote, que discutimos no Capítulo 2, Treinamento de algoritmos simples de
aprendizado de máquina para classificação, e anteriormente neste
capítulo).learning_method='batch'lda'online'learning_method='online'

Maximização de expectativas

A implementação da LDA pela biblioteca scikit-learn usa o algoritmo expectation-


maximization (EM) para atualizar suas estimativas de parâmetros iterativamente.
Não discutimos o algoritmo EM neste capítulo, mas se você estiver curioso para
saber mais, veja a excelente visão geral na Wikipédia
(https://en.wikipedia.org/wiki/Expectation-maximization_algorithm) e o tutorial
detalhado sobre como ele é usado na LDA no tutorial de Colorado Reed, Latent
Dirichlet Allocation: Towards a Deeper Understanding, que está disponível
gratuitamente em http://obphio.us/pdfs/lda_tutorial.pdf.

Depois de ajustar o LDA, agora temos acesso ao atributo da instância, que


armazena uma matriz contendo a palavra importância (aqui, ) para cada um dos
10 tópicos em ordem crescente:components_lda5000

>>> lda.components_.shape

(10, 5000)
CopyExplain

Para analisar os resultados, vamos imprimir as cinco palavras mais importantes


para cada um dos 10 tópicos. Observe que os valores de importância da palavra
são classificados em ordem crescente. Assim, para imprimir as cinco principais
palavras, precisamos classificar a matriz de tópicos em ordem inversa:

>>> n_top_words = 5
>>> feature_names = count.get_feature_names_out()

>>> for topic_idx, topic in enumerate(lda.components_):

... print(f'Topic {(topic_idx + 1)}:')

... print(' '.join([feature_names[i]

... for i in topic.argsort()\

... [:-n_top_words - 1:-1]]))

Topic 1:

worst minutes awful script stupid

Topic 2:

family mother father children girl

Topic 3:

american war dvd music tv

Topic 4:

human audience cinema art sense

Topic 5:

police guy car dead murder

Topic 6:

horror house sex girl woman

Topic 7:

role performance comedy actor performances

Topic 8:

series episode war episodes tv

Topic 9:

book version original read novel

Topic 10:

action fight guy guys cool


CopyExplain
Com base na leitura das cinco palavras mais importantes para cada tópico, você
pode adivinhar que a LDA identificou os seguintes tópicos:

1. Geralmente filmes ruins (não é realmente uma categoria de tópico)


2. Filmes sobre famílias
3. Filmes de guerra
4. Filmes de arte
5. Filmes policiais
6. Filmes de terror
7. Críticas de filmes de comédia
8. Filmes de alguma forma relacionados a programas de TV
9. Filmes baseados em livros
10. Filmes de ação

Para confirmar que as categorias fazem sentido com base nas críticas, vamos
plotar três filmes da categoria de filmes de terror (filmes de terror pertencem à
categoria 6 na posição de índice):5

>>> horror = X_topics[:, 5].argsort()[::-1]

>>> for iter_idx, movie_idx in enumerate(horror[:3]):

... print(f'\nHorror movie #{(iter_idx + 1)}:')

... print(df['review'][movie_idx][:300], '...')

Horror movie #1:

House of Dracula works from the same basic premise as House of Frankenstein from the year before;

namely that Universal's three most famous monsters; Dracula, Frankenstein's Monster and The Wolf

Man are appearing in the movie together. Naturally, the film is rather messy therefore, but the fact

that ...

Horror movie #2:

Okay, what the hell kind of TRASH have I been watching now? "The Witches' Mountain" has got to

be one of the most incoherent and insane Spanish exploitation flicks ever and yet, at the same time,
it's also strangely compelling. There's absolutely nothing that makes sense here and I even doubt there

...

Horror movie #3:

<br /><br />Horror movie time, Japanese style. Uzumaki/Spiral was a total freakfest from start to

finish. A fun freakfest at that, but at times it was a tad too reliant on kitsch rather than the horror. The

story is difficult to summarize succinctly: a carefree, normal teenage girl starts coming fac ...
CopyExplain

Usando o exemplo de código anterior, imprimimos os primeiros 300 caracteres dos


três principais filmes de terror. As críticas – mesmo que não saibamos a qual filme
exato elas pertencem – soam como críticas de filmes de terror (no entanto, pode-
se argumentar que também pode ser um bom ajuste para a categoria de tópico
1: Filmes geralmente ruins).Horror movie #2

Resumo
Neste capítulo, você aprendeu a usar algoritmos de aprendizado de máquina para
classificar documentos de texto com base em sua polaridade, que é uma tarefa
básica na análise de sentimento no campo da PNL. Você não só aprendeu a
codificar um documento como um vetor de recurso usando o modelo bag-of-
words, mas também aprendeu a ponderar o termo frequência por relevância
usando tf-idf.

Trabalhar com dados de texto pode ser computacionalmente bastante caro devido
aos grandes vetores de recursos que são criados durante esse processo; Na
última seção, abordamos como utilizar o aprendizado fora do núcleo ou
incremental para treinar um algoritmo de aprendizado de máquina sem carregar
todo o conjunto de dados na memória de um computador.

Por fim, você foi apresentado ao conceito de modelagem de tópicos usando LDA
para categorizar as críticas de filmes em diferentes categorias de forma não
supervisionada.
Até agora, neste livro, cobrimos muitos conceitos de aprendizado de máquina,
melhores práticas e modelos supervisionados para classificação. No próximo
capítulo, examinaremos outra subcategoria da aprendizagem supervisionada,
a análise de regressão, que permite predizer variáveis de desfecho em escala
contínua, em contraste com os rótulos de classe categórica dos modelos de
classificação com os quais temos trabalhado até agora.

Prevendo Variáveis de Destino


Contínuas com Análise de Regressão
Ao longo dos capítulos anteriores, você aprendeu muito sobre os principais
conceitos por trás do aprendizado supervisionado e treinou muitos modelos
diferentes para tarefas de classificação para prever membros de grupos ou
variáveis categóricas. Neste capítulo, vamos mergulhar em outra subcategoria da
aprendizagem supervisionada: a análise de regressão.

Modelos de regressão são usados para predizer variáveis-alvo em escala


contínua, o que os torna atraentes para abordar muitas questões em ciência. Eles
também têm aplicações na indústria, como entender relações entre variáveis,
avaliar tendências ou fazer previsões. Um exemplo é prever as vendas de uma
empresa nos próximos meses.

Neste capítulo, discutiremos os principais conceitos de modelos de regressão e


abordaremos os seguintes tópicos:

 Explorando e visualizando conjuntos de dados

 Analisando diferentes abordagens para implementar modelos de regressão


linear

 Modelos de regressão de treinamento robustos a outliers

 Avaliação de modelos de regressão e diagnóstico de problemas comuns

 Ajustando modelos de regressão a dados não lineares

Introdução à regressão linear


O objetivo da regressão linear é modelar a relação entre uma ou várias
características e uma variável de destino contínua. Em contraste com a
classificação – uma subcategoria diferente da aprendizagem supervisionada – a
análise de regressão visa prever os resultados em uma escala contínua, em vez
de rótulos de classe categóricos.

Nas subseções a seguir, você será apresentado ao tipo mais básico de regressão
linear, a regressão linear simples, e entenderá como relacioná-la com o caso mais
geral, multivariado (regressão linear com múltiplas características).

Regressão linear simples


O objetivo da regressão linear simples (univariada) é modelar a relação entre
uma única característica (variável explicativa, x) e um alvo de valor contínuo
(variável resposta, y). A equação de um modelo linear com uma variável
explicativa é definida da seguinte forma:

Aqui, o parâmetro (unidade de viés), b, representa o intercepto do eixo y e w1 é o


coeficiente de peso da variável explicativa. Nosso objetivo é aprender os pesos da
equação linear para descrever a relação entre a variável explicativa e a variável
alvo, que pode então ser usada para predizer as respostas de novas variáveis
explicativas que não fizeram parte do conjunto de dados de treinamento.

Com base na equação linear que definimos anteriormente, a regressão linear pode
ser entendida como encontrar a reta mais adequada através dos exemplos de
treinamento, como mostrado na Figura 9.1:
Figura 9.1: Um exemplo simples de regressão linear de um recurso

Essa linha de melhor ajuste também é chamada de linha de regressão, e as


linhas verticais da linha de regressão para os exemplos de treinamento são os
chamados deslocamentos ou resíduos – os erros de nossa previsão.

Regressão linear múltipla


A seção anterior introduziu a regressão linear simples, um caso especial de
regressão linear com uma variável explicativa. Naturalmente, também podemos
generalizar o modelo de regressão linear para múltiplas variáveis explicativas;
Esse processo é chamado de regressão linear múltipla:
A figura 9.2 mostra como o hiperplano bidimensional ajustado de um modelo de
regressão linear múltipla com duas características poderia parecer:

Figura 9.2: Um modelo de regressão linear de duas características

Como você pode ver, visualizações de hiperplanos de regressão linear múltipla em


um gráfico de dispersão tridimensional já são difíceis de interpretar quando se olha
para figuras estáticas. Como não temos bons meios de visualizar hiperplanos com
duas dimensões em um gráfico de dispersão (modelos de regressão linear
múltipla ajustados a conjuntos de dados com três ou mais características), os
exemplos e visualizações neste capítulo se concentrarão principalmente no caso
univariado, usando regressão linear simples. No entanto, a regressão linear
simples e múltipla baseia-se nos mesmos conceitos e nas mesmas técnicas de
avaliação; As implementações de código que discutiremos neste capítulo também
são compatíveis com ambos os tipos de modelo de regressão.
Explorando o conjunto de dados da
Ames Housing
Antes de implementarmos o primeiro modelo de regressão linear, discutiremos um
novo conjunto de dados, o conjunto de dados Ames Housing, que contém
informações sobre imóveis residenciais individuais em Ames, Iowa, de 2006 a
2010. O conjunto de dados foi coletado por Dean De Cock em 2011, e
informações adicionais estão disponíveis nos seguintes links:

 Um relatório descrevendo o conjunto de


dados: http://jse.amstat.org/v19n3/decock.pdf
 Documentação detalhada sobre os recursos do conjunto de
dados: http://jse.amstat.org/v19n3/decock/DataDocumentation.txt
 O conjunto de dados em um formato separado por
tabulação: http://jse.amstat.org/v19n3/decock/AmesHousing.txt

Como acontece com cada novo conjunto de dados, é sempre útil explorar os
dados através de uma visualização simples, para ter uma melhor sensação do que
estamos trabalhando, que é o que faremos nas subseções a seguir.

Carregando o conjunto de dados Ames Housing


em um DataFrame
Nesta seção, carregaremos o conjunto de dados Ames Housing usando a função
pandas, que é rápida e versátil e uma ferramenta recomendada para trabalhar
com dados tabulares armazenados em um formato de texto simples. read_csv

O conjunto de dados Ames Housing consiste em 2.930 exemplos e 80 recursos.


Para simplificar, trabalharemos apenas com um subconjunto dos recursos,
mostrados na lista a seguir. No entanto, se você estiver curioso, siga o link para a
descrição completa do conjunto de dados fornecida no início desta seção, e você
é encorajado a explorar outras variáveis neste conjunto de dados depois de ler
este capítulo.
Os recursos com os quais trabalharemos, incluindo a variável de destino, são os
seguintes:

 Overall Qual: Classificação para o material geral e acabamento da casa em


uma escala de 1 (muito ruim) a 10 (excelente)
 Overall Cond: Avaliação do estado geral da casa em uma escala de 1 (muito
ruim) a 10 (excelente)
 Gr Liv Area: Acima do grau (solo) área de estar em pés quadrados
 Central Air: Ar condicionado central (N=não, Y=sim)
 Total Bsmt SF: Total de metros quadrados da área do porão
 SalePrice: Preço de venda em dólares americanos ($)

Para o restante deste capítulo, consideraremos o preço de venda () como nossa


variável-alvo — a variável que queremos prever usando uma ou mais das cinco
variáveis explicativas. Antes de explorarmos mais esse conjunto de dados, vamos
carregá-lo em um pandas:SalePriceDataFrame

import pandas as pd

columns = ['Overall Qual', 'Overall Cond', 'Gr Liv Area',

'Central Air', 'Total Bsmt SF', 'SalePrice']

df = pd.read_csv('http://jse.amstat.org/v19n3/decock/AmesHousing.txt',

sep='\t',

usecols=columns)

df.head()
CopyExplain

Para confirmar que o conjunto de dados foi carregado com êxito, podemos exibir
as cinco primeiras linhas do conjunto de dados, conforme mostrado na Figura 9.3:
Figura 9.3: As cinco primeiras linhas do conjunto de dados da habitação

Depois de carregar o conjunto de dados, vamos também verificar as dimensões do


para garantir que ele contenha o número esperado de linhas: DataFrame

>>> df.shape

(2930, 6)
CopyExplain

Como podemos ver, o contém 2.930 linhas, como esperado.DataFrame

Outro aspecto que temos que cuidar é a variável, que é codificada como tipo,
como podemos ver na Figura 9.3. Como aprendemos no Capítulo 4, Construindo
bons conjuntos de dados de treinamento – Pré-processamento de dados,
podemos usar o método para converter colunas. O código a seguir converterá a
cadeia de caracteres para o inteiro 1 e a cadeia de caracteres para o inteiro
0:'Central Air'string.mapDataFrame'Y''N'

>>> df['Central Air'] = df['Central Air'].map({'N': 0, 'Y': 1})


CopyExplain

Por fim, vamos verificar se alguma das colunas do quadro de dados contém
valores ausentes:

>>> df.isnull().sum()

Overall Qual 0

Overall Cond 0
Total Bsmt SF 1

Central Air 0

Gr Liv Area 0

SalePrice 0

dtype: int64
CopyExplain

Como podemos ver, a variável de recurso contém um valor ausente. Como temos
um conjunto de dados relativamente grande, a maneira mais fácil de lidar com
esse valor de recurso ausente é remover o exemplo correspondente do conjunto
de dados (para métodos alternativos, consulte o Capítulo 4):Total Bsmt SF

>>> df = df.dropna(axis=0)

>>> df.isnull().sum()

Overall Qual 0

Overall Cond 0

Total Bsmt SF 0

Central Air 0

Gr Liv Area 0

SalePrice 0

dtype: int64
CopyExplain

Visualizando as características importantes de um


conjunto de dados
A análise exploratória de dados (EDA) é um primeiro passo importante e
recomendado antes do treinamento de um modelo de aprendizado de máquina.
No restante desta seção, usaremos algumas técnicas simples, mas úteis, da caixa
de ferramentas gráfica EDA que podem nos ajudar a detectar visualmente a
presença de outliers, a distribuição dos dados e as relações entre os recursos.
Primeiro, criaremos uma matriz de gráfico de dispersão que nos permite
visualizar as correlações pareadas entre as diferentes características desse
conjunto de dados em um só lugar. Para plotar a matriz de gráfico de dispersão,
usaremos a função da biblioteca mlxtend (http://rasbt.github.io/mlxtend/), que é
uma biblioteca Python que contém várias funções de conveniência para aplicativos
de aprendizado de máquina e ciência de dados em Python. scatterplotmatrix

Você pode instalar o pacote via ou . Para este capítulo, utilizamos mlxtend versão
0.19.0.mlxtendconda install mlxtendpip install mlxtend

Quando a instalação estiver concluída, você poderá importar o pacote e criar a


matriz de gráfico de dispersão da seguinte maneira:

>>> import matplotlib.pyplot as plt

>>> from mlxtend.plotting import scatterplotmatrix

>>> scatterplotmatrix(df.values, figsize=(12, 10),

... names=df.columns, alpha=0.5)

>>> plt.tight_layout()

plt.show()
CopyExplain

Como você pode ver na Figura 9.4, a matriz de gráfico de dispersão nos fornece
um resumo gráfico útil das relações em um conjunto de dados:
Figura 9.4: Uma matriz de dispersão dos nossos dados

Usando essa matriz de gráfico de dispersão, agora podemos ver rapidamente


como os dados são distribuídos e se eles contêm outliers. Por exemplo, podemos
ver (quinta coluna da esquerda da linha inferior) que há uma relação um tanto
linear entre o tamanho da área de vida acima do solo () e o preço de venda (). Gr
Liv AreaSalePrice

Além disso, podemos ver no histograma – o subgráfico inferior direito na matriz do


gráfico de dispersão – que a variável parece estar distorcida por vários
outliers.SalePrice

O pressuposto de normalidade da regressão linear


Note que, ao contrário da crença comum, treinar um modelo de regressão linear
não requer que as variáveis explicativas ou alvo sejam normalmente distribuídas.
A suposição de normalidade é apenas um requisito para certas estatísticas e
testes de hipótese que estão além do escopo deste livro (para obter mais
informações sobre este tópico, consulte a Introdução à Análise de Regressão
Linear de Douglas C. Montgomery, Elizabeth A. Peck e G. Geoffrey Vining, Wiley,
páginas: 318-319, 2012).

Analisando relacionamentos usando uma matriz


de correlação
Na seção anterior, visualizamos as distribuições dos dados das variáveis do
conjunto de dados Ames Housing na forma de histogramas e diagramas de
dispersão. Em seguida, criaremos uma matriz de correlação para quantificar e
resumir as relações lineares entre as variáveis. Uma matriz de correlação está
intimamente relacionada com a matriz de covariância que abordamos na seção
Redução de dimensionalidade não supervisionada via análise de componentes
principais no Capítulo 5, Comprimindo dados via redução de dimensionalidade.
Podemos interpretar a matriz de correlação como sendo uma versão
redimensionada da matriz de covariância. De fato, a matriz de correlação é
idêntica a uma matriz de covariância calculada a partir de características
padronizadas.

A matriz de correlação é uma matriz quadrada que contém o coeficiente de


correlação produto-momento de Pearson (frequentemente abreviado como r de
Pearson), que mede a dependência linear entre pares de características. Os
coeficientes de correlação estão no intervalo de –1 a 1. Duas características têm
uma correlação positiva perfeita se r = 1, nenhuma correlação se r = 0, e uma
correlação negativa perfeita se r = –1. Como mencionado anteriormente, o
coeficiente de correlação de Pearson pode ser calculado simplesmente como a
covariância entre duas características, x e y (numerador), dividida pelo produto de
seus desvios padrão (denominador):
Aqui, denota a média da característica correspondente, é a covariância entre as

características x e y,     e     são os desvios padrão das


características.

Covariância versus correlação para características padronizadas

Podemos mostrar que a covariância entre um par de características padronizadas


é, de fato, igual ao seu coeficiente de correlação linear. Para mostrar isso, vamos
primeiro padronizar os recursos x e y para obter seus z-scores, que denotaremos
como x' e y', respectivamente:

Lembre-se de que calculamos a covariância (populacional) entre duas


características da seguinte maneira:

Como a padronização centraliza uma variável de feição na média zero, agora


podemos calcular a covariância entre as características dimensionadas da
seguinte maneira:
Através da substituição, obtém-se o seguinte resultado:

Finalmente, podemos simplificar essa equação da seguinte forma:

No exemplo de código a seguir, usaremos a função de NumPy nas cinco colunas


de feição que visualizamos anteriormente na matriz de gráfico de dispersão e
usaremos a função mlxtend para plotar a matriz de correlação como um mapa de
calor:corrcoefheatmap

>>> import numpy as np

>>> from mlxtend.plotting import heatmap

>>> cm = np.corrcoef(df.values.T)

>>> hm = heatmap(cm, row_names=df.columns, column_names=df.columns)

>>> plt.tight_layout()
>>> plt.show()
CopyExplain

Como você pode ver na Figura 9.5, a matriz de correlação nos fornece outro
gráfico de resumo útil que pode nos ajudar a selecionar características com base
em suas respectivas correlações lineares:

Figura 9.5: Matriz de correlação das variáveis selecionadas

Para ajustar um modelo de regressão linear, estamos interessados naquelas


características que têm uma alta correlação com nossa variável alvo, .
Observando a matriz de correlação anterior, podemos observar que mostra a
maior correlação com a variável (), o que parece ser uma boa escolha para uma
variável exploratória introduzir os conceitos de um modelo de regressão linear
simples na seção seguinte.SalePriceSalePriceGr Liv Area0.71
Implementando um modelo de regressão
linear de mínimos quadrados ordinários
No início deste capítulo, mencionamos que a regressão linear pode ser entendida
como a obtenção da linha reta mais adequada através dos exemplos de nossos
dados de treinamento. No entanto, não definimos o termo melhor ajuste nem
discutimos as diferentes técnicas de ajuste de tal modelo. Nas subseções a seguir,
preencheremos as peças que faltam desse quebra-cabeça usando o método dos
mínimos quadrados ordinários (OLS) (às vezes também chamado de mínimos
quadrados lineares) para estimar os parâmetros da linha de regressão linear que
minimiza a soma das distâncias verticais ao quadrado (resíduos ou erros) aos
exemplos de treinamento.

Resolução de regressão para parâmetros de


regressão com descida de gradiente
Considere nossa implementação do Neurônio Linear Adaptativo (Adaline)
do Capítulo 2, Treinando Algoritmos Simples de Aprendizado de Máquina para
Classificação. Você vai lembrar que o neurônio artificial usa uma função de
ativação linear. Além disso, definimos uma função de perda, L(w), que
minimizamos para aprender os pesos através de algoritmos de
otimização, como descida de gradiente (GD) e descida de gradiente
estocástico (SGD).

Esta função de perda em Adaline é o erro quadrático médio (MSE), que


é idêntico à função de perda que usamos para OLS:
Aqui,   está o valor   previsto (note que

o termo   é usado apenas por conveniência para derivar a regra de atualização


do GD). Essencialmente, a regressão OLS pode ser entendida como Adaline sem
a função threshold, de modo que obtemos valores de destino contínuos em vez
dos rótulos de classe e . Para demonstrar isso, vamos pegar a implementação GD
do Adaline do Capítulo 2 e remover a função threshold para implementar nosso
primeiro modelo de regressão linear: 01

class LinearRegressionGD:

def __init__(self, eta=0.01, n_iter=50, random_state=1):

self.eta = eta

self.n_iter = n_iter

self.random_state = random_state

def fit(self, X, y):

rgen = np.random.RandomState(self.random_state)

self.w_ = rgen.normal(loc=0.0, scale=0.01, size=X.shape[1])

self.b_ = np.array([0.])

self.losses_ = []

for i in range(self.n_iter):

output = self.net_input(X)

errors = (y - output)

self.w_ += self.eta * 2.0 * X.T.dot(errors) / X.shape[0]

self.b_ += self.eta * 2.0 * errors.mean()

loss = (errors**2).mean()

self.losses_.append(loss)

return self
def net_input(self, X):

return np.dot(X, self.w_) + self.b_

def predict(self, X):

return self.net_input(X)
CopyExplain

Atualizações de peso com descida de gradiente

Se você precisar de uma atualização sobre como os pesos são atualizados —


dando um passo na direção oposta do gradiente — revisite a seção Neurônios
lineares adaptativos e a convergência do aprendizado no Capítulo 2.

Para ver nosso regressor em ação, vamos usar o recurso (tamanho da área de


estar acima do solo em metros quadrados) do conjunto de dados Ames Housing
como a variável explicativa e treinar um modelo que pode prever . Além disso,
padronizaremos as variáveis para melhor convergência do algoritmo GD. O código
é o seguinte:LinearRegressionGDGr Living AreaSalePrice

>>> X = df[['Gr Liv Area']].values

>>> y = df['SalePrice'].values

>>> from sklearn.preprocessing import StandardScaler

>>> sc_x = StandardScaler()

>>> sc_y = StandardScaler()

>>> X_std = sc_x.fit_transform(X)

>>> y_std = sc_y.fit_transform(y[:, np.newaxis]).flatten()

>>> lr = LinearRegressionGD(eta=0.1)

>>> lr.fit(X_std, y_std)


CopyExplain

Observe a solução alternativa em relação a , usando e . A maioria das classes de


pré-processamento de dados no scikit-learn espera que os dados sejam
armazenados em matrizes bidimensionais. No exemplo de código anterior, o uso
de in adicionou uma nova dimensão à matriz. Em seguida, depois de retornar a
variável dimensionada, a convertemos de volta para a representação original da
matriz unidimensional usando o método para nossa
conveniência.y_stdnp.newaxisflattennp.newaxisy[:,
np.newaxis]StandardScalerflatten()

Discutimos no Capítulo 2 que é sempre uma boa ideia plotar a perda como uma
função do número de épocas (iterações completas) sobre o conjunto de dados de
treinamento quando estamos usando algoritmos de otimização, como GD, para
verificar se o algoritmo convergiu para um mínimo de perda (aqui, um mínimo de
perda global):

>>> plt.plot(range(1, lr.n_iter+1), lr.losses_)

>>> plt.ylabel('MSE')

>>> plt.xlabel('Epoch')

>>> plt.show()
CopyExplain

Como você pode ver na Figura 9.6, o algoritmo GD convergiu aproximadamente


após a décima época:
Figura 9.6: A função de perda versus o número de épocas

Em seguida, vamos visualizar como a linha de regressão linear se ajusta aos


dados de treinamento. Para fazer isso, definiremos uma função auxiliar simples
que plotará um gráfico de dispersão dos exemplos de treinamento e adicionará a
linha de regressão:

>>> def lin_regplot(X, y, model):

... plt.scatter(X, y, c='steelblue', edgecolor='white', s=70)

... plt.plot(X, model.predict(X), color='black', lw=2)


CopyExplain

Agora, vamos usar essa função para plotar a área de estar contra o preço de
venda:lin_regplot

>>> lin_regplot(X_std, y_std, lr)

>>> plt.xlabel(' Living area above ground (standardized)')

>>> plt.ylabel('Sale price (standardized)')

>>> plt.show()
CopyExplain

Como você pode ver na Figura 9.7, a linha de regressão linear reflete a tendência
geral de que os preços das casas tendem a aumentar com o tamanho da área de
convivência:
Figura 9.7: Gráfico de regressão linear dos preços de venda versus a dimensão da
área de vida

Embora esta observação faça sentido, os dados também nos dizem que o
tamanho da área de vida não explica muito bem os preços das casas em muitos
casos. Mais adiante neste capítulo, discutiremos como quantificar o desempenho
de um modelo de regressão. Curiosamente, também podemos observar vários
outliers, por exemplo, os três pontos de dados correspondentes a uma área de
vida padronizada maior que 6. Discutiremos como podemos lidar com outliers
mais adiante neste capítulo.

Em determinadas aplicações, também pode ser importante relatar as variáveis de


desfecho previstas em sua escala original. Para escalar o preço previsto de volta
ao preço original em dólares americanos, podemos simplesmente aplicar o
método de:inverse_transformStandardScaler

>>> feature_std = sc_x.transform(np.array([[2500]]))

>>> target_std = lr.predict(feature_std)

>>> target_reverted = sc_y.inverse_transform(target_std.reshape(-1, 1))


>>> print(f'Sales price: ${target_reverted.flatten()[0]:.2f}')

Sales price: $292507.07


CopyExplain

Neste exemplo de código, usamos o modelo de regressão linear previamente


treinado para prever o preço de uma casa com uma área de vida acima do solo de
2.500 pés quadrados. De acordo com o nosso modelo, tal casa valerá R$
292.507,07.

Como uma nota lateral, também vale a pena mencionar que tecnicamente não
precisamos atualizar o parâmetro intercept (por exemplo, a unidade de viés, b) se
estivermos trabalhando com variáveis padronizadas, já que o intercepto do
eixo y é sempre 0 nesses casos. Podemos confirmar isso rapidamente imprimindo
os parâmetros do modelo:

>>> print(f'Slope: {lr.w_[0]:.3f}')

Slope: 0.707

>>> print(f'Intercept: {lr.b_[0]:.3f}')

Intercept: -0.000
CopyExplain

Estimando o coeficiente de um modelo de


regressão via scikit-learn
Na seção anterior, implementamos um modelo de trabalho para análise de
regressão; No entanto, em uma aplicação do mundo real, podemos estar
interessados em implementações mais eficientes. Por exemplo, muitos dos
estimadores de regressão do scikit-learn fazem uso da implementação de mínimos
quadrados no SciPy (), que, por sua vez, usa otimizações de código altamente
otimizadas com base no Pacote de Álgebra Linear (LAPACK). A implementação
de regressão linear no scikit-learn também funciona (melhor) com variáveis não
padronizadas, uma vez que não utiliza otimização baseada em (S)GD, então
podemos pular a etapa de padronização: scipy.linalg.lstsq
>>> from sklearn.linear_model import LinearRegression

>>> slr = LinearRegression()

>>> slr.fit(X, y)

>>> y_pred = slr.predict(X)

>>> print(f'Slope: {slr.coef_[0]:.3f}')

Slope: 111.666

>>> print(f'Intercept: {slr.intercept_:.3f}')

Intercept: 13342.979
CopyExplain

Como você pode ver ao executar este código, o modelo scikit-learn, ajustado com
as variáveis e não padronizadas, produziu diferentes coeficientes de modelo, uma
vez que as características não foram padronizadas. No entanto, quando o
comparamos com nossa implementação de GD plotando contra , podemos ver
qualitativamente que ele se encaixa nos dados da mesma
forma:LinearRegressionGr Liv AreaSalePriceSalePriceGr Liv Area

>>> lin_regplot(X, y, slr)

>>> plt.xlabel('Living area above ground in square feet')

>>> plt.ylabel('Sale price in U.S. dollars')

>>> plt.tight_layout()

>>> plt.show()
CopyExplain

Por exemplo, podemos ver que o resultado geral parece idêntico à nossa
implementação GD:
Figura 9.8: Um gráfico de regressão linear usando scikit-learn

Soluções analíticas de regressão linear

Como alternativa ao uso de bibliotecas de aprendizado de máquina, há também


uma solução de forma fechada para resolver OLS envolvendo um sistema de
equações lineares que pode ser encontrado na maioria dos livros didáticos de
estatística introdutória:

Podemos implementá-lo em Python da seguinte maneira:

# adding a column vector of "ones"

>>> Xb = np.hstack((np.ones((X.shape[0], 1)), X))

>>> w = np.zeros(X.shape[1])

>>> z = np.linalg.inv(np.dot(Xb.T, Xb))


>>> w = np.dot(z, np.dot(Xb.T, y))

>>> print(f'Slope: {w[1]:.3f}')

Slope: 111.666

>>> print(f'Intercept: {w[0]:.3f}')

Intercept: 13342.979
CopyExplain

A vantagem deste método é que é garantido encontrar a solução ideal


analiticamente. No entanto, se estivermos trabalhando com conjuntos de dados
muito grandes, pode ser computacionalmente muito caro inverter a matriz nesta
fórmula (às vezes também chamada de equação normal), ou a matriz contendo os
exemplos de treinamento pode ser singular (não invertível), razão pela qual
podemos preferir métodos iterativos em certos casos.

Se você está interessado em mais informações sobre como obter equações


normais, dê uma olhada no capítulo do Dr. Stephen Pollock The Classical Linear
Regression Model, de suas palestras na Universidade de Leicester, que está
disponível gratuitamente
em http://www.le.ac.uk/users/dsgp1/COURSES/MESOMET/ECMETXT/06mesmet.
pdf.

Além disso, se você quiser comparar soluções de regressão linear obtidas via GD,
SGD, a solução de forma fechada, fatoração QR e decomposição vetorial singular,
você pode usar a classe implementada em mlxtend
(http://rasbt.github.io/mlxtend/user_guide/regressor/LinearRegression/), que
permite aos usuários alternar entre essas opções. Outra ótima biblioteca a ser
recomendada para modelagem de regressão em Python é statsmodels, que
implementa modelos de regressão linear mais avançados, como ilustrado em
https://www.statsmodels.org/stable/examples/index.html#regression.LinearRegressi
on

Ajustando um modelo de regressão


robusto usando RANSAC
Modelos de regressão linear podem ser fortemente impactados pela presença de
outliers. Em certas situações, um subconjunto muito pequeno de nossos dados
pode ter um grande efeito sobre os coeficientes estimados do modelo. Muitos
testes estatísticos podem ser usados para detectar outliers, mas estes estão além
do escopo do livro. No entanto, remover outliers sempre requer nosso próprio
julgamento como cientistas de dados, bem como nosso conhecimento de domínio.

Como alternativa para descartar outliers, veremos um método robusto de


regressão usando o algoritmo RANdom SAmple Consensus (RANSAC), que
ajusta um modelo de regressão a um subconjunto dos dados, os
chamados inliers.

Podemos resumir o algoritmo iterativo RANSAC da seguinte forma:

1. Selecione um número aleatório de exemplos para serem inliers e ajustar o


modelo.
2. Teste todos os outros pontos de dados em relação ao modelo ajustado e
adicione os pontos que se enquadram em uma tolerância dada pelo usuário
aos inliers.
3. Reajuste o modelo usando todos os inliers.
4. Estimar o erro do modelo ajustado versus os inliers.
5. Encerrar o algoritmo se o desempenho atender a um determinado limite
definido pelo usuário ou se um número fixo de iterações tiver sido atingido;
Volte para a etapa 1 caso contrário.

Vamos agora usar um modelo linear em combinação com o algoritmo RANSAC


como implementado na aula de scikit-learn: RANSACRegressor

>>> from sklearn.linear_model import RANSACRegressor

>>> ransac = RANSACRegressor(

... LinearRegression(),

... max_trials=100, # default value

... min_samples=0.95,

... residual_threshold=None, # default value


... random_state=123)

>>> ransac.fit(X, y)
CopyExplain

Definimos o número máximo de iterações do para 100 e, usando o , definimos o


número mínimo dos exemplos de treinamento escolhidos aleatoriamente para ser
pelo menos 95% do conjunto de dados.RANSACRegressormin_samples=0.95

Por padrão (via ), scikit-learn usa a estimativa MAD para selecionar o limiar inlier,
onde MAD representa o desvio absoluto mediano dos valores alvo, . No entanto,
a escolha de um valor apropriado para o limiar inlier é específica do problema, o
que é uma desvantagem do RANSAC.residual_threshold=Noney

Muitas abordagens diferentes foram desenvolvidas nos últimos anos para


selecionar um bom limiar inlier automaticamente. Você pode encontrar uma
discussão detalhada em Automatic Estimation of the Inlier Threshold in Robust
Multiple Structures Fitting por R. Toldo e A. Fusiello, Springer, 2009 (in Image
Analysis and Processing–ICIAP 2009, páginas: 123-131).

Uma vez ajustado o modelo RANSAC, vamos obter os inliers e outliers do modelo
de regressão linear RANSAC ajustado e plotá-los juntamente com o ajuste linear:

>>> inlier_mask = ransac.inlier_mask_

>>> outlier_mask = np.logical_not(inlier_mask)

>>> line_X = np.arange(3, 10, 1)

>>> line_y_ransac = ransac.predict(line_X[:, np.newaxis])

>>> plt.scatter(X[inlier_mask], y[inlier_mask],

... c='steelblue', edgecolor='white',

... marker='o', label='Inliers')

>>> plt.scatter(X[outlier_mask], y[outlier_mask],

... c='limegreen', edgecolor='white',

... marker='s', label='Outliers')

>>> plt.plot(line_X, line_y_ransac, color='black', lw=2)


>>> plt.xlabel('Living area above ground in square feet')

>>> plt.ylabel('Sale price in U.S. dollars')

>>> plt.legend(loc='upper left')

>>> plt.tight_layout()

>>> plt.show()
CopyExplain

Como você pode ver na Figura 9.9, o modelo de regressão linear foi ajustado no
conjunto detectado de inliers, que são mostrados como círculos:

Figura 9.9: Valores atípicos e atípicos identificados através de um modelo de


regressão linear RANSAC

Quando imprimimos a inclinação e interceptação do modelo executando o código


a seguir, a linha de regressão linear será ligeiramente diferente do ajuste que
obtivemos na seção anterior sem usar RANSAC:

>>> print(f'Slope: {ransac.estimator_.coef_[0]:.3f}')


Slope: 106.348

>>> print(f'Intercept: {ransac.estimator_.intercept_:.3f}')

Intercept: 20190.093
CopyExplain

Lembre-se de que definimos o parâmetro como , então o RANSAC estava usando


o MAD para calcular o limite para sinalizar inliers e outliers. O MAD, para esse
conjunto de dados, pode ser calculado da seguinte
maneira:residual_thresholdNone

>>> def median_absolute_deviation(data):

... return np.median(np.abs(data - np.median(data)))

>>> median_absolute_deviation(y)

37000.00
CopyExplain

Assim, se quisermos identificar menos pontos de dados como outliers, podemos


escolher um valor maior do que o MAD anterior. Por exemplo, a Figura
9.10 mostra os valores atípicos e atípicos de um modelo de regressão linear
RANSAC com um limiar residual de 65.000:residual_threshold
Figura 9.10: Valores atípicos e atípicos determinados por um modelo de regressão
linear RANSAC com um limiar residual maior

Usando o RANSAC, reduzimos o efeito potencial dos outliers nesse conjunto de


dados, mas não sabemos se essa abordagem terá um efeito positivo no
desempenho preditivo para dados não vistos ou não. Assim, na próxima seção,
examinaremos diferentes abordagens para avaliar um modelo de regressão, que é
uma parte crucial da construção de sistemas para modelagem preditiva.

Avaliação do desempenho de modelos


de regressão linear
Na seção anterior, você aprendeu como ajustar um modelo de regressão em
dados de treinamento. No entanto, você descobriu nos capítulos anteriores que é
crucial testar o modelo em dados que ele não viu durante o treinamento para obter
uma estimativa mais imparcial de seu desempenho de generalização.
Como você deve se lembrar do Capítulo 6, Aprendendo as melhores práticas para
avaliação de modelos e ajuste de hiperparâmetros, queremos dividir nosso
conjunto de dados em conjuntos de dados de treinamento e teste separados, onde
usaremos o primeiro para ajustar o modelo e o segundo para avaliar seu
desempenho em dados não vistos para estimar o desempenho de generalização.
Em vez de prosseguir com o modelo de regressão simples, agora usaremos todos
os cinco recursos no conjunto de dados e treinaremos um modelo de regressão
múltipla:

>>> from sklearn.model_selection import train_test_split

>>> target = 'SalePrice'

>>> features = df.columns[df.columns != target]

>>> X = df[features].values

>>> y = df[target].values

>>> X_train, X_test, y_train, y_test = train_test_split(

... X, y, test_size=0.3, random_state=123)

>>> slr = LinearRegression()

>>> slr.fit(X_train, y_train)

>>> y_train_pred = slr.predict(X_train)

>>> y_test_pred = slr.predict(X_test)


CopyExplain

Como nosso modelo usa múltiplas variáveis explicativas, não podemos visualizar
a reta de regressão linear (ou hiperplano, para ser preciso) em um gráfico
bidimensional, mas podemos plotar os resíduos (as diferenças ou distâncias
verticais entre os valores reais e previstos) versus os valores previstos para
diagnosticar nosso modelo de regressão. Gráficos residuais são uma ferramenta
gráfica comumente utilizada para diagnosticar modelos de regressão. Eles podem
ajudar a detectar não-linearidade e outliers e verificar se os erros são distribuídos
aleatoriamente.
Usando o código a seguir, agora vamos plotar um gráfico residual onde
simplesmente subtraímos as verdadeiras variáveis de destino de nossas respostas
previstas:

>>> x_max = np.max(

... [np.max(y_train_pred), np.max(y_test_pred)])

>>> x_min = np.min(

... [np.min(y_train_pred), np.min(y_test_pred)])

>>> fig, (ax1, ax2) = plt.subplots(

... 1, 2, figsize=(7, 3), sharey=True)

>>> ax1.scatter(

... y_test_pred, y_test_pred - y_test,

... c='limegreen', marker='s',

... edgecolor='white',

... label='Test data')

>>> ax2.scatter(

... y_train_pred, y_train_pred - y_train,

... c='steelblue', marker='o', edgecolor='white',

... label='Training data')

>>> ax1.set_ylabel('Residuals')

>>> for ax in (ax1, ax2):

... ax.set_xlabel('Predicted values')

... ax.legend(loc='upper left')

... ax.hlines(y=0, xmin=x_min-100, xmax=x_max+100,\

... color='black', lw=2)

>>> plt.tight_layout()

>>> plt.show()
CopyExplain
Após a execução do código, devemos ver gráficos residuais para os conjuntos de
dados de teste e treinamento com uma linha passando pela origem do eixo x,
como mostrado na Figura 9.11:

Figura 9.11: Gráficos residuais dos nossos dados

No caso de uma previsão perfeita, os resíduos seriam exatamente zero, o que


provavelmente nunca encontraremos em aplicações realistas e práticas. No
entanto, para um bom modelo de regressão, esperaríamos que os erros fossem
distribuídos aleatoriamente e que os resíduos fossem espalhados aleatoriamente
ao redor da linha central. Se vemos padrões em um gráfico residual, isso significa
que nosso modelo é incapaz de capturar algumas informações explicativas,
que vazaram para os resíduos, como você pode ver até certo ponto em nosso
gráfico residual anterior. Além disso, também podemos usar gráficos residuais
para detectar outliers, que são representados pelos pontos com um grande desvio
da linha central.

Outra medida quantitativa útil do desempenho de um modelo é o erro quadrático


médio (MSE) que discutimos anteriormente como nossa função de perda que
minimizamos para ajustar o modelo de regressão linear. A seguir está uma versão

do MSE sem o   fator de escala que é frequentemente usado para simplificar a


derivada de perda na descida de gradiente:
Semelhante à precisão da predição em contextos de classificação, podemos usar
o MSE para validação cruzada e seleção de modelos, conforme discutido
no Capítulo 6.

Assim como a precisão da classificação, o EPM também normaliza de acordo com


o tamanho da amostra, n. Isso torna possível comparar diferentes tamanhos de
amostra (por exemplo, no contexto de curvas de aprendizado) também.

Vamos agora calcular o MSE de nossas previsões de treinamento e teste:

>>> from sklearn.metrics import mean_squared_error

>>> mse_train = mean_squared_error(y_train, y_train_pred)

>>> mse_test = mean_squared_error(y_test, y_test_pred)

>>> print(f'MSE train: {mse_train:.2f}')

MSE train: 1497216245.85

>>> print(f'MSE test: {mse_test:.2f}')

MSE test: 1516565821.00


CopyExplain

Podemos ver que o MSE no conjunto de dados de treinamento é menor do que no


conjunto de teste, o que é um indicador de que nosso modelo está superajustando
ligeiramente os dados de treinamento neste caso. Note que pode ser mais intuitivo
mostrar o erro na escala de unidade original (aqui, dólar em vez de dólar ao
quadrado), e é por isso que podemos optar por calcular a raiz quadrada do MSE,
chamada raiz média do erro quadrático, ou o erro absoluto médio (MAE), que
enfatiza a previsão incorreta um pouco menos:
Podemos calcular o MAE semelhante ao MSE:

>>> from sklearn.metrics import mean_absolute_error

>>> mae_train = mean_absolute_error(y_train, y_train_pred)

>>> mae_test = mean_absolute_error(y_test, y_test_pred)

>>> print(f'MAE train: {mae_train:.2f}')

MAE train: 25983.03

>>> print(f'MAE test: {mae_test:.2f}')

MAE test: 24921.29


CopyExplain

Com base no conjunto de testes MAE, podemos dizer que o modelo comete um
erro de aproximadamente US$ 25.000 em média.

Quando usamos o MAE ou o MSE para comparar modelos, precisamos estar


cientes de que estes são ilimitados em contraste com a precisão da classificação,
por exemplo. Em outras palavras, as interpretações do MAE e do MSE dependem
do conjunto de dados e do dimensionamento de recursos. Por exemplo, se os
preços de venda fossem apresentados como múltiplos de 1.000 (com o sufixo K),
o mesmo modelo produziria um MAE menor em comparação com um modelo que
funcionasse com recursos não dimensionados. Para ilustrar melhor este ponto,

Assim, às vezes pode ser mais útil relatar o coeficiente de determinação (R2),


que pode ser entendida como uma versão padronizada do MPE, para melhor
interpretabilidade do desempenho do modelo. Ou, em outras palavras, R2 é a
fração da variância de resposta que é capturada pelo modelo. O R2 valor é
definido como:

Aqui, SSE é a soma dos erros quadrados, que é semelhante ao MSE, mas não
inclui a normalização pelo tamanho da amostra n:

E SST é a soma total dos quadrados:

Em outras palavras, SST é simplesmente a variância da resposta.

Agora, vamos mostrar brevemente que R2 na verdade é apenas uma versão


redimensionada do MSE:
Para o conjunto de dados de treinamento, R2 é limitado entre 0 e 1, mas pode se
tornar negativo para o conjunto de dados de teste. Um R negativo2 significa que o
modelo de regressão se ajusta pior aos dados do que uma linha horizontal que
representa a média amostral. (Na prática, isso geralmente acontece no caso de
overfitting extremo, ou se esquecemos de escalar o conjunto de teste da mesma
maneira que escalamos o conjunto de treinamento.) Se R2 = 1, o modelo se ajusta
perfeitamente aos dados com um MSE correspondente = 0.

Avaliado sobre os dados de treinamento, o R2 do nosso modelo é 0,77, o que não
é ótimo, mas também não é muito ruim, dado que trabalhamos apenas com um
pequeno conjunto de recursos. No entanto, o R2 no conjunto de dados de teste é
apenas um pouco menor, em 0,75, o que indica que o modelo está apenas
sobreajustando ligeiramente:

>>> from sklearn.metrics import r2_score

>>> train_r2 = r2_score(y_train, y_train_pred)>>> test_r2 = r2_score(y_test, y_test_pred)


>>> print(f'R^2 train: {train_r2:.3f}, {test_r2:.3f}')

R^2 train: 0.77, test: 0.75


CopyExplain

Usando métodos regularizados para


regressão
Como discutimos no Capítulo 3, A Tour of Machine Learning Classifiers Using
Scikit-Learn, a regularização é uma abordagem para resolver o problema do
overfitting adicionando informações adicionais e, assim, reduzindo os valores de
parâmetro do modelo para induzir uma penalidade contra a complexidade. As
abordagens mais populares para a regressão linear regularizada são a chamada
regressão de crista, operador de seleção e retração mínima absoluta (LASSO)
e rede elástica.

A regressão de Ridge é um modelo penalizado L2 onde simplesmente


adicionamos a soma quadrada dos pesos à função de perda de MSE:

Aqui, o termo L2 é definido da seguinte forma:


Ao aumentar o valor do hiperparâmetro , aumentamos a força de regularização
e, assim, diminuímos os pesos do nosso modelo. Observe que, como mencionado
no Capítulo 3, a unidade b do viés não está regularizada.

Uma abordagem alternativa que pode levar a modelos esparsos é o LASSO.


Dependendo da força de regularização, certos pesos podem se tornar zero, o que
também torna o LASSO útil como uma técnica supervisionada de seleção de
recursos:

Aqui, a penalidade L1 para LASSO é definida como a soma das magnitudes


absolutas dos pesos do modelo, da seguinte forma:

No entanto, uma limitação do LASSO é que ele seleciona no máximo n


características se m > n, onde n é o número de exemplos de treinamento. Isso
pode ser indesejável em certas aplicações de seleção de recursos. Na prática, no
entanto, essa propriedade do LASSO é muitas vezes uma vantagem, pois evita
modelos saturados. A saturação de um modelo ocorre se o número de exemplos
de treinamento for igual ao número de recursos, o que é uma forma de
superparametrização. Como consequência, um modelo saturado sempre pode se
ajustar perfeitamente aos dados de treinamento, mas é apenas uma forma de
interpolação e, portanto, não se espera que generalize bem.
Um compromisso entre a regressão de crista e o LASSO é a rede elástica, que
tem uma penalidade L1 para gerar esparsidade e uma penalidade L2 tal que pode
ser usada para selecionar mais de n características se m > n:

Esses modelos de regressão regularizados estão todos disponíveis via scikit-learn,


e seu uso é semelhante ao modelo de regressão regular, exceto que temos que

especificar a força de regularização através do parâmetro  , por exemplo,


otimizado via validação cruzada k-fold.

Um modelo de regressão de crista pode ser inicializado através de:

>>> from sklearn.linear_model import Ridge

>>> ridge = Ridge(alpha=1.0)


CopyExplain

Note que a força de regularização é regulada pelo parâmetro , que é semelhante

ao parâmetro  . Da mesma forma, podemos inicializar um regressor LASSO a


partir do submódulo:alphalinear_model

>>> from sklearn.linear_model import Lasso

>>> lasso = Lasso(alpha=1.0)


CopyExplain

Por fim, a implementação permite variar a relação L1 para L2: ElasticNet

>>> from sklearn.linear_model import ElasticNet

>>> elanet = ElasticNet(alpha=1.0, l1_ratio=0.5)


CopyExplain
Por exemplo, se definirmos como 1,0, o regressor seria igual à regressão LASSO.
Para obter informações mais detalhadas sobre as diferentes implementações de
regressão linear, consulte a documentação
em http://scikit-learn.org/stable/modules/linear_model.html.l1_ratioElasticNet

Transformando um modelo de regressão


linear em uma curva – regressão
polinomial
Nas seções anteriores, assumimos uma relação linear entre variáveis explicativas
e resposta. Uma maneira de explicar a violação da suposição de linearidade é
usar um modelo de regressão polinomial adicionando termos polinomiais:

Aqui, d denota o grau do polinômio. Embora possamos usar a regressão


polinomial para modelar uma relação não linear, ela ainda é considerada um
modelo de regressão linear múltipla devido aos coeficientes de regressão
linear, w. Nas subseções a seguir, veremos como podemos adicionar tais termos
polinomiais a um conjunto de dados existente convenientemente e ajustar um
modelo de regressão polinomial.

Adicionando termos polinomiais usando scikit-


learn
Agora aprenderemos como usar a classe transformer de scikit-learn para adicionar
um termo quadrático (d = 2) a um problema de regressão simples com uma
variável explicativa. Em seguida, compararemos o polinômio com o ajuste linear
seguindo estes passos:PolynomialFeatures

1. Adicione um termo polinomial de segundo grau:


2. >>> from sklearn.preprocessing import PolynomialFeatures
3. >>> X = np.array([ 258.0, 270.0, 294.0, 320.0, 342.0,

4. ... 368.0, 396.0, 446.0, 480.0, 586.0])\

5. ... [:, np.newaxis]

6. >>> y = np.array([ 236.4, 234.4, 252.8, 298.6, 314.2,

7. ... 342.2, 360.8, 368.0, 391.2, 390.8])

8. >>> lr = LinearRegression()

9. >>> pr = LinearRegression()

10. >>> quadratic = PolynomialFeatures(degree=2)

11. >>> X_quad = quadratic.fit_transform(X)


CopyExplain
12. Ajuste um modelo de regressão linear simples para comparação:
13. >>> lr.fit(X, y)

14. >>> X_fit = np.arange(250, 600, 10)[:, np.newaxis]

15. >>> y_lin_fit = lr.predict(X_fit)


CopyExplain
16. Ajuste um modelo de regressão múltipla nos recursos transformados para
regressão polinomial:
17. >>> pr.fit(X_quad, y)

18. >>> y_quad_fit = pr.predict(quadratic.fit_transform(X_fit))


CopyExplain
19. Plote os resultados:
20. >>> plt.scatter(X, y, label='Training points')

21. >>> plt.plot(X_fit, y_lin_fit,

22. ... label='Linear fit', linestyle='--')

23. >>> plt.plot(X_fit, y_quad_fit,

24. ... label='Quadratic fit')

25. >>> plt.xlabel('Explanatory variable')

26. >>> plt.ylabel('Predicted or known target values')

27. >>> plt.legend(loc='upper left')


28. >>> plt.tight_layout()

29. >>> plt.show()


CopyExplain

No gráfico resultante, você pode ver que o ajuste polinomial captura a relação
entre a resposta e as variáveis explicativas muito melhor do que o ajuste linear:

Figura 9.12: Comparação de um modelo linear e quadrático

Em seguida, vamos computar o MSE e o R2 Métricas de avaliação:

>>> y_lin_pred = lr.predict(X)

>>> y_quad_pred = pr.predict(X_quad)

>>> mse_lin = mean_squared_error(y, y_lin_pred)

>>> mse_quad = mean_squared_error(y, y_quad_pred)

>>> print(f'Training MSE linear: {mse_lin:.3f}'

f', quadratic: {mse_quad:.3f}')

Training MSE linear: 569.780, quadratic: 61.330


>>> r2_lin = r2_score(y, y_lin_pred)

>>> r2_quad = r2_score(y, y_quad_pred)

>>> print(f'Training R^2 linear: {r2_lin:.3f}'

f', quadratic: {r2_quad:.3f}')

Training R^2 linear: 0.832, quadratic: 0.982


CopyExplain

Como você pode ver após a execução do código, o MSE diminuiu de 570 (ajuste
linear) para 61 (ajuste quadrático); também, o coeficiente de determinação reflete
um ajuste mais próximo do modelo quadrático (R2 = 0,982) em oposição ao ajuste
linear (R2 = 0,832) neste problema específico do brinquedo.

Modelando relações não lineares no conjunto de


dados Ames Housing
Na subseção anterior, você aprendeu a construir recursos polinomiais para ajustar
relações não lineares em um problema de brinquedo; vamos agora dar uma
olhada em um exemplo mais concreto e aplicar esses conceitos aos dados no
conjunto de dados Ames Housing. Ao executar o código a seguir, modelaremos a
relação entre os preços de venda e a área de vida acima do solo usando
polinômios de segundo grau (quadrático) e terceiro grau (cúbico) e compararemos
isso com um ajuste linear.

Começamos removendo os três outliers com uma área de vida superior a 4.000
pés quadrados, que podemos ver em figuras anteriores, como na Figura 9.8, para
que esses outliers não distorcam nossos ajustes de regressão:

>>> X = df[['Gr Liv Area']].values

>>> y = df['SalePrice'].values

>>> X = X[(df['Gr Liv Area'] < 4000)]

>>> y = y[(df['Gr Liv Area'] < 4000)]


CopyExplain
A seguir, ajustamos os modelos de regressão:

>>> regr = LinearRegression()

>>> # create quadratic and cubic features

>>> quadratic = PolynomialFeatures(degree=2)

>>> cubic = PolynomialFeatures(degree=3)

>>> X_quad = quadratic.fit_transform(X)

>>> X_cubic = cubic.fit_transform(X)

>>> # fit to features

>>> X_fit = np.arange(X.min()-1, X.max()+2, 1)[:, np.newaxis]

>>> regr = regr.fit(X, y)

>>> y_lin_fit = regr.predict(X_fit)

>>> linear_r2 = r2_score(y, regr.predict(X))

>>> regr = regr.fit(X_quad, y)

>>> y_quad_fit = regr.predict(quadratic.fit_transform(X_fit))

>>> quadratic_r2 = r2_score(y, regr.predict(X_quad))

>>> regr = regr.fit(X_cubic, y)

>>> y_cubic_fit = regr.predict(cubic.fit_transform(X_fit))

>>> cubic_r2 = r2_score(y, regr.predict(X_cubic))

>>> # plot results

>>> plt.scatter(X, y, label='Training points', color='lightgray')

>>> plt.plot(X_fit, y_lin_fit,

... label=f'Linear (d=1), $R^2$={linear_r2:.2f}',

... color='blue',

... lw=2,

... linestyle=':')

>>> plt.plot(X_fit, y_quad_fit,


... label=f'Quadratic (d=2), $R^2$={quadratic_r2:.2f}',

... color='red',

... lw=2,

... linestyle='-')

>>> plt.plot(X_fit, y_cubic_fit,

... label=f'Cubic (d=3), $R^2$={cubic_r2:.2f}',

... color='green',

... lw=2,

... linestyle='--')

>>> plt.xlabel('Living area above ground in square feet')

>>> plt.ylabel('Sale price in U.S. dollars')

>>> plt.legend(loc='upper left')

>>> plt.show()
CopyExplain

The resulting plot is shown in Figure 9.13:


Figure 9.13: A comparison of different curves fitted to the sale price and living area
data

As we can see, using quadratic or cubic features does not really have an effect.
That’s because the relationship between the two variables appears to be linear.
So, let’s take a look at another feature, namely, . The variable rates the overall
quality of the material and finish of the houses and is given on a scale from 1 to 10,
where 10 is best:Overall QualOverall Qual

>>> X = df[['Overall Qual']].values

>>> y = df['SalePrice'].values
CopyExplain

After specifying the and variables, we can reuse the previous code and obtain the
plot in Figure 9.14:Xy
Figure 9.14: A linear, quadratic, and cubic fit on the sale price and house quality
data

Como você pode ver, os ajustes quadrático e cúbico capturam a relação entre os


preços de venda e a qualidade geral da casa melhor do que o ajuste linear. No
entanto, você deve estar ciente de que adicionar mais e mais recursos polinomiais
aumenta a complexidade de um modelo e, portanto, aumenta a chance de
sobreajuste. Assim, na prática, é sempre recomendável avaliar o desempenho do
modelo em um conjunto de dados de teste separado para estimar o desempenho
de generalização.

Lidando com relações não lineares


usando florestas aleatórias
Nesta seção, vamos analisar a regressão aleatória de florestas, que
é conceitualmente diferente dos modelos de regressão anteriores neste capítulo.
Uma floresta aleatória, que é um conjunto de múltiplas árvores de decisão, pode
ser entendida como a soma de funções lineares fragmentadas, em contraste com
os modelos de regressão linear e polinomial global que discutimos anteriormente.
Em outras palavras, através do algoritmo da árvore de decisão, subdividimos o
espaço de entrada em regiões menores que se tornam mais gerenciáveis.

Regressão da árvore de decisão


Uma vantagem do algoritmo de árvore de decisão é que ele trabalha com recursos
arbitrários e não requer nenhuma transformação dos recursos se estivermos
lidando com dados não lineares, porque as árvores de decisão analisam um
recurso de cada vez, em vez de levar em conta combinações ponderadas. (Da
mesma forma, normalizar ou padronizar recursos não é necessário para árvores
de decisão.) Como mencionado no Capítulo 3, A Tour of Machine Learning
Classifiers Using Scikit-Learn, cultivamos uma árvore de decisão dividindo
iterativamente seus nós até que as folhas estejam puras ou um critério de parada
seja satisfeito. Quando usamos árvores de decisão para classificação, definimos
entropia como uma medida de impureza para determinar qual divisão de
característica maximiza o ganho de informação (IG), que pode ser definido da
seguinte forma para uma divisão binária:

Aqui, xeu é o recurso para realizar a divisão, Np é o número de exemplos de


treinamento no nó pai, I é a função de impureza, Dp é o subconjunto de exemplos
de treinamento no nó pai e DEsquerda e DDireita são os subconjuntos de exemplos de
treinamento nos nós filho esquerdo e direito após a divisão. Lembre-se que nosso
objetivo é encontrar a divisão de recursos que maximiza o ganho de informações;
Em outras palavras, queremos encontrar a divisão de recursos que mais reduz as
impurezas nos nós filho. No Capítulo 3, discutimos a impureza de Gini e a entropia
como medidas de impureza, que são ambos critérios úteis para classificação. Para
usar uma árvore de decisão para regressão, no entanto, precisamos de uma
métrica de impureza que seja adequada para variáveis contínuas, então definimos
a medida de impureza de um nó, t, como o MSE em vez disso:
Aqui, Nt é o número de exemplos de treinamento no nó t, Dt é o subconjunto de

treinamento no nó t,   é o valor de destino verdadeiro e   é o valor de


destino previsto (média de amostra):

No contexto da regressão da árvore de decisão, o MSE é frequentemente referido


como variância intra-nó, razão pela qual o critério de divisão também é mais
conhecido como redução da variância.

Para ver como é o ajuste de linha de uma árvore de decisão, vamos usar o
implementado no scikit-learn para modelar a relação entre as variáveis e .
Observe que e não representam necessariamente uma relação não linear, mas
essa combinação de recursos ainda demonstra os aspectos gerais de uma árvore
de regressão muito bem:DecisionTreeRegressorSalePriceGr Living AreaSalePriceGr
Living Area

>>> from sklearn.tree import DecisionTreeRegressor

>>> X = df[['Gr Liv Area']].values

>>> y = df['SalePrice'].values

>>> tree = DecisionTreeRegressor(max_depth=3)

>>> tree.fit(X, y)

>>> sort_idx = X.flatten().argsort()


>>> lin_regplot(X[sort_idx], y[sort_idx], tree)

>>> plt.xlabel('Living area above ground in square feet')

>>> plt.ylabel('Sale price in U.S. dollars')>>> plt.show()


CopyExplain

Como você pode ver no gráfico resultante, a árvore de decisão captura a


tendência geral nos dados. E podemos imaginar que uma árvore de regressão
também poderia capturar tendências em dados não lineares relativamente bem.
No entanto, uma limitação desse modelo é que ele não captura a continuidade e
diferenciabilidade da previsão desejada. Além disso, precisamos ter cuidado ao
escolher um valor apropriado para a profundidade da árvore, de modo a não
sobreajustar ou subajustar os dados; Aqui, uma profundidade de três parecia ser
uma boa escolha.

Figura 9.15: Gráfico de regressão da árvore de decisão

Você é encorajado a experimentar árvores de decisão mais profundas. Observe


que a relação entre e é bastante linear, portanto, você também é encorajado a
aplicar a árvore de decisão à variável.Gr Living AreaSalePriceOverall Qual
Na próxima seção, veremos uma maneira mais robusta de ajustar árvores de
regressão: florestas aleatórias.

Regressão aleatória de florestas


Como você aprendeu no Capítulo 3, o algoritmo de floresta aleatória é uma
técnica de conjunto que combina várias árvores de decisão. Uma floresta aleatória
geralmente tem um melhor desempenho de generalização do que uma árvore de
decisão individual devido à aleatoriedade, o que ajuda a diminuir a variância do
modelo. Outras vantagens das florestas aleatórias são que elas são menos
sensíveis a outliers no conjunto de dados e não exigem muito ajuste de
parâmetros. O único parâmetro em florestas aleatórias que normalmente
precisamos experimentar é o número de árvores no conjunto. O algoritmo básico
de floresta aleatória para regressão é quase idêntico ao algoritmo de floresta
aleatória para classificação que discutimos no Capítulo 3. A única diferença é que
usamos o critério MSE para aumentar as árvores de decisão individuais, e a
variável de destino prevista é calculada como a previsão média em todas as
árvores de decisão.

Agora, vamos usar todos os recursos do conjunto de dados Ames Housing para
ajustar um modelo de regressão de floresta aleatória em 70% dos exemplos e
avaliar seu desempenho nos 30% restantes, como fizemos anteriormente na
seção Avaliando o desempenho de modelos de regressão linear. O código é o
seguinte:

>>> target = 'SalePrice'

>>> features = df.columns[df.columns != target]

>>> X = df[features].values

>>> y = df[target].values

>>> X_train, X_test, y_train, y_test = train_test_split(

... X, y, test_size=0.3, random_state=123)

>>> from sklearn.ensemble import RandomForestRegressor

>>> forest = RandomForestRegressor(


... n_estimators=1000,

... criterion='squared_error',

... random_state=1,

... n_jobs=-1)

>>> forest.fit(X_train, y_train)

>>> y_train_pred = forest.predict(X_train)

>>> y_test_pred = forest.predict(X_test)

>>> mae_train = mean_absolute_error(y_train, y_train_pred)

>>> mae_test = mean_absolute_error(y_test, y_test_pred)

>>> print(f'MAE train: {mae_train:.2f}')

MAE train: 8305.18

>>> print(f'MAE test: {mae_test:.2f}')

MAE test: 20821.77

>>> r2_train = r2_score(y_train, y_train_pred)

>>> r2_test =r2_score(y_test, y_test_pred)

>>> print(f'R^2 train: {r2_train:.2f}')

R^2 train: 0.98

>>> print(f'R^2 test: {r2_test:.2f}')

R^2 test: 0.85


CopyExplain

Infelizmente, você pode ver que a floresta aleatória tende a sobreajustar os dados


de treinamento. No entanto, ainda é capaz de explicar relativamente bem a

relação entre as variáveis alvo e explicativas (  no conjunto


de dados do teste). Para comparação, o modelo linear da seção
anterior, Avaliando o desempenho de modelos de regressão linear, que foi
ajustado ao mesmo conjunto de dados, foi menos superajustado, mas teve pior

desempenho no conjunto de testes ( ).


Por fim, vamos também dar uma olhada nos resíduos da previsão:

>>> x_max = np.max([np.max(y_train_pred), np.max(y_test_pred)])

>>> x_min = np.min([np.min(y_train_pred), np.min(y_test_pred)])

>>> fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(7, 3), sharey=True)

>>> ax1.scatter(y_test_pred, y_test_pred - y_test,

... c='limegreen', marker='s', edgecolor='white',

... label='Test data')

>>> ax2.scatter(y_train_pred, y_train_pred - y_train,

... c='steelblue', marker='o', edgecolor='white',

... label='Training data')

>>> ax1.set_ylabel('Residuals')

>>> for ax in (ax1, ax2):

... ax.set_xlabel('Predicted values')

... ax.legend(loc='upper left')

... ax.hlines(y=0, xmin=x_min-100, xmax=x_max+100,

... color='black', lw=2)

>>> plt.tight_layout()

>>> plt.show()
CopyExplain

Como já foi resumido pelo R2 coeficiente, você pode ver que o modelo se ajusta
melhor aos dados de treinamento do que aos dados de teste, conforme indicado
pelos outliers na direção do eixo y. Além disso, a distribuição dos resíduos não
parece ser completamente aleatória em torno do ponto central zero, indicando que
o modelo não é capaz de capturar todas as informações exploratórias. No entanto,
o gráfico residual indica uma grande melhoria em relação ao gráfico residual do
modelo linear que plotamos anteriormente neste capítulo.
Figura 9.16: Os resíduos da regressão aleatória da floresta

Idealmente, nosso erro de modelo deve ser aleatório ou imprevisível. Em outras


palavras, o erro das previsões não deve estar relacionado a nenhuma das
informações contidas nas variáveis explicativas; em vez disso, deve refletir a
aleatoriedade das distribuições ou padrões do mundo real. Se encontrarmos
padrões nos erros de previsão, por exemplo, inspecionando o gráfico residual, isso
significa que os gráficos residuais contêm informações preditivas. Uma razão
comum para isso pode ser que informações explicativas estão vazando para
esses resíduos.

Infelizmente, não há uma abordagem universal para lidar com a não-aleatoriedade


em parcelas residuais, e isso requer experimentação. Dependendo dos dados que
estão disponíveis para nós, podemos ser capazes de melhorar o modelo
transformando variáveis, ajustando os hiperparâmetros do algoritmo de
aprendizagem, escolhendo modelos mais simples ou mais complexos, removendo
outliers ou incluindo variáveis adicionais.

Amazon Athena provides great flexibility to run queries without adding any complexity
to your project. Moreover, it is a very fast service and your queries return results in a
matter of seconds, even on large datasets. Which of the following facts is NOT true
about Amazon Athena?

Amazon Athena does not require the AWS Glue Data Catalog to register and query S3
data.
No infrastructure is needed to set up Amazon Athena.

It is an interactive query service.

It can be used to analyze data in Amazon S3 using standard SQL.


5.
Within the AWS ecosystem, Data Wrangler is an agile service to load and unload data
from data lakes and databases. What are other capabilities of this service? (Choose all
that apply.)

It extends the power of the NumPy library to AWS.

It connects pandas dataframes and other AWS services.

It extends the power of the pandas library to AWS.

It can be used for loading and unloading data from data lakes and databases.
6.
You work as a Machine Learning engineer in a company and are asked to develop
algorithms to solve 3 tasks:
Task 1: You have a large dataset of unstructured text information. You are asked to
convert/summarize this information into reports.
Task 2: A business has experienced a huge surge in the number of customers. To
improve customer support and engagement, you are asked to build a chatbot.
Task 3: To simplify paperwork and time, you are asked to automate an employee
expense system by building an image scanning system for expense receipts.
Which of these will you treat as Natural Language Processing (NLP) problems?

Tasks 1 and 3

Tasks 1 and 2

All 3 tasks

Task 1 only

Trabalhando com dados não rotulados –


Análise de clustering
Nos capítulos anteriores, usamos técnicas de aprendizado supervisionado para
construir modelos de aprendizado de máquina, usando dados onde a resposta já
era conhecida – os rótulos de classe já estavam disponíveis em nossos dados de
treinamento. Neste capítulo, vamos mudar de marcha e explorar a análise de
cluster, uma categoria de técnicas de aprendizagem não supervisionada que
nos permite descobrir estruturas ocultas em dados onde não sabemos a resposta
certa antecipadamente. O objetivo do clustering é encontrar um agrupamento
natural nos dados para que os itens no mesmo cluster sejam mais semelhantes
entre si do que com aqueles de clusters diferentes.

Dada a sua natureza exploratória, o agrupamento é um tópico interessante e,


neste capítulo, você aprenderá sobre os seguintes conceitos, que podem nos
ajudar a organizar os dados em estruturas significativas:

 Encontrando centros de similaridade usando o popular algoritmo k-means


 Adotando uma abordagem de baixo para cima para a construção de árvores
de agrupamento hierárquico

 Identificando formas arbitrárias de objetos usando uma abordagem de


clustering baseada em densidade

Agrupando objetos por similaridade


usando k-means
Nesta seção, vamos aprender sobre um dos algoritmos de clustering mais
populares, k-means, que é amplamente utilizado na academia, bem como na
indústria. Clustering (ou análise de cluster) é uma técnica que nos permite
encontrar grupos de objetos semelhantes que estão mais relacionados entre si do
que com objetos em outros grupos. Exemplos de aplicativos de clustering
orientados a negócios incluem o agrupamento de documentos, músicas e filmes
por tópicos diferentes ou encontrar clientes que compartilham interesses
semelhantes com base em comportamentos de compra comuns como base para
mecanismos de recomendação.
k-means clustering usando scikit-learn
Como você verá daqui a pouco, o algoritmo k-means é extremamente fácil
de implementar, mas também é computacionalmente muito eficiente em
comparação com outros algoritmos de clustering, o que pode explicar sua
popularidade. O algoritmo k-means pertence à categoria de clustering baseado
em protótipo.

Discutiremos duas outras categorias de agrupamento,


agrupamento hierárquico e agrupamento baseado em densidade, mais adiante
neste capítulo.

Clustering baseado em protótipo significa que cada cluster é representado por um


protótipo, que geralmente é o centroide (média) de pontos semelhantes com
características contínuas, ou o medoide (o mais representativo ou o ponto que
minimiza a distância para todos os outros pontos que pertencem a um
determinado cluster) no caso de características categóricas. Enquanto k-means é
muito bom em identificar clusters com uma forma esférica, uma das desvantagens
deste algoritmo de agrupamento é que temos que especificar o número de
clusters, k, a priori. Uma escolha inadequada para k pode resultar em baixo
desempenho de clustering. Mais adiante neste capítulo, discutiremos o
método do cotovelo e os gráficos de silhuetas, que são técnicas úteis para
avaliar a qualidade de um agrupamento para nos ajudar a determinar o número
ótimo de agrupamentos, k.

Embora o clustering k-means possa ser aplicado a dados em dimensões mais


altas, examinaremos os exemplos a seguir usando um conjunto de dados
bidimensional simples para fins de visualização:

>>> from sklearn.datasets import make_blobs

>>> X, y = make_blobs(n_samples=150,

... n_features=2,

... centers=3,

... cluster_std=0.5,
... shuffle=True,

... random_state=0)

>>> import matplotlib.pyplot as plt

>>> plt.scatter(X[:, 0],

... X[:, 1],

... c='white',

... marker='o',

... edgecolor='black',

... s=50)

>>> plt.xlabel('Feature 1')

>>> plt.ylabel('Feature 2')

>>> plt.grid()

>>> plt.tight_layout()

>>> plt.show()
CopyExplain

O conjunto de dados que acabamos de criar consiste em 150 pontos


gerados aleatoriamente que são agrupados aproximadamente em três regiões
com maior densidade, que é visualizada por meio de um gráfico de dispersão
bidimensional:
Figura 10.1: Um gráfico de dispersão do nosso conjunto de dados não rotulado

Em aplicações do mundo real de clustering, não temos nenhuma informação de


categoria de verdade-base (informação fornecida como evidência empírica em
oposição à inferência) sobre esses exemplos; Se nos dessem rótulos de classe,
essa tarefa se enquadraria na categoria de aprendizagem supervisionada. Assim,
nosso objetivo é agrupar os exemplos com base em suas similaridades de
características, o que pode ser obtido usando o algoritmo k-means, como
resumido pelos quatro passos a seguir:

1. Escolha aleatoriamente k centroides dos exemplos como centros de cluster


iniciais
2. Atribua cada exemplo ao centroide mais

próximo, 
3. Mover os centroides para o centro dos exemplos que lhe foram atribuídos
4. Repita as etapas 2 e 3 até que as atribuições de cluster não sejam alteradas
ou uma tolerância definida pelo usuário ou um número máximo de iterações
seja atingido
Agora, a próxima pergunta é: como medimos a semelhança entre objetos?
Podemos definir similaridade como o oposto da distância, e uma distância
comumente usada para agrupar exemplos com características contínuas
é a distância euclidiana quadrada entre dois pontos, x e y, no espaço m-
dimensional:

Note que, na equação anterior, o índice j refere-se à jésima dimensão (coluna de


feição) das entradas de exemplo, x e y. No restante desta seção, usaremos os
sobrescritos i e j para nos referirmos ao índice do exemplo (registro de dados) e
ao índice de cluster, respectivamente.

Com base nessa métrica de distância euclidiana, podemos descrever o algoritmo


k-means como um problema simples de otimização, uma abordagem iterativa para
minimizar a soma intra-cluster de erros quadrados (SSE), que às vezes também
é chamada de inércia de cluster:

Aqui,   está o ponto representativo (centroide) para o cluster j. w(eu, j) = 1 se


o exemplo, x(eu), está no cluster j ou 0 caso contrário.
Agora que você aprendeu como o algoritmo k-means simples funciona, vamos
aplicá-lo ao nosso conjunto de dados de exemplo usando a classe do módulo
scikit-learn:KMeanscluster

>>> from sklearn.cluster import KMeans

>>> km = KMeans(n_clusters=3,

... init='random',

... n_init=10,

... max_iter=300,

... tol=1e-04,

... random_state=0)

>>> y_km = km.fit_predict(X)


CopyExplain

Usando o código anterior, definimos o número de clusters desejados como ; Ter


que especificar o número de clusters a priori é uma das limitações do k-means.
Nós definimos para executar os algoritmos de agrupamento k-means 10 vezes
independentemente, com diferentes centroides aleatórios para escolher o modelo
final como aquele com o menor SSE. Através do parâmetro, especificamos o
número máximo de iterações para cada execução única (aqui, ). Observe que a
implementação k-means no scikit-learn pára cedo se convergir antes que o
número máximo de iterações seja atingido. No entanto, é possível que k-means
não atinja convergência para uma determinada corrida, o que pode ser
problemático (computacionalmente caro) se escolhermos valores relativamente
grandes para . Uma maneira de lidar com problemas de convergência é escolher
valores maiores para , que é um parâmetro que controla a tolerância em relação
às mudanças no SSE dentro do cluster para declarar convergência. No código
anterior, escolhemos uma tolerância de
(=0,0001).3n_init=10max_iter300max_itertol1e-04
Um problema com k-means é que um ou mais clusters podem estar vazios.
Note que esse problema não existe para k-medoids ou C-means difusos, um
algoritmo que discutiremos mais adiante nesta seção. No entanto, esse problema
é explicado na implementação atual de k-means no scikit-learn. Se um cluster
estiver vazio, o algoritmo procurará o exemplo que está mais distante do centroide
do cluster vazio. Em seguida, ele redesignará o centroide para ser esse ponto
mais distante.

Dimensionamento de recursos

Quando estamos aplicando k-means a dados do mundo real usando uma métrica
de distância euclidiana, queremos garantir que os recursos sejam medidos na
mesma escala e aplicar padronização de escore z ou escala min-max, se
necessário.

Tendo previsto os rótulos de cluster, e discutido alguns dos desafios do algoritmo


k-means, vamos agora visualizar os clusters que k-means identificaram no
conjunto de dados junto com os centroides de cluster. Eles são armazenados sob
o atributo do objeto ajustado:y_kmcluster_centers_KMeans

>>> plt.scatter(X[y_km == 0, 0],

... X[y_km == 0, 1],

... s=50, c='lightgreen',

... marker='s', edgecolor='black',

... label='Cluster 1')

>>> plt.scatter(X[y_km == 1, 0],

... X[y_km == 1, 1],

... s=50, c='orange',

... marker='o', edgecolor='black',

... label='Cluster 2')

>>> plt.scatter(X[y_km == 2, 0],

... X[y_km == 2, 1],


... s=50, c='lightblue',

... marker='v', edgecolor='black',

... label='Cluster 3')

>>> plt.scatter(km.cluster_centers_[:, 0],

... km.cluster_centers_[:, 1],

... s=250, marker='*',

... c='red', edgecolor='black',

... label='Centroids')

>>> plt.xlabel('Feature 1')

>>> plt.ylabel('Feature 2')

>>> plt.legend(scatterpoints=1)

>>> plt.grid()

>>> plt.tight_layout()

>>> plt.show()
CopyExplain

In Figure 10.2, you can see that k-means placed the three centroids at the center
of each sphere, which looks like a reasonable grouping given this dataset:
Figure 10.2: The k-means clusters and their centroids

Although k-means worked well on this toy dataset, we still have the drawback of
having to specify the number of clusters, k, a priori. The number of clusters to
choose may not always be so obvious in real-world applications, especially if we
are working with a higher-dimensional dataset that cannot be visualized. The other
properties of k-means are that clusters do not overlap and are not hierarchical, and
we also assume that there is at least one item in each cluster. Later in this chapter,
we will encounter different types of clustering algorithms, hierarchical and density-
based clustering. Neither type of algorithm requires us to specify the number of
clusters upfront or assume spherical structures in our dataset.

In the next subsection, we will cover a popular variant of the classic k-means
algorithm called k-means++. While it doesn’t address those assumptions and
drawbacks of k-means that were discussed in the previous paragraph, it can
greatly improve the clustering results through more clever seeding of the initial
cluster centers.
A smarter way of placing the initial cluster
centroids using k-means++
So far, we have discussed the classic k-means algorithm, which uses a random
seed to place the initial centroids, which can sometimes result in bad clusterings or
slow convergence if the initial centroids are chosen poorly. One way to address
this issue is to run the k-means algorithm multiple times on a dataset and choose
the best-performing model in terms of the SSE.

Another strategy is to place the initial centroids far away from each other via the k-
means++ algorithm, which leads to better and more consistent results than the
classic k-means (k-means++: The Advantages of Careful Seeding by D.
Arthur and S. Vassilvitskii in Proceedings of the eighteenth annual ACM-SIAM
symposium on Discrete algorithms, pages 1027-1035. Society for Industrial and
Applied Mathematics, 2007).

The initialization in k-means++ can be summarized as follows:

1. Initialize an empty set, M, to store the k centroids being selected.

2. Randomly choose the first centroid,  , from the input examples and
assign it to M.
3. For each example, x(i), that is not in M, find the minimum squared
distance, d(x(i), M)2, to any of the centroids in M.

4. To randomly select the next centroid,  , use a weighted probability

distribution equal to  . For instance, we collect all


points in an array and choose a weighted random sampling, such that the
larger the squared distance, the more likely a point gets chosen as the
centroid.
5. Repeat steps 3 and 4 until k centroids are chosen.
6. Proceed with the classic k-means algorithm.

To use k-means++ with scikit-learn’s object, we just need to set the parameter to .


In fact, is the default argument to the parameter, which is strongly recommended in
practice. The only reason we didn’t use it in the previous example was to not
introduce too many concepts all at once. The rest of this section on k-means
will use k-means++, but you are encouraged to experiment more with the two
different approaches (classic k-means via versus k-means++ via ) for placing the
initial cluster centroids.KMeansinit'k-means++''k-means++'initinit='random'init='k-
means++'

Hard versus soft clustering


Hard clustering describes a family of algorithms where each example in a dataset
is assigned to exactly one cluster, as in the k-means and k-means++
algorithms that we discussed earlier in this chapter. In contrast, algorithms for soft
clustering (sometimes also called fuzzy clustering) assign an example to one or
more clusters. A popular example of soft clustering is the fuzzy C-means (FCM)
algorithm (also called soft k-means or fuzzy k-means). The original idea goes
back to the 1970s, when Joseph C. Dunn first proposed an early version of fuzzy
clustering to improve k-means (A Fuzzy Relative of the ISODATA Process and Its
Use in Detecting Compact Well-Separated Clusters, 1973). Almost a decade later,
James C. Bedzek published his work on the improvement of the fuzzy clustering
algorithm, which is now known as the FCM algorithm (Pattern Recognition with
Fuzzy Objective Function Algorithms, Springer Science+Business Media, 2013).

The FCM procedure is very similar to k-means. However, we replace the hard
cluster assignment with probabilities for each point belonging to each cluster. In k-
means, we could express the cluster membership of an example, x, with a sparse
vector of binary values:
Here, the index position with value 1 indicates the cluster centroid,  , that

the example is assigned to (assuming k = 3,  ). In


contrast, a membership vector in FCM could be represented as follows:

Here, each value falls in the range [0, 1] and represents a probability of
membership of the respective cluster centroid. The sum of the memberships for a
given example is equal to 1. As with the k-means algorithm, we can summarize the
FCM algorithm in four key steps:

1. Specify the number of k centroids and randomly assign the cluster


memberships for each point

2. Compute the cluster centroids, 


3. Update the cluster memberships for each point
4. Repeat steps 2 and 3 until the membership coefficients do not change or a
user-defined tolerance or maximum number of iterations is reached
The objective function of FCM—we abbreviate it as Jm—looks very similar to the
within-cluster SSE that we minimize in k-means:

However, note that the membership indicator, w(i, j), is not a binary value as in k-

means ( ), but a real value that denotes the cluster

membership probability ( ). You also may have


noticed that we added an additional exponent to w(i, j); the exponent m, any number
greater than or equal to one (typically m = 2), is the so-called fuzziness
coefficient (or simply fuzzifier), which controls the degree of fuzziness.

The larger the value of m, the smaller the cluster membership, w(i, j), becomes,
which leads to fuzzier clusters. The cluster membership probability itself is
calculated as follows:

For example, if we chose three cluster centers, as in the previous k-means

example, we could calculate the membership of   belonging to the   


cluster as follows:
The center,  , of a cluster itself is calculated as the mean of all examples
weighted by the degree to which each example belongs to that cluster (

):

Just by looking at the equation to calculate the cluster memberships, we can say
that each iteration in FCM is more expensive than an iteration in k-means. On the
other hand, FCM typically requires fewer iterations overall to reach convergence.
However, it has been found, in practice, that both k-means and FCM produce very
similar clustering outputs, as described in a study (Comparative Analysis of k-
means and Fuzzy C-Means Algorithms by S. Ghosh and S. K. Dubey, IJACSA, 4:
35–38, 2013). Unfortunately, the FCM algorithm is not implemented in scikit-learn
currently, but interested readers can try out the FCM implementation from
the scikit-fuzzy package, which is available at https://github.com/scikit-fuzzy/scikit-
fuzzy.

Using the elbow method to find the optimal number


of clusters
One of the main challenges in unsupervised learning is that we do not know the
definitive answer. We don’t have the ground-truth class labels in our dataset that
allow us to apply the techniques that we used in Chapter 6, Learning Best
Practices for Model Evaluation and Hyperparameter Tuning, to evaluate the
performance of a supervised model. Thus, to quantify the quality of clustering, we
need to use intrinsic metrics—such as the within-cluster SSE (distortion)—to
compare the performance of different k-means clustering models.

Conveniently, we don’t need to compute the within-cluster SSE explicitly when we


are using scikit-learn, as it is already accessible via the attribute after fitting
a model:inertia_KMeans

>>> print(f'Distortion: {km.inertia_:.2f}')

Distortion: 72.48
CopyExplain

Based on the within-cluster SSE, we can use a graphical tool, the so-called elbow
method, to estimate the optimal number of clusters, k, for a given task. We can
say that if k increases, the distortion will decrease. This is because the examples
will be closer to the centroids they are assigned to. The idea behind the elbow
method is to identify the value of k where the distortion begins to increase most
rapidly, which will become clearer if we plot the distortion for different values of k:

>>> distortions = []

>>> for i in range(1, 11):

... km = KMeans(n_clusters=i,

... init='k-means++',

... n_init=10,

... max_iter=300,

... random_state=0)

... km.fit(X)

... distortions.append(km.inertia_)

>>> plt.plot(range(1,11), distortions, marker='o')

>>> plt.xlabel('Number of clusters')

>>> plt.ylabel('Distortion')

>>> plt.tight_layout()
>>> plt.show()
CopyExplain

Como você pode ver na Figura 10.3, o cotovelo está localizado em k = 3, então


isso é uma evidência de apoio de que k = 3 é realmente uma boa escolha para
este conjunto de dados:

Figura 10.3: Encontrar o número ideal de agrupamentos utilizando o método do


cotovelo

Quantificando a qualidade do agrupamento


através de gráficos de silhuetas
Outra métrica intrínseca para avaliar a qualidade de um clustering é a análise de
silhuetas, que também pode ser aplicada a algoritmos de clustering diferentes de
k-means que discutiremos mais adiante neste capítulo. A análise de silhuetas
pode ser usada como uma ferramenta gráfica para traçar uma medida de quão
agrupados estão os exemplos nos clusters. Para calcular o coeficiente de
silhueta de um único exemplo em nosso conjunto de dados, podemos aplicar as
três etapas a seguir:

1. Calcular a coesão do cluster, a(eu), como a distância média entre um


exemplo, x(eu)e todos os outros pontos no mesmo cluster.
2. Calcular a separação do cluster, b(eu), do próximo cluster mais próximo como
a distância média entre o exemplo, x(eu)e todos os exemplos no cluster mais
próximo.
3. Calcule a silhueta, s(eu), como a diferença entre coesão de agrupamento e
separação dividida pelo maior dos dois, como mostrado aqui:

O coeficiente de silhueta é limitado no intervalo –1 a 1. Com base na equação


anterior, podemos observar que o coeficiente de silhueta é 0 se a separação e a
coesão do agrupamento forem iguais (b(eu) = um(eu)). Além disso, aproximamo-nos
de um coeficiente de silhueta ideal de 1 se b(eu) >> um(eu), uma vez
que b(eu) quantifica o quão diferente é um exemplo de outros clusters, e um(eu) nos
diz como ele é semelhante aos outros exemplos em seu próprio cluster.

O coeficiente de silhueta está disponível a partir do módulo scikit-learn e,


opcionalmente, a função pode ser importada por conveniência. A função calcula o
coeficiente médio da silhueta em todos os exemplos, que é equivalente a . Ao
executar o código a seguir, agora criaremos um gráfico dos coeficientes de
silhueta para um agrupamento k-means com k =
3:silhouette_samplesmetricsilhouette_scoressilhouette_scoresnumpy.mean(silhouett
e_samples(...))

>>> km = KMeans(n_clusters=3,

... init='k-means++',

... n_init=10,

... max_iter=300,
... tol=1e-04,

... random_state=0)

>>> y_km = km.fit_predict(X)

>>> import numpy as np

>>> from matplotlib import cm

>>> from sklearn.metrics import silhouette_samples

>>> cluster_labels = np.unique(y_km)

>>> n_clusters = cluster_labels.shape[0]

>>> silhouette_vals = silhouette_samples(

... X, y_km, metric='euclidean'

... )

>>> y_ax_lower, y_ax_upper = 0, 0

>>> yticks = []

>>> for i, c in enumerate(cluster_labels):

... c_silhouette_vals = silhouette_vals[y_km == c]

... c_silhouette_vals.sort()

... y_ax_upper += len(c_silhouette_vals)

... color = cm.jet(float(i) / n_clusters)

... plt.barh(range(y_ax_lower, y_ax_upper),

... c_silhouette_vals,

... height=1.0,

... edgecolor='none',

... color=color)

... yticks.append((y_ax_lower + y_ax_upper) / 2.)

... y_ax_lower += len(c_silhouette_vals)

>>> silhouette_avg = np.mean(silhouette_vals)


>>> plt.axvline(silhouette_avg,

... color="red",

... linestyle="--")

>>> plt.yticks(yticks, cluster_labels + 1)

>>> plt.ylabel('Cluster')

>>> plt.xlabel('Silhouette coefficient')

>>> plt.tight_layout()

>>> plt.show()
CopyExplain

Através de uma inspeção visual do gráfico de silhuetas, podemos examinar


rapidamente os tamanhos dos diferentes agrupamentos e identificar clusters que
contêm outliers:

Figura 10.4: Um gráfico de silhuetas para um bom exemplo de agrupamento


No entanto, como você pode ver no gráfico de silhueta anterior, os coeficientes de
silhueta não estão próximos de 0 e estão aproximadamente igualmente longe do
escore médio de silhuetas, que é, neste caso, um indicador de bom agrupamento.
Além disso, para resumir a bondade de nosso agrupamento, adicionamos o
coeficiente médio de silhueta ao gráfico (linha pontilhada).

Para ver como é um gráfico de silhueta para um agrupamento relativamente ruim,


vamos semear o algoritmo k-means com apenas dois centroides:

>>> km = KMeans(n_clusters=2,

... init='k-means++',

... n_init=10,

... max_iter=300,

... tol=1e-04,

... random_state=0)

>>> y_km = km.fit_predict(X)

>>> plt.scatter(X[y_km == 0, 0],

... X[y_km == 0, 1],

... s=50, c='lightgreen',

... edgecolor='black',

... marker='s',

... label='Cluster 1')

>>> plt.scatter(X[y_km == 1, 0],

... X[y_km == 1, 1],

... s=50,

... c='orange',

... edgecolor='black',

... marker='o',

... label='Cluster 2')


>>> plt.scatter(km.cluster_centers_[:, 0],

... km.cluster_centers_[:, 1],

... s=250,

... marker='*',

... c='red',

... label='Centroids')

>>> plt.xlabel('Feature 1')

>>> plt.ylabel('Feature 2')

>>> plt.legend()

>>> plt.grid()

>>> plt.tight_layout()

>>> plt.show()
CopyExplain

Como você pode ver na Figura 10.5, um dos centroides fica entre dois dos três
agrupamentos esféricos dos dados de entrada.

Embora o agrupamento não pareça completamente terrível, ele é subótimo:


Figura 10.5: Um exemplo subótimo de agrupamento

Tenha em mente que normalmente não temos o luxo de visualizar conjuntos de


dados em gráficos de dispersão bidimensionais em problemas do mundo real, já
que normalmente trabalhamos com dados em dimensões mais altas. Então, a
seguir, vamos criar o gráfico de silhuetas para avaliar os resultados:

>>> cluster_labels = np.unique(y_km)

>>> n_clusters = cluster_labels.shape[0]

>>> silhouette_vals = silhouette_samples(

... X, y_km, metric='euclidean'

... )

>>> y_ax_lower, y_ax_upper = 0, 0

>>> yticks = []

>>> for i, c in enumerate(cluster_labels):

... c_silhouette_vals = silhouette_vals[y_km == c]

... c_silhouette_vals.sort()
... y_ax_upper += len(c_silhouette_vals)

... color = cm.jet(float(i) / n_clusters)

... plt.barh(range(y_ax_lower, y_ax_upper),

... c_silhouette_vals,

... height=1.0,

... edgecolor='none',

... color=color)

... yticks.append((y_ax_lower + y_ax_upper) / 2.)

... y_ax_lower += len(c_silhouette_vals)

>>> silhouette_avg = np.mean(silhouette_vals)

>>> plt.axvline(silhouette_avg, color="red", linestyle="--")

>>> plt.yticks(yticks, cluster_labels + 1)

>>> plt.ylabel('Cluster')

>>> plt.xlabel('Silhouette coefficient')

>>> plt.tight_layout()

>>> plt.show()
CopyExplain

Como você pode ver na Figura 10.6, as silhuetas agora têm comprimentos e
larguras visivelmente diferentes, o que é evidência de um agrupamento
relativamente ruim ou pelo menos subótimo:
Figura 10.6: Um gráfico de silhuetas para um exemplo subótimo de agrupamento

Agora, depois de termos obtido uma boa compreensão de como o clustering


funciona, a próxima seção introduzirá o agrupamento hierárquico como uma
abordagem alternativa para k-means.

Organizando clusters como uma árvore


hierárquica
Nesta seção, veremos uma abordagem alternativa para clustering baseado em
protótipo: cluster hierárquico. Uma vantagem do algoritmo de agrupamento
hierárquico é que ele nos permite plotar dendrogramas (visualizações de um
agrupamento hierárquico binário), o que pode ajudar na interpretação dos
resultados criando taxonomias significativas. Outra vantagem dessa abordagem
hierárquica é que não precisamos especificar o número de clusters
antecipadamente.
As duas principais abordagens para o agrupamento hierárquico são o
agrupamento hierárquico aglomerativo e o agrupamento hierárquico divisivo. No
agrupamento hierárquico divisivo, começamos com um cluster que engloba o
conjunto de dados completo e dividimos iterativamente o cluster em clusters
menores até que cada cluster contenha apenas um exemplo. Nesta seção,
enfocaremos a aglomeração aglomerativa, que adota a abordagem oposta.
Começamos com cada exemplo como um cluster individual e mesclamos os pares
de clusters mais próximos até que reste apenas um cluster.

Agrupando clusters de baixo para cima


Os dois algoritmos padrão para agrupamento hierárquico
aglomerativo são ligação única e ligação completa. Usando ligação única,
calculamos as distâncias entre os membros mais semelhantes para cada par de
clusters e mesclamos os dois clusters para os quais a distância entre os membros
mais semelhantes é a menor. A abordagem de ligação completa é semelhante à
ligação única, mas, em vez de comparar os membros mais semelhantes em cada
par de clusters, comparamos os membros mais diferentes para realizar a fusão.
Isso é mostrado na Figura 10.7:
Figura 10.7: A abordagem completa da ligação

Tipos alternativos de ligações

Outros algoritmos comumente usados para agrupamento hierárquico aglomerativo


incluem ligação média e ligação de Ward. Na ligação média, mesclamos os pares
de clusters com base nas distâncias médias mínimas entre todos os membros do
grupo nos dois clusters. Na ligação de Ward, os dois clusters que levam ao
aumento mínimo do total dentro do cluster SSE são fundidos.

Nesta seção, nos concentraremos no agrupamento de aglomerados usando a


abordagem de ligação completa. O agrupamento de vinculação hierárquica
completa é um procedimento iterativo que pode ser resumido pelas seguintes
etapas:

1. Calcule uma matriz de distância em pares de todos os exemplos.


2. Represente cada ponto de dados como um cluster singleton.
3. Mescle os dois clusters mais próximos com base na distância entre os
membros mais diferentes (distantes).
4. Atualize a matriz de vinculação do cluster.
5. Repita as etapas de 2 a 4 até que um único cluster permaneça.

A seguir, discutiremos como calcular a matriz de distâncias (etapa 1). Mas


primeiro, vamos gerar uma amostra de dados aleatória para trabalhar. As linhas
representam observações diferentes (IDs 0-4), e as colunas são as diferentes
características (, , ) desses exemplos:XYZ

>>> import pandas as pd

>>> import numpy as np

>>> np.random.seed(123)

>>> variables = ['X', 'Y', 'Z']

>>> labels = ['ID_0', 'ID_1', 'ID_2', 'ID_3', 'ID_4']

>>> X = np.random.random_sample([5, 3])*10

>>> df = pd.DataFrame(X, columns=variables, index=labels)


>>> df
CopyExplain

Depois de executar o código anterior, agora devemos ver o seguinte contendo os


exemplos gerados aleatoriamente:DataFrame

Figura 10.8: Uma amostra de dados gerada aleatoriamente

Executando agrupamento hierárquico em uma


matriz de distância
Para calcular a matriz de distância como entrada para o algoritmo de agrupamento
hierárquico, usaremos a função do submódulo do SciPy:pdistspatial.distance

>>> from scipy.spatial.distance import pdist, squareform

>>> row_dist = pd.DataFrame(squareform(

... pdist(df, metric='euclidean')),

... columns=labels, index=labels)

>>> row_dist
CopyExplain
Usando o código anterior, calculamos a distância euclidiana entre cada par de
exemplos de entrada em nosso conjunto de dados com base nos recursos , e . XYZ

Fornecemos a matriz de distância condensada — retornada por — como entrada


para a função para criar uma matriz simétrica das distâncias pareadas, conforme
mostrado aqui:pdistsquareform

Figura 10.9: As distâncias calculadas em pares dos nossos dados

Em seguida, aplicaremos a aglomeração de ligação completa aos nossos clusters


usando a função do submódulo do SciPy, que retorna uma chamada matriz de
ligação.linkagecluster.hierarchy

No entanto, antes de chamar a função, vamos dar uma olhada cuidadosa na


documentação da função:linkage

>>> from scipy.cluster.hierarchy import linkage

>>> help(linkage)

[...]

Parameters:

y : ndarray

A condensed or redundant distance matrix. A condensed


distance matrix is a flat array containing the upper

triangular of the distance matrix. This is the form

that pdist returns. Alternatively, a collection of m

observation vectors in n dimensions may be passed as

an m by n array.

method : str, optional

The linkage algorithm to use. See the Linkage Methods

section below for full descriptions.

metric : str, optional

The distance metric to use. See the distance.pdist

function for a list of valid distance metrics.

Returns:

Z : ndarray

The hierarchical clustering encoded as a linkage matrix.

[...]
CopyExplain

Com base na descrição da função, entendemos que podemos usar uma matriz de
distância condensada (triangular superior) da função como um atributo de entrada.
Alternativamente, também podemos fornecer a matriz de dados inicial e usar a
métrica como um argumento de função no . No entanto, não devemos usar a
matriz de distância que definimos anteriormente, pois ela produziria valores de
distância diferentes do esperado. Para resumir, os três cenários possíveis estão
listados aqui:pdist'euclidean'linkagesquareform

 Abordagem incorreta: usar a matriz de distância, conforme mostrado no


trecho de código a seguir, leva a resultados incorretos: squareform
 >>> row_clusters = linkage(row_dist,
 ... method='complete',

 ... metric='euclidean')
CopyExplain
 Abordagem correta: O uso da matriz de distância condensada, conforme
mostrado no exemplo de código a seguir, produz a matriz de vinculação
correta:
 >>> row_clusters = linkage(pdist(df, metric='euclidean'),

 ... method='complete')
CopyExplain
 Abordagem correta: O uso da matriz de exemplo de entrada completa (a
chamada matriz de design), conforme mostrado no trecho de código a seguir,
também leva a uma matriz de vinculação correta semelhante à abordagem
anterior:
 >>> row_clusters = linkage(df.values,

 ... method='complete',

 ... metric='euclidean')
CopyExplain

Para dar uma olhada mais de perto nos resultados do agrupamento, podemos
transformar esses resultados em um pandas (melhor visualizado em um caderno
Jupyter) da seguinte maneira:DataFrame

>>> pd.DataFrame(row_clusters,

... columns=['row label 1',

... 'row label 2',

... 'distance',

... 'no. of items in clust.'],

... index=[f'cluster {(i + 1)}' for i in

... range(row_clusters.shape[0])])
CopyExplain
Como mostrado na Figura 10.10, a matriz de vinculação consiste em várias linhas
onde cada linha representa uma mesclagem. A primeira e a segunda colunas
denotam os membros mais diferentes em cada cluster, e a terceira coluna relata a
distância entre esses membros.

A última coluna retorna a contagem dos membros em cada cluster:

Figura 10.10: A matriz de ligação

Agora que calculamos a matriz de ligação, podemos visualizar os resultados na


forma de um dendrograma:

>>> from scipy.cluster.hierarchy import dendrogram

>>> # make dendrogram black (part 1/2)

>>> # from scipy.cluster.hierarchy import set_link_color_palette

>>> # set_link_color_palette(['black'])

>>> row_dendr = dendrogram(

... row_clusters,

... labels=labels,

... # make dendrogram black (part 2/2)

... # color_threshold=np.inf

... )

>>> plt.tight_layout()
>>> plt.ylabel('Euclidean distance')

>>> plt.show()
CopyExplain

Se você estiver executando o código anterior ou lendo uma versão de e-book


deste livro, você notará que as ramificações no dendrograma resultante são
mostradas em cores diferentes. O esquema de cores é derivado de uma lista de
cores Matplotlib que são cicladas para os limites de distância no dendrograma. Por
exemplo, para exibir os dendrogramas em preto, você pode descomentar as
respectivas seções que foram inseridas no código anterior:

Figura 10.11: Um dendrograma dos nossos dados

Tal dendrograma resume os diferentes agrupamentos que se formaram durante o


agrupamento hierárquico aglomerativo; por exemplo, você pode ver que os
exemplos e , seguidos de e , são os mais semelhantes com base na métrica de
distância euclidiana.ID_0ID_4ID_1ID_2

Anexando dendrogramas a um mapa de calor


Em aplicações práticas, dendrogramas de agrupamento hierárquico são
frequentemente usados em combinação com um mapa de calor, o que nos
permite representar os valores individuais na matriz de dados ou matriz que
contém nossos exemplos de treinamento com um código de cores. Nesta seção,
discutiremos como anexar um dendrograma a um gráfico de mapa de calor e
ordenar as linhas no mapa de calor correspondentemente.

No entanto, anexar um dendrograma a um mapa de calor pode ser um pouco


complicado, então vamos passar por este procedimento passo a passo:

1. Criamos um novo objeto e definimos a posição do eixo x, a posição do eixo y,


a largura e a altura do dendrograma através do atributo. Além disso, giramos
o dendrograma 90 graus no sentido anti-horário. O código é o
seguinte: figureadd_axes
2. >>> fig = plt.figure(figsize=(8, 8), facecolor='white')

3. >>> axd = fig.add_axes([0.09, 0.1, 0.2, 0.6])

4. >>> row_dendr = dendrogram(row_clusters,

5. ... orientation='left')

6. >>> # note: for matplotlib < v1.5.1, please use

7. >>> # orientation='right'
CopyExplain
8. Em seguida, reordenamos os dados em nossa inicial de acordo com os
rótulos de clustering que podem ser acessados a partir do objeto, que é
essencialmente um dicionário Python, através da chave. O código é o
seguinte: DataFramedendrogramleaves
9. >>> df_rowclust = df.iloc[row_dendr['leaves'][::-1]]
CopyExplain
10. Agora, construímos o mapa de calor a partir do reordenado e o posicionamos
ao lado do dendrograma: DataFrame
11. >>> axm = fig.add_axes([0.23, 0.1, 0.6, 0.6])

12. >>> cax = axm.matshow(df_rowclust,

13. ... interpolation='nearest',

14. ... cmap='hot_r')


CopyExplain
15. Finalmente, modificamos a estética do dendrograma, removendo os
carrapatos do eixo e ocultando os espinhos do eixo. Além disso, adicionamos
uma barra de cores e atribuímos os nomes de registro de recursos e dados
aos rótulos de tick dos eixos x e y, respectivamente:
16. >>> axd.set_xticks([])

17. >>> axd.set_yticks([])

18. >>> for i in axd.spines.values():

19. ... i.set_visible(False)

20. >>> fig.colorbar(cax)

21. >>> axm.set_xticklabels([''] + list(df_rowclust.columns))

22. >>> axm.set_yticklabels([''] + list(df_rowclust.index))

23. >>> plt.show()


CopyExplain

Após seguir os passos anteriores, o mapa de calor deve ser exibido com o
dendrograma anexado:
Figura 10.12: Um mapa de calor e dendrograma dos nossos dados

Como você pode ver, a ordem das linhas no mapa de calor reflete o agrupamento
dos exemplos no dendrograma. Além de um dendrograma simples, os valores
codificados por cores de cada exemplo e recurso no mapa de calor nos fornecem
um bom resumo do conjunto de dados.

Aplicação de aglomerações via scikit-learn


Na subseção anterior, você viu como executar agrupamento hierárquico
aglomerativo usando o SciPy. No entanto, há também uma implementação
no scikit-learn, que nos permite escolher o número de clusters que queremos
retornar. Isso é útil se quisermos podar a árvore de cluster
hierárquica. AgglomerativeClustering
Ao definir o parâmetro como , agora vamos agrupar os exemplos de entrada em
três grupos usando a mesma abordagem de ligação completa baseada na métrica
de distância euclidiana como antes:n_cluster3

>>> from sklearn.cluster import AgglomerativeClustering

>>> ac = AgglomerativeClustering(n_clusters=3,

... affinity='euclidean',

... linkage='complete')

>>> labels = ac.fit_predict(X)

>>> print(f'Cluster labels: {labels}')

Cluster labels: [1 0 0 2 1]
CopyExplain

Observando os rótulos de cluster previstos, podemos ver que o primeiro e o quinto


exemplos ( e ) foram atribuídos a um cluster (rótulo) e os exemplos e foram
atribuídos a um segundo cluster (rótulo). O exemplo foi colocado em seu próprio
cluster (rótulo). No geral, os resultados são consistentes com os resultados que
observamos no dendrograma. Devemos notar, no entanto, que é mais semelhante
a e do que a e , como mostrado na figura do dendrograma anterior; Isso não está
claro nos resultados de agrupamento do Scikit-Learn. Agora vamos executar
novamente o uso no seguinte trecho de
código:ID_0ID_41ID_1ID_20ID_32ID_3ID_4ID_0ID_1ID_2AgglomerativeClusteringn_clust
er=2

>>> ac = AgglomerativeClustering(n_clusters=2,

... affinity='euclidean',

... linkage='complete')

>>> labels = ac.fit_predict(X)

>>> print(f'Cluster labels: {labels}')

Cluster labels: [0 1 1 0 0]
CopyExplain
Como você pode ver, nessa hierarquia de clustering removida, o rótulo foi
atribuído ao mesmo cluster que e , conforme o esperado. ID_3ID_0ID_4

Localizando regiões de alta densidade


via DBSCAN
Embora não possamos cobrir o grande número de diferentes algoritmos de
clustering neste capítulo, vamos pelo menos incluir mais uma abordagem para
clustering: clustering espacial baseado em densidade de aplicativos com
ruído (DBSCAN), que não faz suposições sobre clusters esféricos como k-means,
nem particiona o conjunto de dados em hierarquias que exigem um ponto de corte
manual. Como o próprio nome indica, o clustering baseado em densidade atribui
rótulos de cluster com base em regiões densas de pontos. Em DBSCAN, a noção
de densidade é definida como o número de pontos dentro de um raio

especificado,  .

De acordo com o algoritmo DBSCAN, um rótulo especial é atribuído a cada


exemplo (ponto de dados) usando os seguintes critérios:

 Um ponto é considerado um ponto central se pelo menos um número


especificado (MinPts) de pontos vizinhos estiver dentro do raio

especificado, 
 Um ponto de fronteira é um ponto que tem menos vizinhos do que MinPts

dentro do , mas está dentro  do raio   de um ponto central


 Todos os outros pontos que não são pontos centrais nem de fronteira
são considerados pontos de ruído

Depois de rotular os pontos como núcleo, borda ou ruído, o algoritmo DBSCAN


pode ser resumido em duas etapas simples:
1. Forme um cluster separado para cada ponto central ou grupo conectado de
pontos principais. (Os pontos principais são conectados se não estiverem

mais distantes do que  .)


2. Atribua cada ponto de borda ao cluster de seu ponto central correspondente.

Para entender melhor como pode ser o resultado do DBSCAN, antes de pular para
a implementação, vamos resumir o que acabamos de aprender sobre pontos
principais, pontos de fronteira e pontos de ruído na Figura 10.13:

Figura 10.13: Pontos de núcleo, ruído e borda do DBSCAN

Uma das principais vantagens do uso do DBSCAN é que ele não assume que os
clusters têm uma forma esférica como em k-means. Além disso, o DBSCAN é
diferente de k-means e cluster hierárquico na medida em que não
necessariamente atribui cada ponto a um cluster, mas é capaz de remover pontos
de ruído.
Para um exemplo mais ilustrativo, vamos criar um novo conjunto de dados de
estruturas em forma de meia-lua para comparar agrupamento k-means, cluster
hierárquico e DBSCAN:

>>> from sklearn.datasets import make_moons

>>> X, y = make_moons(n_samples=200,

... noise=0.05,

... random_state=0)

>>> plt.scatter(X[:, 0], X[:, 1])

>>> plt.xlabel('Feature 1')

>>> plt.ylabel('Feature 2')

>>> plt.tight_layout()

>>> plt.show()
CopyExplain

Como você pode ver no gráfico resultante, há dois grupos visíveis, em forma de
meia-lua, consistindo de 100 exemplos (pontos de dados) cada:
Figura 10.14: Um conjunto de dados em forma de meia-lua de duas características

Começaremos usando o algoritmo k-means e o agrupamento de ligação completo


para ver se um desses algoritmos de agrupamento discutidos anteriormente pode
identificar com sucesso as formas de meia-lua como aglomerados separados. O
código é o seguinte:

>>> f, (ax1, ax2) = plt.subplots(1, 2, figsize=(8, 3))

>>> km = KMeans(n_clusters=2,

... random_state=0)

>>> y_km = km.fit_predict(X)

>>> ax1.scatter(X[y_km == 0, 0],

... X[y_km == 0, 1],

... c='lightblue',

... edgecolor='black',

... marker='o',

... s=40,

... label='cluster 1')

>>> ax1.scatter(X[y_km == 1, 0],

... X[y_km == 1, 1],

... c='red',

... edgecolor='black',

... marker='s',

... s=40,

... label='cluster 2')

>>> ax1.set_title('K-means clustering')

>>> ax1.set_xlabel('Feature 1')

>>> ax1.set_ylabel('Feature 2')

>>> ac = AgglomerativeClustering(n_clusters=2,
... affinity='euclidean',

... linkage='complete')

>>> y_ac = ac.fit_predict(X)

>>> ax2.scatter(X[y_ac == 0, 0],

... X[y_ac == 0, 1],

... c='lightblue',

... edgecolor='black',

... marker='o',

... s=40,

... label='Cluster 1')

>>> ax2.scatter(X[y_ac == 1, 0],

... X[y_ac == 1, 1],

... c='red',

... edgecolor='black',

... marker='s',

... s=40,

... label='Cluster 2')

>>> ax2.set_title('Agglomerative clustering')

>>> ax2.set_xlabel('Feature 1')

>>> ax2.set_ylabel('Feature 2')

>>> plt.legend()

>>> plt.tight_layout()

>>> plt.show()
CopyExplain

Com base nos resultados de agrupamento visualizados, podemos ver que o


algoritmo k-means não foi capaz de separar os dois clusters, e também, o
algoritmo de agrupamento hierárquico foi desafiado por essas formas complexas:
Figura 10.15: k-médias e aglomeração no conjunto de dados em forma de meia-
lua

Finalmente, vamos tentar o algoritmo DBSCAN neste conjunto de dados para ver
se ele pode encontrar os dois clusters em forma de meia-lua usando uma
abordagem baseada em densidade:

>>> from sklearn.cluster import DBSCAN

>>> db = DBSCAN(eps=0.2,

... min_samples=5,

... metric='euclidean')

>>> y_db = db.fit_predict(X)

>>> plt.scatter(X[y_db == 0, 0],

... X[y_db == 0, 1],

... c='lightblue',

... edgecolor='black',

... marker='o',

... s=40,

... label='Cluster 1')

>>> plt.scatter(X[y_db == 1, 0],

... X[y_db == 1, 1],


... c='red',

... edgecolor='black',

... marker='s',

... s=40,

... label='Cluster 2')

>>> plt.xlabel('Feature 1')

>>> plt.ylabel('Feature 2')

>>> plt.legend()

>>> plt.tight_layout()

>>> plt.show()
CopyExplain

O algoritmo DBSCAN pode detectar com êxito as formas de meia-lua, o que


destaca um dos pontos fortes do DBSCAN — agrupar dados de formas arbitrárias:

Figura 10.16: Agrupamento DBSCAN no conjunto de dados em forma de meia-lua


No entanto, também devemos notar algumas das desvantagens do DBSCAN.
Com um número crescente de recursos em nosso conjunto de dados — supondo
um número fixo de exemplos de treinamento — o efeito negativo da maldição da
dimensionalidade aumenta. Isso é especialmente um problema se estivermos
usando a métrica de distância euclidiana. No entanto, o problema da maldição da
dimensionalidade não é exclusivo do DBSCAN: ele também afeta outros
algoritmos de agrupamento que usam a métrica de distância euclidiana, por
exemplo, k-means e algoritmos de agrupamento hierárquico. Além disso, temos

dois hiperparâmetros no DBSCAN (MinPts e  ) que precisam ser otimizados


para produzir bons resultados de clustering. Encontrar uma boa combinação de

MinPts e   pode ser problemático se as diferenças de densidade no conjunto de


dados forem relativamente grandes.

Clustering baseado em gráfico

Até agora, vimos três das categorias mais fundamentais de algoritmos de


clustering: clustering baseado em protótipo com k-means, cluster hierárquico
aglomerativo e clustering baseado em densidade via DBSCAN. No entanto, há
também uma quarta classe de algoritmos de clustering mais avançados que não
abordamos neste capítulo: o clustering baseado em grafos. Provavelmente os
membros mais proeminentes da família de agrupamento baseada em grafos são
os algoritmos de agrupamento espectral.

Embora existam muitas implementações diferentes de agrupamento espectral, o


que todas elas têm em comum é que elas usam os autovetores de uma matriz de
similaridade ou distância para derivar as relações de agrupamento. Uma vez que o
agrupamento espectral está além do escopo deste livro, você pode ler o excelente
tutorial de Ulrike von Luxburg para aprender mais sobre este tópico (A tutorial on
spectral clustering, Statistics and Computing, 17(4): 395-416, 2007). Está
disponível gratuitamente a partir do arXiv em http://arxiv.org/pdf/0711.0189v1.pdf.

Observe que, na prática, nem sempre é óbvio qual algoritmo de clustering terá
melhor desempenho em um determinado conjunto de dados, especialmente se os
dados vierem em várias dimensões que dificultam ou impossibilitam a
visualização. Além disso, é importante enfatizar que um clustering bem-sucedido
não depende apenas do algoritmo e de seus hiperparâmetros; em vez disso, a
escolha de uma métrica de distância apropriada e o uso de conhecimento de
domínio que possa ajudar a orientar o arranjo experimental podem ser ainda mais
importantes.

No contexto da maldição da dimensionalidade, é prática comum aplicar técnicas


de redução de dimensionalidade antes de realizar o agrupamento. Tais técnicas
de redução de dimensionalidade para conjuntos de dados não supervisionados
incluem análise de componentes principais e t-SNE, que abordamos no Capítulo
5, Compactando dados via redução de dimensionalidade. Além disso, é
particularmente comum compactar conjuntos de dados em subespaços
bidimensionais, o que nos permite visualizar os clusters e rótulos atribuídos
usando gráficos de dispersão bidimensionais, que são particularmente úteis para
avaliar os resultados.

Implementando uma Rede Neural


Artificial Multicamada do Zero
Como você deve saber, o deep learning está recebendo muita atenção da
imprensa e é, sem dúvida, o tópico mais quente no campo do aprendizado de
máquina. O aprendizado profundo pode ser entendido como um subcampo do
aprendizado de máquina que se preocupa em treinar redes neurais artificiais
(NNs) com muitas camadas de forma eficiente. Neste capítulo, você aprenderá os
conceitos básicos de NNs artificiais para que você esteja bem equipado para os
capítulos seguintes, que apresentarão bibliotecas avançadas de aprendizado
profundo baseadas em Python e arquiteturas de rede neural profunda (DNN) que
são particularmente adequadas para análises de imagem e texto.

Os tópicos que abordaremos neste capítulo são os seguintes:

 Obtendo uma compreensão conceitual de NNs multicamadas

 Implementando o algoritmo fundamental de backpropagation para


treinamento de NN do zero
 Treinamento de um NN multicamada básico para classificação de imagens

Modelando funções complexas com


redes neurais artificiais
No início deste livro, começamos nossa jornada através de algoritmos de
aprendizado de máquina com neurônios artificiais no Capítulo 2, Treinando
algoritmos simples de aprendizado de máquina para classificação. Os neurônios
artificiais representam os blocos de construção dos NNs artificiais multicamadas
que discutiremos neste capítulo.

O conceito básico por trás dos NNs artificiais foi construído sobre hipóteses e
modelos de como o cérebro humano funciona para resolver tarefas de problemas
complexos. Embora os NNs artificiais tenham ganhado muita popularidade nos
últimos anos, os primeiros estudos sobre NNs remontam à década de 1940,
quando Warren McCulloch e Walter Pitts descreveram pela primeira vez como os
neurônios poderiam funcionar. (Um cálculo lógico das ideias imanentes à
atividade nervosa, por W. S. McCulloch e W. Pitts, The Bulletin of Mathematical
Biophysics, 5(4):115–133, 1943.)

No entanto, nas décadas que se seguiram à primeira implementação do modelo


de neurônios McCulloch-Pitts – o perceptron de Rosenblatt na década de 1950
– muitos pesquisadores e praticantes de aprendizado de máquina lentamente
começaram a perder o interesse em NNs, já que ninguém tinha uma boa solução
para treinar um NN com várias camadas. Eventualmente, o interesse em NNs foi
reacendido em 1986, quando D.E. Rumelhart, G.E. Hinton e R.J. Williams
estiveram envolvidos na (re)descoberta e popularização do algoritmo de
backpropagation para treinar NNs de forma mais eficiente, que discutiremos mais
detalhadamente mais adiante neste capítulo (Learning representations by
backpropagating errors, por D.E. Rumelhart, G.E. Hinton e R.J.
Williams, Natureza, 323 (6088): 533–536, 1986). Os leitores que se interessam
pela história da inteligência artificial (IA), aprendizado de máquina e NNs
também são incentivados a ler o artigo da Wikipédia sobre os chamados invernos
de IA, que são os períodos de tempo em que uma grande parcela da comunidade
de pesquisa perdeu o interesse no estudo de NNs
(https://en.wikipedia.org/wiki/AI_winter).

No entanto, os NNs são mais populares hoje do que nunca graças aos muitos
avanços que foram feitos na década anterior, que resultaram no que agora
chamamos de algoritmos e arquiteturas de aprendizado profundo – NNs que são
compostos de muitas camadas. NNs são um tópico quente não apenas na
pesquisa acadêmica, mas também em grandes empresas de tecnologia, como
Facebook, Microsoft, Amazon, Uber, Google e muitas outras que investem
pesadamente em NNs artificiais e pesquisa de aprendizagem profunda.

A partir de hoje, NNs complexos alimentados por algoritmos de aprendizagem


profunda são considerados soluções de última geração para a resolução de
problemas complexos, como reconhecimento de imagem e voz. Algumas das
aplicações recentes incluem:

 Prever as necessidades de recursos COVID-19 a partir de uma série de raios-


X (https://arxiv.org/abs/2101.04909)
 Modelando mutações virais
(https://science.sciencemag.org/content/371/6526/284)
 Aproveitando dados de plataformas de mídia social para gerenciar eventos
climáticos extremos (https://onlinelibrary.wiley.com/doi/abs/10.1111/1468-
5973.12311)
 Melhorar as descrições de fotos para pessoas cegas ou com deficiência
visual (https://tech.fb.com/how-facebook-is-using-ai-to-improve-photo-
descriptions-for-people-who-are-blind-or-visually-impaired/)

Recapitulação de rede neural de camada única


Este capítulo é sobre NNs multicamadas, como eles funcionam e como treiná-los
para resolver problemas complexos. No entanto, antes de nos aprofundarmos em
uma arquitetura NN multicamada específica, vamos reiterar brevemente alguns
dos conceitos de NNs de camada única que introduzimos no Capítulo 2, a saber, o
algoritmo ADAptive LInear NEuron (Adaline), que é mostrado na Figura 11.1:
Figura 11.1: O algoritmo Adaline

No Capítulo 2, implementamos o algoritmo Adaline para realizar a classificação


binária, e usamos o algoritmo de otimização de descida de gradiente para
aprender os coeficientes de peso do modelo. Em cada época (passar por cima do
conjunto de dados de treinamento), atualizamos o vetor de peso w e a unidade de
viés b usando a seguinte regra de atualização:

onde   e para a unidade de viés

e   cada peso wj no vetor de peso w.

Em outras palavras, calculamos o gradiente com base em todo o conjunto de


dados de treinamento e atualizamos os pesos do modelo dando um passo na

direção oposta do gradiente de perda . (Para simplificar, vamos nos


concentrar nos pesos e omitir a unidade de viés nos parágrafos seguintes; no
entanto, como você lembra do Capítulo 2, os mesmos conceitos se aplicam.) Para
encontrar os pesos ótimos do modelo, otimizamos uma função objetivo que
definimos como a média dos erros quadrados (MSE) função de perda L(w).

Além disso, multiplicamos o gradiente por um fator, a taxa  de aprendizagem,


que tivemos que escolher cuidadosamente para equilibrar a velocidade de
aprendizagem contra o risco de ultrapassar o mínimo global da função de perda.

Na otimização da descida do gradiente, atualizamos todos os pesos


simultaneamente após cada época e definimos a derivada parcial para cada
peso wj No vetor peso, W, da seguinte forma:

Aqui, y(eu) é o rótulo da classe de destino de uma amostra específica x(eu)e um(eu) é


a ativação do neurônio, que é uma função linear no caso especial de Adaline.

Além disso, definimos a função   de ativação da seguinte forma:

Aqui, a entrada líquida, z, é uma combinação linear dos pesos que estão
conectando a camada de entrada à camada de saída:
Enquanto usamos a ativação   para calcular a atualização de gradiente,
implementamos uma função de limite para esmagar a saída de valor contínuo em
rótulos de classe binária para previsão:

Convenção de nomenclatura de camada única

Observe que, embora o Adaline consista em duas camadas, uma camada de


entrada e uma camada de saída, ele é chamado de rede de camada única por
causa de seu único link entre as camadas de entrada e saída.

Além disso, aprendemos sobre um certo truque para acelerar o aprendizado do


modelo, a chamada otimização de descida de gradiente estocástico (SGD). O
SGD aproxima a perda de uma única amostra de treinamento (aprendizado on-
line) ou de um pequeno subconjunto de exemplos de treinamento (aprendizado
em minilote). Faremos uso desse conceito mais adiante neste capítulo, quando
implementarmos e treinarmos um perceptron multicamadas (MLP). Além do
aprendizado mais rápido – devido às atualizações de peso mais frequentes em
comparação com a descida de gradiente – sua natureza ruidosa também é
considerada benéfica ao treinar NNs multicamadas com funções de ativação não
linear, que não têm uma função de perda convexa. Aqui, o ruído adicional pode
ajudar a escapar dos mínimos de perda local, mas discutiremos esse tópico com
mais detalhes mais adiante neste capítulo.

Apresentando a arquitetura de rede neural


multicamada
Nesta seção, você aprenderá como conectar vários neurônios únicos a um NN de
feedforward multicamadas; esse tipo especial de rede totalmente
conectada também é chamado de MLP.
A figura 11.2 ilustra o conceito de um MLP composto por duas camadas:

Figure 11.2: A two-layer MLP

Next to the data input, the MLP depicted in Figure 11.2 has one hidden layer and
one output layer. The units in the hidden layer are fully connected to the input
features, and the output layer is fully connected to the hidden layer. If such a
network has more than one hidden layer, we also call it a deep NN. (Note that in
some contexts, the inputs are also regarded as a layer. However, in this case, it
would make the Adaline model, which is a single-layer neural network, a two-layer
neural network, which may be counterintuitive.)

Adding additional hidden layers

We can add any number of hidden layers to the MLP to create deeper network
architectures. Practically, we can think of the number of layers and units in an NN
as additional hyperparameters that we want to optimize for a given problem task
using the cross-validation technique, which we discussed in Chapter 6, Learning
Best Practices for Model Evaluation and Hyperparameter Tuning.

However, the loss gradients for updating the network’s parameters, which we will
calculate later via backpropagation, will become increasingly small as more layers
are added to a network. This vanishing gradient problem makes model learning
more challenging. Therefore, special algorithms have been developed to help train
such DNN structures; this is known as deep learning, which we will discuss in
more detail in the following chapters.

As shown in Figure 11.2, we denote the ith activation unit in the lth layer as 
. To make the math and code implementations a bit more intuitive, we will not use
numerical indices to refer to layers, but we will use the in superscript for the input
features, the h superscript for the hidden layer, and the out superscript for the

output layer. For instance,   refers to the ith input feature value,   

refers to the ith unit in the hidden layer, and   refers to the ith unit in
the output layer. Note that the b’s in Figure 11.2 denote the bias units. In
fact, b(h) and b(out) are vectors with the number of elements being equal to the
number of nodes in the layer they correspond to. For example, b(h) stores d bias
units, where d is the number of nodes in the hidden layer. If this sounds confusing,
don’t worry. Looking at the code implementation later, where we initialize weight
matrices and bias unit vectors, will help clarify these concepts.

Each node in layer l is connected to all nodes in layer l + 1 via a weight coefficient.
For example, the connection between the kth unit in layer l to the jth unit in

layer l + 1 will be written as  . Referring back to Figure 11.2, we


denote the weight matrix that connects the input to the hidden layer as W(h), and we
write the matrix that connects the hidden layer to the output layer as W(out).
While one unit in the output layer would suffice for a binary classification task, we
saw a more general form of an NN in the preceding figure, which allows us to
perform multiclass classification via a generalization of the one-versus-all (OvA)
technique. To better understand how this works, remember the one-
hot representation of categorical variables that we introduced in Chapter
4, Building Good Training Datasets – Data Preprocessing.

For example, we can encode the three class labels in the familiar Iris dataset
(0=Setosa, 1=Versicolor, 2=Virginica) as follows:

This one-hot vector representation allows us to tackle classification tasks with an


arbitrary number of unique class labels present in the training dataset.

If you are new to NN representations, the indexing notation (subscripts and


superscripts) may look a little bit confusing at first. What may seem overly
complicated at first will make much more sense in later sections when we vectorize
the NN representation. As introduced earlier, we summarize the weights that
connect the input and hidden layers by a d×m dimensional matrix W(h), where d is
the number of hidden units and m is the number of input units.

Activating a neural network via forward


propagation
In this section, we will describe the process of forward propagation to calculate
the output of an MLP model. To understand how it fits into the context of learning
an MLP model, let’s summarize the MLP learning procedure in three simple steps:

1. Starting at the input layer, we forward propagate the patterns of the training
data through the network to generate an output.
2. Based on the network’s output, we calculate the loss that we want to minimize
using a loss function that we will describe later.
3. We backpropagate the loss, find its derivative with respect to each weight and
bias unit in the network, and update the model.

Finally, after we repeat these three steps for multiple epochs and learn the weight
and bias parameters of the MLP, we use forward propagation to calculate the
network output and apply a threshold function to obtain the predicted class labels
in the one-hot representation, which we described in the previous section.

Now, let’s walk through the individual steps of forward propagation to generate an
output from the patterns in the training data. Since each unit in the hidden layer is
connected to all units in the input layers, we first calculate the activation unit of the

hidden layer   as follows:

Aqui, é a entrada líquida e   é a função de ativação,   que tem que


ser diferenciável para aprender os pesos que conectam os neurônios usando uma
abordagem baseada em gradiente. Para sermos capazes de resolver problemas
complexos, como a classificação de imagens, precisamos de funções de ativação
não linear em nosso modelo MLP, por exemplo, a função de ativação sigmoide
(logística) que lembramos da seção sobre regressão logística no Capítulo 3, A
Tour of Machine Learning Classifiers Using Scikit-Learn:
Como você deve se lembrar, a função sigmoide é uma curva em forma de S que
mapeia a entrada líquida z em uma distribuição logística no intervalo de 0 a 1, que
corta o eixo y em z = 0, como mostrado na Figura 11.3:

Figura 11.3: A função de ativação do sigmoide

MLP é um exemplo típico de um NN artificial feedforward. O


termo feedforward refere-se ao fato de que cada camada serve como
entrada para a próxima camada sem loops, em contraste com NNs recorrentes —
uma arquitetura que discutiremos mais adiante neste capítulo e discutiremos com
mais detalhes no Capítulo 15, Modelando dados sequenciais usando redes
neurais recorrentes. O termo perceptron multicamada pode soar um pouco
confuso, uma vez que os neurônios artificiais nesta arquitetura de rede são
tipicamente unidades sigmoides, não perceptrons. Podemos pensar nos neurônios
do MLP como unidades de regressão logística que retornam valores na faixa
contínua entre 0 e 1.

Para fins de eficiência de código e legibilidade, agora escreveremos a ativação em


uma forma mais compacta usando os conceitos de álgebra linear básica, o que
nos permitirá vetorizar nossa implementação de código via NumPy em vez de
escrever vários loops Python aninhados e computacionalmente caros: for

Aqui, x(em) é o nosso vetor de feição dimensional × 1 m. O(h) é uma matriz de peso


dimensional D×m onde D é o número de unidades na camada
oculta; consequentemente, a matriz W transposta(h)T é m×d dimensional. O vetor
de viés b(h) consiste em unidades de viés d (uma unidade de viés por nó oculto).

Após a multiplicação matriz-vetor, obtém-se o vetor de entrada líquido


1×d dimensional z(h) Para calcular a ativação de

um(h) (onde  ).

Além disso, podemos generalizar esse cálculo para todos os n exemplos no


conjunto de dados de treinamento:

Z(h) = X(em)W(h)T + b(h)

Aqui, X(em) é agora uma matriz n×m, e a multiplicação da matriz resultará em uma


matriz de entrada líquida n×d dimensional, Z(h). Finalmente, aplicamos a

função   de ativação a cada valor na matriz de entrada líquida para obter
a matriz de ativação n×d na próxima camada (aqui, a camada de saída):

Da mesma forma, podemos escrever a ativação da camada de saída em forma


vetorizada para vários exemplos:
Z(fora) = Um(h)W(fora)T + b(fora)

Aqui, multiplicamos a transposição da matriz t×d W(fora) (t é o número de unidades


de saída) pela matriz dimensional n×d, A(h)e adicionar o vetor de
viés t dimensional b(fora) para obter a matriz n×t dimensional, Z(fora). (As linhas nessa
matriz representam as saídas de cada exemplo.)

Por fim, aplicamos a função de ativação sigmoide para obter a saída de valor
contínuo de nossa rede:

Similares a Z(fora), A(fora) é uma matriz n×t dimensional.

Classificando dígitos manuscritos


Na seção anterior, cobrimos muito da teoria em torno dos NNs, o que pode ser um
pouco esmagador se você é novo neste tópico. Antes de continuarmos com a
discussão do algoritmo para aprender os pesos do modelo MLP, backpropagation,
vamos fazer uma pequena pausa na teoria e ver um NN em ação.

Recursos adicionais sobre retropropagação

A teoria NN pode ser bastante complexa; Assim, queremos fornecer aos leitores


recursos adicionais que cobrem alguns dos tópicos que discutimos neste capítulo
com mais detalhes ou de uma perspectiva diferente:

 Capítulo 6, Deep Feedforward Networks, Deep Learning, por I. Goodfellow, Y.


Bengio e A. Courville, MIT Press, 2016 (manuscritos livremente acessíveis
em http://www.deeplearningbook.org).
 Reconhecimento de Padrões e Machine Learning, por C. M. Bishop, Springer
New York, 2006.
 Slides em vídeo da palestra do curso de aprendizagem profunda de
Sebastian Raschka:
https://sebastianraschka.com/blog/2021/dl-course.html#l08-multinomial-
logistic-regression--softmax-regression

https://sebastianraschka.com/blog/2021/dl-course.html#l09-multilayer-
perceptrons-and-backpropration

Nesta seção, implementaremos e treinaremos nosso primeiro NN multicamada


para classificar dígitos manuscritos do popular conjunto de dados do Mixed
National Institute of Standards and Technology (MNIST) que foi construído por
Yann LeCun e outros e serve como um conjunto de dados de referência popular
para algoritmos de aprendizado de máquina (Gradient-Based Learning Applied to
Document Recognition by Y. LeCun, L. Bottou, Y. Bengio e P. Haffner, Anais do
IEEE, 86(11): 2278-2324, 1998).

Obtendo e preparando o conjunto de dados


MNIST
O conjunto de dados MNIST está disponível publicamente
em http://yann.lecun.com/exdb/mnist/ e consiste nas quatro partes a seguir:

1. Imagens do conjunto de dados de treinamento: (9,9 MB, 47 MB


descompactados e 60.000 exemplos)train-images-idx3-ubyte.gz
2. Rótulos do conjunto de dados de treinamento: (29 KB, 60 KB
descompactados e 60.000 rótulos)train-labels-idx1-ubyte.gz
3. Testar imagens de conjunto de dados: (1,6 MB, 7,8 MB descompactados e
10.000 exemplos)t10k-images-idx3-ubyte.gz
4. Rótulos do conjunto de dados de teste: (5 KB, 10 KB descompactados e
10.000 rótulos)t10k-labels-idx1-ubyte.gz

O conjunto de dados MNIST foi construído a partir de dois conjuntos de dados


do Instituto Nacional de Padrões e Tecnologia dos EUA (NIST). O conjunto de
dados de treinamento consiste em dígitos manuscritos de 250 pessoas diferentes,
50% estudantes do ensino médio e 50% funcionários do Census Bureau. Observe
que o conjunto de dados de teste contém dígitos manuscritos de pessoas
diferentes seguindo a mesma divisão.

Em vez de baixar os arquivos de conjunto de dados acima mencionados e pré-


processá-los em matrizes NumPy, usaremos a nova função do scikit-learn, que
nos permite carregar o conjunto de dados MNIST de forma mais
conveniente:fetch_openml

>>> from sklearn.datasets import fetch_openml

>>> X, y = fetch_openml('mnist_784', version=1,

... return_X_y=True)

>>> X = X.values

>>> y = y.astype(int).values
CopyExplain

No scikit-learn, a função baixa o conjunto de dados MNIST do OpenML


(https://www.openml.org/d/554) como objetos pandas e Series, e é por isso que
usamos o atributo para obter as matrizes NumPy subjacentes. (Se você estiver
usando uma versão scikit-learn anterior à 1.0, baixe matrizes NumPy diretamente
para que você possa omitir o uso do atributo.) A matriz dimensional n×m consiste
em 70.000 imagens com 784 pixels cada, e a matriz armazena os rótulos de
classe 70.000 correspondentes, que podemos confirmar verificando as dimensões
dos arrays da seguinte maneira:fetch_openmlDataFrame.valuesfetch_openml.valuesXy

>>> print(X.shape)

(70000, 784)

>>> print(y.shape)

(70000,)
CopyExplain

As imagens no conjunto de dados MNIST consistem em 28×28 pixels, e cada pixel


é representado por um valor de intensidade em escala de cinza. Aqui, já
desenrolamos os 28×28 pixels em vetores de linha unidimensionais, que
representam as linhas em nossa matriz (784 por linha ou imagem) acima. A
segunda matriz () retornada pela função contém a variável de destino
correspondente, os rótulos de classe (inteiros 0-9) dos dígitos
manuscritos.fetch_openmlXyfetch_openml

Em seguida, vamos normalizar os valores de pixels no MNIST para o intervalo –1


a 1 (originalmente 0 a 255) por meio da seguinte linha de código:

>>> X = ((X / 255.) - .5) * 2


CopyExplain

A razão por trás disso é que a otimização baseada em gradiente é muito mais
estável nessas condições, como discutido no Capítulo 2. Observe que
dimensionamos as imagens pixel a pixel, o que é diferente da abordagem de
dimensionamento de recursos que adotamos nos capítulos anteriores.

Anteriormente, derivamos parâmetros de dimensionamento do conjunto de dados


de treinamento e os usamos para dimensionar cada coluna no conjunto de dados
de treinamento e no conjunto de dados de teste. No entanto, ao trabalhar com
pixels de imagem, centralizá-los em zero e reescaloná-los para um intervalo [–1, 1]
também é comum e geralmente funciona bem na prática.

Para ter uma ideia de como essas imagens no MNIST se parecem, vamos
visualizar exemplos dos dígitos de 0 a 9 depois de remodelar os vetores de 784
pixels de nossa matriz de recursos para a imagem original de 28×28 que podemos
plotar através da função de Matplotlib: imshow

>>> import matplotlib.pyplot as plt

>>> fig, ax = plt.subplots(nrows=2, ncols=5,

... sharex=True, sharey=True)

>>> ax = ax.flatten()

>>> for i in range(10):

... img = X[y == i][0].reshape(28, 28)

... ax[i].imshow(img, cmap='Greys')

>>> ax[0].set_xticks([])
>>> ax[0].set_yticks([])

>>> plt.tight_layout()

>>> plt.show()
CopyExplain

Agora devemos ver um gráfico das 2×5 subfiguras mostrando uma imagem
representativa de cada dígito único:

Figura 11.4: Gráfico mostrando um dígito manuscrito escolhido aleatoriamente de


cada classe

Além disso, também vamos plotar vários exemplos do mesmo dígito para ver o
quão diferente a caligrafia para cada um realmente é:

>>> fig, ax = plt.subplots(nrows=5,

... ncols=5,

... sharex=True,

... sharey=True)

>>> ax = ax.flatten()

>>> for i in range(25):


... img = X[y == 7][i].reshape(28, 28)

... ax[i].imshow(img, cmap='Greys')

>>> ax[0].set_xticks([])

>>> ax[0].set_yticks([])

>>> plt.tight_layout()

>>> plt.show()
CopyExplain

Depois de executar o código, agora devemos ver as primeiras 25 variantes do


dígito 7:

Figura 11.5: Diferentes variantes do dígito manuscrito 7

Finalmente, vamos dividir o conjunto de dados em subconjuntos de treinamento,


validação e teste. O código a seguir dividirá o conjunto de dados de forma que
55.000 imagens sejam usadas para treinamento, 5.000 imagens para validação e
10.000 imagens para teste:
>>> from sklearn.model_selection import train_test_split

>>> X_temp, X_test, y_temp, y_test = train_test_split(

... X, y, test_size=10000, random_state=123, stratify=y

... )

>>> X_train, X_valid, y_train, y_valid = train_test_split(

... X_temp, y_temp, test_size=5000,

... random_state=123, stratify=y_temp

... )
CopyExplain

Implementando um perceptron multicamada


Nesta subseção, agora implementaremos um MLP do zero para classificar as
imagens no conjunto de dados MNIST. Para manter as coisas simples, vamos
implementar um MLP com apenas uma camada oculta. Como a abordagem pode
parecer um pouco complicada no início, você é encorajado a baixar o código de
exemplo para este capítulo do site Packt Publishing ou do GitHub
(https://github.com/rasbt/machine-learning-book) para que você possa exibir essa
implementação MLP anotada com comentários e realce de sintaxe para melhor
legibilidade.

Se você não estiver executando o código do arquivo Jupyter Notebook que o


acompanha ou não tiver acesso à Internet, copie o código deste capítulo em um
arquivo de script Python em seu diretório de trabalho atual (por exemplo, , que
você pode importar para sua sessão Python atual através do seguinte
comando:NeuralNetMLPneuralnet.py)

from neuralnet import NeuralNetMLP


CopyExplain

O código conterá partes sobre as quais ainda não falamos, como o algoritmo de


backpropagation. Não se preocupe se nem todo o código faz sentido imediato
para você; Seguiremos algumas partes mais adiante neste capítulo. No entanto,
rever o código neste estágio pode tornar mais fácil seguir a teoria mais tarde.
Então, vamos examinar a seguinte implementação de um MLP, começando com
as duas funções auxiliares para calcular a ativação do sigmoide logístico e
converter matrizes de rótulo de classe inteira em rótulos codificados em um hot
codificado:

import numpy as np

def sigmoid(z):

return 1. / (1. + np.exp(-z))

def int_to_onehot(y, num_labels):

ary = np.zeros((y.shape[0], num_labels))

for i, val in enumerate(y):

ary[i, val] = 1

return ary
CopyExplain

Abaixo, implementamos a classe principal para o nosso MLP, que chamamos de .


Existem três métodos de classe, , e , que discutiremos um a um, começando com
o construtor:NeuralNetMLP.__init__().forward().backward()__init__

class NeuralNetMLP:

def __init__(self, num_features, num_hidden,

num_classes, random_seed=123):

super().__init__()

self.num_classes = num_classes

# hidden

rng = np.random.RandomState(random_seed)

self.weight_h = rng.normal(

loc=0.0, scale=0.1, size=(num_hidden, num_features))


self.bias_h = np.zeros(num_hidden)

# output

self.weight_out = rng.normal(

loc=0.0, scale=0.1, size=(num_classes, num_hidden))

self.bias_out = np.zeros(num_classes)
CopyExplain

O construtor instancia as matrizes de peso e vetores de polarização para a


camada oculta e a camada de saída. A seguir, vamos ver como eles são usados
no método para fazer previsões:__init__forward

def forward(self, x):

# Hidden layer

# input dim: [n_examples, n_features]

# dot [n_hidden, n_features].T

# output dim: [n_examples, n_hidden]

z_h = np.dot(x, self.weight_h.T) + self.bias_h

a_h = sigmoid(z_h)

# Output layer

# input dim: [n_examples, n_hidden]

# dot [n_classes, n_hidden].T

# output dim: [n_examples, n_classes]

z_out = np.dot(a_h, self.weight_out.T) + self.bias_out

a_out = sigmoid(z_out)

return a_h, a_out


CopyExplain

O método usa um ou mais exemplos de treinamento e retorna as previsões. Na


verdade, ele retorna os valores de ativação da camada oculta e da camada de
saída, e . Embora represente as probabilidades de associação de classe que
podemos converter em rótulos de classe, com as quais nos preocupamos,
também precisamos dos valores de ativação da camada oculta, , para otimizar os
parâmetros do modelo; isto é, as unidades de peso e viés das camadas ocultas e
de saída.forwarda_ha_outa_outa_h

Por fim, vamos falar sobre o método, que atualiza os parâmetros de peso e viés
da rede neural:backward

def backward(self, x, a_h, a_out, y):

#########################

### Output layer weights

#########################

# one-hot encoding

y_onehot = int_to_onehot(y, self.num_classes)

# Part 1: dLoss/dOutWeights

## = dLoss/dOutAct * dOutAct/dOutNet * dOutNet/dOutWeight

## where DeltaOut = dLoss/dOutAct * dOutAct/dOutNet

## for convenient re-use

# input/output dim: [n_examples, n_classes]

d_loss__d_a_out = 2.*(a_out - y_onehot) / y.shape[0]

# input/output dim: [n_examples, n_classes]

d_a_out__d_z_out = a_out * (1. - a_out) # sigmoid derivative

# output dim: [n_examples, n_classes]

delta_out = d_loss__d_a_out * d_a_out__d_z_out

# gradient for output weights


# [n_examples, n_hidden]

d_z_out__dw_out = a_h

# input dim: [n_classes, n_examples]

# dot [n_examples, n_hidden]

# output dim: [n_classes, n_hidden]

d_loss__dw_out = np.dot(delta_out.T, d_z_out__dw_out)

d_loss__db_out = np.sum(delta_out, axis=0)

#################################

# Part 2: dLoss/dHiddenWeights

## = DeltaOut * dOutNet/dHiddenAct * dHiddenAct/dHiddenNet

# * dHiddenNet/dWeight

# [n_classes, n_hidden]

d_z_out__a_h = self.weight_out

# output dim: [n_examples, n_hidden]

d_loss__a_h = np.dot(delta_out, d_z_out__a_h)

# [n_examples, n_hidden]

d_a_h__d_z_h = a_h * (1. - a_h) # sigmoid derivative

# [n_examples, n_features]

d_z_h__d_w_h = x

# output dim: [n_hidden, n_features]

d_loss__d_w_h = np.dot((d_loss__a_h * d_a_h__d_z_h).T,

d_z_h__d_w_h)
d_loss__d_b_h = np.sum((d_loss__a_h * d_a_h__d_z_h), axis=0)

return (d_loss__dw_out, d_loss__db_out,

d_loss__d_w_h, d_loss__d_b_h)
CopyExplain

The method implements the so-called backpropagation algorithm, which calculates


the gradients of the loss with respect to the weight and bias parameters. Similar to
Adaline, these gradients are then used to update these parameters via gradient
descent. Note that multilayer NNs are more complex than their single-layer
siblings, and we will go over the mathematical concepts of how to compute the
gradients in a later section after discussing the code. For now, just consider
the method as a way for computing gradients that are used for the gradient
descent updates. For simplicity, the loss function this derivation is based on is the
same MSE loss that we used in Adaline. In later chapters, we will look at
alternative loss functions, such as multi-category cross-entropy loss, which is a
generalization of the binary logistic regression loss to multiple
classes.backwardbackward

Looking at this code implementation of the class, you may have noticed that this
object-oriented implementation differs from the familiar scikit-learn API that is
centered around the and methods. Instead, the main methods of the class are
the and methods. One of the reasons behind this is that it makes a complex neural
network a bit easier to understand in terms of how the information flows through
the networks.NeuralNetMLP.fit().predict()NeuralNetMLP.forward().backward()

Another reason is that this implementation is relatively similar to how more


advanced deep learning libraries such as PyTorch operate, which we will introduce
and use in the upcoming chapters to implement more complex neural networks.

After we have implemented the class, we use the following code to instantiate a


new object:NeuralNetMLPNeuralNetMLP

>>> model = NeuralNetMLP(num_features=28*28,

... num_hidden=50,

... num_classes=10)
CopyExplain

The accepts MNIST images reshaped into 784-dimensional vectors (in the format
of , , or , which we defined previously) for the 10 integer classes (digits 0-9). The
hidden layer consists of 50 nodes. Also, as you may be able to tell from looking at
the previously defined method, we use a sigmoid activation function after the first
hidden layer and output layer to keep things simple. In later chapters, we will learn
about alternative activation functions for both the hidden and output
layers.modelX_trainX_validX_test.forward()

Figure 11.6 summarizes the neural network architecture that we instantiated


above:

Figure 11.6: The NN architecture for labeling handwritten digits

In the next subsection, we are going to implement the training function that we can
use to train the network on mini-batches of the data via backpropagation.

Coding the neural network training loop


Now that we have implemented the class in the previous subsection and initiated a
model, the next step is to train the model. We will tackle this in multiple steps. First,
we will define some helper functions for data loading. Second, we will embed these
functions into the training loop that iterates over the dataset in multiple
epochs.NeuralNetMLP

The first function we are going to define is a mini-batch generator, which takes in
our dataset and divides it into mini-batches of a desired size for stochastic gradient
descent training. The code is as follows:

>>> import numpy as np

>>> num_epochs = 50

>>> minibatch_size = 100

>>> def minibatch_generator(X, y, minibatch_size):

... indices = np.arange(X.shape[0])

... np.random.shuffle(indices)

... for start_idx in range(0, indices.shape[0] - minibatch_size

... + 1, minibatch_size):

... batch_idx = indices[start_idx:start_idx + minibatch_size]

... yield X[batch_idx], y[batch_idx]


CopyExplain

Before we move on to the next functions, let’s confirm that the mini-batch generator
works as intended and produces mini-batches of the desired size. The following
code will attempt to iterate through the dataset, and then we will print the
dimension of the mini-batches. Note that in the following code examples, we will
remove the statements. The code is as follows:break

>>> # iterate over training epochs

>>> for i in range(num_epochs):

... # iterate over minibatches

... minibatch_gen = minibatch_generator(


... X_train, y_train, minibatch_size)

... for X_train_mini, y_train_mini in minibatch_gen:

... break

... break

>>> print(X_train_mini.shape)

(100, 784)

>>> print(y_train_mini.shape)

(100,)
CopyExplain

As we can see, the network returns mini-batches of size 100 as intended.

Next, we have to define our loss function and performance metric that we can use
to monitor the training process and evaluate the model. The MSE loss and
accuracy function can be implemented as follows:

>>> def mse_loss(targets, probas, num_labels=10):

... onehot_targets = int_to_onehot(

... targets, num_labels=num_labels

... )

... return np.mean((onehot_targets - probas)**2)

>>> def accuracy(targets, predicted_labels):

... return np.mean(predicted_labels == targets)


CopyExplain

Let’s test the preceding function and compute the initial validation set MSE and
accuracy of the model we instantiated in the previous section:

>>> _, probas = model.forward(X_valid)

>>> mse = mse_loss(y_valid, probas)

>>> print(f'Initial validation MSE: {mse:.1f}')


Initial validation MSE: 0.3

>>> predicted_labels = np.argmax(probas, axis=1)

>>> acc = accuracy(y_valid, predicted_labels)

>>> print(f'Initial validation accuracy: {acc*100:.1f}%')

Initial validation accuracy: 9.4%


CopyExplain

In this code example, note that returns the hidden and output layer activations.
Remember that we have 10 output nodes (one corresponding to each unique class
label). Hence, when computing the MSE, we first converted the class labels into
one-hot encoded class labels in the function. In practice, it does not make a
difference whether we average over the row or the columns of the squared-
difference matrix first, so we simply call without any axis specification so that it
returns a scalar.model.forward()mse_loss()np.mean()

The output layer activations, since we used the logistic sigmoid function, are values
in the range [0, 1]. For each input, the output layer produces 10 values in the range
[0, 1], so we used the function to select the index position of the largest value,
which yields the predicted class label. We then compared the true labels with the
predicted class labels to compute the accuracy via the function we defined. As we
can see from the preceding output, the accuracy is not very high. However, given
that we have a balanced dataset with 10 classes, a prediction accuracy of
approximately 10 percent is what we would expect for an untrained model
producing random predictions.np.argmax()accuracy()

Using the previous code, we can compute the performance on, for example, the
whole training set if we provide as input to targets and the predicted labels from
feeding the model with . However, in practice, our computer memory is usually a
limiting factor for how much data the model can ingest in one forward pass (due to
the large matrix multiplications). Hence, we are defining our MSE and accuracy
computation based on our previous mini-batch generator. The following function
will compute the MSE and accuracy incrementally by iterating over the dataset one
mini-batch at a time to be more memory-efficient: y_trainX_train

>>> def compute_mse_and_acc(nnet, X, y, num_labels=10,


... minibatch_size=100):

... mse, correct_pred, num_examples = 0., 0, 0

... minibatch_gen = minibatch_generator(X, y, minibatch_size)

... for i, (features, targets) in enumerate(minibatch_gen):

... _, probas = nnet.forward(features)

... predicted_labels = np.argmax(probas, axis=1)

... onehot_targets = int_to_onehot(

... targets, num_labels=num_labels

... )

... loss = np.mean((onehot_targets - probas)**2)

... correct_pred += (predicted_labels == targets).sum()

... num_examples += targets.shape[0]

... mse += loss

... mse = mse/i

... acc = correct_pred/num_examples

... return mse, acc


CopyExplain

Before we implement the training loop, let’s test the function and compute the initial
training set MSE and accuracy of the model we instantiated in the previous section
and make sure it works as intended:

>>> mse, acc = compute_mse_and_acc(model, X_valid, y_valid)

>>> print(f'Initial valid MSE: {mse:.1f}')

Initial valid MSE: 0.3

>>> print(f'Initial valid accuracy: {acc*100:.1f}%')

Initial valid accuracy: 9.4%


CopyExplain
As we can see from the results, our generator approach produces the same results
as the previously defined MSE and accuracy functions, except for a small rounding
error in the MSE (0.27 versus 0.28), which is negligible for our purposes.

Let’s now get to the main part and implement the code to train our model:

>>> def train(model, X_train, y_train, X_valid, y_valid, num_epochs,

... learning_rate=0.1):

... epoch_loss = []

... epoch_train_acc = []

... epoch_valid_acc = []

...

... for e in range(num_epochs):

... # iterate over minibatches

... minibatch_gen = minibatch_generator(

... X_train, y_train, minibatch_size)

... for X_train_mini, y_train_mini in minibatch_gen:

... #### Compute outputs ####

... a_h, a_out = model.forward(X_train_mini)

... #### Compute gradients ####

... d_loss__d_w_out, d_loss__d_b_out, \

... d_loss__d_w_h, d_loss__d_b_h = \

... model.backward(X_train_mini, a_h, a_out,

... y_train_mini)

...

... #### Update weights ####

... model.weight_h -= learning_rate * d_loss__d_w_h

... model.bias_h -= learning_rate * d_loss__d_b_h


... model.weight_out -= learning_rate * d_loss__d_w_out

... model.bias_out -= learning_rate * d_loss__d_b_out

...

... #### Epoch Logging ####

... train_mse, train_acc = compute_mse_and_acc(

... model, X_train, y_train

... )

... valid_mse, valid_acc = compute_mse_and_acc(

... model, X_valid, y_valid

... )

... train_acc, valid_acc = train_acc*100, valid_acc*100

... epoch_train_acc.append(train_acc)

... epoch_valid_acc.append(valid_acc)

... epoch_loss.append(train_mse)

... print(f'Epoch: {e+1:03d}/{num_epochs:03d} '

... f'| Train MSE: {train_mse:.2f} '

... f'| Train Acc: {train_acc:.2f}% '

... f'| Valid Acc: {valid_acc:.2f}%')

...

... return epoch_loss, epoch_train_acc, epoch_valid_acc


CopyExplain

On a high level, the function iterates over multiple epochs, and in each epoch, it
used the previously defined function to iterate over the whole training set in mini-
batches for stochastic gradient descent training. Inside the mini-batch
generator loop, we obtain the outputs from the model, and , via its method. Then,
we compute the loss gradients via the model’s method—the theory will be
explained in a later section. Using the loss gradients, we update the weights by
adding the negative gradient multiplied by the learning rate. This is the same
concept that we discussed earlier for Adaline. For example, to update the model
weights of the hidden layer, we defined the following
line:train()minibatch_generator()fora_ha_out.forward().backward()

model.weight_h -= learning_rate * d_loss__d_w_h


CopyExplain

For a single weight, wj, this corresponds to the following partial derivative-based


update:

Finally, the last portion of the previous code computes the losses and prediction
accuracies on the training and test sets to track the training progress.

Let’s now execute this function to train our model for 50 epochs, which may take a
few minutes to finish:

>>> np.random.seed(123) # for the training set shuffling

>>> epoch_loss, epoch_train_acc, epoch_valid_acc = train(

... model, X_train, y_train, X_valid, y_valid,

... num_epochs=50, learning_rate=0.1)


CopyExplain

During training, we should see the following output:

Epoch: 001/050 | Train MSE: 0.05 | Train Acc: 76.17% | Valid Acc: 76.02%

Epoch: 002/050 | Train MSE: 0.03 | Train Acc: 85.46% | Valid Acc: 84.94%

Epoch: 003/050 | Train MSE: 0.02 | Train Acc: 87.89% | Valid Acc: 87.64%

Epoch: 004/050 | Train MSE: 0.02 | Train Acc: 89.36% | Valid Acc: 89.38%

Epoch: 005/050 | Train MSE: 0.02 | Train Acc: 90.21% | Valid Acc: 90.16%
...

Epoch: 048/050 | Train MSE: 0.01 | Train Acc: 95.57% | Valid Acc: 94.58%

Epoch: 049/050 | Train MSE: 0.01 | Train Acc: 95.55% | Valid Acc: 94.54%

Epoch: 050/050 | Train MSE: 0.01 | Train Acc: 95.59% | Valid Acc: 94.74%
CopyExplain

The reason why we print all this output is that, in NN training, it is really useful to
compare training and validation accuracy. This helps us judge whether the network
model performs well, given the architecture and hyperparameters. For example, if
we observe a low training and validation accuracy, there is likely an issue with the
training dataset, or the hyperparameters’ settings are not ideal.

In general, training (deep) NNs is relatively expensive compared with the other
models we’ve discussed so far. Thus, we want to stop it early in certain
circumstances and start over with different hyperparameter settings. On the other
hand, if we find that it increasingly tends to overfit the training data (noticeable by
an increasing gap between training and validation dataset performance), we may
want to stop the training early, as well.

In the next subsection, we will discuss the performance of our NN model in more
detail.

Evaluating the neural network performance


Before we discuss backpropagation, the training procedure of NNs, in more detail
in the next section, let’s look at the performance of the model that we trained in the
previous subsection.

In , we collected the training loss and the training and validation accuracy for each
epoch so that we can visualize the results using Matplotlib. Let’s look at the training
MSE loss first:train()

>>> plt.plot(range(len(epoch_loss)), epoch_loss)

>>> plt.ylabel('Mean squared error')

>>> plt.xlabel('Epoch')
>>> plt.show()
CopyExplain

The preceding code plots the loss over the 50 epochs, as shown in Figure 11.7:

Figure 11.7: A plot of the MSE by the number of training epochs

As we can see, the loss decreased substantially during the first 10 epochs and
seems to slowly converge in the last 10 epochs. However, the small slope between
epoch 40 and epoch 50 indicates that the loss would further decrease with training
over additional epochs.

Next, let’s take a look at the training and validation accuracy:

>>> plt.plot(range(len(epoch_train_acc)), epoch_train_acc,

... label='Training')

>>> plt.plot(range(len(epoch_valid_acc)), epoch_valid_acc,

... label='Validation')

>>> plt.ylabel('Accuracy')
>>> plt.xlabel('Epochs')

>>> plt.legend(loc='lower right')

>>> plt.show()
CopyExplain

The preceding code examples plot those accuracy values over the 50 training
epochs, as shown in Figure 11.8:

Figure 11.8: Classification accuracy by the number of training epochs

The plot reveals that the gap between training and validation accuracy increases
as we train for more epochs. At approximately the 25th epoch, the training and
validation accuracy values are almost equal, and then, the network starts to slightly
overfit the training data.

Reducing overfitting

One way to decrease the effect of overfitting is to increase the regularization


strength via L2 regularization, which we introduced in Chapter 3, A Tour of
Machine Learning Classifiers Using Scikit-Learn. Another useful technique for
tackling overfitting in NNs is dropout, which will be covered in Chapter
14, Classifying Images with Deep Convolutional Neural Networks.

Finally, let’s evaluate the generalization performance of the model by calculating


the prediction accuracy on the test dataset:

>>> test_mse, test_acc = compute_mse_and_acc(model, X_test, y_test)

>>> print(f'Test accuracy: {test_acc*100:.2f}%')

Test accuracy: 94.51%


CopyExplain

We can see that the test accuracy is very close to the validation set accuracy
corresponding to the last epoch (94.74%), which we reported during the training in
the last subsection. Moreover, the respective training accuracy is only minimally
higher at 95.59%, reaffirming that our model only slightly overfits the training data.

To further fine-tune the model, we could change the number of hidden units, the
learning rate, or use various other tricks that have been developed over the years
but are beyond the scope of this book. In Chapter 14, Classifying Images with
Deep Convolutional Neural Networks, you will learn about a different NN
architecture that is known for its good performance on image datasets.

Also, the chapter will introduce additional performance-enhancing tricks such as


adaptive learning rates, more sophisticated SGD-based optimization algorithms,
batch normalization, and dropout.

Other common tricks that are beyond the scope of the following chapters include:

 Adding skip-connections, which are the main contribution of residual NNs


(Deep residual learning for image recognition by K. He, X. Zhang, S. Ren,
and J. Sun, Proceedings of the IEEE Conference on Computer Vision and
Pattern Recognition, pp. 770-778, 2016)
 Using learning rate schedulers that change the learning rate during training
(Cyclical learning rates for training neural networks by L.N. Smith, 2017 IEEE
Winter Conference on Applications of Computer Vision (WACV), pp. 464-472,
2017)
 Attaching loss functions to earlier layers in the networks as it’s being done in
the popular Inception v3 architecture (Rethinking the Inception architecture for
computer vision by C. Szegedy, V. Vanhoucke, S. Ioffe, J. Shlens, and Z.
Wojna, Proceedings of the IEEE Conference on Computer Vision and Pattern
Recognition, pp. 2818-2826, 2016)

Lastly, let’s take a look at some of the images that our MLP struggles with by
extracting and plotting the first 25 misclassified samples from the test set:

>>> X_test_subset = X_test[:1000, :]

>>> y_test_subset = y_test[:1000]

>>> _, probas = model.forward(X_test_subset)

>>> test_pred = np.argmax(probas, axis=1)

>>> misclassified_images = \

... X_test_subset[y_test_subset != test_pred][:25]

>>> misclassified_labels = test_pred[y_test_subset != test_pred][:25]

>>> correct_labels = y_test_subset[y_test_subset != test_pred][:25]

>>> fig, ax = plt.subplots(nrows=5, ncols=5,

... sharex=True, sharey=True,

... figsize=(8, 8))

>>> ax = ax.flatten()

>>> for i in range(25):

... img = misclassified_images[i].reshape(28, 28)

... ax[i].imshow(img, cmap='Greys', interpolation='nearest')

... ax[i].set_title(f'{i+1}) '

... f'True: {correct_labels[i]}\n'

... f' Predicted: {misclassified_labels[i]}')

>>> ax[0].set_xticks([])

>>> ax[0].set_yticks([])
>>> plt.tight_layout()

>>> plt.show()
CopyExplain

We should now see a 5×5 subplot matrix where the first number in the subtitles
indicates the plot index, the second number represents the true class label (), and
the third number stands for the predicted class label (): TruePredicted

Figure 11.9: Handwritten digits that the model fails to classify correctly
Como podemos ver na Figura 11.9, entre outros, a rede encontra 7s desafiadores
quando incluem uma linha horizontal como nos exemplos 19 e 20. Olhando para
trás em uma figura anterior neste capítulo, onde traçamos diferentes exemplos de
treinamento do número 7, podemos hipotetizar que o dígito manuscrito 7 com uma
linha horizontal está sub-representado em nosso conjunto de dados e é
frequentemente classificado incorretamente.

Treinando uma rede neural artificial


Agora que vimos um NN em ação e ganhamos uma compreensão básica de como
ele funciona examinando o código, vamos nos aprofundar um pouco mais em
alguns dos conceitos, como o cálculo de perda e o algoritmo de backpropagation
que implementamos para aprender os parâmetros do modelo.

Calculando a função de perda


Como mencionado anteriormente, usamos uma perda de MSE (como em Adaline)
para treinar o NN multicamada, pois torna a derivação dos gradientes um pouco
mais fácil de seguir. Nos próximos capítulos, discutiremos outras funções de
perda, como a perda de entropia cruzada multicategoria (uma generalização da
perda de regressão logística binária), que é uma escolha mais comum para o
treinamento de classificadores NN.

Na seção anterior, implementamos um MLP para classificação multiclasse que


retorna um vetor de saída de elementos t que precisamos comparar com o
vetor alvo t×1 dimensional na representação de codificação one-hot. Se
prevermos o rótulo de classe de uma imagem de entrada com rótulo de classe 2,
usando esse MLP, a ativação da terceira camada e do destino pode ter a seguinte
aparência:
Assim, nossa perda de MSE tem que somar ou fazer a média sobre as
unidades de ativação t em nossa rede, além da média sobre os n exemplos no
conjunto de dados ou mini-lote:

Aqui, novamente, o sobrescrito [i] é o índice de um exemplo específico em nosso


conjunto de dados de treinamento.

Lembre-se que nosso objetivo é minimizar a perda de função L(W); assim,


precisamos calcular a derivada parcial dos parâmetros W com relação a cada
peso para cada camada da rede:

Na próxima seção, falaremos sobre o algoritmo de backpropagation, que nos


permite calcular essas derivadas parciais para minimizar a função de perda.

Observe que W consiste em múltiplas matrizes. Em um MLP com uma camada


oculta, temos a matriz de peso, W(h), que conecta a entrada à camada oculta
e W(fora), que conecta a camada oculta à camada de saída. Uma visualização do
tensor tridimensional W é fornecida na Figura 11.10:

Figura 11.10: Visualização de um tensor tridimensional

Nesta figura simplificada, pode parecer que ambos W(h) e W(fora) têm o mesmo


número de linhas e colunas, o que normalmente não é o caso, a menos
que inicializemos um MLP com o mesmo número de unidades ocultas, unidades
de saída e recursos de entrada.
Se isso soa confuso, fique atento para a próxima seção, onde discutiremos a
dimensionalidade de W(h) e W(fora) em mais detalhes no contexto do algoritmo de
backpropagation. Além disso, você é encorajado a ler o código de novamente, que
é anotado com comentários úteis sobre a dimensionalidade das diferentes
matrizes e transformações vetoriais.NeuralNetMLP

Desenvolvendo sua compreensão de


backpropagation
Embora a backpropagation tenha sido introduzida na comunidade de redes
neurais há mais de 30 anos (Learning representations by backpropagating errors,
por D.E. Rumelhart, G.E. Hinton e R.J. Williams, Nature, 323: 6088, páginas 533–
536, 1986), ela continua sendo um dos algoritmos mais amplamente utilizados
para treinar NNs artificiais de forma muito eficiente. Se você está interessado em
referências adicionais sobre a história do backpropagation, Juergen Schmidhuber
escreveu um belo artigo de pesquisa, Who Invented Backpropagation?, que você
pode encontrar online em http://people.idsia.ch/~juergen/who-invented-
backpropagation.html.

Esta seção fornecerá um resumo curto e claro e uma visão geral de como esse
algoritmo fascinante funciona antes de mergulharmos em mais detalhes
matemáticos. Em essência, podemos pensar em backpropagation como uma
abordagem computacionalmente muito eficiente para calcular as derivadas
parciais de uma função de perda complexa e não convexa em NNs multicamadas.
Aqui, nosso objetivo é usar esses derivados para aprender os coeficientes de peso
para parametrizar tal NN artificial multicamadas. O desafio na parametrização de
NNs é que normalmente estamos lidando com um número muito grande de
parâmetros de modelo em um espaço de feição de alta dimensão. Em contraste
com as funções de perda de NNs de camada única, como Adaline ou regressão
logística, que vimos nos capítulos anteriores, a superfície de erro de uma função
de perda de NN não é convexa ou suave em relação aos parâmetros. Há muitos
solavancos nesta superfície de perda de alta dimensão (mínimos locais) que
temos que superar para encontrar o mínimo global da função de perda.
Você pode se lembrar do conceito da regra de cadeia de suas aulas introdutórias
de cálculo. A regra de cadeia é uma abordagem para calcular a derivada de uma
função complexa aninhada, como f(g(x)), da seguinte maneira:

Da mesma forma, podemos usar a regra de cadeia para uma composição de


função arbitrariamente longa. Por exemplo, vamos supor que temos cinco funções
diferentes, f(x), g(x), h(x), u(x) e v(x), e deixar F ser a composição da função: F(x)
= f(g(h(u(v(x)))). Aplicando a regra de cadeia, podemos calcular a derivada desta
função da seguinte maneira:

No contexto da álgebra computacional, um conjunto de técnicas, conhecido


como diferenciação automática, foi desenvolvido para resolver tais problemas de
forma muito eficiente. Se você está interessado em aprender mais sobre
diferenciação automática em aplicativos de aprendizado de máquina, leia o artigo
de A.G. Baydin e B.A. Pearlmutter, Automatic Differentiation of Algorithms for
Machine Learning, arXiv preprint arXiv:1404.7456, 2014, que está disponível
gratuitamente no arXiv em http://arxiv.org/pdf/1404.7456.pdf.

A diferenciação automática vem com dois modos, os modos para frente e para
trás; backpropagation é simplesmente um caso especial de diferenciação
automática de modo reverso. O ponto chave é que aplicar a regra de cadeia no
modo forward poderia ser bastante caro, já que teríamos que multiplicar grandes
matrizes para cada camada (jacobianos) que eventualmente multiplicaríamos por
um vetor para obter a saída.

O truque do modo reverso é que atravessamos a regra de cadeia da direita para a


esquerda. Multiplicamos uma matriz por um vetor, o que produz outro vetor que é
multiplicado pela próxima matriz, e assim por diante. A multiplicação matriz-vetor é
computacionalmente muito mais barata do que a multiplicação matriz-matriz, razão
pela qual a retropropagação é um dos algoritmos mais populares usados no
treinamento de NN.

Uma atualização básica de cálculo

Para entender completamente o backpropagation, precisamos tomar emprestado


certos conceitos do cálculo diferencial, o que está fora do escopo deste livro. No
entanto, você pode consultar um capítulo de revisão dos conceitos mais
fundamentais, que você pode achar útil neste contexto. Discute derivadas de
função, derivadas parciais, gradientes e o jacobiano. Este texto pode
ser acessado gratuitamente em
https://sebastianraschka.com/pdf/books/dlb/appendix_d_calculus.pdf. Se você não
estiver familiarizado com cálculo ou precisar de uma breve atualização, considere
ler este texto como um recurso de suporte adicional antes de ler a próxima seção.

Treinamento de redes neurais via backpropagation


Nesta seção, vamos passar pela matemática da retropropagação para entender
como você pode aprender os pesos em um NN de forma muito eficiente.
Dependendo de quão confortável você está com representações matemáticas, as
equações a seguir podem parecer relativamente complicadas no início.

Em uma seção anterior, vimos como calcular a perda como a diferença entre a
ativação da última camada e o rótulo da classe de destino. Agora, veremos como
o algoritmo de backpropagation funciona para atualizar os pesos em nosso
modelo MLP de uma perspectiva matemática, que implementamos no método da
classe. Como lembramos do início deste capítulo, primeiro precisamos aplicar a
propagação direta para obter a ativação da camada de saída, que formulamos da
seguinte forma:.backward()NeuralNetMLP()
Resumidamente, apenas propagamos os recursos de entrada através das
conexões na rede, como mostram as setas na Figura 11.11 para uma rede com
dois recursos de entrada, três nós ocultos e dois nós de saída:

Figura 11.11: Propagação direta dos recursos de entrada de um NN

No backpropagation, propagamos o erro da direita para a esquerda. Podemos


pensar nisso como uma aplicação da regra de cadeia ao cálculo do passe para
frente para calcular o gradiente da perda em relação aos pesos do modelo (e
unidades de viés). Para simplificar, ilustraremos este processo para a derivada
parcial usada para atualizar o primeiro peso na matriz de peso da camada de
saída. Os caminhos da computação que retropropagamos são destacados através
das setas em negrito abaixo:

Figura 11.12: Retropropagando o erro de um NN

Se incluirmos as entradas líquidas z explicitamente, o cálculo de derivada parcial


mostrado na figura anterior se expande da seguinte maneira:

Para calcular essa derivada parcial, que é usada para atualizar ,


podemos calcular os três termos individuais da derivada parcial e multiplicar os
resultados. Para simplificar, omitiremos a média sobre os exemplos individuais no

minilote, então descartaremos o   termo de média das equações a


seguir.
Vamos começar com , que é a derivada parcial da perda de MSE (que simplifica

para o erro quadrático se omitirmos a dimensão de minilote) com 


relação à pontuação de saída prevista do primeiro nó de saída:

O próximo termo é a derivada da função de ativação do sigmoide logístico que


usamos na camada de saída:

Por fim, calculamos a derivada da entrada líquida em relação ao peso:

Juntando tudo isso, temos o seguinte:

Em seguida, usamos esse valor para atualizar o peso por meio da atualização de

descida de gradiente estocástico familiar com uma taxa de aprendizado de :


Em nossa implementação de código do , implementamos a

computação   em forma vetorizada no método da seguinte


forma:NeuralNetMLP().backward()

# Part 1: dLoss/dOutWeights

## = dLoss/dOutAct * dOutAct/dOutNet * dOutNet/dOutWeight

## where DeltaOut = dLoss/dOutAct * dOutAct/dOutNet for convenient re-use

# input/output dim: [n_examples, n_classes]

d_loss__d_a_out = 2.*(a_out - y_onehot) / y.shape[0]

# input/output dim: [n_examples, n_classes]

d_a_out__d_z_out = a_out * (1. - a_out) # sigmoid derivative

# output dim: [n_examples, n_classes]

delta_out = d_loss__d_a_out * d_a_out__d_z_out # "delta (rule)

# placeholder"

# gradient for output weights

# [n_examples, n_hidden]

d_z_out__dw_out = a_h

# input dim: [n_classes, n_examples] dot [n_examples, n_hidden]

# output dim: [n_classes, n_hidden]


d_loss__dw_out = np.dot(delta_out.T, d_z_out__dw_out)

d_loss__db_out = np.sum(delta_out, axis=0)


CopyExplain

As annotated in the code snippet above, we created the following “delta”


placeholder variable:

This is because   terms are involved in computing the partial


derivatives (or gradients) of the hidden layer weights as well; hence, we can

reuse  .

Speaking of hidden layer weights, Figure 11.13 illustrates how to compute the


partial derivative of the loss with respect to the first weight of the hidden layer:

Figure 11.13: Computing the partial derivatives of the loss with respect to the first
hidden layer weight
It is important to highlight that since the weight   is connected to both
output nodes, we have to use the multi-variable chain rule to sum the two paths
highlighted with bold arrows. As before, we can expand it to include the net
inputs z and then solve the individual terms:

Notice that if we reuse   computed previously, this equation can be


simplified as follows:

Os termos anteriores podem ser resolvidos individualmente com relativa facilidade,


como fizemos anteriormente, porque não há novos derivados envolvidos. Por
exemplo, é a derivada da ativação sigmoide, isto é,   

e assim por diante. Deixaremos a resolução das


partes individuais como um exercício opcional para você.

Sobre convergência em redes neurais


Você deve estar se perguntando por que não usamos a descida de gradiente
regular, mas em vez disso usamos o aprendizado em mini-lote para treinar nosso
NN para a classificação de dígitos manuscritos anteriormente. Você deve se
lembrar de nossa discussão sobre SGD que usamos para implementar o
aprendizado on-line. No aprendizado on-line, calculamos o gradiente com base em
um único exemplo de treinamento (k = 1) de cada vez para realizar a atualização
de peso. Embora esta seja uma abordagem estocástica, muitas vezes leva a
soluções muito precisas com uma convergência muito mais rápida do que a
descida de gradiente regular. O aprendizado em mini-lote é uma forma especial de
SGD onde calculamos o gradiente com base em um subconjunto k dos n
exemplos de treinamento com 1 < k < n. O aprendizado em mini-lote tem uma
vantagem sobre o aprendizado on-line na medida em que podemos fazer uso de
nossas implementações vetorizadas para melhorar a eficiência computacional. No
entanto, podemos atualizar os pesos muito mais rápido do que na descida de
gradiente regular. Intuitivamente, você pode pensar em aprendizado de minilote
como prever a participação eleitoral de uma eleição presidencial a partir de uma
pesquisa, perguntando apenas a um subconjunto representativo da população, em
vez de perguntar a toda a população (o que seria igual a disputar a eleição real).

NNs multicamadas são muito mais difíceis de treinar do que algoritmos mais
simples, como Adaline, regressão logística ou máquinas de vetores de suporte.
Em NNs multicamadas, normalmente temos centenas, milhares ou até bilhões de
pesos que precisamos otimizar. Infelizmente, a função de saída tem uma
superfície áspera, e o algoritmo de otimização pode facilmente ficar preso em
mínimos locais, como mostrado na Figura 11.14:

Figura 11.14: Os algoritmos de otimização podem ficar presos em mínimos locais

Note que essa representação é extremamente simplificada, pois nosso NN tem


muitas dimensões; Isso torna impossível visualizar a superfície de perda real para
o olho humano. Aqui, mostramos apenas a superfície de perda para um único
peso no eixo x. No entanto, a mensagem principal é que não queremos que nosso
algoritmo fique preso em mínimos locais. Ao aumentar a taxa de aprendizagem,
podemos escapar mais facilmente desses mínimos locais. Por outro lado, também
aumentamos a chance de ultrapassar o ótimo global se a taxa de aprendizado for
muito grande. Como inicializamos os pesos aleatoriamente, começamos com uma
solução para o problema de otimização que normalmente está irremediavelmente
errada.

Algumas últimas palavras sobre a


implementação da rede neural
Você pode estar se perguntando por que passamos por toda essa teoria apenas
para implementar uma rede artificial multicamada simples que pode classificar
dígitos manuscritos em vez de usar uma biblioteca de aprendizado de máquina
Python de código aberto. Na verdade, apresentaremos modelos NN mais
complexos nos próximos capítulos, que treinaremos usando a biblioteca de código
aberto PyTorch (https://pytorch.org).

Embora a implementação do zero neste capítulo pareça um pouco tediosa no


início, foi um bom exercício para entender o básico por trás do backpropagation e
do treinamento de NN. Uma compreensão básica de algoritmos é crucial para
aplicar técnicas de aprendizado de máquina de forma adequada e bem-sucedida.

Agora que você aprendeu como os NNs feedforward funcionam, estamos prontos
para explorar DNNs mais sofisticados usando o PyTorch, o que nos permite
construir NNs de forma mais eficiente, como veremos no Capítulo
12, Paralelizando o treinamento de redes neurais com o PyTorch.

O PyTorch, que foi lançado originalmente em setembro de 2016, ganhou muita


popularidade entre os pesquisadores de aprendizado de máquina, que o usam
para construir DNNs por causa de sua capacidade de otimizar expressões
matemáticas para cálculos em matrizes multidimensionais utilizando unidades de
processamento gráfico (GPUs).

Por fim, devemos notar que o scikit-learn também inclui uma implementação MLP
básica, que você pode encontrar
em https://scikit-learn.org/stable/modules/generated/sklearn.neural_network.MLPC
lassifier.html. Embora essa implementação seja ótima e muito conveniente para o
treinamento de MLPs básicos, recomendamos fortemente bibliotecas
especializadas de aprendizado profundo, como o PyTorch, para implementar e
treinar NNs multicamadas.MLPClassifier

Implementando uma Rede Neural


Artificial Multicamada do Zero
Como você deve saber, o deep learning está recebendo muita atenção da
imprensa e é, sem dúvida, o tópico mais quente no campo do aprendizado de
máquina. O aprendizado profundo pode ser entendido como um subcampo do
aprendizado de máquina que se preocupa em treinar redes neurais artificiais
(NNs) com muitas camadas de forma eficiente. Neste capítulo, você aprenderá os
conceitos básicos de NNs artificiais para que você esteja bem equipado para os
capítulos seguintes, que apresentarão bibliotecas avançadas de aprendizado
profundo baseadas em Python e arquiteturas de rede neural profunda (DNN) que
são particularmente adequadas para análises de imagem e texto.

Os tópicos que abordaremos neste capítulo são os seguintes:

 Obtendo uma compreensão conceitual de NNs multicamadas

 Implementando o algoritmo fundamental de backpropagation para


treinamento de NN do zero

 Treinamento de um NN multicamada básico para classificação de imagens

Modelando funções complexas com


redes neurais artificiais
No início deste livro, começamos nossa jornada através de algoritmos de
aprendizado de máquina com neurônios artificiais no Capítulo 2, Treinando
algoritmos simples de aprendizado de máquina para classificação. Os neurônios
artificiais representam os blocos de construção dos NNs artificiais multicamadas
que discutiremos neste capítulo.

O conceito básico por trás dos NNs artificiais foi construído sobre hipóteses e
modelos de como o cérebro humano funciona para resolver tarefas de problemas
complexos. Embora os NNs artificiais tenham ganhado muita popularidade nos
últimos anos, os primeiros estudos sobre NNs remontam à década de 1940,
quando Warren McCulloch e Walter Pitts descreveram pela primeira vez como os
neurônios poderiam funcionar. (Um cálculo lógico das ideias imanentes à
atividade nervosa, por W. S. McCulloch e W. Pitts, The Bulletin of Mathematical
Biophysics, 5(4):115–133, 1943.)
No entanto, nas décadas que se seguiram à primeira implementação do modelo
de neurônios McCulloch-Pitts – o perceptron de Rosenblatt na década de 1950
– muitos pesquisadores e praticantes de aprendizado de máquina lentamente
começaram a perder o interesse em NNs, já que ninguém tinha uma boa solução
para treinar um NN com várias camadas. Eventualmente, o interesse em NNs foi
reacendido em 1986, quando D.E. Rumelhart, G.E. Hinton e R.J. Williams
estiveram envolvidos na (re)descoberta e popularização do algoritmo de
backpropagation para treinar NNs de forma mais eficiente, que discutiremos mais
detalhadamente mais adiante neste capítulo (Learning representations by
backpropagating errors, por D.E. Rumelhart, G.E. Hinton e R.J.
Williams, Natureza, 323 (6088): 533–536, 1986). Os leitores que se interessam
pela história da inteligência artificial (IA), aprendizado de máquina e NNs
também são incentivados a ler o artigo da Wikipédia sobre os chamados invernos
de IA, que são os períodos de tempo em que uma grande parcela da comunidade
de pesquisa perdeu o interesse no estudo de NNs
(https://en.wikipedia.org/wiki/AI_winter).

No entanto, os NNs são mais populares hoje do que nunca graças aos muitos
avanços que foram feitos na década anterior, que resultaram no que agora
chamamos de algoritmos e arquiteturas de aprendizado profundo – NNs que são
compostos de muitas camadas. NNs são um tópico quente não apenas na
pesquisa acadêmica, mas também em grandes empresas de tecnologia, como
Facebook, Microsoft, Amazon, Uber, Google e muitas outras que investem
pesadamente em NNs artificiais e pesquisa de aprendizagem profunda.

A partir de hoje, NNs complexos alimentados por algoritmos de aprendizagem


profunda são considerados soluções de última geração para a resolução de
problemas complexos, como reconhecimento de imagem e voz. Algumas das
aplicações recentes incluem:

 Prever as necessidades de recursos COVID-19 a partir de uma série de raios-


X (https://arxiv.org/abs/2101.04909)
 Modelando mutações virais
(https://science.sciencemag.org/content/371/6526/284)
 Aproveitando dados de plataformas de mídia social para gerenciar eventos
climáticos extremos (https://onlinelibrary.wiley.com/doi/abs/10.1111/1468-
5973.12311)
 Melhorar as descrições de fotos para pessoas cegas ou com deficiência
visual (https://tech.fb.com/how-facebook-is-using-ai-to-improve-photo-
descriptions-for-people-who-are-blind-or-visually-impaired/)

Recapitulação de rede neural de camada única


Este capítulo é sobre NNs multicamadas, como eles funcionam e como treiná-los
para resolver problemas complexos. No entanto, antes de nos aprofundarmos em
uma arquitetura NN multicamada específica, vamos reiterar brevemente alguns
dos conceitos de NNs de camada única que introduzimos no Capítulo 2, a saber, o
algoritmo ADAptive LInear NEuron (Adaline), que é mostrado na Figura 11.1:

Figura 11.1: O algoritmo Adaline

No Capítulo 2, implementamos o algoritmo Adaline para realizar a classificação


binária, e usamos o algoritmo de otimização de descida de gradiente para
aprender os coeficientes de peso do modelo. Em cada época (passar por cima do
conjunto de dados de treinamento), atualizamos o vetor de peso w e a unidade de
viés b usando a seguinte regra de atualização:
onde   e para a unidade de viés

e   cada peso wj no vetor de peso w.

Em outras palavras, calculamos o gradiente com base em todo o conjunto de


dados de treinamento e atualizamos os pesos do modelo dando um passo na

direção oposta do gradiente de perda . (Para simplificar, vamos nos


concentrar nos pesos e omitir a unidade de viés nos parágrafos seguintes; no
entanto, como você lembra do Capítulo 2, os mesmos conceitos se aplicam.) Para
encontrar os pesos ótimos do modelo, otimizamos uma função objetivo que
definimos como a média dos erros quadrados (MSE) função de perda L(w).

Além disso, multiplicamos o gradiente por um fator, a taxa  de aprendizagem,


que tivemos que escolher cuidadosamente para equilibrar a velocidade de
aprendizagem contra o risco de ultrapassar o mínimo global da função de perda.

Na otimização da descida do gradiente, atualizamos todos os pesos


simultaneamente após cada época e definimos a derivada parcial para cada
peso wj No vetor peso, W, da seguinte forma:

Aqui, y(eu) é o rótulo da classe de destino de uma amostra específica x(eu)e um(eu) é


a ativação do neurônio, que é uma função linear no caso especial de Adaline.
Além disso, definimos a função   de ativação da seguinte forma:

Aqui, a entrada líquida, z, é uma combinação linear dos pesos que estão
conectando a camada de entrada à camada de saída:

Enquanto usamos a ativação   para calcular a atualização de gradiente,


implementamos uma função de limite para esmagar a saída de valor contínuo em
rótulos de classe binária para previsão:

Convenção de nomenclatura de camada única

Observe que, embora o Adaline consista em duas camadas, uma camada de


entrada e uma camada de saída, ele é chamado de rede de camada única por
causa de seu único link entre as camadas de entrada e saída.

Além disso, aprendemos sobre um certo truque para acelerar o aprendizado do


modelo, a chamada otimização de descida de gradiente estocástico (SGD). O
SGD aproxima a perda de uma única amostra de treinamento (aprendizado on-
line) ou de um pequeno subconjunto de exemplos de treinamento (aprendizado
em minilote). Faremos uso desse conceito mais adiante neste capítulo, quando
implementarmos e treinarmos um perceptron multicamadas (MLP). Além do
aprendizado mais rápido – devido às atualizações de peso mais frequentes em
comparação com a descida de gradiente – sua natureza ruidosa também é
considerada benéfica ao treinar NNs multicamadas com funções de ativação não
linear, que não têm uma função de perda convexa. Aqui, o ruído adicional pode
ajudar a escapar dos mínimos de perda local, mas discutiremos esse tópico com
mais detalhes mais adiante neste capítulo.

Apresentando a arquitetura de rede neural


multicamada
Nesta seção, você aprenderá como conectar vários neurônios únicos a um NN de
feedforward multicamadas; esse tipo especial de rede totalmente
conectada também é chamado de MLP.

A figura 11.2 ilustra o conceito de um MLP composto por duas camadas:

Figure 11.2: A two-layer MLP


Next to the data input, the MLP depicted in Figure 11.2 has one hidden layer and
one output layer. The units in the hidden layer are fully connected to the input
features, and the output layer is fully connected to the hidden layer. If such a
network has more than one hidden layer, we also call it a deep NN. (Note that in
some contexts, the inputs are also regarded as a layer. However, in this case, it
would make the Adaline model, which is a single-layer neural network, a two-layer
neural network, which may be counterintuitive.)

Adding additional hidden layers

We can add any number of hidden layers to the MLP to create deeper network
architectures. Practically, we can think of the number of layers and units in an NN
as additional hyperparameters that we want to optimize for a given problem task
using the cross-validation technique, which we discussed in Chapter 6, Learning
Best Practices for Model Evaluation and Hyperparameter Tuning.

However, the loss gradients for updating the network’s parameters, which we will
calculate later via backpropagation, will become increasingly small as more layers
are added to a network. This vanishing gradient problem makes model learning
more challenging. Therefore, special algorithms have been developed to help train
such DNN structures; this is known as deep learning, which we will discuss in
more detail in the following chapters.

As shown in Figure 11.2, we denote the ith activation unit in the lth layer as 
. To make the math and code implementations a bit more intuitive, we will not use
numerical indices to refer to layers, but we will use the in superscript for the input
features, the h superscript for the hidden layer, and the out superscript for the

output layer. For instance,   refers to the ith input feature value,   

refers to the ith unit in the hidden layer, and   refers to the ith unit in
the output layer. Note that the b’s in Figure 11.2 denote the bias units. In
fact, b(h) and b(out) are vectors with the number of elements being equal to the
number of nodes in the layer they correspond to. For example, b(h) stores d bias
units, where d is the number of nodes in the hidden layer. If this sounds confusing,
don’t worry. Looking at the code implementation later, where we initialize weight
matrices and bias unit vectors, will help clarify these concepts.

Each node in layer l is connected to all nodes in layer l + 1 via a weight coefficient.
For example, the connection between the kth unit in layer l to the jth unit in

layer l + 1 will be written as  . Referring back to Figure 11.2, we


denote the weight matrix that connects the input to the hidden layer as W(h), and we
write the matrix that connects the hidden layer to the output layer as W(out).

While one unit in the output layer would suffice for a binary classification task, we
saw a more general form of an NN in the preceding figure, which allows us to
perform multiclass classification via a generalization of the one-versus-all (OvA)
technique. To better understand how this works, remember the one-
hot representation of categorical variables that we introduced in Chapter
4, Building Good Training Datasets – Data Preprocessing.

For example, we can encode the three class labels in the familiar Iris dataset
(0=Setosa, 1=Versicolor, 2=Virginica) as follows:

This one-hot vector representation allows us to tackle classification tasks with an


arbitrary number of unique class labels present in the training dataset.

If you are new to NN representations, the indexing notation (subscripts and


superscripts) may look a little bit confusing at first. What may seem overly
complicated at first will make much more sense in later sections when we vectorize
the NN representation. As introduced earlier, we summarize the weights that
connect the input and hidden layers by a d×m dimensional matrix W(h), where d is
the number of hidden units and m is the number of input units.

Activating a neural network via forward


propagation
In this section, we will describe the process of forward propagation to calculate
the output of an MLP model. To understand how it fits into the context of learning
an MLP model, let’s summarize the MLP learning procedure in three simple steps:

1. Starting at the input layer, we forward propagate the patterns of the training
data through the network to generate an output.
2. Based on the network’s output, we calculate the loss that we want to minimize
using a loss function that we will describe later.
3. We backpropagate the loss, find its derivative with respect to each weight and
bias unit in the network, and update the model.

Finally, after we repeat these three steps for multiple epochs and learn the weight
and bias parameters of the MLP, we use forward propagation to calculate the
network output and apply a threshold function to obtain the predicted class labels
in the one-hot representation, which we described in the previous section.

Now, let’s walk through the individual steps of forward propagation to generate an
output from the patterns in the training data. Since each unit in the hidden layer is
connected to all units in the input layers, we first calculate the activation unit of the

hidden layer   as follows:


Aqui, é a entrada líquida e   é a função de ativação,   que tem que
ser diferenciável para aprender os pesos que conectam os neurônios usando uma
abordagem baseada em gradiente. Para sermos capazes de resolver problemas
complexos, como a classificação de imagens, precisamos de funções de ativação
não linear em nosso modelo MLP, por exemplo, a função de ativação sigmoide
(logística) que lembramos da seção sobre regressão logística no Capítulo 3, A
Tour of Machine Learning Classifiers Using Scikit-Learn:

Como você deve se lembrar, a função sigmoide é uma curva em forma de S que
mapeia a entrada líquida z em uma distribuição logística no intervalo de 0 a 1, que
corta o eixo y em z = 0, como mostrado na Figura 11.3:

Figura 11.3: A função de ativação do sigmoide


MLP é um exemplo típico de um NN artificial feedforward. O
termo feedforward refere-se ao fato de que cada camada serve como
entrada para a próxima camada sem loops, em contraste com NNs recorrentes —
uma arquitetura que discutiremos mais adiante neste capítulo e discutiremos com
mais detalhes no Capítulo 15, Modelando dados sequenciais usando redes
neurais recorrentes. O termo perceptron multicamada pode soar um pouco
confuso, uma vez que os neurônios artificiais nesta arquitetura de rede são
tipicamente unidades sigmoides, não perceptrons. Podemos pensar nos neurônios
do MLP como unidades de regressão logística que retornam valores na faixa
contínua entre 0 e 1.

Para fins de eficiência de código e legibilidade, agora escreveremos a ativação em


uma forma mais compacta usando os conceitos de álgebra linear básica, o que
nos permitirá vetorizar nossa implementação de código via NumPy em vez de
escrever vários loops Python aninhados e computacionalmente caros: for

Aqui, x(em) é o nosso vetor de feição dimensional × 1 m. O(h) é uma matriz de peso


dimensional D×m onde D é o número de unidades na camada
oculta; consequentemente, a matriz W transposta(h)T é m×d dimensional. O vetor
de viés b(h) consiste em unidades de viés d (uma unidade de viés por nó oculto).

Após a multiplicação matriz-vetor, obtém-se o vetor de entrada líquido


1×d dimensional z(h) Para calcular a ativação de

um(h) (onde  ).

Além disso, podemos generalizar esse cálculo para todos os n exemplos no


conjunto de dados de treinamento:
Z(h) = X(em)W(h)T + b(h)

Aqui, X(em) é agora uma matriz n×m, e a multiplicação da matriz resultará em uma


matriz de entrada líquida n×d dimensional, Z(h). Finalmente, aplicamos a

função   de ativação a cada valor na matriz de entrada líquida para obter
a matriz de ativação n×d na próxima camada (aqui, a camada de saída):

Da mesma forma, podemos escrever a ativação da camada de saída em forma


vetorizada para vários exemplos:

Z(fora) = Um(h)W(fora)T + b(fora)

Aqui, multiplicamos a transposição da matriz t×d W(fora) (t é o número de unidades


de saída) pela matriz dimensional n×d, A(h)e adicionar o vetor de
viés t dimensional b(fora) para obter a matriz n×t dimensional, Z(fora). (As linhas nessa
matriz representam as saídas de cada exemplo.)

Por fim, aplicamos a função de ativação sigmoide para obter a saída de valor
contínuo de nossa rede:

Similares a Z(fora), A(fora) é uma matriz n×t dimensional.

Classificando dígitos manuscritos


Na seção anterior, cobrimos muito da teoria em torno dos NNs, o que pode ser um
pouco esmagador se você é novo neste tópico. Antes de continuarmos com a
discussão do algoritmo para aprender os pesos do modelo MLP, backpropagation,
vamos fazer uma pequena pausa na teoria e ver um NN em ação.
Recursos adicionais sobre retropropagação

A teoria NN pode ser bastante complexa; Assim, queremos fornecer aos leitores


recursos adicionais que cobrem alguns dos tópicos que discutimos neste capítulo
com mais detalhes ou de uma perspectiva diferente:

 Capítulo 6, Deep Feedforward Networks, Deep Learning, por I. Goodfellow, Y.


Bengio e A. Courville, MIT Press, 2016 (manuscritos livremente acessíveis
em http://www.deeplearningbook.org).
 Reconhecimento de Padrões e Machine Learning, por C. M. Bishop, Springer
New York, 2006.
 Slides em vídeo da palestra do curso de aprendizagem profunda de
Sebastian Raschka:

https://sebastianraschka.com/blog/2021/dl-course.html#l08-multinomial-
logistic-regression--softmax-regression

https://sebastianraschka.com/blog/2021/dl-course.html#l09-multilayer-
perceptrons-and-backpropration

Nesta seção, implementaremos e treinaremos nosso primeiro NN multicamada


para classificar dígitos manuscritos do popular conjunto de dados do Mixed
National Institute of Standards and Technology (MNIST) que foi construído por
Yann LeCun e outros e serve como um conjunto de dados de referência popular
para algoritmos de aprendizado de máquina (Gradient-Based Learning Applied to
Document Recognition by Y. LeCun, L. Bottou, Y. Bengio e P. Haffner, Anais do
IEEE, 86(11): 2278-2324, 1998).

Obtendo e preparando o conjunto de dados


MNIST
O conjunto de dados MNIST está disponível publicamente
em http://yann.lecun.com/exdb/mnist/ e consiste nas quatro partes a seguir:
1. Imagens do conjunto de dados de treinamento: (9,9 MB, 47 MB
descompactados e 60.000 exemplos)train-images-idx3-ubyte.gz
2. Rótulos do conjunto de dados de treinamento: (29 KB, 60 KB
descompactados e 60.000 rótulos)train-labels-idx1-ubyte.gz
3. Testar imagens de conjunto de dados: (1,6 MB, 7,8 MB descompactados e
10.000 exemplos)t10k-images-idx3-ubyte.gz
4. Rótulos do conjunto de dados de teste: (5 KB, 10 KB descompactados e
10.000 rótulos)t10k-labels-idx1-ubyte.gz

O conjunto de dados MNIST foi construído a partir de dois conjuntos de dados


do Instituto Nacional de Padrões e Tecnologia dos EUA (NIST). O conjunto de
dados de treinamento consiste em dígitos manuscritos de 250 pessoas diferentes,
50% estudantes do ensino médio e 50% funcionários do Census Bureau. Observe
que o conjunto de dados de teste contém dígitos manuscritos de pessoas
diferentes seguindo a mesma divisão.

Em vez de baixar os arquivos de conjunto de dados acima mencionados e pré-


processá-los em matrizes NumPy, usaremos a nova função do scikit-learn, que
nos permite carregar o conjunto de dados MNIST de forma mais
conveniente:fetch_openml

>>> from sklearn.datasets import fetch_openml

>>> X, y = fetch_openml('mnist_784', version=1,

... return_X_y=True)

>>> X = X.values

>>> y = y.astype(int).values
CopyExplain

No scikit-learn, a função baixa o conjunto de dados MNIST do OpenML


(https://www.openml.org/d/554) como objetos pandas e Series, e é por isso que
usamos o atributo para obter as matrizes NumPy subjacentes. (Se você estiver
usando uma versão scikit-learn anterior à 1.0, baixe matrizes NumPy diretamente
para que você possa omitir o uso do atributo.) A matriz dimensional n×m consiste
em 70.000 imagens com 784 pixels cada, e a matriz armazena os rótulos de
classe 70.000 correspondentes, que podemos confirmar verificando as dimensões
dos arrays da seguinte maneira:fetch_openmlDataFrame.valuesfetch_openml.valuesXy

>>> print(X.shape)

(70000, 784)

>>> print(y.shape)

(70000,)
CopyExplain

As imagens no conjunto de dados MNIST consistem em 28×28 pixels, e cada pixel


é representado por um valor de intensidade em escala de cinza. Aqui, já
desenrolamos os 28×28 pixels em vetores de linha unidimensionais, que
representam as linhas em nossa matriz (784 por linha ou imagem) acima. A
segunda matriz () retornada pela função contém a variável de destino
correspondente, os rótulos de classe (inteiros 0-9) dos dígitos
manuscritos.fetch_openmlXyfetch_openml

Em seguida, vamos normalizar os valores de pixels no MNIST para o intervalo –1


a 1 (originalmente 0 a 255) por meio da seguinte linha de código:

>>> X = ((X / 255.) - .5) * 2


CopyExplain

A razão por trás disso é que a otimização baseada em gradiente é muito mais
estável nessas condições, como discutido no Capítulo 2. Observe que
dimensionamos as imagens pixel a pixel, o que é diferente da abordagem de
dimensionamento de recursos que adotamos nos capítulos anteriores.

Anteriormente, derivamos parâmetros de dimensionamento do conjunto de dados


de treinamento e os usamos para dimensionar cada coluna no conjunto de dados
de treinamento e no conjunto de dados de teste. No entanto, ao trabalhar com
pixels de imagem, centralizá-los em zero e reescaloná-los para um intervalo [–1, 1]
também é comum e geralmente funciona bem na prática.

Para ter uma ideia de como essas imagens no MNIST se parecem, vamos
visualizar exemplos dos dígitos de 0 a 9 depois de remodelar os vetores de 784
pixels de nossa matriz de recursos para a imagem original de 28×28 que podemos
plotar através da função de Matplotlib: imshow

>>> import matplotlib.pyplot as plt

>>> fig, ax = plt.subplots(nrows=2, ncols=5,

... sharex=True, sharey=True)

>>> ax = ax.flatten()

>>> for i in range(10):

... img = X[y == i][0].reshape(28, 28)

... ax[i].imshow(img, cmap='Greys')

>>> ax[0].set_xticks([])

>>> ax[0].set_yticks([])

>>> plt.tight_layout()

>>> plt.show()
CopyExplain

Agora devemos ver um gráfico das 2×5 subfiguras mostrando uma imagem
representativa de cada dígito único:
Figura 11.4: Gráfico mostrando um dígito manuscrito escolhido aleatoriamente de
cada classe

Além disso, também vamos plotar vários exemplos do mesmo dígito para ver o
quão diferente a caligrafia para cada um realmente é:

>>> fig, ax = plt.subplots(nrows=5,

... ncols=5,

... sharex=True,

... sharey=True)

>>> ax = ax.flatten()

>>> for i in range(25):

... img = X[y == 7][i].reshape(28, 28)

... ax[i].imshow(img, cmap='Greys')

>>> ax[0].set_xticks([])

>>> ax[0].set_yticks([])

>>> plt.tight_layout()

>>> plt.show()
CopyExplain

Depois de executar o código, agora devemos ver as primeiras 25 variantes do


dígito 7:
Figura 11.5: Diferentes variantes do dígito manuscrito 7

Finalmente, vamos dividir o conjunto de dados em subconjuntos de treinamento,


validação e teste. O código a seguir dividirá o conjunto de dados de forma que
55.000 imagens sejam usadas para treinamento, 5.000 imagens para validação e
10.000 imagens para teste:

>>> from sklearn.model_selection import train_test_split

>>> X_temp, X_test, y_temp, y_test = train_test_split(

... X, y, test_size=10000, random_state=123, stratify=y

... )

>>> X_train, X_valid, y_train, y_valid = train_test_split(

... X_temp, y_temp, test_size=5000,

... random_state=123, stratify=y_temp

... )
CopyExplain
Implementando um perceptron multicamada
Nesta subseção, agora implementaremos um MLP do zero para classificar as
imagens no conjunto de dados MNIST. Para manter as coisas simples, vamos
implementar um MLP com apenas uma camada oculta. Como a abordagem pode
parecer um pouco complicada no início, você é encorajado a baixar o código de
exemplo para este capítulo do site Packt Publishing ou do GitHub
(https://github.com/rasbt/machine-learning-book) para que você possa exibir essa
implementação MLP anotada com comentários e realce de sintaxe para melhor
legibilidade.

Se você não estiver executando o código do arquivo Jupyter Notebook que o


acompanha ou não tiver acesso à Internet, copie o código deste capítulo em um
arquivo de script Python em seu diretório de trabalho atual (por exemplo, , que
você pode importar para sua sessão Python atual através do seguinte
comando:NeuralNetMLPneuralnet.py)

from neuralnet import NeuralNetMLP


CopyExplain

O código conterá partes sobre as quais ainda não falamos, como o algoritmo de


backpropagation. Não se preocupe se nem todo o código faz sentido imediato
para você; Seguiremos algumas partes mais adiante neste capítulo. No entanto,
rever o código neste estágio pode tornar mais fácil seguir a teoria mais tarde.

Então, vamos examinar a seguinte implementação de um MLP, começando com


as duas funções auxiliares para calcular a ativação do sigmoide logístico e
converter matrizes de rótulo de classe inteira em rótulos codificados em um hot
codificado:

import numpy as np

def sigmoid(z):

return 1. / (1. + np.exp(-z))

def int_to_onehot(y, num_labels):

ary = np.zeros((y.shape[0], num_labels))


for i, val in enumerate(y):

ary[i, val] = 1

return ary
CopyExplain

Abaixo, implementamos a classe principal para o nosso MLP, que chamamos de .


Existem três métodos de classe, , e , que discutiremos um a um, começando com
o construtor:NeuralNetMLP.__init__().forward().backward()__init__

class NeuralNetMLP:

def __init__(self, num_features, num_hidden,

num_classes, random_seed=123):

super().__init__()

self.num_classes = num_classes

# hidden

rng = np.random.RandomState(random_seed)

self.weight_h = rng.normal(

loc=0.0, scale=0.1, size=(num_hidden, num_features))

self.bias_h = np.zeros(num_hidden)

# output

self.weight_out = rng.normal(

loc=0.0, scale=0.1, size=(num_classes, num_hidden))

self.bias_out = np.zeros(num_classes)
CopyExplain

O construtor instancia as matrizes de peso e vetores de polarização para a


camada oculta e a camada de saída. A seguir, vamos ver como eles são usados
no método para fazer previsões:__init__forward
def forward(self, x):

# Hidden layer

# input dim: [n_examples, n_features]

# dot [n_hidden, n_features].T

# output dim: [n_examples, n_hidden]

z_h = np.dot(x, self.weight_h.T) + self.bias_h

a_h = sigmoid(z_h)

# Output layer

# input dim: [n_examples, n_hidden]

# dot [n_classes, n_hidden].T

# output dim: [n_examples, n_classes]

z_out = np.dot(a_h, self.weight_out.T) + self.bias_out

a_out = sigmoid(z_out)

return a_h, a_out


CopyExplain

O método usa um ou mais exemplos de treinamento e retorna as previsões. Na


verdade, ele retorna os valores de ativação da camada oculta e da camada de
saída, e . Embora represente as probabilidades de associação de classe que
podemos converter em rótulos de classe, com as quais nos preocupamos,
também precisamos dos valores de ativação da camada oculta, , para otimizar os
parâmetros do modelo; isto é, as unidades de peso e viés das camadas ocultas e
de saída.forwarda_ha_outa_outa_h

Por fim, vamos falar sobre o método, que atualiza os parâmetros de peso e viés
da rede neural:backward

def backward(self, x, a_h, a_out, y):

#########################
### Output layer weights

#########################

# one-hot encoding

y_onehot = int_to_onehot(y, self.num_classes)

# Part 1: dLoss/dOutWeights

## = dLoss/dOutAct * dOutAct/dOutNet * dOutNet/dOutWeight

## where DeltaOut = dLoss/dOutAct * dOutAct/dOutNet

## for convenient re-use

# input/output dim: [n_examples, n_classes]

d_loss__d_a_out = 2.*(a_out - y_onehot) / y.shape[0]

# input/output dim: [n_examples, n_classes]

d_a_out__d_z_out = a_out * (1. - a_out) # sigmoid derivative

# output dim: [n_examples, n_classes]

delta_out = d_loss__d_a_out * d_a_out__d_z_out

# gradient for output weights

# [n_examples, n_hidden]

d_z_out__dw_out = a_h

# input dim: [n_classes, n_examples]

# dot [n_examples, n_hidden]

# output dim: [n_classes, n_hidden]

d_loss__dw_out = np.dot(delta_out.T, d_z_out__dw_out)

d_loss__db_out = np.sum(delta_out, axis=0)

#################################
# Part 2: dLoss/dHiddenWeights

## = DeltaOut * dOutNet/dHiddenAct * dHiddenAct/dHiddenNet

# * dHiddenNet/dWeight

# [n_classes, n_hidden]

d_z_out__a_h = self.weight_out

# output dim: [n_examples, n_hidden]

d_loss__a_h = np.dot(delta_out, d_z_out__a_h)

# [n_examples, n_hidden]

d_a_h__d_z_h = a_h * (1. - a_h) # sigmoid derivative

# [n_examples, n_features]

d_z_h__d_w_h = x

# output dim: [n_hidden, n_features]

d_loss__d_w_h = np.dot((d_loss__a_h * d_a_h__d_z_h).T,

d_z_h__d_w_h)

d_loss__d_b_h = np.sum((d_loss__a_h * d_a_h__d_z_h), axis=0)

return (d_loss__dw_out, d_loss__db_out,

d_loss__d_w_h, d_loss__d_b_h)
CopyExplain

The method implements the so-called backpropagation algorithm, which calculates


the gradients of the loss with respect to the weight and bias parameters. Similar to
Adaline, these gradients are then used to update these parameters via gradient
descent. Note that multilayer NNs are more complex than their single-layer
siblings, and we will go over the mathematical concepts of how to compute the
gradients in a later section after discussing the code. For now, just consider
the method as a way for computing gradients that are used for the gradient
descent updates. For simplicity, the loss function this derivation is based on is the
same MSE loss that we used in Adaline. In later chapters, we will look at
alternative loss functions, such as multi-category cross-entropy loss, which is a
generalization of the binary logistic regression loss to multiple
classes.backwardbackward

Looking at this code implementation of the class, you may have noticed that this
object-oriented implementation differs from the familiar scikit-learn API that is
centered around the and methods. Instead, the main methods of the class are
the and methods. One of the reasons behind this is that it makes a complex neural
network a bit easier to understand in terms of how the information flows through
the networks.NeuralNetMLP.fit().predict()NeuralNetMLP.forward().backward()

Another reason is that this implementation is relatively similar to how more


advanced deep learning libraries such as PyTorch operate, which we will introduce
and use in the upcoming chapters to implement more complex neural networks.

After we have implemented the class, we use the following code to instantiate a


new object:NeuralNetMLPNeuralNetMLP

>>> model = NeuralNetMLP(num_features=28*28,

... num_hidden=50,

... num_classes=10)
CopyExplain

The accepts MNIST images reshaped into 784-dimensional vectors (in the format
of , , or , which we defined previously) for the 10 integer classes (digits 0-9). The
hidden layer consists of 50 nodes. Also, as you may be able to tell from looking at
the previously defined method, we use a sigmoid activation function after the first
hidden layer and output layer to keep things simple. In later chapters, we will learn
about alternative activation functions for both the hidden and output
layers.modelX_trainX_validX_test.forward()

Figure 11.6 summarizes the neural network architecture that we instantiated


above:
Figure 11.6: The NN architecture for labeling handwritten digits

In the next subsection, we are going to implement the training function that we can
use to train the network on mini-batches of the data via backpropagation.

Coding the neural network training loop


Now that we have implemented the class in the previous subsection and initiated a
model, the next step is to train the model. We will tackle this in multiple steps. First,
we will define some helper functions for data loading. Second, we will embed these
functions into the training loop that iterates over the dataset in multiple
epochs.NeuralNetMLP

The first function we are going to define is a mini-batch generator, which takes in
our dataset and divides it into mini-batches of a desired size for stochastic gradient
descent training. The code is as follows:

>>> import numpy as np


>>> num_epochs = 50

>>> minibatch_size = 100

>>> def minibatch_generator(X, y, minibatch_size):

... indices = np.arange(X.shape[0])

... np.random.shuffle(indices)

... for start_idx in range(0, indices.shape[0] - minibatch_size

... + 1, minibatch_size):

... batch_idx = indices[start_idx:start_idx + minibatch_size]

... yield X[batch_idx], y[batch_idx]


CopyExplain

Before we move on to the next functions, let’s confirm that the mini-batch generator
works as intended and produces mini-batches of the desired size. The following
code will attempt to iterate through the dataset, and then we will print the
dimension of the mini-batches. Note that in the following code examples, we will
remove the statements. The code is as follows:break

>>> # iterate over training epochs

>>> for i in range(num_epochs):

... # iterate over minibatches

... minibatch_gen = minibatch_generator(

... X_train, y_train, minibatch_size)

... for X_train_mini, y_train_mini in minibatch_gen:

... break

... break

>>> print(X_train_mini.shape)

(100, 784)

>>> print(y_train_mini.shape)

(100,)
CopyExplain
As we can see, the network returns mini-batches of size 100 as intended.

Next, we have to define our loss function and performance metric that we can use
to monitor the training process and evaluate the model. The MSE loss and
accuracy function can be implemented as follows:

>>> def mse_loss(targets, probas, num_labels=10):

... onehot_targets = int_to_onehot(

... targets, num_labels=num_labels

... )

... return np.mean((onehot_targets - probas)**2)

>>> def accuracy(targets, predicted_labels):

... return np.mean(predicted_labels == targets)


CopyExplain

Let’s test the preceding function and compute the initial validation set MSE and
accuracy of the model we instantiated in the previous section:

>>> _, probas = model.forward(X_valid)

>>> mse = mse_loss(y_valid, probas)

>>> print(f'Initial validation MSE: {mse:.1f}')

Initial validation MSE: 0.3

>>> predicted_labels = np.argmax(probas, axis=1)

>>> acc = accuracy(y_valid, predicted_labels)

>>> print(f'Initial validation accuracy: {acc*100:.1f}%')

Initial validation accuracy: 9.4%


CopyExplain

In this code example, note that returns the hidden and output layer activations.
Remember that we have 10 output nodes (one corresponding to each unique class
label). Hence, when computing the MSE, we first converted the class labels into
one-hot encoded class labels in the function. In practice, it does not make a
difference whether we average over the row or the columns of the squared-
difference matrix first, so we simply call without any axis specification so that it
returns a scalar.model.forward()mse_loss()np.mean()

The output layer activations, since we used the logistic sigmoid function, are values
in the range [0, 1]. For each input, the output layer produces 10 values in the range
[0, 1], so we used the function to select the index position of the largest value,
which yields the predicted class label. We then compared the true labels with the
predicted class labels to compute the accuracy via the function we defined. As we
can see from the preceding output, the accuracy is not very high. However, given
that we have a balanced dataset with 10 classes, a prediction accuracy of
approximately 10 percent is what we would expect for an untrained model
producing random predictions.np.argmax()accuracy()

Using the previous code, we can compute the performance on, for example, the
whole training set if we provide as input to targets and the predicted labels from
feeding the model with . However, in practice, our computer memory is usually a
limiting factor for how much data the model can ingest in one forward pass (due to
the large matrix multiplications). Hence, we are defining our MSE and accuracy
computation based on our previous mini-batch generator. The following function
will compute the MSE and accuracy incrementally by iterating over the dataset one
mini-batch at a time to be more memory-efficient: y_trainX_train

>>> def compute_mse_and_acc(nnet, X, y, num_labels=10,

... minibatch_size=100):

... mse, correct_pred, num_examples = 0., 0, 0

... minibatch_gen = minibatch_generator(X, y, minibatch_size)

... for i, (features, targets) in enumerate(minibatch_gen):

... _, probas = nnet.forward(features)

... predicted_labels = np.argmax(probas, axis=1)

... onehot_targets = int_to_onehot(

... targets, num_labels=num_labels

... )
... loss = np.mean((onehot_targets - probas)**2)

... correct_pred += (predicted_labels == targets).sum()

... num_examples += targets.shape[0]

... mse += loss

... mse = mse/i

... acc = correct_pred/num_examples

... return mse, acc


CopyExplain

Before we implement the training loop, let’s test the function and compute the initial
training set MSE and accuracy of the model we instantiated in the previous section
and make sure it works as intended:

>>> mse, acc = compute_mse_and_acc(model, X_valid, y_valid)

>>> print(f'Initial valid MSE: {mse:.1f}')

Initial valid MSE: 0.3

>>> print(f'Initial valid accuracy: {acc*100:.1f}%')

Initial valid accuracy: 9.4%


CopyExplain

As we can see from the results, our generator approach produces the same results
as the previously defined MSE and accuracy functions, except for a small rounding
error in the MSE (0.27 versus 0.28), which is negligible for our purposes.

Let’s now get to the main part and implement the code to train our model:

>>> def train(model, X_train, y_train, X_valid, y_valid, num_epochs,

... learning_rate=0.1):

... epoch_loss = []

... epoch_train_acc = []

... epoch_valid_acc = []
...

... for e in range(num_epochs):

... # iterate over minibatches

... minibatch_gen = minibatch_generator(

... X_train, y_train, minibatch_size)

... for X_train_mini, y_train_mini in minibatch_gen:

... #### Compute outputs ####

... a_h, a_out = model.forward(X_train_mini)

... #### Compute gradients ####

... d_loss__d_w_out, d_loss__d_b_out, \

... d_loss__d_w_h, d_loss__d_b_h = \

... model.backward(X_train_mini, a_h, a_out,

... y_train_mini)

...

... #### Update weights ####

... model.weight_h -= learning_rate * d_loss__d_w_h

... model.bias_h -= learning_rate * d_loss__d_b_h

... model.weight_out -= learning_rate * d_loss__d_w_out

... model.bias_out -= learning_rate * d_loss__d_b_out

...

... #### Epoch Logging ####

... train_mse, train_acc = compute_mse_and_acc(

... model, X_train, y_train

... )

... valid_mse, valid_acc = compute_mse_and_acc(

... model, X_valid, y_valid


... )

... train_acc, valid_acc = train_acc*100, valid_acc*100

... epoch_train_acc.append(train_acc)

... epoch_valid_acc.append(valid_acc)

... epoch_loss.append(train_mse)

... print(f'Epoch: {e+1:03d}/{num_epochs:03d} '

... f'| Train MSE: {train_mse:.2f} '

... f'| Train Acc: {train_acc:.2f}% '

... f'| Valid Acc: {valid_acc:.2f}%')

...

... return epoch_loss, epoch_train_acc, epoch_valid_acc


CopyExplain

On a high level, the function iterates over multiple epochs, and in each epoch, it
used the previously defined function to iterate over the whole training set in mini-
batches for stochastic gradient descent training. Inside the mini-batch
generator loop, we obtain the outputs from the model, and , via its method. Then,
we compute the loss gradients via the model’s method—the theory will be
explained in a later section. Using the loss gradients, we update the weights by
adding the negative gradient multiplied by the learning rate. This is the same
concept that we discussed earlier for Adaline. For example, to update the model
weights of the hidden layer, we defined the following
line:train()minibatch_generator()fora_ha_out.forward().backward()

model.weight_h -= learning_rate * d_loss__d_w_h


CopyExplain

For a single weight, wj, this corresponds to the following partial derivative-based


update:
Finally, the last portion of the previous code computes the losses and prediction
accuracies on the training and test sets to track the training progress.

Let’s now execute this function to train our model for 50 epochs, which may take a
few minutes to finish:

>>> np.random.seed(123) # for the training set shuffling

>>> epoch_loss, epoch_train_acc, epoch_valid_acc = train(

... model, X_train, y_train, X_valid, y_valid,

... num_epochs=50, learning_rate=0.1)


CopyExplain

During training, we should see the following output:

Epoch: 001/050 | Train MSE: 0.05 | Train Acc: 76.17% | Valid Acc: 76.02%

Epoch: 002/050 | Train MSE: 0.03 | Train Acc: 85.46% | Valid Acc: 84.94%

Epoch: 003/050 | Train MSE: 0.02 | Train Acc: 87.89% | Valid Acc: 87.64%

Epoch: 004/050 | Train MSE: 0.02 | Train Acc: 89.36% | Valid Acc: 89.38%

Epoch: 005/050 | Train MSE: 0.02 | Train Acc: 90.21% | Valid Acc: 90.16%

...

Epoch: 048/050 | Train MSE: 0.01 | Train Acc: 95.57% | Valid Acc: 94.58%

Epoch: 049/050 | Train MSE: 0.01 | Train Acc: 95.55% | Valid Acc: 94.54%

Epoch: 050/050 | Train MSE: 0.01 | Train Acc: 95.59% | Valid Acc: 94.74%
CopyExplain

The reason why we print all this output is that, in NN training, it is really useful to
compare training and validation accuracy. This helps us judge whether the network
model performs well, given the architecture and hyperparameters. For example, if
we observe a low training and validation accuracy, there is likely an issue with the
training dataset, or the hyperparameters’ settings are not ideal.

In general, training (deep) NNs is relatively expensive compared with the other
models we’ve discussed so far. Thus, we want to stop it early in certain
circumstances and start over with different hyperparameter settings. On the other
hand, if we find that it increasingly tends to overfit the training data (noticeable by
an increasing gap between training and validation dataset performance), we may
want to stop the training early, as well.

In the next subsection, we will discuss the performance of our NN model in more
detail.

Evaluating the neural network performance


Before we discuss backpropagation, the training procedure of NNs, in more detail
in the next section, let’s look at the performance of the model that we trained in the
previous subsection.

In , we collected the training loss and the training and validation accuracy for each
epoch so that we can visualize the results using Matplotlib. Let’s look at the training
MSE loss first:train()

>>> plt.plot(range(len(epoch_loss)), epoch_loss)

>>> plt.ylabel('Mean squared error')

>>> plt.xlabel('Epoch')

>>> plt.show()
CopyExplain

The preceding code plots the loss over the 50 epochs, as shown in Figure 11.7:
Figure 11.7: A plot of the MSE by the number of training epochs

As we can see, the loss decreased substantially during the first 10 epochs and
seems to slowly converge in the last 10 epochs. However, the small slope between
epoch 40 and epoch 50 indicates that the loss would further decrease with training
over additional epochs.

Next, let’s take a look at the training and validation accuracy:

>>> plt.plot(range(len(epoch_train_acc)), epoch_train_acc,

... label='Training')

>>> plt.plot(range(len(epoch_valid_acc)), epoch_valid_acc,

... label='Validation')

>>> plt.ylabel('Accuracy')

>>> plt.xlabel('Epochs')

>>> plt.legend(loc='lower right')

>>> plt.show()
CopyExplain
The preceding code examples plot those accuracy values over the 50 training
epochs, as shown in Figure 11.8:

Figure 11.8: Classification accuracy by the number of training epochs

The plot reveals that the gap between training and validation accuracy increases
as we train for more epochs. At approximately the 25th epoch, the training and
validation accuracy values are almost equal, and then, the network starts to slightly
overfit the training data.

Reducing overfitting

One way to decrease the effect of overfitting is to increase the regularization


strength via L2 regularization, which we introduced in Chapter 3, A Tour of
Machine Learning Classifiers Using Scikit-Learn. Another useful technique for
tackling overfitting in NNs is dropout, which will be covered in Chapter
14, Classifying Images with Deep Convolutional Neural Networks.

Finally, let’s evaluate the generalization performance of the model by calculating


the prediction accuracy on the test dataset:
>>> test_mse, test_acc = compute_mse_and_acc(model, X_test, y_test)

>>> print(f'Test accuracy: {test_acc*100:.2f}%')

Test accuracy: 94.51%


CopyExplain

We can see that the test accuracy is very close to the validation set accuracy
corresponding to the last epoch (94.74%), which we reported during the training in
the last subsection. Moreover, the respective training accuracy is only minimally
higher at 95.59%, reaffirming that our model only slightly overfits the training data.

To further fine-tune the model, we could change the number of hidden units, the
learning rate, or use various other tricks that have been developed over the years
but are beyond the scope of this book. In Chapter 14, Classifying Images with
Deep Convolutional Neural Networks, you will learn about a different NN
architecture that is known for its good performance on image datasets.

Also, the chapter will introduce additional performance-enhancing tricks such as


adaptive learning rates, more sophisticated SGD-based optimization algorithms,
batch normalization, and dropout.

Other common tricks that are beyond the scope of the following chapters include:

 Adding skip-connections, which are the main contribution of residual NNs


(Deep residual learning for image recognition by K. He, X. Zhang, S. Ren,
and J. Sun, Proceedings of the IEEE Conference on Computer Vision and
Pattern Recognition, pp. 770-778, 2016)
 Using learning rate schedulers that change the learning rate during training
(Cyclical learning rates for training neural networks by L.N. Smith, 2017 IEEE
Winter Conference on Applications of Computer Vision (WACV), pp. 464-472,
2017)
 Attaching loss functions to earlier layers in the networks as it’s being done in
the popular Inception v3 architecture (Rethinking the Inception architecture for
computer vision by C. Szegedy, V. Vanhoucke, S. Ioffe, J. Shlens, and Z.
Wojna, Proceedings of the IEEE Conference on Computer Vision and Pattern
Recognition, pp. 2818-2826, 2016)
Lastly, let’s take a look at some of the images that our MLP struggles with by
extracting and plotting the first 25 misclassified samples from the test set:

>>> X_test_subset = X_test[:1000, :]

>>> y_test_subset = y_test[:1000]

>>> _, probas = model.forward(X_test_subset)

>>> test_pred = np.argmax(probas, axis=1)

>>> misclassified_images = \

... X_test_subset[y_test_subset != test_pred][:25]

>>> misclassified_labels = test_pred[y_test_subset != test_pred][:25]

>>> correct_labels = y_test_subset[y_test_subset != test_pred][:25]

>>> fig, ax = plt.subplots(nrows=5, ncols=5,

... sharex=True, sharey=True,

... figsize=(8, 8))

>>> ax = ax.flatten()

>>> for i in range(25):

... img = misclassified_images[i].reshape(28, 28)

... ax[i].imshow(img, cmap='Greys', interpolation='nearest')

... ax[i].set_title(f'{i+1}) '

... f'True: {correct_labels[i]}\n'

... f' Predicted: {misclassified_labels[i]}')

>>> ax[0].set_xticks([])

>>> ax[0].set_yticks([])

>>> plt.tight_layout()

>>> plt.show()
CopyExplain
We should now see a 5×5 subplot matrix where the first number in the subtitles
indicates the plot index, the second number represents the true class label (), and
the third number stands for the predicted class label (): TruePredicted

Figure 11.9: Handwritten digits that the model fails to classify correctly

Como podemos ver na Figura 11.9, entre outros, a rede encontra 7s desafiadores
quando incluem uma linha horizontal como nos exemplos 19 e 20. Olhando para
trás em uma figura anterior neste capítulo, onde traçamos diferentes exemplos de
treinamento do número 7, podemos hipotetizar que o dígito manuscrito 7 com uma
linha horizontal está sub-representado em nosso conjunto de dados e é
frequentemente classificado incorretamente.

Treinando uma rede neural artificial


Agora que vimos um NN em ação e ganhamos uma compreensão básica de como
ele funciona examinando o código, vamos nos aprofundar um pouco mais em
alguns dos conceitos, como o cálculo de perda e o algoritmo de backpropagation
que implementamos para aprender os parâmetros do modelo.

Calculando a função de perda


Como mencionado anteriormente, usamos uma perda de MSE (como em Adaline)
para treinar o NN multicamada, pois torna a derivação dos gradientes um pouco
mais fácil de seguir. Nos próximos capítulos, discutiremos outras funções de
perda, como a perda de entropia cruzada multicategoria (uma generalização da
perda de regressão logística binária), que é uma escolha mais comum para o
treinamento de classificadores NN.

Na seção anterior, implementamos um MLP para classificação multiclasse que


retorna um vetor de saída de elementos t que precisamos comparar com o
vetor alvo t×1 dimensional na representação de codificação one-hot. Se
prevermos o rótulo de classe de uma imagem de entrada com rótulo de classe 2,
usando esse MLP, a ativação da terceira camada e do destino pode ter a seguinte
aparência:
Assim, nossa perda de MSE tem que somar ou fazer a média sobre as
unidades de ativação t em nossa rede, além da média sobre os n exemplos no
conjunto de dados ou mini-lote:

Aqui, novamente, o sobrescrito [i] é o índice de um exemplo específico em nosso


conjunto de dados de treinamento.

Lembre-se que nosso objetivo é minimizar a perda de função L(W); assim,


precisamos calcular a derivada parcial dos parâmetros W com relação a cada
peso para cada camada da rede:

Na próxima seção, falaremos sobre o algoritmo de backpropagation, que nos


permite calcular essas derivadas parciais para minimizar a função de perda.

Observe que W consiste em múltiplas matrizes. Em um MLP com uma camada


oculta, temos a matriz de peso, W(h), que conecta a entrada à camada oculta
e W(fora), que conecta a camada oculta à camada de saída. Uma visualização do
tensor tridimensional W é fornecida na Figura 11.10:
Figura 11.10: Visualização de um tensor tridimensional

Nesta figura simplificada, pode parecer que ambos W(h) e W(fora) têm o mesmo


número de linhas e colunas, o que normalmente não é o caso, a menos
que inicializemos um MLP com o mesmo número de unidades ocultas, unidades
de saída e recursos de entrada.

Se isso soa confuso, fique atento para a próxima seção, onde discutiremos a
dimensionalidade de W(h) e W(fora) em mais detalhes no contexto do algoritmo de
backpropagation. Além disso, você é encorajado a ler o código de novamente, que
é anotado com comentários úteis sobre a dimensionalidade das diferentes
matrizes e transformações vetoriais.NeuralNetMLP

Desenvolvendo sua compreensão de


backpropagation
Embora a backpropagation tenha sido introduzida na comunidade de redes
neurais há mais de 30 anos (Learning representations by backpropagating errors,
por D.E. Rumelhart, G.E. Hinton e R.J. Williams, Nature, 323: 6088, páginas 533–
536, 1986), ela continua sendo um dos algoritmos mais amplamente utilizados
para treinar NNs artificiais de forma muito eficiente. Se você está interessado em
referências adicionais sobre a história do backpropagation, Juergen Schmidhuber
escreveu um belo artigo de pesquisa, Who Invented Backpropagation?, que você
pode encontrar online em http://people.idsia.ch/~juergen/who-invented-
backpropagation.html.

Esta seção fornecerá um resumo curto e claro e uma visão geral de como esse
algoritmo fascinante funciona antes de mergulharmos em mais detalhes
matemáticos. Em essência, podemos pensar em backpropagation como uma
abordagem computacionalmente muito eficiente para calcular as derivadas
parciais de uma função de perda complexa e não convexa em NNs multicamadas.
Aqui, nosso objetivo é usar esses derivados para aprender os coeficientes de peso
para parametrizar tal NN artificial multicamadas. O desafio na parametrização de
NNs é que normalmente estamos lidando com um número muito grande de
parâmetros de modelo em um espaço de feição de alta dimensão. Em contraste
com as funções de perda de NNs de camada única, como Adaline ou regressão
logística, que vimos nos capítulos anteriores, a superfície de erro de uma função
de perda de NN não é convexa ou suave em relação aos parâmetros. Há muitos
solavancos nesta superfície de perda de alta dimensão (mínimos locais) que
temos que superar para encontrar o mínimo global da função de perda.

Você pode se lembrar do conceito da regra de cadeia de suas aulas introdutórias


de cálculo. A regra de cadeia é uma abordagem para calcular a derivada de uma
função complexa aninhada, como f(g(x)), da seguinte maneira:
Da mesma forma, podemos usar a regra de cadeia para uma composição de
função arbitrariamente longa. Por exemplo, vamos supor que temos cinco funções
diferentes, f(x), g(x), h(x), u(x) e v(x), e deixar F ser a composição da função: F(x)
= f(g(h(u(v(x)))). Aplicando a regra de cadeia, podemos calcular a derivada desta
função da seguinte maneira:

No contexto da álgebra computacional, um conjunto de técnicas, conhecido


como diferenciação automática, foi desenvolvido para resolver tais problemas de
forma muito eficiente. Se você está interessado em aprender mais sobre
diferenciação automática em aplicativos de aprendizado de máquina, leia o artigo
de A.G. Baydin e B.A. Pearlmutter, Automatic Differentiation of Algorithms for
Machine Learning, arXiv preprint arXiv:1404.7456, 2014, que está disponível
gratuitamente no arXiv em http://arxiv.org/pdf/1404.7456.pdf.

A diferenciação automática vem com dois modos, os modos para frente e para
trás; backpropagation é simplesmente um caso especial de diferenciação
automática de modo reverso. O ponto chave é que aplicar a regra de cadeia no
modo forward poderia ser bastante caro, já que teríamos que multiplicar grandes
matrizes para cada camada (jacobianos) que eventualmente multiplicaríamos por
um vetor para obter a saída.

O truque do modo reverso é que atravessamos a regra de cadeia da direita para a


esquerda. Multiplicamos uma matriz por um vetor, o que produz outro vetor que é
multiplicado pela próxima matriz, e assim por diante. A multiplicação matriz-vetor é
computacionalmente muito mais barata do que a multiplicação matriz-matriz, razão
pela qual a retropropagação é um dos algoritmos mais populares usados no
treinamento de NN.
Uma atualização básica de cálculo

Para entender completamente o backpropagation, precisamos tomar emprestado


certos conceitos do cálculo diferencial, o que está fora do escopo deste livro. No
entanto, você pode consultar um capítulo de revisão dos conceitos mais
fundamentais, que você pode achar útil neste contexto. Discute derivadas de
função, derivadas parciais, gradientes e o jacobiano. Este texto pode
ser acessado gratuitamente em
https://sebastianraschka.com/pdf/books/dlb/appendix_d_calculus.pdf. Se você não
estiver familiarizado com cálculo ou precisar de uma breve atualização, considere
ler este texto como um recurso de suporte adicional antes de ler a próxima seção.

Treinamento de redes neurais via backpropagation


Nesta seção, vamos passar pela matemática da retropropagação para entender
como você pode aprender os pesos em um NN de forma muito eficiente.
Dependendo de quão confortável você está com representações matemáticas, as
equações a seguir podem parecer relativamente complicadas no início.

Em uma seção anterior, vimos como calcular a perda como a diferença entre a
ativação da última camada e o rótulo da classe de destino. Agora, veremos como
o algoritmo de backpropagation funciona para atualizar os pesos em nosso
modelo MLP de uma perspectiva matemática, que implementamos no método da
classe. Como lembramos do início deste capítulo, primeiro precisamos aplicar a
propagação direta para obter a ativação da camada de saída, que formulamos da
seguinte forma:.backward()NeuralNetMLP()
Resumidamente, apenas propagamos os recursos de entrada através das
conexões na rede, como mostram as setas na Figura 11.11 para uma rede com
dois recursos de entrada, três nós ocultos e dois nós de saída:

Figura 11.11: Propagação direta dos recursos de entrada de um NN

No backpropagation, propagamos o erro da direita para a esquerda. Podemos


pensar nisso como uma aplicação da regra de cadeia ao cálculo do passe para
frente para calcular o gradiente da perda em relação aos pesos do modelo (e
unidades de viés). Para simplificar, ilustraremos este processo para a derivada
parcial usada para atualizar o primeiro peso na matriz de peso da camada de
saída. Os caminhos da computação que retropropagamos são destacados através
das setas em negrito abaixo:
Figura 11.12: Retropropagando o erro de um NN

Se incluirmos as entradas líquidas z explicitamente, o cálculo de derivada parcial


mostrado na figura anterior se expande da seguinte maneira:

Para calcular essa derivada parcial, que é usada para atualizar ,


podemos calcular os três termos individuais da derivada parcial e multiplicar os
resultados. Para simplificar, omitiremos a média sobre os exemplos individuais no

minilote, então descartaremos o   termo de média das equações a


seguir.

Vamos começar com , que é a derivada parcial da perda de MSE (que simplifica

para o erro quadrático se omitirmos a dimensão de minilote) com 


relação à pontuação de saída prevista do primeiro nó de saída:
O próximo termo é a derivada da função de ativação do sigmoide logístico que
usamos na camada de saída:

Por fim, calculamos a derivada da entrada líquida em relação ao peso:

Juntando tudo isso, temos o seguinte:

Em seguida, usamos esse valor para atualizar o peso por meio da atualização de

descida de gradiente estocástico familiar com uma taxa de aprendizado de :


Em nossa implementação de código do , implementamos a

computação   em forma vetorizada no método da seguinte


forma:NeuralNetMLP().backward()

# Part 1: dLoss/dOutWeights

## = dLoss/dOutAct * dOutAct/dOutNet * dOutNet/dOutWeight

## where DeltaOut = dLoss/dOutAct * dOutAct/dOutNet for convenient re-use

# input/output dim: [n_examples, n_classes]

d_loss__d_a_out = 2.*(a_out - y_onehot) / y.shape[0]

# input/output dim: [n_examples, n_classes]

d_a_out__d_z_out = a_out * (1. - a_out) # sigmoid derivative

# output dim: [n_examples, n_classes]

delta_out = d_loss__d_a_out * d_a_out__d_z_out # "delta (rule)

# placeholder"

# gradient for output weights

# [n_examples, n_hidden]

d_z_out__dw_out = a_h

# input dim: [n_classes, n_examples] dot [n_examples, n_hidden]

# output dim: [n_classes, n_hidden]

d_loss__dw_out = np.dot(delta_out.T, d_z_out__dw_out)

d_loss__db_out = np.sum(delta_out, axis=0)


CopyExplain

As annotated in the code snippet above, we created the following “delta”


placeholder variable:
This is because   terms are involved in computing the partial
derivatives (or gradients) of the hidden layer weights as well; hence, we can

reuse  .

Speaking of hidden layer weights, Figure 11.13 illustrates how to compute the


partial derivative of the loss with respect to the first weight of the hidden layer:

Figure 11.13: Computing the partial derivatives of the loss with respect to the first
hidden layer weight

It is important to highlight that since the weight   is connected to both


output nodes, we have to use the multi-variable chain rule to sum the two paths
highlighted with bold arrows. As before, we can expand it to include the net
inputs z and then solve the individual terms:
Notice that if we reuse   computed previously, this equation can be
simplified as follows:

Os termos anteriores podem ser resolvidos individualmente com relativa facilidade,


como fizemos anteriormente, porque não há novos derivados envolvidos. Por

exemplo, é a derivada da ativação sigmoide, isto é,   

e assim por diante. Deixaremos a resolução das


partes individuais como um exercício opcional para você.
Sobre convergência em redes neurais
Você deve estar se perguntando por que não usamos a descida de gradiente
regular, mas em vez disso usamos o aprendizado em mini-lote para treinar nosso
NN para a classificação de dígitos manuscritos anteriormente. Você deve se
lembrar de nossa discussão sobre SGD que usamos para implementar o
aprendizado on-line. No aprendizado on-line, calculamos o gradiente com base em
um único exemplo de treinamento (k = 1) de cada vez para realizar a atualização
de peso. Embora esta seja uma abordagem estocástica, muitas vezes leva a
soluções muito precisas com uma convergência muito mais rápida do que a
descida de gradiente regular. O aprendizado em mini-lote é uma forma especial de
SGD onde calculamos o gradiente com base em um subconjunto k dos n
exemplos de treinamento com 1 < k < n. O aprendizado em mini-lote tem uma
vantagem sobre o aprendizado on-line na medida em que podemos fazer uso de
nossas implementações vetorizadas para melhorar a eficiência computacional. No
entanto, podemos atualizar os pesos muito mais rápido do que na descida de
gradiente regular. Intuitivamente, você pode pensar em aprendizado de minilote
como prever a participação eleitoral de uma eleição presidencial a partir de uma
pesquisa, perguntando apenas a um subconjunto representativo da população, em
vez de perguntar a toda a população (o que seria igual a disputar a eleição real).

NNs multicamadas são muito mais difíceis de treinar do que algoritmos mais
simples, como Adaline, regressão logística ou máquinas de vetores de suporte.
Em NNs multicamadas, normalmente temos centenas, milhares ou até bilhões de
pesos que precisamos otimizar. Infelizmente, a função de saída tem uma
superfície áspera, e o algoritmo de otimização pode facilmente ficar preso em
mínimos locais, como mostrado na Figura 11.14:
Figura 11.14: Os algoritmos de otimização podem ficar presos em mínimos locais

Note que essa representação é extremamente simplificada, pois nosso NN tem


muitas dimensões; Isso torna impossível visualizar a superfície de perda real para
o olho humano. Aqui, mostramos apenas a superfície de perda para um único
peso no eixo x. No entanto, a mensagem principal é que não queremos que nosso
algoritmo fique preso em mínimos locais. Ao aumentar a taxa de aprendizagem,
podemos escapar mais facilmente desses mínimos locais. Por outro lado, também
aumentamos a chance de ultrapassar o ótimo global se a taxa de aprendizado for
muito grande. Como inicializamos os pesos aleatoriamente, começamos com uma
solução para o problema de otimização que normalmente está irremediavelmente
errada.

Algumas últimas palavras sobre a


implementação da rede neural
Você pode estar se perguntando por que passamos por toda essa teoria apenas
para implementar uma rede artificial multicamada simples que pode classificar
dígitos manuscritos em vez de usar uma biblioteca de aprendizado de máquina
Python de código aberto. Na verdade, apresentaremos modelos NN mais
complexos nos próximos capítulos, que treinaremos usando a biblioteca de código
aberto PyTorch (https://pytorch.org).

Embora a implementação do zero neste capítulo pareça um pouco tediosa no


início, foi um bom exercício para entender o básico por trás do backpropagation e
do treinamento de NN. Uma compreensão básica de algoritmos é crucial para
aplicar técnicas de aprendizado de máquina de forma adequada e bem-sucedida.

Agora que você aprendeu como os NNs feedforward funcionam, estamos prontos
para explorar DNNs mais sofisticados usando o PyTorch, o que nos permite
construir NNs de forma mais eficiente, como veremos no Capítulo
12, Paralelizando o treinamento de redes neurais com o PyTorch.

O PyTorch, que foi lançado originalmente em setembro de 2016, ganhou muita


popularidade entre os pesquisadores de aprendizado de máquina, que o usam
para construir DNNs por causa de sua capacidade de otimizar expressões
matemáticas para cálculos em matrizes multidimensionais utilizando unidades de
processamento gráfico (GPUs).

Por fim, devemos notar que o scikit-learn também inclui uma implementação MLP
básica, que você pode encontrar
em https://scikit-learn.org/stable/modules/generated/sklearn.neural_network.MLPC
lassifier.html. Embora essa implementação seja ótima e muito conveniente para o
treinamento de MLPs básicos, recomendamos fortemente bibliotecas
especializadas de aprendizado profundo, como o PyTorch, para implementar e
treinar NNs multicamadas.MLPClassifier

Paralelizando o treinamento de redes


neurais com o PyTorch
Neste capítulo, passaremos dos fundamentos matemáticos do aprendizado de
máquina e do aprendizado profundo para nos concentrarmos no PyTorch. O
PyTorch é uma das bibliotecas de aprendizado profundo mais populares
atualmente disponíveis e nos permite implementar redes neurais (NNs) com
muito mais eficiência do que qualquer uma de nossas implementações anteriores
do NumPy. Neste capítulo, vamos começar a usar o PyTorch e ver como ele traz
benefícios significativos para o desempenho do treinamento.

Este capítulo iniciará a próxima etapa de nossa jornada em aprendizado de


máquina e aprendizado profundo, e exploraremos os seguintes tópicos:

 Como o PyTorch melhora o desempenho do treinamento

 Trabalhando com PyTorch's e para construir pipelines de entrada e permitir


treinamento de modelo eficienteDatasetDataLoader
 Trabalhando com o PyTorch para escrever código de aprendizado de
máquina otimizado

 Usando o módulo para implementar arquiteturas comuns de aprendizado


profundo convenientementetorch.nn
 Escolhendo funções de ativação para NNs artificiais

PyTorch e desempenho de treinamento


O PyTorch pode acelerar nossas tarefas de aprendizado de máquina
significativamente. Para entender como ele pode fazer isso, vamos
começar discutindo alguns dos desafios de desempenho que normalmente
enfrentamos quando executamos cálculos caros em nosso hardware. Em seguida,
daremos uma olhada de alto nível no que é PyTorch e qual será nossa abordagem
de aprendizado neste capítulo.

Desafios de desempenho
O desempenho dos processadores de computador tem, naturalmente, melhorado
continuamente nos últimos anos. Isso nos permite treinar sistemas de
aprendizagem mais poderosos e complexos, o que significa que podemos
melhorar o desempenho preditivo de nossos modelos de aprendizado de máquina.
Mesmo o hardware de computador desktop mais barato que está disponível agora
vem com unidades de processamento que têm vários núcleos.
Nos capítulos anteriores, vimos que muitas funções no scikit-learn nos permitem
espalhar esses cálculos por várias unidades de processamento. No entanto, por
padrão, o Python é limitado à execução em um núcleo devido ao bloqueio global
do interpretador (GIL). Então, embora realmente aproveitemos a biblioteca de
multiprocessamento do Python para distribuir nossos cálculos por vários núcleos,
ainda temos que considerar que o hardware de desktop mais avançado raramente
vem com mais de 8 ou 16 desses núcleos.

Você vai se lembrar do Capítulo 11, Implementando uma Rede Neural Artificial


Multicamada do Zero, que implementamos um perceptron multicamada (MLP)
muito simples com apenas uma camada oculta consistindo de 100 unidades.
Tivemos que otimizar aproximadamente 80.000 parâmetros de peso ([784*100 +
100] + [100 * 10] + 10 = 79.510) para uma tarefa muito simples de classificação de
imagens. As imagens no MNIST são bastante pequenas (28×28), e só podemos
imaginar a explosão no número de parâmetros se quisermos adicionar camadas
ocultas adicionais ou trabalhar com imagens que têm densidades de pixels mais
altas. Tal tarefa rapidamente se tornaria inviável para uma única unidade de
processamento. A questão que se coloca é: como podemos enfrentar estes
problemas de forma mais eficaz?

A solução óbvia para esse problema é usar unidades de processamento


gráfico (GPUs), que são verdadeiros cavalos de batalha. Você pode pensar em
uma placa gráfica como um pequeno cluster de computador dentro de sua
máquina. Outra vantagem é que as GPUs modernas são de grande valor em
comparação com as unidades centrais de processamento (CPUs) de última
geração, como você pode ver na visão geral a seguir:
Figura 12.1: Comparação de uma CPU e GPU de última geração

As fontes de informação da Figura 12.1 são os seguintes sites (data acessada:


julho de 2021):

 https://ark.intel.com/content/www/us/en/ark/products/215570/intel-core-i9-
11900kb-processor-24m-cache-up-to-4-90-ghz.html
 https://www.nvidia.com/en-us/geforce/graphics-cards/30-series/rtx-3080-
3080ti/

Com 2,2 vezes o preço de uma CPU moderna, podemos obter uma GPU que tem
640 vezes mais núcleos e é capaz de cerca de 46 vezes mais cálculos de ponto
flutuante por segundo. Então, o que está nos impedindo de utilizar GPUs para
nossas tarefas de aprendizado de máquina? O desafio é que escrever código para
GPUs de destino não é tão simples quanto executar código Python em nosso
interpretador. Existem pacotes especiais, como CUDA e OpenCL, que nos
permitem direcionar a GPU. No entanto, escrever código em CUDA ou OpenCL
provavelmente não é a maneira mais conveniente de implementar e executar
algoritmos de aprendizado de máquina. A boa notícia é que é para isso que o
PyTorch foi desenvolvido!

O que é PyTorch?
O PyTorch é uma interface de programação escalável e multiplataforma para
implementar e executar algoritmos de aprendizado de máquina, incluindo
wrappers de conveniência para aprendizado profundo. O PyTorch foi desenvolvido
principalmente pelos pesquisadores e engenheiros do laboratório Facebook AI
Research (FAIR). Seu desenvolvimento também envolve muitas contribuições da
comunidade. PyTorch foi inicialmente lançado em setembro de 2016 e é livre e de
código aberto sob a licença BSD modificada. Muitos pesquisadores de
aprendizado de máquina e profissionais da academia e da indústria adaptaram o
PyTorch para desenvolver soluções de aprendizado profundo, como o Tesla
Autopilot, o Pyro da Uber e o Transformers (https://pytorch.org/ecosystem/) da
Hugging Face.
Para melhorar o desempenho de modelos de aprendizado de máquina de
treinamento, o PyTorch permite a execução em CPUs, GPUs e dispositivos XLA,
como TPUs. No entanto, seus maiores recursos de desempenho podem ser
descobertos ao usar GPUs e dispositivos XLA. O PyTorch suporta GPUs
habilitadas para CUDA e ROCm oficialmente. O desenvolvimento do PyTorch é
baseado na biblioteca Torch (www.torch.ch). Como o próprio nome indica, a
interface Python é o foco principal de desenvolvimento do PyTorch.

PyTorch é construído em torno de um gráfico de computação composto por um


conjunto de nós. Cada nó representa uma operação que pode ter zero ou mais
entradas ou saídas. O PyTorch fornece um ambiente de programação imperativo
que avalia operações, executa cálculos e retorna valores concretos
imediatamente. Assim, o gráfico de computação no PyTorch é definido
implicitamente, em vez de construído antecipadamente e executado depois.

Matematicamente, tensores podem ser entendidos como uma generalização de


escalares, vetores, matrizes e assim por diante. Mais concretamente, um escalar
pode ser definido como um tensor rank-0, um vetor pode ser definido como um
tensor rank-1, uma matriz pode ser definida como um tensor rank-2, e matrizes
empilhadas em uma terceira dimensão podem ser definidas como tensores rank-3.
Os tensores no PyTorch são semelhantes aos arrays do NumPy, exceto que os
tensores são otimizados para diferenciação automática e podem ser executados
em GPUs.

Para tornar o conceito de tensor mais claro, considere a Figura 12.2, que
representa tensores das fileiras 0 e 1 na primeira linha, e tensores das fileiras 2 e
3 na segunda linha:
Figura 12.2: Diferentes tipos de tensor no PyTorch

Agora que sabemos o que é o PyTorch, vamos ver como usá-lo.

Como vamos aprender PyTorch


Primeiro, vamos abordar o modelo de programação do PyTorch, em particular, a
criação e manipulação de tensores. Em seguida, veremos como carregar dados e
utilizar o módulo, o que nos permitirá iterar através de um conjunto de dados de
forma eficiente. Além disso, discutiremos os conjuntos de dados existentes e
prontos para uso no submódulo e aprenderemos como usá-
los.torch.utils.datatorch.utils.data.Dataset

Depois de aprender sobre esses conceitos básicos, o módulo de rede neural


PyTorch será introduzido. Em seguida, avançaremos para a construção de
modelos de aprendizado de máquina, aprenderemos como compor e treinar os
modelos e aprenderemos como salvar os modelos treinados em disco para
avaliação futura.torch.nn

Primeiros passos com o PyTorch


Nesta seção, daremos nossos primeiros passos no uso da API PyTorch de baixo
nível. Depois de instalar o PyTorch, abordaremos como criar tensores no PyTorch
e diferentes maneiras de manipulá-los, como alterar sua forma, tipo de dados e
assim por diante.

Instalando o PyTorch
Para instalar o PyTorch, recomendamos consultar as instruções mais recentes no
site oficial da https://pytorch.org. Abaixo, descreveremos as etapas básicas que
funcionarão na maioria dos sistemas.

Dependendo de como seu sistema está configurado, você normalmente pode usar
o instalador do Python e instalar o PyTorch a partir do PyPI executando o seguinte
a partir do seu terminal:pip

pip install torch torchvision


CopyExplain

Isso instalará a versão estável mais recente, que é 1.9.0 no momento da escrita.


Para instalar a versão 1.9.0, que é garantida para ser compatível com os
seguintes exemplos de código, você pode modificar o comando anterior da
seguinte maneira:

pip install torch==1.9.0 torchvision==0.10.0


CopyExplain

Se você quiser usar GPUs (recomendado), você precisa de uma placa gráfica
NVIDIA compatível que suporte CUDA e cuDNN. Se sua máquina atender a esses
requisitos, você poderá instalar o PyTorch com suporte a GPU, da seguinte
maneira:

pip install torch==1.9.0+cu111 torchvision==0.10.0+cu111 -f

https://download.pytorch.org/whl/torch_stable.html
CopyExplain

para CUDA 11.1 ou:


pip install torch==1.9.0 torchvision==0.10.0\ -f https://download.pytorch.org/whl/torch_stable.html
CopyExplain

para CUDA 10.2 no momento em que este artigo foi escrito.

Como os binários do macOS não suportam CUDA, você pode instalar a partir do
código-fonte: https://pytorch.org/get-started/locally/#mac-from-source.

Para obter mais informações sobre o processo de instalação e configuração,


consulte as recomendações oficiais em https://pytorch.org/get-started/locally/.

Note que o PyTorch está em desenvolvimento ativo; portanto, a cada dois meses,
novas versões são lançadas com mudanças significativas. Você pode verificar sua
versão do PyTorch a partir do seu terminal, da seguinte maneira:

python -c 'import torch; print(torch.__version__)'


CopyExplain

Solucionando problemas de instalação do PyTorch

Se você tiver problemas com o procedimento de instalação, leia mais sobre as


recomendações específicas do sistema e da plataforma fornecidas
em https://pytorch.org/get-started/locally/. Observe que todo o código neste
capítulo pode ser executado em sua CPU; usar uma GPU é totalmente opcional,
mas recomendado se você quiser aproveitar plenamente os benefícios do
PyTorch. Por exemplo, enquanto o treinamento de alguns modelos NN em uma
CPU pode levar uma semana, os mesmos modelos podem ser treinados em
apenas algumas horas em uma GPU moderna. Se você tiver uma placa gráfica,
consulte a página de instalação para configurá-la adequadamente. Além disso,
você pode achar este guia de configuração útil, que explica como instalar os
drivers da placa gráfica NVIDIA, CUDA e cuDNN no Ubuntu (requisitos não
necessários, mas recomendados para executar o PyTorch em uma
GPU): https://sebastianraschka.com/pdf/books/dlb/appendix_h_cloud-
computing.pdf. Além disso, como você verá no Capítulo 17, Generative
Adversarial Networks for Synthesizing New Data, você também pode treinar seus
modelos usando uma GPU gratuitamente via Google Colab.
Criando tensores no PyTorch
Agora, vamos considerar algumas maneiras diferentes de criar tensores e, em
seguida, ver algumas de suas propriedades e como manipulá-los. Em primeiro
lugar, podemos simplesmente criar um tensor a partir de uma lista ou uma matriz
NumPy usando a função ou a função da seguinte
maneira:torch.tensortorch.from_numpy

>>> import torch

>>> import numpy as np

>>> np.set_printoptions(precision=3)

>>> a = [1, 2, 3]

>>> b = np.array([4, 5, 6], dtype=np.int32)

>>> t_a = torch.tensor(a)

>>> t_b = torch.from_numpy(b)

>>> print(t_a)

>>> print(t_b)

tensor([1, 2, 3])

tensor([4, 5, 6], dtype=torch.int32)


CopyExplain

Isso resultou em tensores e , com suas propriedades, e , adotados a partir de sua


fonte. Semelhante às matrizes NumPy, também podemos ver
estas propriedades:t_at_bshape=(3,)dtype=int32

>>> t_ones = torch.ones(2, 3)

>>> t_ones.shape

torch.Size([2, 3])

>>> print(t_ones)

tensor([[1., 1., 1.],


[1., 1., 1.]])
CopyExplain

Finalmente, a criação de um tensor de valores aleatórios pode ser feita da


seguinte maneira:

>>> rand_tensor = torch.rand(2,3)

>>> print(rand_tensor)

tensor([[0.1409, 0.2848, 0.8914],

[0.9223, 0.2924, 0.7889]])


CopyExplain

Manipulando o tipo de dados e a forma de um


tensor
Aprender maneiras de manipular tensores é necessário para torná-los compatíveis
para entrada em um modelo ou uma operação. Nesta seção, você aprenderá a
manipular tipos e formas de dados tensores por meio de várias funções do
PyTorch que convertem, remodelam, transpõem e apertam (removem dimensões).

A função pode ser usada para alterar o tipo de dados de um tensor para um tipo
desejado:torch.to()

>>> t_a_new = t_a.to(torch.int64)

>>> print(t_a_new.dtype)

torch.int64
CopyExplain

Consulte https://pytorch.org/docs/stable/tensor_attributes.html para todos os
outros tipos de dados.

Como você verá nos próximos capítulos, certas operações exigem que os


tensores de entrada tenham um certo número de dimensões (ou seja,
classificação) associadas a um certo número de elementos (forma). Assim,
podemos precisar mudar a forma de um tensor, adicionar uma nova dimensão ou
espremer uma dimensão desnecessária. O PyTorch fornece funções (ou
operações) úteis para conseguir isso, como , e . Vejamos alguns
exemplos:torch.transpose()torch.reshape()torch.squeeze()

 Transpondo um tensor:
 >>> t = torch.rand(3, 5)

 >>> t_tr = torch.transpose(t, 0, 1)

 >>> print(t.shape, ' --> ', t_tr.shape)

 torch.Size([3, 5]) --> torch.Size([5, 3])


CopyExplain
 Remodelando um tensor (por exemplo, de um vetor 1D para uma matriz 2D):
 >>> t = torch.zeros(30)

 >>> t_reshape = t.reshape(5, 6)

 >>> print(t_reshape.shape)

 torch.Size([5, 6])
CopyExplain
 Removendo as dimensões desnecessárias (dimensões que têm tamanho 1,
que não são necessárias):
 >>> t = torch.zeros(1, 2, 1, 4, 1)

 >>> t_sqz = torch.squeeze(t, 2)

 >>> print(t.shape, ' --> ', t_sqz.shape)

 torch.Size([1, 2, 1, 4, 1]) --> torch.Size([1, 2, 4, 1])


CopyExplain

Aplicação de operações matemáticas a tensores


A aplicação de operações matemáticas, em particular operações de álgebra linear,
é necessária para a construção da maioria dos modelos de aprendizado de
máquina. Nesta subseção, abordaremos algumas operações de álgebra linear
amplamente utilizadas, como produto elementar, multiplicação de matrizes e
cálculo da norma de um tensor.
Primeiro, vamos instanciar dois tensores aleatórios, um com distribuição uniforme
no intervalo [–1, 1) e outro com uma distribuição normal padrão:

>>> torch.manual_seed(1)

>>> t1 = 2 * torch.rand(5, 2) - 1

>>> t2 = torch.normal(mean=0, std=1, size=(5, 2))


CopyExplain

Observe que retorna um tensor preenchido com números aleatórios de uma


distribuição uniforme no intervalo de [0, 1].torch.rand

Observe isso e tenha o mesmo formato. Agora, para calcular o produto elementar
de e , podemos usar o seguinte:t1t2t1t2

>>> t3 = torch.multiply(t1, t2)

>>> print(t3)

tensor([[ 0.4426, -0.3114],

[ 0.0660, -0.5970],

[ 1.1249, 0.0150],

[ 0.1569, 0.7107],

[-0.0451, -0.0352]])
CopyExplain

Para calcular a média, a soma e o desvio padrão ao longo de um determinado


eixo (ou eixos), podemos usar , e . Por exemplo, a média de cada coluna em pode
ser calculada da seguinte maneira:torch.mean()torch.sum()torch.std()t1

>>> t4 = torch.mean(t1, axis=0)

>>> print(t4)

tensor([-0.1373, 0.2028])
CopyExplain
O produto matriz-matriz entre e (isto é,  , onde o T sobrescrito é
para transposição) pode ser calculado usando a função da seguinte
maneira:t1t2torch.matmul()

>>> t5 = torch.matmul(t1, torch.transpose(t2, 0, 1))

>>> print(t5)

tensor([[ 0.1312, 0.3860, -0.6267, -1.0096, -0.2943],

[ 0.1647, -0.5310, 0.2434, 0.8035, 0.1980],

[-0.3855, -0.4422, 1.1399, 1.5558, 0.4781],

[ 0.1822, -0.5771, 0.2585, 0.8676, 0.2132],

[ 0.0330, 0.1084, -0.1692, -0.2771, -0.0804]])


CopyExplain

Por outro lado, a computação   é realizada por transposição ,


resultando em uma matriz de tamanho 2×2: t1

>>> t6 = torch.matmul(torch.transpose(t1, 0, 1), t2)

>>> print(t6)

tensor([[ 1.7453, 0.3392],

[-1.6038, -0.2180]])
CopyExplain

Finalmente, a função é útil para calcular o Ltorch.linalg.norm()p norma de um


tensor. Por exemplo, podemos calcular o L2 Norma do seguinte:t1

>>> norm_t1 = torch.linalg.norm(t1, ord=2, dim=1)

>>> print(norm_t1)

tensor([0.6785, 0.5078, 1.1162, 0.5488, 0.1853])


CopyExplain
Para verificar se esse trecho de código calcula o L2 De forma correta, você pode
comparar os resultados com a seguinte função
NumPy: .t1np.sqrt(np.sum(np.square(t1.numpy()), axis=1))

Dividir, empilhar e concatenar tensores


Nesta subseção, abordaremos as operações do PyTorch para dividir um tensor
em vários tensores, ou o inverso: empilhar e concatenar vários tensores em um
único.

Suponha que temos um único tensor e queremos dividi-lo em dois ou mais


tensores. Para isso, o PyTorch fornece uma função conveniente, que divide um
tensor de entrada em uma lista de tensores de tamanho igual.
Podemos determinar o número desejado de divisões como um inteiro usando o
argumento para dividir um tensor ao longo da dimensão desejada especificada
pelo argumento. Nesse caso, o tamanho total do tensor de entrada ao longo da
dimensão especificada deve ser divisível pelo número desejado de divisões.
Alternativamente, podemos fornecer os tamanhos desejados em uma lista usando
a função. Vamos dar uma olhada em um exemplo dessas duas
opções:torch.chunk()chunksdimtorch.split()

 Fornecendo o número de divisões:


 >>> torch.manual_seed(1)

 >>> t = torch.rand(6)

 >>> print(t)

 tensor([0.7576, 0.2793, 0.4031, 0.7347, 0.0293, 0.7999])

 >>> t_splits = torch.chunk(t, 3)

 >>> [item.numpy() for item in t_splits]

 [array([0.758, 0.279], dtype=float32),

 array([0.403, 0.735], dtype=float32),

 array([0.029, 0.8 ], dtype=float32)]


CopyExplain
Neste exemplo, um tensor de tamanho 6 foi dividido em uma lista de três
tensores cada um com tamanho 2. Se o tamanho do tensor não for divisível
pelo valor, o último bloco será menor. chunks

 Fornecendo os tamanhos de diferentes divisões:

Alternativamente, em vez de definir o número de divisões, também podemos


especificar os tamanhos dos tensores de saída diretamente. Aqui, estamos
dividindo um tensor de tamanho em tensores de tamanhos e : 532

>>> torch.manual_seed(1)

>>> t = torch.rand(5)

>>> print(t)

tensor([0.7576, 0.2793, 0.4031, 0.7347, 0.0293])

>>> t_splits = torch.split(t, split_size_or_sections=[3, 2])

>>> [item.numpy() for item in t_splits]

[array([0.758, 0.279, 0.403], dtype=float32),

array([0.735, 0.029], dtype=float32)]


CopyExplain

Às vezes, estamos trabalhando com vários tensores e precisamos concatená-los


ou empilha-los para criar um único tensor. Neste caso, o PyTorch funciona como e
vem a calhar. Por exemplo, vamos criar um tensor 1D, , contendo 1s com tamanho
e um tensor 1D, , contendo 0s com tamanho e concatená-los em um tensor 1D, ,
de tamanho :torch.stack()torch.cat()A3,B2,C5

>>> A = torch.ones(3)

>>> B = torch.zeros(2)

>>> C = torch.cat([A, B], axis=0)

>>> print(C)

tensor([1., 1., 1., 0., 0.])


CopyExplain
Se criarmos tensores 1D e , ambos com tamanho, então podemos empilha-los
juntos para formar um tensor 2D, :AB3S

>>> A = torch.ones(3)

>>> B = torch.zeros(3)

>>> S = torch.stack([A, B], axis=1)

>>> print(S)

tensor([[1., 0.],

[1., 0.],

[1., 0.]])
CopyExplain

A API do PyTorch tem muitas operações que você pode usar para criar um
modelo, processar seus dados e muito mais. No entanto, cobrir todas as funções
está fora do escopo deste livro, onde nos concentraremos nas mais essenciais.
Para obter a lista completa de operações e funções, você pode consultar a página
de documentação do PyTorch em https://pytorch.org/docs/stable/index.html.

Construção de dutos de entrada no


PyTorch
Quando estamos treinando um modelo NN profundo, geralmente treinamos o
modelo incrementalmente usando um algoritmo de otimização iterativa, como a
descida do gradiente estocástico, como vimos nos capítulos anteriores.

Como mencionado no início deste capítulo, é um módulo para a construção de


modelos NN. Nos casos em que o conjunto de dados de treinamento é bastante
pequeno e pode ser carregado como um tensor na memória, podemos usar
diretamente esse tensor para treinamento. Em casos de uso típicos, no entanto,
quando o conjunto de dados é muito grande para caber na memória do
computador, precisaremos carregar os dados do dispositivo de armazenamento
principal (por exemplo, o disco rígido ou a unidade de estado sólido) em partes, ou
seja, lote por lote. (Observe o uso do termo "lote" em vez de "minilote" neste
capítulo para ficar perto da terminologia PyTorch.) Além disso, talvez precisemos
construir um pipeline de processamento de dados para aplicar certas
transformações e etapas de pré-processamento aos nossos dados, como
centralização média, dimensionamento ou adição de ruído para aumentar o
procedimento de treinamento e evitar o overfitting. torch.nn

Aplicar funções de pré-processamento manualmente todas as vezes pode ser


bastante complicado. Felizmente, o PyTorch fornece uma classe especial para a
construção de tubulações de pré-processamento eficientes e convenientes. Nesta
seção, veremos uma visão geral de diferentes métodos para construir um PyTorch
e , e implementar carregamento de dados, embaralhamento e processamento em
lote.DatasetDataLoader

Criando um DataLoader PyTorch a partir de


tensores existentes
Se os dados já existirem na forma de um objeto tensor, uma lista Python ou uma
matriz NumPy, podemos facilmente criar um carregador de conjunto de dados
usando a classe. Ele retorna um objeto da classe, que podemos usar para iterar
através dos elementos individuais no conjunto de dados de entrada. Como um
exemplo simples, considere o código a seguir, que cria um conjunto de dados a
partir de uma lista de valores de 0 a 5:torch.utils.data.DataLoader()DataLoader

>>> from torch.utils.data import DataLoader

>>> t = torch.arange(6, dtype=torch.float32)

>>> data_loader = DataLoader(t)


CopyExplain

Podemos facilmente iterar através de uma entrada de conjunto de dados por


entrada da seguinte maneira:

>>> for item in data_loader:

... print(item)

tensor([0.])
tensor([1.])

tensor([2.])

tensor([3.])

tensor([4.])

tensor([5.])
CopyExplain

Se quisermos criar lotes a partir desse conjunto de dados, com um tamanho de


lote desejado de , podemos fazer isso com o argumento da seguinte
maneira:3batch_size

>>> data_loader = DataLoader(t, batch_size=3, drop_last=False)

>>> for i, batch in enumerate(data_loader, 1):

... print(f'batch {i}:', batch)

batch 1: tensor([0., 1., 2.])

batch 2: tensor([3., 4., 5.])


CopyExplain

Isso criará dois lotes a partir desse conjunto de dados, onde os três primeiros
elementos vão para o lote #1 e os elementos restantes vão para o lote #2. O
argumento opcional é útil para casos em que o número de elementos no tensor
não é divisível pelo tamanho de lote desejado. Podemos descartar o último lote
não completo definindo como . O valor padrão para
é .drop_lastdrop_lastTruedrop_lastFalse

Sempre podemos iterar através de um conjunto de dados diretamente, mas como


você acabou de ver, fornece um lote automático e personalizável para um
conjunto de dados.DataLoader

Combinando dois tensores em um conjunto de


dados conjunto
Muitas vezes, podemos ter os dados em dois (ou possivelmente mais) tensores.
Por exemplo, poderíamos ter um tensor para características e um tensor para
rótulos. Nesses casos, precisamos construir um conjunto de dados que combine
esses tensores, o que nos permitirá recuperar os elementos desses tensores em
tuplas.

Suponha que temos dois tensores, e . O Tensor mantém nossos valores de


recurso, cada um de tamanho, e armazena os rótulos de classe. Para este
exemplo, primeiro criamos esses dois tensores da seguinte maneira: t_xt_yt_x3t_y

>>> torch.manual_seed(1)

>>> t_x = torch.rand([4, 3], dtype=torch.float32)

>>> t_y = torch.arange(4)


CopyExplain

Agora, queremos criar um conjunto de dados conjunto a partir desses dois


tensores. Primeiro, precisamos criar uma classe da seguinte maneira: Dataset

>>> from torch.utils.data import Dataset

>>> class JointDataset(Dataset):

... def __init__(self, x, y):

... self.x = x

... self.y = y

...

... def __len__(self):

... return len(self.x)

...

... def __getitem__(self, idx):

... return self.x[idx], self.y[idx]


CopyExplain

Uma classe personalizada deve conter os seguintes métodos a serem usados pelo
carregador de dados posteriormente: Dataset
 __init__(): É aqui que a lógica inicial acontece, como ler matrizes existentes,
carregar um arquivo, filtrar dados e assim por diante.
 __getitem__(): Isso retorna a amostra correspondente para o índice fornecido.

Em seguida, criamos um conjunto de dados conjunto de e com a classe


personalizada da seguinte maneira:t_xt_yDataset

>>> from torch.utils.data import TensorDataset

>>> joint_dataset = TensorDataset(t_x, t_y)


CopyExplain

Finalmente, podemos imprimir cada exemplo do conjunto de dados conjunto da


seguinte maneira:

>>> for example in joint_dataset:

... print(' x: ', example[0], ' y: ', example[1])

x: tensor([0.7576, 0.2793, 0.4031]) y: tensor(0)

x: tensor([0.7347, 0.0293, 0.7999]) y: tensor(1)

x: tensor([0.3971, 0.7544, 0.5695]) y: tensor(2)

x: tensor([0.4388, 0.6387, 0.5247]) y: tensor(3)


CopyExplain

Também podemos simplesmente utilizar a classe, se o segundo conjunto de


dados for um conjunto de dados rotulado na forma de tensores. Assim, em vez de
usar nossa classe autodefinida, podemos criar um conjunto de dados conjunto da
seguinte maneira:torch.utils.data.TensorDatasetDatasetJointDataset

>>> joint_dataset = TensorDataset(t_x, t_y)


CopyExplain

Observe que uma fonte comum de erro pode ser que a correspondência elementar
entre os recursos originais (x) e os rótulos (y) pode ser perdida (por exemplo, se
os dois conjuntos de dados forem embaralhados separadamente). No entanto,
uma vez que eles são mesclados em um conjunto de dados, é seguro aplicar
essas operações.
Se tivermos um conjunto de dados criado a partir da lista de nomes de arquivos de
imagem no disco, podemos definir uma função para carregar as imagens desses
nomes de arquivo. Você verá um exemplo de aplicação de várias transformações
a um conjunto de dados mais adiante neste capítulo.

Embaralhar, agrupar e repetir


Como foi mencionado no Capítulo 2, Training Simple Machine Learning Algorithms
for Classification, ao treinar um modelo NN usando otimização de descida de
gradiente estocástico, é importante alimentar os dados de treinamento como lotes
embaralhados aleatoriamente. Você já viu como especificar o tamanho do lote
usando o argumento de um objeto de carregador de dados. Agora, além de criar
lotes, você verá como embaralhar e reiterar sobre os conjuntos de dados.
Continuaremos a trabalhar com o conjunto de dados conjunto anterior. batch_size

Primeiro, vamos criar um carregador de dados de versão embaralhada a partir do


conjunto de dados:joint_dataset

>>> torch.manual_seed(1)

>>> data_loader = DataLoader(dataset=joint_dataset, batch_size=2, shuffle=True)


CopyExplain

Aqui, cada lote contém dois registros de dados (x) e os rótulos correspondentes
(y). Agora nós iteramos através da entrada do carregador de dados por entrada da
seguinte maneira:

>>> for i, batch in enumerate(data_loader, 1):

... print(f'batch {i}:', 'x:', batch[0],

'\n y:', batch[1])

batch 1: x: tensor([[0.4388, 0.6387, 0.5247],

[0.3971, 0.7544, 0.5695]])

y: tensor([3, 2])

batch 2: x: tensor([[0.7576, 0.2793, 0.4031],


[0.7347, 0.0293, 0.7999]])

y: tensor([0, 1])
CopyExplain

As linhas são embaralhadas sem perder a correspondência um-para-um entre as


entradas em e .xy

Além disso, ao treinar um modelo para várias épocas, precisamos embaralhar e


iterar sobre o conjunto de dados pelo número desejado de épocas. Então, vamos
iterar sobre o conjunto de dados em lote duas vezes:

>>> for epoch in range(2):

>>> print(f'epoch {epoch+1}')

>>> for i, batch in enumerate(data_loader, 1):

... print(f'batch {i}:', 'x:', batch[0],

'\n y:', batch[1])

epoch 1

batch 1: x: tensor([[0.7347, 0.0293, 0.7999],

[0.3971, 0.7544, 0.5695]])

y: tensor([1, 2])

batch 2: x: tensor([[0.4388, 0.6387, 0.5247],

[0.7576, 0.2793, 0.4031]])

y: tensor([3, 0])

epoch 2

batch 1: x: tensor([[0.3971, 0.7544, 0.5695],

[0.7576, 0.2793, 0.4031]])

y: tensor([2, 0])

batch 2: x: tensor([[0.7347, 0.0293, 0.7999],

[0.4388, 0.6387, 0.5247]])


y: tensor([1, 3])
CopyExplain

Isso resulta em dois conjuntos diferentes de lotes. Na primeira época, o primeiro


lote contém um par de valores e o segundo lote contém um par de valores. Na
segunda época, dois lotes contêm um par de valores e respectivamente. Para
cada iteração, os elementos dentro de um lote também são embaralhados. [y=1,
y=2][y=3, y=0][y=2, y=0][y=1, y=3]

Criando um conjunto de dados a partir de arquivos


no disco de armazenamento local
Nesta seção, construiremos um conjunto de dados a partir de arquivos de imagem
armazenados em disco. Há uma pasta de imagens associada ao conteúdo online
deste capítulo. Depois de baixar a pasta, você deve ser capaz de ver seis imagens
de cães e gatos em formato JPEG.

Esse pequeno conjunto de dados mostrará como a criação de um conjunto de


dados a partir de arquivos armazenados geralmente funciona. Para conseguir
isso, vamos usar dois módulos adicionais: in para ler o conteúdo do arquivo de
imagem e in para decodificar o conteúdo bruto e redimensionar as
imagens.ImagePILtransformstorchvision

Os módulos e fornecem uma série de funções adicionais e úteis, que estão além
do escopo do livro. Você é encorajado a navegar pela documentação oficial para
saber mais sobre estas funções:PIL.Imagetorchvision.transforms

https://pillow.readthedocs.io/en/stable/reference/Image.html para PIL.Image

https://pytorch.org/vision/stable/transforms.html para torchvision.transforms

Antes de começarmos, vamos dar uma olhada no conteúdo desses arquivos.


Usaremos a biblioteca para gerar uma lista de arquivos de imagem: pathlib

>>> import pathlib

>>> imgdir_path = pathlib.Path('cat_dog_images')


>>> file_list = sorted([str(path) for path in

... imgdir_path.glob('*.jpg')])

>>> print(file_list)

['cat_dog_images/dog-03.jpg', 'cat_dog_images/cat-01.jpg', 'cat_dog_images/cat-02.jpg',

'cat_dog_images/cat-03.jpg', 'cat_dog_images/dog-01.jpg', 'cat_dog_images/dog-02.jpg']


CopyExplain

Next, we will visualize these image examples using Matplotlib:

>>> import matplotlib.pyplot as plt

>>> import os

>>> from PIL import Image

>>> fig = plt.figure(figsize=(10, 5))

>>> for i, file in enumerate(file_list):

... img = Image.open(file)

... print('Image shape:', np.array(img).shape)

... ax = fig.add_subplot(2, 3, i+1)

... ax.set_xticks([]); ax.set_yticks([])

... ax.imshow(img)

... ax.set_title(os.path.basename(file), size=15)

>>> plt.tight_layout()

>>> plt.show()

Image shape: (900, 1200, 3)

Image shape: (900, 1200, 3)

Image shape: (900, 1200, 3)

Image shape: (900, 742, 3)

Image shape: (800, 1200, 3)

Image shape: (800, 1200, 3)


CopyExplain
Figure 12.3 shows the example images:

Figure 12.3: Images of cats and dogs

Just from this visualization and the printed image shapes, we can already see that
the images have different aspect ratios. If you print the aspect ratios (or data array
shapes) of these images, you will see that some images are 900 pixels high and
1200 pixels wide (900×1200), some are 800×1200, and one is 900×742. Later, we
will preprocess these images to a consistent size. Another point to consider is that
the labels for these images are provided within their filenames. So, we extract
these labels from the list of filenames, assigning label to dogs and label to cats:10

>>> labels = [1 if 'dog' in

... os.path.basename(file) else 0

... for file in file_list]

>>> print(labels)

[0, 0, 0, 1, 1, 1]
CopyExplain

Now, we have two lists: a list of filenames (or paths of each image) and a list of
their labels. In the previous section, you learned how to create a joint dataset from
two arrays. Here, we will do the following:
>>> class ImageDataset(Dataset):

... def __init__(self, file_list, labels):

... self.file_list = file_list

... self.labels = labels

...

... def __getitem__(self, index):

... file = self.file_list[index]

... label = self.labels[index]

... return file, label

...

... def __len__(self):

... return len(self.labels)

>>> image_dataset = ImageDataset(file_list, labels)

>>> for file, label in image_dataset:

... print(file, label)

cat_dog_images/cat-01.jpg 0

cat_dog_images/cat-02.jpg 0

cat_dog_images/cat-03.jpg 0

cat_dog_images/dog-01.jpg 1

cat_dog_images/dog-02.jpg 1

cat_dog_images/dog-03.jpg 1
CopyExplain

The joint dataset has filenames and labels.

Next, we need to apply transformations to this dataset: load the image content from
its file path, decode the raw content, and resize it to a desired size, for example,
80×120. As mentioned before, we use the module to resize the images and
convert the loaded pixels into tensors as follows: torchvision.transforms
>>> import torchvision.transforms as transforms

>>> img_height, img_width = 80, 120

>>> transform = transforms.Compose([

... transforms.ToTensor(),

... transforms.Resize((img_height, img_width)),

... ])
CopyExplain

Now we update the class with the we just defined:ImageDatasettransform

>>> class ImageDataset(Dataset):

... def __init__(self, file_list, labels, transform=None):

... self.file_list = file_list

... self.labels = labels

... self.transform = transform

...

... def __getitem__(self, index):

... img = Image.open(self.file_list[index])

... if self.transform is not None:

... img = self.transform(img)

... label = self.labels[index]

... return img, label

...

... def __len__(self):

... return len(self.labels)

>>>

>>> image_dataset = ImageDataset(file_list, labels, transform)


CopyExplain
Finally, we visualize these transformed image examples using Matplotlib:

>>> fig = plt.figure(figsize=(10, 6))

>>> for i, example in enumerate(image_dataset):

... ax = fig.add_subplot(2, 3, i+1)

... ax.set_xticks([]); ax.set_yticks([])

... ax.imshow(example[0].numpy().transpose((1, 2, 0)))

... ax.set_title(f'{example[1]}', size=15)

...

>>> plt.tight_layout()

>>> plt.show()
CopyExplain

This results in the following visualization of the retrieved example images, along


with their labels:

Figure 12.4: Images are labeled

The method in the class wraps all four steps into a single function, including the
loading of the raw content (images and labels), decoding the images into tensors,
and resizing the images. The function then returns a dataset that we can iterate
over and apply other operations that we learned about in the previous sections via
a data loader, such as shuffling and batching.__getitem__ImageDataset

Fetching available datasets from the


torchvision.datasets library
The library provides a nice collection of freely available image datasets for training
or evaluating deep learning models. Similarly, the library provides datasets for
natural language. Here, we use as an
example.torchvision.datasetstorchtext.datasetstorchvision.datasets

The datasets (https://pytorch.org/vision/stable/datasets.html) are nicely formatted


and come with informative descriptions, including the format of features and labels
and their type and dimensionality, as well as the link to the original source of the
dataset. Another advantage is that these datasets are all subclasses of , so all the
functions we covered in the previous sections can be used directly. So, let’s see
how to use these datasets in action.torchvisiontorch.utils.data.Dataset

First, if you haven’t already installed together with PyTorch earlier, you need to
install the library via from the command line:torchvisiontorchvisionpip

pip install torchvision


CopyExplain

You can take a look at the list of available datasets


at https://pytorch.org/vision/stable/datasets.html.

In the following paragraphs, we will cover fetching two different datasets: CelebA ()
and the MNIST digit dataset.celeb_a

Let’s first work with the CelebA dataset


(http://mmlab.ie.cuhk.edu.hk/projects/CelebA.html)
with (https://pytorch.org/vision/stable/datasets.html#celeba). The description
of provides some useful information to help us understand the structure of this
dataset:torchvision.datasets.CelebAtorchvision.datasets.CelebA
 The database has three subsets, , , and . We can select a specific subset or
load all of them with the parameter.'train''valid''test'split
 The images are stored in format. And we can obtain a transformed version
using a custom function, such
as and .PIL.Imagetransformtransforms.ToTensortransforms.Resize
 There are different types of targets we can use, including , , and . is 40 facial
attributes for the person in the image, such as facial expression, makeup, hair
properties, and so on; is the person ID for an image; and refers to the
dictionary of extracted facial points, such as the position of the eyes, nose,
and so
on.'attributes''identity''landmarks''attributes''identity''landmarks'

Next, we will call the class to download the data, store it on disk in a designated
folder, and load it into
a object:torchvision.datasets.CelebAtorch.utils.data.Dataset

>>> import torchvision

>>> image_path = './'

>>> celeba_dataset = torchvision.datasets.CelebA(

... image_path, split='train', target_type='attr', download=True

... )

1443490838/? [01:28<00:00, 6730259.81it/s]

26721026/? [00:03<00:00, 8225581.57it/s]

3424458/? [00:00<00:00, 14141274.46it/s]

6082035/? [00:00<00:00, 21695906.49it/s]

12156055/? [00:00<00:00, 12002767.35it/s]

2836386/? [00:00<00:00, 3858079.93it/s]


CopyExplain

You may run into a error, or ; it just means that Google Drive has a daily maximum
quota that is exceeded by the CelebA files. To work around it, you can manually
download the files from the
source: http://mmlab.ie.cuhk.edu.hk/projects/CelebA.html. In the downloaded
folder, , you can unzip the file. The is the root of the downloaded folder, . If you
have already downloaded the files once, you can simply set . For additional
information and guidance, we highly recommend to see accompanying code
notebook
at https://github.com/rasbt/machine-learning-book/blob/main/ch12/ch12_part1.ipyn
b.BadZipFile: File is not a zip fileRuntimeError: The daily quota of the file
img_align_celeba.zip is exceeded and it can't be downloaded. This is a
limitation of Google Drive and can only be overcome by trying again
laterceleba/img_align_celeba.zipimage_pathceleba/download=False

Now that we have instantiated the datasets, let’s check if the object is of
the class:torch.utils.data.Dataset

>>> assert isinstance(celeba_dataset, torch.utils.data.Dataset)


CopyExplain

As mentioned, the dataset is already split into train, test, and validation datasets,
and we only load the train set. And we only use the target. In order to see what the
data examples look like, we can execute the following code: 'attributes'

>>> example = next(iter(celeba_dataset))

>>> print(example)

(<PIL.JpegImagePlugin.JpegImageFile image mode=RGB size=178x218 at 0x120C6C668>,

tensor([0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0,

0, 1]))
CopyExplain

Note that the sample in this dataset comes in a tuple of . If we want to pass this
dataset to a supervised deep learning model during training, we have to reformat it
as a tuple of . For the label, we will use the category from the attributes as an
example, which is the 31st element.(PIL.Image, attributes)(features tensor,
label)'Smiling'

Finally, let’s take the first 18 examples from it to visualize them with


their labels:'Smiling'
>>> from itertools import islice

>>> fig = plt.figure(figsize=(12, 8))

>>> for i, (image, attributes) in islice(enumerate(celeba_dataset), 18):

... ax = fig.add_subplot(3, 6, i+1)

... ax.set_xticks([]); ax.set_yticks([])

... ax.imshow(image)

... ax.set_title(f'{attributes[31]}', size=15)

>>> plt.show()
CopyExplain

The examples and their labels that are retrieved from are shown in Figure
12.5:celeba_dataset

Figure 12.5: Model predicts smiling celebrities

This was all we needed to do to fetch and use the CelebA image dataset.
Next, we will proceed with the second dataset
from (https://pytorch.org/vision/stable/datasets.html#mnist). Let’s see how it can be
used to fetch the MNIST digit dataset:torchvision.datasets.MNIST

 The database has two partitions, and . We need to select a specific subset to


load.'train''test'
 The images are stored in format. And we can obtain a transformed version
using a custom function, such
as and .PIL.Imagetransformtransforms.ToTensortransforms.Resize
 There are 10 classes for the target, from to .09

Now, we can download the partition, convert the elements to tuples, and visualize
10 examples:'train'

>>> mnist_dataset = torchvision.datasets.MNIST(image_path, 'train', download=True)

>>> assert isinstance(mnist_dataset, torch.utils.data.Dataset)

>>> example = next(iter(mnist_dataset))

>>> print(example)

(<PIL.Image.Image image mode=L size=28x28 at 0x126895B00>, 5)

>>> fig = plt.figure(figsize=(15, 6))

>>> for i, (image, label) in islice(enumerate(mnist_dataset), 10):

... ax = fig.add_subplot(2, 5, i+1)

... ax.set_xticks([]); ax.set_yticks([])

... ax.imshow(image, cmap='gray_r')

... ax.set_title(f'{label}', size=15)

>>> plt.show()
CopyExplain

The retrieved example handwritten digits from this dataset are shown as follows:
Figure 12.6: Correctly identifying handwritten digits

Isso conclui nossa cobertura de construção e manipulação de conjuntos de dados


e busca de conjuntos de dados da biblioteca. A seguir, veremos como construir
modelos NN no PyTorch.torchvision.datasets

Construindo um modelo NN no PyTorch


Até agora, neste capítulo, você aprendeu sobre os componentes básicos de
utilidade do PyTorch para manipular tensores e organizar dados em formatos que
podemos iterar durante o treinamento. Nesta seção, finalmente implementaremos
nosso primeiro modelo preditivo no PyTorch. Como o PyTorch é um pouco mais
flexível, mas também mais complexo do que bibliotecas de aprendizado de
máquina, como o scikit-learn, começaremos com um modelo de regressão linear
simples.

O módulo de rede neural PyTorch (torch.nn)


torch.nn éum módulo elegantemente projetado desenvolvido para ajudar a criar e
treinar NNs. Permite a fácil prototipagem e a construção de modelos complexos
em apenas algumas linhas de código.

Para utilizar totalmente o poder do módulo e personalizá-lo para o seu problema,


você precisa entender o que ele está fazendo. Para desenvolver esse
entendimento, primeiro treinaremos um modelo básico de regressão linear em um
conjunto de dados de brinquedo sem usar nenhum recurso do módulo; não
usaremos nada além das operações básicas do tensor PyTorch. torch.nn

Em seguida, adicionaremos incrementalmente recursos de e . Como você verá


nas subseções a seguir, esses módulos tornam a construção de um modelo NN
extremamente fácil. Também aproveitaremos as funcionalidades de pipeline de
conjunto de dados suportadas no PyTorch, como e , que você aprendeu na seção
anterior. Neste livro, usaremos o módulo para construir modelos
NN.torch.nntorch.optimDatasetDataLoadertorch.nn

A abordagem mais comumente usada para construir um NN no PyTorch é através


do , que permite que as camadas sejam empilhadas para formar uma rede. Isso
nos dá mais controle sobre o passe para frente. Veremos exemplos de criação de
um modelo NN usando a classe.nn.Modulenn.Module

Finalmente, como você verá nas subseções a seguir, um modelo treinado pode
ser salvo e recarregado para uso futuro.

Construindo um modelo de regressão linear


Nesta subseção, construiremos um modelo simples para resolver um problema de
regressão linear. Primeiro, vamos criar um conjunto de dados de brinquedo no
NumPy e visualizá-lo:

>>> X_train = np.arange(10, dtype='float32').reshape((10, 1))

>>> y_train = np.array([1.0, 1.3, 3.1, 2.0, 5.0,

... 6.3, 6.6,7.4, 8.0,

... 9.0], dtype='float32')

>>> plt.plot(X_train, y_train, 'o', markersize=10)

>>> plt.xlabel('x')

>>> plt.ylabel('y')

>>> plt.show()
CopyExplain
Como resultado, os exemplos de treinamento serão mostrados em um gráfico de
dispersão da seguinte maneira:

Figura 12.7: Gráfico de dispersão dos exemplos de formação

Em seguida, padronizaremos as características (centralização média e divisão


pelo desvio padrão) e criaremos um PyTorch para o conjunto de treinamento e um
correspondente:DatasetDataLoader

>>> from torch.utils.data import TensorDataset

>>> X_train_norm = (X_train - np.mean(X_train)) / np.std(X_train)

>>> X_train_norm = torch.from_numpy(X_train_norm)

>>> y_train = torch.from_numpy(y_train).float()

>>> train_ds = TensorDataset(X_train_norm, y_train)

>>> batch_size = 1

>>> train_dl = DataLoader(train_ds, batch_size, shuffle=True)


CopyExplain
Aqui, definimos um tamanho de lote de para o . 1DataLoader

Agora, podemos definir nosso modelo de regressão linear como z = wx + b. Aqui,


vamos usar o módulo. Ele fornece camadas predefinidas para a criação de
modelos NN complexos, mas para começar, você aprenderá a definir um modelo
do zero. Mais adiante neste capítulo, você verá como usar essas camadas
predefinidas.torch.nn

Para este problema de regressão, definiremos um modelo de regressão linear do


zero. Definiremos os parâmetros do nosso modelo, e , que correspondem aos
parâmetros de peso e viés, respectivamente. Finalmente, definiremos a função
para determinar como esse modelo usa os dados de entrada para gerar sua
saída:weightbiasmodel()

>>> torch.manual_seed(1)

>>> weight = torch.randn(1)

>>> weight.requires_grad_()

>>> bias = torch.zeros(1, requires_grad=True)

>>> def model(xb):

... return xb @ weight + bias


CopyExplain

Depois de definir o modelo, podemos definir a função de perda que queremos


minimizar para encontrar os pesos ideais do modelo. Aqui, vamos escolher o erro
quadrático médio (MSE) como nossa função de perda:

>>> def loss_fn(input, target):

... return (input-target).pow(2).mean()


CopyExplain

Além disso, para aprender os parâmetros de peso do modelo, utilizaremos


a descida do gradiente estocástico. Nesta subseção, implementaremos este
treinamento através do procedimento de descida de gradiente estocástico por nós
mesmos, mas na próxima subseção, usaremos o método do pacote de otimização,
, para fazer a mesma coisa.SGDtorch.optim
Para implementar o algoritmo de descida de gradiente estocástico, precisamos
calcular os gradientes. Em vez de calcular manualmente os gradientes, usaremos
a função do PyTorch. Abordaremos suas diferentes classes e funções para
implementar a diferenciação automática no Capítulo 13, Indo mais fundo – A
Mecânica da Tocha.torch.autograd.backwardtorch.autograd

Agora, podemos definir a taxa de aprendizado e treinar o modelo para 200


épocas. O código para treinar o modelo em relação à versão em lote do conjunto
de dados é o seguinte:

>>> learning_rate = 0.001

>>> num_epochs = 200

>>> log_epochs = 10

>>> for epoch in range(num_epochs):

... for x_batch, y_batch in train_dl:

... pred = model(x_batch)

... loss = loss_fn(pred, y_batch.long())

... loss.backward()

... with torch.no_grad():

... weight -= weight.grad * learning_rate

... bias -= bias.grad * learning_rate

... weight.grad.zero_()

... bias.grad.zero_()

... if epoch % log_epochs==0:

... print(f'Epoch {epoch} Loss {loss.item():.4f}')

Epoch 0 Loss 5.1701

Epoch 10 Loss 30.3370

Epoch 20 Loss 26.9436

Epoch 30 Loss 0.9315


Epoch 40 Loss 3.5942

Epoch 50 Loss 5.8960

Epoch 60 Loss 3.7567

Epoch 70 Loss 1.5877

Epoch 80 Loss 0.6213

Epoch 90 Loss 1.5596

Epoch 100 Loss 0.2583

Epoch 110 Loss 0.6957

Epoch 120 Loss 0.2659

Epoch 130 Loss 0.1615

Epoch 140 Loss 0.6025

Epoch 150 Loss 0.0639

Epoch 160 Loss 0.1177

Epoch 170 Loss 0.3501

Epoch 180 Loss 0.3281

Epoch 190 Loss 0.0970


CopyExplain

Vamos olhar para o modelo treinado e plotá-lo. Para os dados de teste, criaremos
uma matriz NumPy de valores espaçados uniformemente entre 0 e 9. Como
treinamos nosso modelo com recursos padronizados, também aplicaremos a
mesma padronização aos dados de teste:

>>> print('Final Parameters:', weight.item(), bias.item())

Final Parameters: 2.669806480407715 4.879569053649902

>>> X_test = np.linspace(0, 9, num=100, dtype='float32').reshape(-1, 1)

>>> X_test_norm = (X_test - np.mean(X_train)) / np.std(X_train)

>>> X_test_norm = torch.from_numpy(X_test_norm)

>>> y_pred = model(X_test_norm).detach().numpy()


>>> fig = plt.figure(figsize=(13, 5))

>>> ax = fig.add_subplot(1, 2, 1)

>>> plt.plot(X_train_norm, y_train, 'o', markersize=10)

>>> plt.plot(X_test_norm, y_pred, '--', lw=3)

>>> plt.legend(['Training examples', 'Linear reg.'], fontsize=15)

>>> ax.set_xlabel('x', size=15)

>>> ax.set_ylabel('y', size=15)

>>> ax.tick_params(axis='both', which='major', labelsize=15)

>>> plt.show()
CopyExplain

A figura 12.8 mostra um gráfico de dispersão dos exemplos de treinamento e do


modelo de regressão linear treinada:

Figura 12.8: O modelo de regressão linear ajusta-se bem aos dados


Treinamento de modelos através dos módulos
torch.nn e torch.optim
No exemplo anterior, vimos como treinar um modelo escrevendo uma função de
perda personalizada e aplicando otimização de descida de gradiente estocástico.
No entanto, escrever a função de perda e as atualizações de gradiente pode ser
uma tarefa repetível em diferentes projetos. O módulo fornece um conjunto de
funções de perda e suporta algoritmos de otimização mais comumente usados
que podem ser chamados para atualizar os parâmetros com base nos gradientes
computados. Para ver como eles funcionam, vamos criar uma nova função de
perda de MSE e um otimizador de descida de gradiente
estocástico:loss_fn()torch.nntorch.optim

>>> import torch.nn as nn

>>> loss_fn = nn.MSELoss(reduction='mean')

>>> input_size = 1

>>> output_size = 1

>>> model = nn.Linear(input_size, output_size)

>>> optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate)


CopyExplain

Note que aqui usamos a classe para a camada linear em vez de defini-la
manualmente.torch.nn.Linear

Agora, podemos simplesmente chamar o método do para treinar o modelo.


Podemos passar um conjunto de dados em lote (como , que foi criado no exemplo
anterior):step()optimizertrain_dl

>>> for epoch in range(num_epochs):

... for x_batch, y_batch in train_dl:

... # 1. Generate predictions

... pred = model(x_batch)[:, 0]


... # 2. Calculate loss

... loss = loss_fn(pred, y_batch)

... # 3. Compute gradients

... loss.backward()

... # 4. Update parameters using gradients

... optimizer.step()

... # 5. Reset the gradients to zero

... optimizer.zero_grad()

... if epoch % log_epochs==0:

... print(f'Epoch {epoch} Loss {loss.item():.4f}')


CopyExplain

Depois que o modelo for treinado, visualize os resultados e certifique-se de que


eles sejam semelhantes aos resultados do método anterior. Para obter os
parâmetros de peso e viés, podemos fazer o seguinte:

>>> print('Final Parameters:', model.weight.item(), model.bias.item())

Final Parameters: 2.646660089492798 4.883835315704346


CopyExplain

Construindo um perceptron multicamadas para


classificar flores no conjunto de dados Iris
No exemplo anterior, você viu como criar um modelo do zero. Este modelo foi
treinado usando otimização para descida do gradiente estocástico. Embora
tenhamos começado nossa jornada com base no exemplo mais simples possível,
você pode ver que definir o modelo do zero, mesmo para um caso tão simples,
não é atraente nem uma boa prática. Em vez disso, o PyTorch fornece camadas já
definidas através das quais podem ser prontamente usadas como blocos de
construção de um modelo NN. Nesta seção, você aprenderá a usar essas
camadas para resolver uma tarefa de classificação usando o conjunto de dados de
flores de íris (identificando entre três espécies de íris) e construir um perceptron de
duas camadas usando o módulo. Primeiro, vamos obter os dados
de:torch.nntorch.nnsklearn.datasets

>>> from sklearn.datasets import load_iris

>>> from sklearn.model_selection import train_test_split

>>> iris = load_iris()

>>> X = iris['data']

>>> y = iris['target']

>>> X_train, X_test, y_train, y_test = train_test_split(

... X, y, test_size=1./3, random_state=1)


CopyExplain

Aqui, selecionamos aleatoriamente 100 amostras (2/3) para treinamento e 50


amostras (1/3) para teste.

Em seguida, padronizamos as características (centralização média e divisão pelo


desvio padrão) e criamos um PyTorch para o conjunto de treinamento e um
correspondente:DatasetDataLoader

>>> X_train_norm = (X_train - np.mean(X_train)) / np.std(X_train)

>>> X_train_norm = torch.from_numpy(X_train_norm).float()

>>> y_train = torch.from_numpy(y_train)

>>> train_ds = TensorDataset(X_train_norm, y_train)

>>> torch.manual_seed(1)

>>> batch_size = 2

>>> train_dl = DataLoader(train_ds, batch_size, shuffle=True)


CopyExplain

Aqui, definimos o tamanho do lote para o . 2DataLoader

Agora, estamos prontos para usar o módulo para construir um modelo de forma
eficiente. Em particular, usando a classe, podemos empilhar algumas camadas e
construir um NN. Você pode ver a lista de todas as camadas que já estão
disponíveis em https://pytorch.org/docs/stable/nn.html. Para este problema, vamos
usar a camada, que também é conhecida como uma camada totalmente
conectada ou camada densa, e pode ser melhor representada por f(w × x + b),
onde x representa um tensor contendo as características de entrada, w e b são a
matriz de peso e o vetor de polarização, e f é a função de
ativação. torch.nnnn.ModuleLinear

Cada camada em um NN recebe suas entradas da camada anterior; portanto, sua


dimensionalidade (classificação e forma) é fixa. Normalmente, precisamos nos
preocupar com a dimensionalidade da saída apenas quando projetamos uma
arquitetura NN. Aqui, queremos definir um modelo com duas camadas ocultas. O
primeiro recebe uma entrada de quatro características e as projeta para 16
neurônios. A segunda camada recebe a saída da camada anterior (que tem um
tamanho de 16) e os projeta para três neurônios de saída, já que temos três
rótulos de classe. Isso pode ser feito da seguinte maneira:

>>> class Model(nn.Module):

... def __init__(self, input_size, hidden_size, output_size):

... super().__init__()

... self.layer1 = nn.Linear(input_size, hidden_size)

... self.layer2 = nn.Linear(hidden_size, output_size)

... def forward(self, x):

... x = self.layer1(x)

... x = nn.Sigmoid()(x)

... x = self.layer2(x)

... return x

>>> input_size = X_train_norm.shape[1]

>>> hidden_size = 16

>>> output_size = 3

>>> model = Model(input_size, hidden_size, output_size)


CopyExplain
Aqui, usamos a função de ativação sigmoide para a primeira camada e a ativação
softmax para a última camada (saída). A ativação Softmax na última camada é
usada para suportar a classificação multiclasse, já que temos três rótulos de
classe aqui (e é por isso que temos três neurônios na camada de saída).
Discutiremos as diferentes funções de ativação e suas aplicações mais adiante
neste capítulo.

Em seguida, especificamos a função de perda como perda de entropia cruzada e


o otimizador como Adam:

O otimizador Adam é um método de otimização robusto, baseado em gradiente,


sobre o qual falaremos em detalhes no Capítulo 14, Classificando imagens com
redes neurais convolucionais profundas.

>>> learning_rate = 0.001

>>> loss_fn = nn.CrossEntropyLoss()

>>> optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)


CopyExplain

Agora, podemos treinar o modelo. Especificaremos o número de épocas a serem .


O código de treinamento do modelo de classificação de flores é o seguinte:100

>>> num_epochs = 100

>>> loss_hist = [0] * num_epochs

>>> accuracy_hist = [0] * num_epochs

>>> for epoch in range(num_epochs):

... for x_batch, y_batch in train_dl:

... pred = model(x_batch)

... loss = loss_fn(pred, y_batch)

... loss.backward()

... optimizer.step()

... optimizer.zero_grad()
... loss_hist[epoch] += loss.item()*y_batch.size(0)

... is_correct = (torch.argmax(pred, dim=1) == y_batch).float()

... accuracy_hist[epoch] += is_correct.sum()

... loss_hist[epoch] /= len(train_dl.dataset)

... accuracy_hist[epoch] /= len(train_dl.dataset)


CopyExplain

As listas e mantêm a perda de treinamento e a precisão do treinamento após cada


época. Podemos usar isso para visualizar as curvas de aprendizado da seguinte
maneira:loss_histaccuracy_hist

>>> fig = plt.figure(figsize=(12, 5))

>>> ax = fig.add_subplot(1, 2, 1)

>>> ax.plot(loss_hist, lw=3)

>>> ax.set_title('Training loss', size=15)

>>> ax.set_xlabel('Epoch', size=15)

>>> ax.tick_params(axis='both', which='major', labelsize=15)

>>> ax = fig.add_subplot(1, 2, 2)

>>> ax.plot(accuracy_hist, lw=3)

>>> ax.set_title('Training accuracy', size=15)

>>> ax.set_xlabel('Epoch', size=15)

>>> ax.tick_params(axis='both', which='major', labelsize=15)

>>> plt.show()
CopyExplain

As curvas de aprendizado (perda de treinamento e precisão do treinamento) são


as seguintes:
Figura 12.9: Curvas de perda e precisão do treinamento

Avaliando o modelo treinado no conjunto de dados


de teste
Agora podemos avaliar a precisão de classificação do modelo treinado no conjunto
de dados de teste:

>>> X_test_norm = (X_test - np.mean(X_train)) / np.std(X_train)

>>> X_test_norm = torch.from_numpy(X_test_norm).float()

>>> y_test = torch.from_numpy(y_test)

>>> pred_test = model(X_test_norm)

>>> correct = (torch.argmax(pred_test, dim=1) == y_test).float()

>>> accuracy = correct.mean()

>>> print(f'Test Acc.: {accuracy:.4f}')

Test Acc.: 0.9800


CopyExplain

Como treinamos nosso modelo com características padronizadas, também


aplicamos a mesma padronização aos dados de teste. A precisão da classificação
é de 0,98 (ou seja, 98%).
Salvando e recarregando o modelo treinado
Os modelos treinados podem ser salvos em disco para uso futuro. Isso pode ser
feito da seguinte maneira:

>>> path = 'iris_classifier.pt'

>>> torch.save(model, path)


CopyExplain

A chamada salvará a arquitetura do modelo e todos os parâmetros aprendidos.


Como uma convenção comum, podemos salvar modelos usando uma extensão de
arquivo ou.save(model)'pt''pth'

Agora, vamos recarregar o modelo salvo. Como salvamos a arquitetura do modelo


e os pesos, podemos facilmente reconstruir e recarregar os parâmetros em
apenas uma linha:

>>> model_new = torch.load(path)


CopyExplain

Tente verificar a arquitetura do modelo chamando : model_new.eval()

>>> model_new.eval()

Model(

(layer1): Linear(in_features=4, out_features=16, bias=True)

(layer2): Linear(in_features=16, out_features=3, bias=True)

)
CopyExplain

Finalmente, vamos avaliar esse novo modelo que é recarregado no conjunto de


dados de teste para verificar se os resultados são os mesmos de antes:

>>> pred_test = model_new(X_test_norm)

>>> correct = (torch.argmax(pred_test, dim=1) == y_test).float()


>>> accuracy = correct.mean()

>>> print(f'Test Acc.: {accuracy:.4f}')

Test Acc.: 0.9800


CopyExplain

Se você quiser salvar apenas os parâmetros aprendidos, você pode usar o


seguinte:save(model.state_dict())

>>> path = 'iris_classifier_state.pt'

>>> torch.save(model.state_dict(), path)


CopyExplain

Para recarregar os parâmetros salvos, primeiro precisamos construir o modelo


como fizemos antes e, em seguida, alimentar os parâmetros carregados para o
modelo:

>>> model_new = Model(input_size, hidden_size, output_size)

>>> model_new.load_state_dict(torch.load(path))
CopyExplain

Escolhendo funções de ativação para


redes neurais multicamadas
Para simplificar, discutimos apenas a função de ativação sigmoide no contexto de
NNs feedforward multicamadas até agora; nós o usamos na camada oculta, bem
como na camada de saída na implementação MLP no Capítulo 11.

Note que neste livro, a função logística sigmoidal, , é referida como a

função sigmoide para brevidade,  que é comum na


literatura de aprendizado de máquina. Nas subseções a seguir, você aprenderá
mais sobre funções não lineares alternativas que são úteis para implementar NNs
de várias camadas.
Tecnicamente, podemos usar qualquer função como uma função de ativação em
NNs multicamadas, desde que seja diferenciável. Podemos até usar funções de
ativação linear, como em Adaline (Capítulo 2, Treinamento de Algoritmos Simples
de Aprendizado de Máquina para Classificação). No entanto, na prática, não seria
muito útil usar funções de ativação linear para camadas ocultas e de saída, uma
vez que queremos introduzir a não-linearidade em um NN artificial típico para
poder resolver problemas complexos. A soma das funções lineares produz uma
função linear, afinal.

A função de ativação logística (sigmoide) que usamos no Capítulo


11 provavelmente imita o conceito de um neurônio em um cérebro mais de perto –
podemos pensar nisso como a probabilidade de um neurônio disparar. No entanto,
a função de ativação logística (sigmoide) pode ser problemática se tivermos
entrada altamente negativa, uma vez que a saída da função sigmoide será
próxima de zero neste caso. Se a função sigmoide retornar uma saída próxima de
zero, o NN aprenderá muito lentamente e terá maior probabilidade de ficar preso
nos mínimos locais da paisagem de perda durante o treinamento. É por isso que
as pessoas geralmente preferem uma tangente hiperbólica como uma função de
ativação em camadas ocultas.

Antes de discutirmos como é uma tangente hiperbólica, vamos recapitular


brevemente alguns dos fundamentos da função logística e examinar uma
generalização que a torna mais útil para problemas de classificação multiclasse.

Recapitulação da função logística


Como foi mencionado na introdução desta seção, a função logística é, de fato, um
caso especial de uma função sigmoide. Você se lembrará da seção sobre
regressão logística no Capítulo 3, A Tour of Machine Learning Classifiers Using
Scikit-Learn, que podemos usar uma função logística para modelar a
probabilidade de que a amostra x pertença à classe positiva (classe) em uma
tarefa de classificação binária.1

A entrada líquida dada, z, é mostrada na seguinte equação:


A função logística (sigmoide) calculará o seguinte:

Note que w0 é a unidade de polarização (intercepto do eixo y, que significa x0 = 1).


Para fornecer um exemplo mais concreto, vamos pegar um modelo para um ponto
de dados bidimensional, x, e um modelo com os seguintes coeficientes de peso
atribuídos ao vetor w:

>>> import numpy as np

>>> X = np.array([1, 1.4, 2.5]) ## first value must be 1

>>> w = np.array([0.4, 0.3, 0.5])

>>> def net_input(X, w):

... return np.dot(X, w)

>>> def logistic(z):

... return 1.0 / (1.0 + np.exp(-z))

>>> def logistic_activation(X, w):

... z = net_input(X, w)

... return logistic(z)

>>> print(f'P(y=1|x) = {logistic_activation(X, w):.3f}')

P(y=1|x) = 0.888
CopyExplain

Se calcularmos a entrada líquida (z) e a usarmos para ativar um neurônio logístico


com esses valores de características particulares e coeficientes de peso,
obteremos um valor de , que podemos interpretar como uma probabilidade de
88,8% de que essa amostra em particular, x, pertença à classe positiva.0.888

No Capítulo 11, usamos a técnica de codificação one-hot para representar rótulos


verdade terrestres multiclasse e projetamos a camada de saída consistindo de
várias unidades de ativação logística. No entanto, como será demonstrado pelo
exemplo de código a seguir, uma camada de saída consistindo de várias unidades
de ativação logística não produz valores de probabilidade significativos e
interpretáveis:

>>> # W : array with shape = (n_output_units, n_hidden_units+1)

>>> # note that the first column are the bias units

>>> W = np.array([[1.1, 1.2, 0.8, 0.4],

... [0.2, 0.4, 1.0, 0.2],

... [0.6, 1.5, 1.2, 0.7]])

>>> # A : data array with shape = (n_hidden_units + 1, n_samples)

>>> # note that the first column of this array must be 1

>>> A = np.array([[1, 0.1, 0.4, 0.6]])

>>> Z = np.dot(W, A[0])

>>> y_probas = logistic(Z)

>>> print('Net Input: \n', Z)

Net Input:

[1.78 0.76 1.65]

>>> print('Output Units:\n', y_probas)

Output Units:

[ 0.85569687 0.68135373 0.83889105]


CopyExplain

Como você pode ver na saída, os valores resultantes não podem ser interpretados
como probabilidades para um problema de três classes. A razão para isso é que
eles não somam 1. No entanto, isso não é, de fato, uma grande preocupação se
usarmos nosso modelo para prever apenas os rótulos de classe e não as
probabilidades de associação de classe. Uma maneira de prever o rótulo de
classe a partir das unidades de saída obtidas anteriormente é usar o valor
máximo:

>>> y_class = np.argmax(Z, axis=0)

>>> print('Predicted class label:', y_class)

Predicted class label: 0


CopyExplain

Em determinados contextos, pode ser útil calcular probabilidades de classe


significativas para previsões multiclasse. Na próxima seção, veremos uma
generalização da função logística, a função, que pode nos ajudar nessa
tarefa.softmax

Estimando probabilidades de classe na


classificação multiclasse através da função
softmax
Na seção anterior, você viu como podemos obter um rótulo de classe usando a
função. Anteriormente, na seção Building a multilayer perceptron for classification
flowers in the Iris dataset (Construindo um perceptron multicamadas para
classificar flores na seção do conjunto de dados Iris), determinamos na última
camada do modelo MLP. A função é uma forma suave da função; Em vez de dar
um único índice de classe, ele fornece a probabilidade de cada classe. Portanto,
permite calcular probabilidades de classe significativas em configurações
multiclasse (regressão logística
multinomial).argmaxactivation='softmax'softmaxargmax

Em , a probabilidade de uma determinada amostra com entrada líquida


z pertencer à i-ésima classe pode ser calculada com um termo de normalização no
denominador, isto é, a soma das funções lineares exponencialmente
ponderadas:softmax
Para ver em ação, vamos codificá-lo em Python: softmax

>>> def softmax(z):

... return np.exp(z) / np.sum(np.exp(z))

>>> y_probas = softmax(Z)

>>> print('Probabilities:\n', y_probas)

Probabilities:

[ 0.44668973 0.16107406 0.39223621]

>>> np.sum(y_probas)

1.0
CopyExplain

Como você pode ver, as probabilidades de classe previstas agora somam 1, como
seria de esperar. Também é notável que o rótulo de classe previsto é o mesmo de
quando aplicamos a função à saída logística. argmax

Pode ajudar a pensar no resultado da função como uma saída normalizada que é


útil para obter previsões significativas de associação de classe em configurações
multiclasse. Portanto, quando construímos um modelo de classificação multiclasse
no PyTorch, podemos usar a função para estimar as probabilidades de cada
associação de classe para um lote de entrada de exemplos. Para ver como
podemos usar a função de ativação no PyTorch, vamos converter para um tensor
no código a seguir, com uma dimensão adicional reservada para o tamanho do
lote:softmaxtorch.softmax()torch.softmax()Z

>>> torch.softmax(torch.from_numpy(Z), dim=0)

tensor([0.4467, 0.1611, 0.3922], dtype=torch.float64)


CopyExplain
Ampliando o espectro de saída usando uma
tangente hiperbólica
Outra função sigmoidal que é frequentemente usada nas camadas ocultas de NNs
artificiais é a tangente hiperbólica (comumente conhecida como tanh), que pode
ser interpretada como uma versão redimensionada da função logística:

A vantagem da tangente hiperbólica sobre a função logística é que ela tem um


espectro de saída mais amplo variando no intervalo aberto (–1, 1), o que pode
melhorar a convergência do algoritmo de retropropagação (Neural Networks for
Pattern Recognition, C. M. Bishop, Oxford University Press, páginas: 500-
501, 1995).

Em contraste, a função logística retorna um sinal de saída variando no intervalo


aberto (0, 1). Para uma comparação simples da função logística e da tangente
hiperbólica, vamos plotar as duas funções sigmoidais:

>>> import matplotlib.pyplot as plt

>>> def tanh(z):

... e_p = np.exp(z)

... e_m = np.exp(-z)

... return (e_p - e_m) / (e_p + e_m)

>>> z = np.arange(-5, 5, 0.005)

>>> log_act = logistic(z)

>>> tanh_act = tanh(z)


>>> plt.ylim([-1.5, 1.5])

>>> plt.xlabel('net input $z$')

>>> plt.ylabel('activation $\phi(z)$')

>>> plt.axhline(1, color='black', linestyle=':')

>>> plt.axhline(0.5, color='black', linestyle=':')

>>> plt.axhline(0, color='black', linestyle=':')

>>> plt.axhline(-0.5, color='black', linestyle=':')

>>> plt.axhline(-1, color='black', linestyle=':')

>>> plt.plot(z, tanh_act,

... linewidth=3, linestyle='--',

... label='tanh')

>>> plt.plot(z, log_act,

... linewidth=3,

... label='logistic')

>>> plt.legend(loc='lower right')

>>> plt.tight_layout()

>>> plt.show()
CopyExplain

Como você pode ver, as formas das duas curvas sigmoidais são muito


semelhantes; no entanto, a função tem o dobro do espaço de saída da
função:tanhlogistic
Figura 12.10: Comparação das funções tanh e logística

Observe que implementamos anteriormente as funções e detalhadamente para


fins de ilustração. Na prática, podemos usar a função do NumPy. logistictanhtanh

Alternativamente, ao construir um modelo NN, podemos usar no PyTorch para


obter os mesmos resultados:torch.tanh(x)

>>> np.tanh(z)

array([-0.9999092 , -0.99990829, -0.99990737, ..., 0.99990644,

0.99990737, 0.99990829])

>>> torch.tanh(torch.from_numpy(z))

tensor([-0.9999, -0.9999, -0.9999, ..., 0.9999, 0.9999, 0.9999],

dtype=torch.float64)
CopyExplain

Além disso, a função logística está disponível no módulo SciPy: special

>>> from scipy.special import expit


>>> expit(z)

array([0.00669285, 0.00672617, 0.00675966, ..., 0.99320669, 0.99324034,

0.99327383])
CopyExplain

Da mesma forma, podemos usar a função no PyTorch para fazer a mesma


computação, da seguinte forma:torch.sigmoid()

>>> torch.sigmoid(torch.from_numpy(z))

tensor([0.0067, 0.0067, 0.0068, ..., 0.9932, 0.9932, 0.9933],

dtype=torch.float64)
CopyExplain

Observe que o uso produz resultados equivalentes ao , que usamos


anteriormente. é uma classe para a qual você pode passar parâmetros para
construir um objeto a fim de controlar o comportamento. Em contrapartida, é uma
função.torch.sigmoid(x)torch.nn.Sigmoid()(x)torch.nn.Sigmoidtorch.sigmoid

Ativação de unidade linear retificada


A unidade linear retificada (ReLU) é outra função de ativação que é
frequentemente usada em NNs profundos. Antes de nos aprofundarmos no ReLU,
devemos dar um passo atrás e entender o problema do gradiente de
desaparecimento de tanh e ativações logísticas.

Para entender esse problema, vamos supor que inicialmente temos a entrada
líquida z1 = 20, que muda para z2 = 25. Computando a ativação tanh,

obtemos   e , que não mostra nenhuma mudança na


saída (devido ao comportamento assintótico da função tanh

e  erros numéricos).

Isso significa que a derivada de ativações em relação à entrada líquida diminui à


medida que z se torna grande. Como resultado, o aprendizado dos pesos durante
a fase de treinamento torna-se muito lento, pois os termos do gradiente podem ser
muito próximos de zero. A ativação do ReLU resolve esse problema.
Matematicamente, ReLU é definido da seguinte forma:

ReLU ainda é uma função não linear que é boa para aprender funções complexas
com NNs. Além disso, a derivada de ReLU, com relação à sua entrada, é sempre
1 para valores de entrada positivos. Portanto, ele resolve o problema de
gradientes de desaparecimento, tornando-o adequado para NNs profundos. No
PyTorch, podemos aplicar a ativação ReLU da seguinte forma: torch.relu()

>>> torch.relu(torch.from_numpy(z))

tensor([0.0000, 0.0000, 0.0000, ..., 4.9850, 4.9900, 4.9950],

dtype=torch.float64)
CopyExplain

Usaremos a função de ativação ReLU no próximo capítulo como uma função de


ativação para NNs convolucionais multicamadas.

Agora que sabemos mais sobre as diferentes funções de ativação que são
comumente usadas em NNs artificiais, vamos concluir esta seção com uma visão
geral das diferentes funções de ativação que encontramos até agora neste livro:
Figura 12.11: As funções de ativação abordadas neste livro

Você pode encontrar a lista de todas as funções de ativação disponíveis no


módulo em https://pytorch.org/docs/stable/nn.functional.html#non-linear-activation-
functions.torch.nn
Indo mais fundo – A mecânica do
PyTorch
No Capítulo 12, Parallelizing Neural Network Training with PyTorch, abordamos
como definir e manipular tensores e trabalhamos com o módulo para construir
pipelines de entrada. Além disso, construímos e treinamos um perceptron
multicamadas para classificar o conjunto de dados Iris usando o módulo de rede
neural PyTorch ().torch.utils.datatorch.nn

Agora que temos alguma experiência prática com treinamento de rede neural
PyTorch e aprendizado de máquina, é hora de dar um mergulho mais profundo na
biblioteca PyTorch e explorar seu rico conjunto de recursos, o que nos permitirá
implementar modelos de aprendizado profundo mais avançados nos próximos
capítulos.

Neste capítulo, usaremos diferentes aspectos da API do PyTorch para


implementar NNs. Em particular, usaremos novamente o módulo, que fornece
várias camadas de abstração para tornar a implementação de arquiteturas padrão
muito conveniente. Ele também nos permite implementar camadas NN
personalizadas, o que é muito útil em projetos orientados à pesquisa que exigem
mais personalização. Mais adiante neste capítulo, implementaremos essa camada
personalizada.torch.nn

Para ilustrar as diferentes formas de construção de modelos usando o módulo,


também consideraremos o clássico problema exclusivo ou (XOR). Em primeiro
lugar, construiremos perceptrons multicamadas usando a classe. Em seguida,
consideraremos outros métodos, como a subclassificação para definir camadas
personalizadas. Finalmente, trabalharemos em dois projetos do mundo real que
cobrem as etapas de aprendizado de máquina desde a entrada bruta até a
previsão.torch.nnSequentialnn.Module

Os tópicos que abordaremos são os seguintes:

 Entendendo e trabalhando com gráficos de computação PyTorch

 Trabalhando com objetos tensores PyTorch


 Resolvendo o problema clássico do XOR e entendendo a capacidade do
modelo

 Construindo modelos NN complexos usando a classe de PyTorch e a


classeSequentialnn.Module
 Computando gradientes usando diferenciação automática e torch.autograd

As principais características do PyTorch


No capítulo anterior, vimos que o PyTorch nos fornece uma interface de
programação escalável e multiplataforma para implementar e executar algoritmos
de aprendizado de máquina. Após seu lançamento inicial em 2016 e seu
lançamento 1.0 em 2018, o PyTorch evoluiu para um dos dois frameworks mais
populares para aprendizado profundo. Ele usa gráficos computacionais dinâmicos,
que têm a vantagem de serem mais flexíveis em comparação com seus
homólogos estáticos. Gráficos computacionais dinâmicos são amigáveis para
depuração: o PyTorch permite intercalar as etapas de declaração de gráfico e
avaliação de gráficos. Você pode executar o código linha por linha enquanto tem
acesso total a todas as variáveis. Esta é uma característica muito importante que
torna o desenvolvimento e treinamento de NNs muito conveniente.

Embora o PyTorch seja uma biblioteca de código aberto e possa ser usado
gratuitamente por todos, seu desenvolvimento é financiado e apoiado pelo
Facebook. Isso envolve uma grande equipe de engenheiros de software que
expandem e melhoram a biblioteca continuamente. Como o PyTorch é uma
biblioteca de código aberto, ele também tem forte apoio de outros
desenvolvedores fora do Facebook, que contribuem avidamente e fornecem
feedback aos usuários. Isso tornou a biblioteca PyTorch mais útil para
pesquisadores acadêmicos e desenvolvedores. Uma outra consequência desses
fatores é que o PyTorch tem extensa documentação e tutoriais para ajudar novos
usuários.

Outra característica importante do PyTorch, que também foi notada no capítulo


anterior, é sua capacidade de trabalhar com unidades de processamento
gráfico (GPUs) únicas ou múltiplas. Isso permite que os usuários treinem
modelos de aprendizado profundo de forma muito eficiente em grandes conjuntos
de dados e sistemas de grande escala.

Por último, mas não menos importante, o PyTorch suporta implantação móvel, o
que também o torna uma ferramenta muito adequada para produção.

Na próxima seção, veremos como um tensor e uma função no PyTorch estão


interconectados através de um gráfico de computação.

Gráficos computacionais do PyTorch


O PyTorch realiza seus cálculos com base em um gráfico acíclico direcionado
(DAG). Nesta seção, veremos como esses gráficos podem ser definidos para um
cálculo aritmético simples. Em seguida, veremos o paradigma do grafo dinâmico,
bem como como o gráfico é criado em tempo real no PyTorch.

Entendendo gráficos computacionais


O PyTorch depende da construção de um gráfico de computação em seu núcleo,
e ele usa esse gráfico de computação para derivar relações entre tensores da
entrada até a saída. Digamos que temos tensores de classificação 0 (escalares) a,
b e c e queremos avaliar z = 2 × (a – b) + c.

Essa avaliação pode ser representada como um gráfico computacional, como


mostra a Figura 13.1:
Figura 13.1: Como funciona um gráfico de computação

Como você pode ver, o gráfico de computação é simplesmente uma rede de nós.
Cada nó se assemelha a uma operação, que aplica uma função ao seu tensor ou
tensores de entrada e retorna zero ou mais tensores como a saída. O PyTorch
constrói esse gráfico de computação e o usa para calcular os gradientes de
acordo. Na próxima subseção, veremos alguns exemplos de criação de um gráfico
para essa computação usando o PyTorch.

Criando um gráfico no PyTorch


Vejamos um exemplo simples que ilustra como criar um gráfico no PyTorch para
avaliar z = 2 × (a – b) + c, como mostrado na figura anterior. As
variáveis a, b e c são escalares (números únicos), e as definimos como tensores
de PyTorch. Para criar o gráfico, podemos simplesmente definir uma função
Python regular com , e como seus argumentos de entrada, por exemplo: abc
>>> import torch

>>> def compute_z(a, b, c):

... r1 = torch.sub(a, b)

... r2 = torch.mul(r1, 2)

... z = torch.add(r2, c)

... return z
CopyExplain

Agora, para realizar a computação, podemos simplesmente chamar essa função


com objetos tensores como argumentos de função. Observe que funções do
PyTorch como , (ou ), e (ou ) também nos permitem fornecer entradas de
classificações mais altas na forma de um objeto tensor PyTorch. No exemplo de
código a seguir, fornecemos entradas escalares (rank 0), bem como entradas rank
1 e rank 2, como listas:addsubsubtractmulmultiply

>>> print('Scalar Inputs:', compute_z(torch.tensor(1),

... torch.tensor(2), torch.tensor(3)))

Scalar Inputs: tensor(1)

>>> print('Rank 1 Inputs:', compute_z(torch.tensor([1]),

... torch.tensor([2]), torch.tensor([3])))

Rank 1 Inputs: tensor([1])

>>> print('Rank 2 Inputs:', compute_z(torch.tensor([[1]]),

... torch.tensor([[2]]), torch.tensor([[3]])))

Rank 2 Inputs: tensor([[1]])


CopyExplain

Nesta seção, você viu como é simples criar um gráfico de computação no


PyTorch. Em seguida, veremos os tensores PyTorch que podem ser usados para
armazenar e atualizar parâmetros de modelo.
Objetos tensores PyTorch para
armazenar e atualizar parâmetros de
modelo
Nós cobrimos objetos tensores no Capítulo 12, Paralelizando o treinamento de
redes neurais com o PyTorch. No PyTorch, um objeto tensor especial para o qual
gradientes precisam ser computados nos permite armazenar e atualizar os
parâmetros de nossos modelos durante o treinamento. Esse tensor pode
ser criado apenas atribuindo a valores iniciais especificados pelo usuário. Note
que a partir de agora (meados de 2021), apenas tensores de ponto flutuante e
complexo podem exigir gradientes. No código a seguir, vamos gerar objetos
tensores do tipo :requires_gradTruedtypefloat32

>>> a = torch.tensor(3.14, requires_grad=True)

>>> print(a)

tensor(3.1400, requires_grad=True)

>>> b = torch.tensor([1.0, 2.0, 3.0], requires_grad=True)

>>> print(b)

tensor([1., 2., 3.], requires_grad=True)


CopyExplain

Observe que está definido como por padrão. Esse valor pode ser definido
eficientemente para executando o .requires_gradFalseTruerequires_grad_()

method_() éum método in-loco no PyTorch que é usado para operações sem fazer
uma cópia da entrada.

Vejamos o exemplo a seguir:

>>> w = torch.tensor([1.0, 2.0, 3.0])

>>> print(w.requires_grad)
False

>>> w.requires_grad_()

>>> print(w.requires_grad)

True
CopyExplain

Você se lembrará de que, para modelos NN, inicializar parâmetros de modelo com
pesos aleatórios é necessário para quebrar a simetria durante a retropropagação
— caso contrário, um NN de várias camadas não seria mais útil do que um NN de
camada única, como a regressão logística. Ao criar um tensor PyTorch, também
podemos usar um esquema de inicialização aleatória. PyTorch pode gerar
números aleatórios com base em uma variedade de distribuições de probabilidade
(veja https://pytorch.org/docs/stable/torch.html#random-sampling). No exemplo a
seguir, examinaremos alguns métodos de inicialização padrão que também estão
disponíveis no módulo
(consulte https://pytorch.org/docs/stable/nn.init.html).torch.nn.init

Então, vamos ver como podemos criar um tensor com a inicialização Glorot, que é
um esquema clássico de inicialização aleatória que foi proposto por Xavier Glorot
e Yoshua Bengio. Para isso, primeiro criamos um tensor vazio e um operador
chamado como um objeto de classe . Em seguida, preenchemos esse tensor com
valores de acordo com a inicialização de Glorot chamando o método. No exemplo
a seguir, inicializamos um tensor da forma 2×3:initGlorotNormalxavier_normal_()

>>> import torch.nn as nn

>>> torch.manual_seed(1)

>>> w = torch.empty(2, 3)

>>> nn.init.xavier_normal_(w)

>>> print(w)

tensor([[ 0.4183, 0.1688, 0.0390],

[ 0.3930, -0.2858, -0.1051]])


CopyExplain
Inicialização do Xavier (ou Glorot)

No desenvolvimento inicial do deep learning, observou-se que a inicialização


aleatória uniforme ou aleatória de peso normal poderia frequentemente resultar
em baixo desempenho do modelo durante o treinamento.

Em 2010, Glorot e Bengio investigaram o efeito da inicialização e propuseram um


novo esquema de inicialização mais robusto para facilitar o treinamento de redes
profundas. A ideia geral por trás da inicialização do Xavier é equilibrar
aproximadamente a variância dos gradientes em diferentes camadas. Caso
contrário, algumas camadas podem receber muita atenção durante o treinamento,
enquanto as outras camadas ficam para trás.

De acordo com o artigo de pesquisa de Glorot e Bengio, se quisermos inicializar


os pesos em uma distribuição uniforme, devemos escolher o intervalo dessa
distribuição uniforme da seguinte forma:

Aqui, nem é o número de neurônios de entrada que são multiplicados pelos pesos,


e nfora é o número de neurônios de saída que alimentam a próxima camada. Para
inicializar os pesos da distribuição Gaussiana (normal), recomendamos que você
escolha o desvio padrão deste Gaussiano para ser:

O PyTorch suporta a inicialização Xavier em distribuições uniformes e normais de


pesos.
Para obter mais informações sobre o esquema de inicialização de Glorot e Bengio,
incluindo o raciocínio e a motivação matemática, recomendamos o artigo original
(Understanding the difficulty of deep feedforward neural networks, Xavier
Glorot and Yoshua Bengio, 2010), que está disponível gratuitamente
em http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf.

Agora, para colocar isso no contexto de um caso de uso mais prático, vamos ver
como podemos definir dois objetos dentro da classe base: Tensornn.Module

>>> class MyModule(nn.Module):

... def __init__(self):

... super().__init__()

... self.w1 = torch.empty(2, 3, requires_grad=True)

... nn.init.xavier_normal_(self.w1)

... self.w2 = torch.empty(1, 2, requires_grad=True)

... nn.init.xavier_normal_(self.w2)
CopyExplain

Esses dois tensores podem então ser usados como pesos cujos gradientes serão
calculados via diferenciação automática.

Gradientes computacionais via


diferenciação automática
Como você já sabe, otimizar NNs requer calcular os gradientes da perda em
relação aos pesos NN. Isso é necessário para algoritmos de otimização, como
a descida do gradiente estocástico (SGD). Além disso, os gradientes têm outras
aplicações, como diagnosticar a rede para descobrir por que um modelo NN está
fazendo uma previsão específica para um exemplo de teste. Portanto, nesta
seção, abordaremos como calcular gradientes de uma computação com relação
às suas variáveis de entrada.
Calculando os gradientes da perda em relação às
variáveis treináveis
O PyTorch suporta diferenciação automática, que pode ser pensada como uma
implementação da regra de cadeia para calcular gradientes de funções aninhadas.
Note que, por uma questão de simplicidade, usaremos o termo gradiente para nos
referirmos tanto a derivadas parciais quanto a gradientes.

Derivadas parciais e gradientes

Uma derivada parcial pode ser entendida como a taxa de mudança de uma função

multivariada   — uma função com múltiplas entradas, f(x1, x2, ...), com


relação a uma de suas entradas (aqui: x1). O gradiente, , de uma função é um

vetor composto de todas as derivadas parciais das entradas, 

Quando definimos uma série de operações que resultam em alguma saída ou


mesmo tensores intermediários, o PyTorch fornece um contexto para calcular
gradientes desses tensores computados em relação aos seus nós dependentes no
gráfico de computação. Para calcular esses gradientes, podemos chamar o
método do módulo. Ele calcula a soma dos gradientes do tensor dado em relação
aos nós da folha (nós terminais) no gráfico.backwardtorch.autograd

Vamos trabalhar com um exemplo simples onde vamos calcular z = wx + b e


definir a perda como a perda quadrática entre o alvo y e a previsão z, Perda =
(y – z)2. No caso mais geral, onde podemos ter várias previsões e metas,
calculamos a perda como a soma do erro

quadrado,  . Para implementar este


cálculo no PyTorch, definiremos os parâmetros do modelo, w e b, como variáveis
(tensores com o atributo definido como ), e a entrada, x e y, como tensores
padrão. Vamos calcular o tensor de perda e usá-lo para calcular os gradientes dos
parâmetros do modelo, w e b, da seguinte forma:requires_gradientTrue

>>> w = torch.tensor(1.0, requires_grad=True)

>>> b = torch.tensor(0.5, requires_grad=True)

>>> x = torch.tensor([1.4])

>>> y = torch.tensor([2.1])

>>> z = torch.add(torch.mul(w, x), b)

>>> loss = (y-z).pow(2).sum()

>>> loss.backward()

>>> print('dL/dw : ', w.grad)

>>> print('dL/db : ', b.grad)

dL/dw : tensor(-0.5600)

dL/db : tensor(-0.4000)
CopyExplain

Computando o valor z é um passe para frente em um NN. Utilizou-se o método no

tensor para cálculo   e  . Como este é um exemplo muito

simples, podemos obter   


simbolicamente para verificar se os gradientes computados correspondem aos
resultados que obtivemos no exemplo de código anterior: backwardloss

>>> # verifying the computed gradient

>>> print(2 * x * ((w * x + b) - y))

tensor([-0.5600], grad_fn=<MulBackward0>)
CopyExplain
Deixamos a verificação de b como um exercício para o leitor.

Entendendo a diferenciação automática


A diferenciação automática representa um conjunto de técnicas computacionais
para calcular gradientes de operações aritméticas arbitrárias. Durante este
processo, gradientes de um cálculo (expressos como uma série de operações)
são obtidos acumulando os gradientes através de aplicações repetidas da regra
de cadeia. Para entender melhor o conceito por trás da diferenciação automática,
vamos considerar uma série de cálculos aninhados, y = f(g(h(x))), com entrada x e
saída y. Isso pode ser dividido em uma série de etapas:

 u0 = x
 u1 = h(x)
 u2 = g(u1)
 u3 = f(u2) = y

A derivada   pode ser calculada de duas maneiras diferentes: acumulação a


termo, que começa com , e acumulação inversa, que começa

com  . Observe que o
PyTorch usa o último, acumulação reversa, que é mais eficiente para implementar
o backpropagation.

Exemplos contraditórios
Gradientes computacionais da perda em relação ao exemplo de entrada são
usados para gerar exemplos adversários (ou ataques adversários). Em visão
computacional, exemplos contraditórios são exemplos que são gerados pela
adição de algum ruído pequeno e imperceptível (ou perturbações) ao exemplo de
entrada, o que resulta em um NN profundo classificando-os erroneamente. Cobrir
exemplos adversários está além do escopo deste livro, mas se você estiver
interessado, você pode encontrar o artigo original de Christian Szegedy et
al., Propriedades intrigantes de redes
neurais em https://arxiv.org/pdf/1312.6199.pdf.

Simplificando implementações de
arquiteturas comuns através do módulo
torch.nn
Você já viu alguns exemplos de criação de um modelo NN feedforward (por
exemplo, um perceptron multicamadas) e definição de uma sequência de
camadas usando a classe. Antes de nos aprofundarmos no , vamos analisar
brevemente outra abordagem para conjurar essas camadas
via .nn.Modulenn.Modulenn.Sequential

Implementação de modelos baseados em nn.


Sequencial
Com
(https://pytorch.org/docs/master/generated/torch.nn.Sequential.html#sequential),
as camadas armazenadas dentro do modelo são conectadas de forma em
cascata. No exemplo a seguir, construiremos um modelo com duas camadas
densamente (totalmente) conectadas:nn.Sequential

>>> model = nn.Sequential(

... nn.Linear(4, 16),

... nn.ReLU(),

... nn.Linear(16, 32),

... nn.ReLU()

... )

>>> model
Sequential(

(0): Linear(in_features=4, out_features=16, bias=True)

(1): ReLU()

(2): Linear(in_features=16, out_features=32, bias=True)

(3): ReLU()

)
CopyExplain

Especificamos as camadas e instanciamos as depois de passar as camadas para


a classe. A saída da primeira camada totalmente conectada é usada como entrada
para a primeira camada ReLU. A saída da primeira camada ReLU torna-se a
entrada para a segunda camada totalmente conectada. Finalmente, a saída da
segunda camada totalmente conectada é usada como entrada para a segunda
camada ReLU.modelnn.Sequential

Podemos configurar ainda mais essas camadas, por exemplo, aplicando


diferentes funções de ativação, inicializadores ou métodos de regularização aos
parâmetros. Uma lista abrangente e completa de opções disponíveis para a
maioria dessas categorias pode ser encontrada na documentação oficial:

 Escolhendo funções de ativação: https://pytorch.org/docs/stable/nn.html#non-


linear-activations-weighted-sum-nonlinearity
 Inicializando os parâmetros da camada
via : https://pytorch.org/docs/stable/nn.init.htmlnn.init
 Aplicação da regularização L2 aos parâmetros da camada (para evitar
overfitting) através do parâmetro de alguns otimizadores
em : https://pytorch.org/docs/stable/optim.htmlweight_decaytorch.optim
 Aplicando a regularização L1 aos parâmetros da camada (para evitar
overfitting) adicionando o termo de penalidade L1 ao tensor de perda, que
implementaremos a seguir

No exemplo de código a seguir, configuraremos a primeira camada totalmente


conectada especificando a distribuição de valor inicial para o peso. Em seguida,
configuraremos a segunda camada totalmente conectada calculando o termo de
penalidade L1 para a matriz de peso:

>>> nn.init.xavier_uniform_(model[0].weight)

>>> l1_weight = 0.01

>>> l1_penalty = l1_weight * model[2].weight.abs().sum()


CopyExplain

Aqui, inicializamos o peso da primeira camada linear com a inicialização do Xavier.


E calculamos a norma L1 do peso da segunda camada linear.

Além disso, também podemos especificar o tipo de otimizador e a função de perda


para treinamento. Novamente, uma lista abrangente de todas as opções
disponíveis pode ser encontrada na documentação oficial:

 Otimizadores
via : https://pytorch.org/docs/stable/optim.html#algorithms torch.optim
 Funções de perda: https://pytorch.org/docs/stable/nn.html#loss-functions

Escolhendo uma função de perda


Em relação às escolhas para algoritmos de otimização, SGD e Adam são os
métodos mais utilizados. A escolha da função de perda depende da tarefa; Por
exemplo, você pode usar a perda de erro quadrático médio para um problema de
regressão.

A família de funções de perda de entropia cruzada fornece as escolhas possíveis


para tarefas de classificação, que são extensivamente discutidas no Capítulo
14, Classificando imagens com redes neurais convolucionais profundas.

Além disso, você pode usar as técnicas aprendidas nos capítulos anteriores (como
técnicas para avaliação de modelos do Capítulo 6, Aprendendo práticas
recomendadas para avaliação de modelos e Ajuste de hiperparâmetros)
combinadas com as métricas apropriadas para o problema. Por exemplo, precisão
e recordação, acurácia, área sob a curva (AUC) e escores falsos negativos e
falsos positivos são métricas apropriadas para avaliar modelos de classificação.
Neste exemplo, usaremos o otimizador SGD e a perda de entropia cruzada para
classificação binária:

>>> loss_fn = nn.BCELoss()

>>> optimizer = torch.optim.SGD(model.parameters(), lr=0.001)


CopyExplain

A seguir, veremos um exemplo mais prático: resolver o problema clássico de


classificação XOR. Primeiro, usaremos a classe para construir o modelo. Ao longo
do caminho, você também aprenderá sobre a capacidade de um modelo para lidar
com limites de decisão não lineares. Em seguida, abordaremos a construção de
um modelo via que nos dará mais flexibilidade e controle sobre as camadas da
rede.nn.Sequential()nn.Module

Resolvendo um problema de classificação XOR


O problema de classificação XOR é um problema clássico para analisar a
capacidade de um modelo no que diz respeito à captura do limite de decisão não
linear entre duas classes. Geramos um conjunto de dados de brinquedos de 200
exemplos de treinamento com dois recursos (x0, x1) extraído de uma distribuição
uniforme entre [–1, 1). Em seguida, atribuímos o rótulo de verdade básica para o
exemplo de treinamento i de acordo com a seguinte regra:

Utilizaremos metade dos dados (100 exemplos de treinamento) para treinamento e


a outra metade para validação. O código para gerar os dados e dividi-los nos
conjuntos de dados de treinamento e validação é o seguinte:

>>> import matplotlib.pyplot as plt

>>> import numpy as np

>>> torch.manual_seed(1)
>>> np.random.seed(1)

>>> x = np.random.uniform(low=-1, high=1, size=(200, 2))

>>> y = np.ones(len(x))

>>> y[x[:, 0] * x[:, 1]<0] = 0

>>> n_train = 100

>>> x_train = torch.tensor(x[:n_train, :], dtype=torch.float32)

>>> y_train = torch.tensor(y[:n_train], dtype=torch.float32)

>>> x_valid = torch.tensor(x[n_train:, :], dtype=torch.float32)

>>> y_valid = torch.tensor(y[n_train:], dtype=torch.float32)

>>> fig = plt.figure(figsize=(6, 6))

>>> plt.plot(x[y==0, 0], x[y==0, 1], 'o', alpha=0.75, markersize=10)

>>> plt.plot(x[y==1, 0], x[y==1, 1], '<', alpha=0.75, markersize=10)

>>> plt.xlabel(r'$x_1$', size=15)

>>> plt.ylabel(r'$x_2$', size=15)

>>> plt.show()
CopyExplain

O código resulta no seguinte gráfico de dispersão dos exemplos de treinamento e


validação, mostrados com marcadores diferentes com base em seu rótulo de
classe:
Figura 13.2: Gráfico de dispersão dos exemplos de formação e validação

Na subseção anterior, abordamos as ferramentas essenciais que precisamos para


implementar um classificador no PyTorch. Agora precisamos decidir qual
arquitetura devemos escolher para essa tarefa e conjunto de dados. Como regra
geral, quanto mais camadas tivermos, e quanto mais neurônios tivermos em cada
camada, maior será a capacidade do modelo. Aqui, a capacidade do modelo pode
ser pensada como uma medida de quão prontamente o modelo pode se aproximar
de funções complexas. Embora ter mais parâmetros signifique que a rede pode se
ajustar a funções mais complexas, modelos maiores geralmente são mais difíceis
de treinar (e propensos a sobreajustes). Na prática, é sempre uma boa ideia
começar com um modelo simples como linha de base, por exemplo, um NN de
camada única como regressão logística:
>>> model = nn.Sequential(

... nn.Linear(2, 1),

... nn.Sigmoid()

... )

>>> model

Sequential(

(0): Linear(in_features=2, out_features=1, bias=True)

(1): Sigmoid()

)
CopyExplain

Depois de definir o modelo, inicializaremos a função de perda de entropia cruzada


para classificação binária e o otimizador SGD:

>>> loss_fn = nn.BCELoss()

>>> optimizer = torch.optim.SGD(model.parameters(), lr=0.001)


CopyExplain

Em seguida, criaremos um carregador de dados que usa um tamanho de lote de 2


para os dados do trem:

>>> from torch.utils.data import DataLoader, TensorDataset

>>> train_ds = TensorDataset(x_train, y_train)

>>> batch_size = 2

>>> torch.manual_seed(1)

>>> train_dl = DataLoader(train_ds, batch_size, shuffle=True)


CopyExplain

Agora vamos treinar o modelo para 200 épocas e registrar uma história de épocas
de treinamento:

>>> torch.manual_seed(1)
>>> num_epochs = 200

>>> def train(model, num_epochs, train_dl, x_valid, y_valid):

... loss_hist_train = [0] * num_epochs

... accuracy_hist_train = [0] * num_epochs

... loss_hist_valid = [0] * num_epochs

... accuracy_hist_valid = [0] * num_epochs

... for epoch in range(num_epochs):

... for x_batch, y_batch in train_dl:

... pred = model(x_batch)[:, 0]

... loss = loss_fn(pred, y_batch)

... loss.backward()

... optimizer.step()

... optimizer.zero_grad()

... loss_hist_train[epoch] += loss.item()

... is_correct = ((pred>=0.5).float() == y_batch).float()

... accuracy_hist_train[epoch] += is_correct.mean()

... loss_hist_train[epoch] /= n_train/batch_size

... accuracy_hist_train[epoch] /= n_train/batch_size

... pred = model(x_valid)[:, 0]

... loss = loss_fn(pred, y_valid)

... loss_hist_valid[epoch] = loss.item()

... is_correct = ((pred>=0.5).float() == y_valid).float()

... accuracy_hist_valid[epoch] += is_correct.mean()

... return loss_hist_train, loss_hist_valid, \

... accuracy_hist_train, accuracy_hist_valid

>>> history = train(model, num_epochs, train_dl, x_valid, y_valid)


CopyExplain
Notice that the history of training epochs includes the train loss and validation loss
and the train accuracy and validation accuracy, which is useful for visual inspection
after training. In the following code, we will plot the learning curves, including the
training and validation loss, as well as their accuracies.

The following code will plot the training performance:

>>> fig = plt.figure(figsize=(16, 4))

>>> ax = fig.add_subplot(1, 2, 1)

>>> plt.plot(history[0], lw=4)

>>> plt.plot(history[1], lw=4)

>>> plt.legend(['Train loss', 'Validation loss'], fontsize=15)

>>> ax.set_xlabel('Epochs', size=15)

>>> ax = fig.add_subplot(1, 2, 2)

>>> plt.plot(history[2], lw=4)

>>> plt.plot(history[3], lw=4)

>>> plt.legend(['Train acc.', 'Validation acc.'], fontsize=15)

>>> ax.set_xlabel('Epochs', size=15)


CopyExplain

This results in the following figure, with two separate panels for the losses and
accuracies:

Figure 13.3: Loss and accuracy results


As you can see, a simple model with no hidden layer can only derive a linear
decision boundary, which is unable to solve the XOR problem. As a consequence,
we can observe that the loss terms for both the training and the validation datasets
are very high, and the classification accuracy is very low.

To derive a nonlinear decision boundary, we can add one or more hidden layers
connected via nonlinear activation functions. The universal approximation theorem
states that a feedforward NN with a single hidden layer and a relatively large
number of hidden units can approximate arbitrary continuous functions relatively
well. Thus, one approach for tackling the XOR problem more satisfactorily is to add
a hidden layer and compare different numbers of hidden units until we observe
satisfactory results on the validation dataset. Adding more hidden units would
correspond to increasing the width of a layer.

Alternatively, we can also add more hidden layers, which will make the model
deeper. The advantage of making a network deeper rather than wider is that fewer
parameters are required to achieve a comparable model capacity.

However, a downside of deep (versus wide) models is that deep models are prone
to vanishing and exploding gradients, which make them harder to train.

As an exercise, try adding one, two, three, and four hidden layers, each with four
hidden units. In the following example, we will take a look at the results of a
feedforward NN with two hidden layers:

>>> model = nn.Sequential(

... nn.Linear(2, 4),

... nn.ReLU(),

... nn.Linear(4, 4),

... nn.ReLU(),

... nn.Linear(4, 1),

... nn.Sigmoid()

... )

>>> loss_fn = nn.BCELoss()


>>> optimizer = torch.optim.SGD(model.parameters(), lr=0.015)

>>> model

Sequential(

(0): Linear(in_features=2, out_features=4, bias=True)

(1): ReLU()

(2): Linear(in_features=4, out_features=4, bias=True)

(3): ReLU()

(4): Linear(in_features=4, out_features=1, bias=True)

(5): Sigmoid()

>>> history = train(model, num_epochs, train_dl, x_valid, y_valid)


CopyExplain

We can repeat the previous code for visualization, which produces the following:

Figure 13.4: Loss and accuracy results after adding two hidden layers

Now, we can see that the model is able to derive a nonlinear decision boundary for
this data, and the model reaches 100 percent accuracy on the training dataset. The
validation dataset’s accuracy is 95 percent, which indicates that the model is
slightly overfitting.

Making model building more flexible with


nn.Module
In the previous example, we used the PyTorch class to create a fully connected NN
with multiple layers. This is a very common and convenient way of building models.
However, it unfortunately doesn’t allow us to create more complex models that
have multiple input, output, or intermediate branches. That’s where comes in
handy.Sequentialnn.Module

The alternative way to build complex models is by subclassing . In this approach,


we create a new class derived from and define the method, , as a constructor.
The method is used to specify the forward pass. In the constructor function, , we
define the layers as attributes of the class so that they can be accessed via
the reference attribute. Then, in the method, we specify how these layers are to be
used in the forward pass of the NN. The code for defining a new class that
implements the previous model is as
follows:nn.Modulenn.Module__init__()forward()__init__()selfforward()

>>> class MyModule(nn.Module):

... def __init__(self):

... super().__init__()

... l1 = nn.Linear(2, 4)

... a1 = nn.ReLU()

... l2 = nn.Linear(4, 4)

... a2 = nn.ReLU()

... l3 = nn.Linear(4, 1)

... a3 = nn.Sigmoid()

... l = [l1, a1, l2, a2, l3, a3]

... self.module_list = nn.ModuleList(l)

...

... def forward(self, x):

... for f in self.module_list:

... x = f(x)

... return x
CopyExplain

Notice that we put all layers in the object, which is just a object composed of items.
This makes the code more readable and easier to
follow.nn.ModuleListlistnn.Module

Once we define an instance of this new class, we can train it as we did previously:

>>> model = MyModule()

>>> model

MyModule(

(module_list): ModuleList(

(0): Linear(in_features=2, out_features=4, bias=True)

(1): ReLU()

(2): Linear(in_features=4, out_features=4, bias=True)

(3): ReLU()

(4): Linear(in_features=4, out_features=1, bias=True)

(5): Sigmoid()

>>> loss_fn = nn.BCELoss()

>>> optimizer = torch.optim.SGD(model.parameters(), lr=0.015)

>>> history = train(model, num_epochs, train_dl, x_valid, y_valid)


CopyExplain

Next, besides the train history, we will use the mlxtend library to visualize the
validation data and the decision boundary.

Mlxtend can be installed via or as follows:condapip

conda install mlxtend -c conda-forge

pip install mlxtend


CopyExplain

To compute the decision boundary of our model, we need to add a method in


the class:predict()MyModule

>>> def predict(self, x):

... x = torch.tensor(x, dtype=torch.float32)

... pred = self.forward(x)[:, 0]

... return (pred>=0.5).float()


CopyExplain

It will return the predicted class (0 or 1) for a sample.

The following code will plot the training performance along with the decision region
bias:

>>> from mlxtend.plotting import plot_decision_regions

>>> fig = plt.figure(figsize=(16, 4))

>>> ax = fig.add_subplot(1, 3, 1)

>>> plt.plot(history[0], lw=4)

>>> plt.plot(history[1], lw=4)

>>> plt.legend(['Train loss', 'Validation loss'], fontsize=15)

>>> ax.set_xlabel('Epochs', size=15)

>>> ax = fig.add_subplot(1, 3, 2)

>>> plt.plot(history[2], lw=4)

>>> plt.plot(history[3], lw=4)

>>> plt.legend(['Train acc.', 'Validation acc.'], fontsize=15)

>>> ax.set_xlabel('Epochs', size=15)

>>> ax = fig.add_subplot(1, 3, 3)

>>> plot_decision_regions(X=x_valid.numpy(),

... y=y_valid.numpy().astype(np.integer),
... clf=model)

>>> ax.set_xlabel(r'$x_1$', size=15)

>>> ax.xaxis.set_label_coords(1, -0.025)

>>> ax.set_ylabel(r'$x_2$', size=15)

>>> ax.yaxis.set_label_coords(-0.025, 1)

>>> plt.show()
CopyExplain

This results in Figure 13.5, with three separate panels for the losses, accuracies,
and the scatterplot of the validation examples, along with the decision boundary:

Figure 13.5: Results, including a scatterplot

Writing custom layers in PyTorch


In cases where we want to define a new layer that is not already supported by
PyTorch, we can define a new class derived from the class. This is especially
useful when designing a new layer or customizing an existing layer. nn.Module

To illustrate the concept of implementing custom layers, let’s consider a simple


example. Imagine we want to define a new linear layer that

computes  , where   refers to a random variable


as a noise variable. To implement this computation, we define a new class as a
subclass of . For this new class, we have to define both the constructor method
and the method. In the constructor, we define the variables and other required
tensors for our customized layer. We can create variables and initialize them in the
constructor if the is given to the constructor. Alternatively, we can delay the
variable initialization (for instance, if we do not know the exact input shape upfront)
and delegate it to another method for late variable
creation.nn.Module__init__()forward()input_size

To look at a concrete example, we are going to define a new layer called ,

which implements the computation  , which was


mentioned in the preceding paragraph: NoisyLinear

>>> class NoisyLinear(nn.Module):

... def __init__(self, input_size, output_size,

... noise_stddev=0.1):

... super().__init__()

... w = torch.Tensor(input_size, output_size)

... self.w = nn.Parameter(w) # nn.Parameter is a Tensor

... # that's a module parameter.

... nn.init.xavier_uniform_(self.w)

... b = torch.Tensor(output_size).fill_(0)

... self.b = nn.Parameter(b)

... self.noise_stddev = noise_stddev

...

... def forward(self, x, training=False):

... if training:

... noise = torch.normal(0.0, self.noise_stddev, x.shape)

... x_new = torch.add(x, noise)

... else:

... x_new = x

... return torch.add(torch.mm(x_new, self.w), self.b)


CopyExplain

In the constructor, we have added an argument, , to specify the standard

deviation for the distribution of  , which is sampled from a Gaussian distribution.


Furthermore, notice that in the method, we have used an additional argument, . We
use it to distinguish whether the layer is used during training or only for prediction
(this is sometimes also called inference) or evaluation. Also, there are certain
methods that behave differently in training and prediction modes. You will
encounter an example of such a method, , in the upcoming chapters. In the

previous code snippet, we also specified that the random vector,  , was to be
generated and added to the input during training only and not used for inference or
evaluation.noise_stddevforward()training=FalseDropout

Before we go a step further and use our custom layer in a model, let’s test it in the
context of a simple example.NoisyLinear

1. In the following code, we will define a new instance of this layer, and execute it
on an input tensor. Then, we will call the layer three times on the same input
tensor:
2. >>> torch.manual_seed(1)

3. >>> noisy_layer = NoisyLinear(4, 2)

4. >>> x = torch.zeros((1, 4))

5. >>> print(noisy_layer(x, training=True))

6. tensor([[ 0.1154, -0.0598]], grad_fn=<AddBackward0>)

7. >>> print(noisy_layer(x, training=True))

8. tensor([[ 0.0432, -0.0375]], grad_fn=<AddBackward0>)

9. >>> print(noisy_layer(x, training=False))

10. tensor([[0., 0.]], grad_fn=<AddBackward0>)


CopyExplain
Note that the outputs for the first two calls differ because the layer added
random noise to the input tensor. The third call outputs [0, 0] as we didn’t add
noise by specifying .NoisyLineartraining=False

11. Now, let’s create a new model similar to the previous one for solving the XOR
classification task. As before, we will use the class for model building, but this
time, we will use our layer as the first hidden layer of the multilayer perceptron.
The code is as follows: nn.ModuleNoisyLinear
12. >>> class MyNoisyModule(nn.Module):

13. ... def __init__(self):

14. ... super().__init__()

15. ... self.l1 = NoisyLinear(2, 4, 0.07)

16. ... self.a1 = nn.ReLU()

17. ... self.l2 = nn.Linear(4, 4)

18. ... self.a2 = nn.ReLU()

19. ... self.l3 = nn.Linear(4, 1)

20. ... self.a3 = nn.Sigmoid()

21. ...

22. ... def forward(self, x, training=False):

23. ... x = self.l1(x, training)

24. ... x = self.a1(x)

25. ... x = self.l2(x)

26. ... x = self.a2(x)

27. ... x = self.l3(x)

28. ... x = self.a3(x)

29. ... return x

30. ...

31. ... def predict(self, x):

32. ... x = torch.tensor(x, dtype=torch.float32)


33. ... pred = self.forward(x)[:, 0]

34. ... return (pred>=0.5).float()

35. ...

36. >>> torch.manual_seed(1)

37. >>> model = MyNoisyModule()

38. >>> model

39. MyNoisyModule(

40. (l1): NoisyLinear()

41. (a1): ReLU()

42. (l2): Linear(in_features=4, out_features=4, bias=True)

43. (a2): ReLU()

44. (l3): Linear(in_features=4, out_features=1, bias=True)

45. (a3): Sigmoid()

46. )
CopyExplain
47. Similarly, we will train the model as we did previously. At this time, to compute
the prediction on the training batch, we use instead of : pred = model(x_batch,
True)[:, 0]pred = model(x_batch)[:, 0]
48. >>> loss_fn = nn.BCELoss()

49. >>> optimizer = torch.optim.SGD(model.parameters(), lr=0.015)

50. >>> torch.manual_seed(1)

51. >>> loss_hist_train = [0] * num_epochs

52. >>> accuracy_hist_train = [0] * num_epochs

53. >>> loss_hist_valid = [0] * num_epochs

54. >>> accuracy_hist_valid = [0] * num_epochs

55. >>> for epoch in range(num_epochs):

56. ... for x_batch, y_batch in train_dl:

57. ... pred = model(x_batch, True)[:, 0]


58. ... loss = loss_fn(pred, y_batch)

59. ... loss.backward()

60. ... optimizer.step()

61. ... optimizer.zero_grad()

62. ... loss_hist_train[epoch] += loss.item()

63. ... is_correct = (

64. ... (pred>=0.5).float() == y_batch

65. ... ).float()

66. ... accuracy_hist_train[epoch] += is_correct.mean()

67. ... loss_hist_train[epoch] /= n_train/batch_size

68. ... accuracy_hist_train[epoch] /= n_train/batch_size

69. ... pred = model(x_valid)[:, 0]

70. ... loss = loss_fn(pred, y_valid)

71. ... loss_hist_valid[epoch] = loss.item()

72. ... is_correct = ((pred>=0.5).float() == y_valid).float()

73. ... accuracy_hist_valid[epoch] += is_correct.mean()


CopyExplain
74. After the model is trained, we can plot the losses, accuracies, and the decision
boundary:
75. >>> fig = plt.figure(figsize=(16, 4))

76. >>> ax = fig.add_subplot(1, 3, 1)

77. >>> plt.plot(loss_hist_train, lw=4)

78. >>> plt.plot(loss_hist_valid, lw=4)

79. >>> plt.legend(['Train loss', 'Validation loss'], fontsize=15)

80. >>> ax.set_xlabel('Epochs', size=15)

81. >>> ax = fig.add_subplot(1, 3, 2)

82. >>> plt.plot(accuracy_hist_train, lw=4)


83. >>> plt.plot(accuracy_hist_valid, lw=4)

84. >>> plt.legend(['Train acc.', 'Validation acc.'], fontsize=15)

85. >>> ax.set_xlabel('Epochs', size=15)

86. >>> ax = fig.add_subplot(1, 3, 3)

87. >>> plot_decision_regions(

88. ... X=x_valid.numpy(),

89. ... y=y_valid.numpy().astype(np.integer),

90. ... clf=model

91. ... )

92. >>> ax.set_xlabel(r'$x_1$', size=15)

93. >>> ax.xaxis.set_label_coords(1, -0.025)

94. >>> ax.set_ylabel(r'$x_2$', size=15)

95. >>> ax.yaxis.set_label_coords(-0.025, 1)

96. >>> plt.show()


CopyExplain
97. The resulting figure will be as follows:

Figure 13.6: Results using NoisyLinear as the first hidden layer

Aqui, nosso objetivo era aprender a definir uma nova camada personalizada


subclassificada de e usá-la como usaríamos qualquer outra camada padrão.
Embora, com este exemplo em particular, não tenha ajudado a melhorar o
desempenho, por favor, tenha em mente que nosso objetivo era principalmente
aprender a escrever uma camada personalizada do zero. Em geral, escrever uma
nova camada personalizada pode ser útil em outras aplicações, por exemplo, se
você desenvolver um novo algoritmo que dependa de uma nova camada além das
existentes.nn.Moduletorch.nnNoisyLinear

Projeto um – prevendo a eficiência de


combustível de um carro
Até agora, neste capítulo, nos concentramos principalmente no módulo. Usamos
para construir os modelos para simplificar. Em seguida, flexibilizamos a
construção de modelos e implementamos NNs feedforward, aos quais
adicionamos camadas personalizadas. Nesta seção, trabalharemos em um projeto
do mundo real de prever a eficiência de combustível de um carro em milhas por
galão (MPG). Abordaremos as etapas subjacentes em tarefas de aprendizado de
máquina, como pré-processamento de dados, engenharia de recursos,
treinamento, previsão (inferência) e avaliação. torch.nnnn.Sequentialnn.Module

Trabalhando com colunas de recursos


Em aplicações de aprendizado de máquina e aprendizado profundo, podemos
encontrar vários tipos diferentes de recursos: contínuo, categórico não ordenado
(nominal) e categórico ordenado (ordinal). Você vai se lembrar que no Capítulo
4, Building Good Training Datasets – Data Preprocessing, abordamos diferentes
tipos de recursos e aprendemos a lidar com cada tipo. Observe que, embora os
dados numéricos possam ser contínuos ou discretos, no contexto do aprendizado
de máquina com o PyTorch, os dados "numéricos" referem-se especificamente a
dados contínuos do tipo ponto flutuante.

Às vezes, os conjuntos de recursos são compostos por uma mistura de diferentes


tipos de recursos. Por exemplo, considere um cenário com um conjunto de sete
recursos diferentes, conforme mostrado na Figura 13.7:
Figura 13.7: Estrutura de dados MPG automática

As características mostradas na figura (ano do modelo, cilindros, cilindrada,


potência, peso, aceleração e origem) foram obtidas do conjunto de dados Auto
MPG, que é um conjunto de dados de benchmark de aprendizado de máquina
comum para prever a eficiência de combustível de um carro em MPG. O conjunto
de dados completo e sua descrição estão disponíveis no repositório de
aprendizado de máquina da UCI
em https://archive.ics.uci.edu/ml/datasets/auto+mpg.

Vamos tratar cinco características do conjunto de dados Auto MPG (número de


cilindros, cilindrada, potência, peso e aceleração) como recursos "numéricos"
(aqui, contínuos). O ano modelo pode ser considerado como uma característica
categórica ordenada (ordinal). Por fim, a origem de fabricação pode ser
considerada como uma característica categórica (nominal) não ordenada com três
valores discretos possíveis, 1, 2 e 3, que correspondem aos EUA, Europa e
Japão, respectivamente.

Vamos primeiro carregar os dados e aplicar as etapas de pré-processamento


necessárias, incluindo descartar as linhas incompletas, particionar o conjunto de
dados em conjuntos de dados de treinamento e teste, bem como padronizar os
recursos contínuos:
>>> import pandas as pd

>>> url = 'http://archive.ics.uci.edu/ml/' \

... 'machine-learning-databases/auto-mpg/auto-mpg.data'

>>> column_names = ['MPG', 'Cylinders', 'Displacement', 'Horsepower',

... 'Weight', 'Acceleration', 'Model Year', 'Origin']

>>> df = pd.read_csv(url, names=column_names,

... na_values = "?", comment='\t',

... sep=" ", skipinitialspace=True)

>>>

>>> ## drop the NA rows

>>> df = df.dropna()

>>> df = df.reset_index(drop=True)

>>>

>>> ## train/test splits:

>>> import sklearn

>>> import sklearn.model_selection

>>> df_train, df_test = sklearn.model_selection.train_test_split(

... df, train_size=0.8, random_state=1

... )

>>> train_stats = df_train.describe().transpose()

>>>

>>> numeric_column_names = [

... 'Cylinders', 'Displacement',

... 'Horsepower', 'Weight',

... 'Acceleration'

... ]
>>> df_train_norm, df_test_norm = df_train.copy(), df_test.copy()

>>> for col_name in numeric_column_names:

... mean = train_stats.loc[col_name, 'mean']

... std = train_stats.loc[col_name, 'std']

... df_train_norm.loc[:, col_name] = \

... (df_train_norm.loc[:, col_name] - mean)/std

... df_test_norm.loc[:, col_name] = \

... (df_test_norm.loc[:, col_name] - mean)/std

>>> df_train_norm.tail()
CopyExplain

Isso resulta no seguinte:

Figura 13.8: Dados MG automáticos pré-processados

Os pandas que criamos por meio do trecho de código anterior contém cinco
colunas com valores do tipo . Essas colunas constituirão os recursos
contínuos.DataFramefloat

Em seguida, vamos agrupar as informações de ano de modelo () bastante


refinadas em buckets para simplificar a tarefa de aprendizado para o modelo que
vamos treinar mais tarde. Concretamente, vamos atribuir cada carro em uma
caçamba de quatro anos, da seguinte forma:ModelYear
Note que os intervalos escolhidos foram selecionados arbitrariamente para ilustrar
os conceitos de "bucketing". A fim de agrupar os carros nessas caçambas,
primeiro definiremos três valores de corte: [73, 76, 79] para o recurso de ano
modelo. Esses valores de corte são usados para especificar intervalos
semifechados, por exemplo, (–∞, 73), [73, 76), [76, 79) e [76, ∞). Em seguida, as
características numéricas originais serão passadas para a função
(https://pytorch.org/docs/stable/generated/torch.bucketize.html) para gerar os
índices dos buckets. O código é o seguinte:torch.bucketize

>>> boundaries = torch.tensor([73, 76, 79])

>>> v = torch.tensor(df_train_norm['Model Year'].values)

>>> df_train_norm['Model Year Bucketed'] = torch.bucketize(

... v, boundaries, right=True

... )

>>> v = torch.tensor(df_test_norm['Model Year'].values)

>>> df_test_norm['Model Year Bucketed'] = torch.bucketize(

... v, boundaries, right=True

... )

>>> numeric_column_names.append('Model Year Bucketed')


CopyExplain

Adicionamos essa coluna de recurso bucketizada à lista do


Python .numeric_column_names
Em seguida, prosseguiremos com a definição de uma lista para o recurso
categórico não ordenado, . No PyTorch, há duas maneiras de trabalhar com um
recurso categórico: usando uma camada de incorporação via
(https://pytorch.org/docs/stable/generated/torch.nn.Embedding.html) ou usando
vetores codificados a quente (também chamados de indicador). Na abordagem de
codificação, por exemplo, o índice 0 será codificado como [1, 0, 0], o índice 1 será
codificado como [0, 1, 0] e assim por diante. Por outro lado, a camada de
incorporação mapeia cada índice para um vetor de números aleatórios do tipo,
que pode ser treinado. (Você pode pensar na camada de incorporação como uma
implementação mais eficiente de uma codificação única multiplicada por uma
matriz de peso treinável.)Originnn.Embeddingfloat

Quando o número de categorias é grande, usar a camada de incorporação com


menos dimensões do que o número de categorias pode melhorar o desempenho.

No trecho de código a seguir, usaremos a abordagem one-hot-encoding no


recurso categórico para convertê-lo no formato denso:

>>> from torch.nn.functional import one_hot

>>> total_origin = len(set(df_train_norm['Origin']))

>>> origin_encoded = one_hot(torch.from_numpy(

... df_train_norm['Origin'].values) % total_origin)

>>> x_train_numeric = torch.tensor(

... df_train_norm[numeric_column_names].values)

>>> x_train = torch.cat([x_train_numeric, origin_encoded], 1).float()

>>> origin_encoded = one_hot(torch.from_numpy(

... df_test_norm['Origin'].values) % total_origin)

>>> x_test_numeric = torch.tensor(

... df_test_norm[numeric_column_names].values)

>>> x_test = torch.cat([x_test_numeric, origin_encoded], 1).float()


CopyExplain
Depois de codificar o recurso categórico em um recurso denso tridimensional, nós
o concatenamos com os recursos numéricos que processamos na etapa anterior.
Finalmente, criaremos os tensores de rótulo a partir dos valores MPG verdade
fundamental da seguinte maneira:

>>> y_train = torch.tensor(df_train_norm['MPG'].values).float()

>>> y_test = torch.tensor(df_test_norm['MPG'].values).float()


CopyExplain

Nesta seção, abordamos as abordagens mais comuns para pré-processamento e


criação de recursos no PyTorch.

Treinando um modelo de regressão DNN


Agora, depois de construir os recursos e etiquetas obrigatórios, criaremos um
carregador de dados que usa um tamanho de lote de 8 para os dados do trem:

>>> train_ds = TensorDataset(x_train, y_train)

>>> batch_size = 8

>>> torch.manual_seed(1)

>>> train_dl = DataLoader(train_ds, batch_size, shuffle=True)


CopyExplain

Em seguida, construiremos um modelo com duas camadas totalmente conectadas


onde uma tem 8 unidades ocultas e outra tem 4:

>>> hidden_units = [8, 4]

>>> input_size = x_train.shape[1]

>>> all_layers = []

>>> for hidden_unit in hidden_units:

... layer = nn.Linear(input_size, hidden_unit)

... all_layers.append(layer)

... all_layers.append(nn.ReLU())
... input_size = hidden_unit

>>> all_layers.append(nn.Linear(hidden_units[-1], 1))

>>> model = nn.Sequential(*all_layers)

>>> model

Sequential(

(0): Linear(in_features=9, out_features=8, bias=True)

(1): ReLU()

(2): Linear(in_features=8, out_features=4, bias=True)

(3): ReLU()

(4): Linear(in_features=4, out_features=1, bias=True)

)
CopyExplain

Após a definição do modelo, definiremos a função de perda de MSE para


regressão e utilizaremos a descida do gradiente estocástico para otimização:

>>> loss_fn = nn.MSELoss()

>>> optimizer = torch.optim.SGD(model.parameters(), lr=0.001)


CopyExplain

Agora vamos treinar o modelo para 200 épocas e exibir a perda de trem para cada
20 épocas:

>>> torch.manual_seed(1)

>>> num_epochs = 200

>>> log_epochs = 20

>>> for epoch in range(num_epochs):

... loss_hist_train = 0

... for x_batch, y_batch in train_dl:

... pred = model(x_batch)[:, 0]


... loss = loss_fn(pred, y_batch)

... loss.backward()

... optimizer.step()

... optimizer.zero_grad()

... loss_hist_train += loss.item()

... if epoch % log_epochs==0:

... print(f'Epoch {epoch} Loss '

... f'{loss_hist_train/len(train_dl):.4f}')

Epoch 0 Loss 536.1047

Epoch 20 Loss 8.4361

Epoch 40 Loss 7.8695

Epoch 60 Loss 7.1891

Epoch 80 Loss 6.7062

Epoch 100 Loss 6.7599

Epoch 120 Loss 6.3124

Epoch 140 Loss 6.6864

Epoch 160 Loss 6.7648

Epoch 180 Loss 6.2156


CopyExplain

Após 200 épocas, a perda de trens foi de cerca de 5. Agora podemos avaliar o
desempenho de regressão do modelo treinado no conjunto de dados de teste.
Para prever os valores de destino em novos pontos de dados, podemos alimentar
seus recursos para o modelo:

>>> with torch.no_grad():

... pred = model(x_test.float())[:, 0]

... loss = loss_fn(pred, y_test)

... print(f'Test MSE: {loss.item():.4f}')


... print(f'Test MAE: {nn.L1Loss()(pred, y_test).item():.4f}')

Test MSE: 9.6130

Test MAE: 2.1211


CopyExplain

O EPM no conjunto de teste é de 9,6 e o erro absoluto médio (EAM) é de 2,1.


Após este projeto de regressão, trabalharemos em um projeto de classificação na
próxima seção.

Projeto dois – classificação de dígitos


manuscritos MNIST
Para este projeto de classificação, vamos categorizar os dígitos manuscritos do
MNIST. Na seção anterior, abordamos as quatro etapas essenciais para o
aprendizado de máquina no PyTorch em detalhes, que precisaremos repetir nesta
seção.

Você vai se lembrar que no Capítulo 12 você aprendeu a maneira de carregar


conjuntos de dados disponíveis a partir do módulo. Primeiro, vamos carregar o
conjunto de dados MNIST usando o módulo.torchvisiontorchvision

1. A etapa de configuração inclui carregar o conjunto de dados e especificar


hiperparâmetros (o tamanho do conjunto de trens e do conjunto de teste e o
tamanho dos minilotes):
2. >>> import torchvision

3. >>> from torchvision import transforms

4. >>> image_path = './'

5. >>> transform = transforms.Compose([

6. ... transforms.ToTensor()

7. ... ])

8. >>> mnist_train_dataset = torchvision.datasets.MNIST(


9. ... root=image_path, train=True,

10. ... transform=transform, download=False

11. ... )

12. >>> mnist_test_dataset = torchvision.datasets.MNIST(

13. ... root=image_path, train=False,

14. ... transform=transform, download=False

15. ... )

16. >>> batch_size = 64

17. >>> torch.manual_seed(1)

18. >>> train_dl = DataLoader(mnist_train_dataset,

19. ... batch_size, shuffle=True)


CopyExplain

Aqui, construímos um carregador de dados com lotes de 64 amostras. Em


seguida, vamos pré-processar os conjuntos de dados carregados.

20. Nós pré-processamos os recursos de entrada e as etiquetas. Os recursos


deste projeto são os pixels das imagens que lemos do Passo 1. Definimos
uma transformação personalizada usando o . Neste caso simples, nossa
transformação consistiu em apenas um método, . O método converte os
recursos de pixel em um tensor de tipo flutuante e também normaliza os
pixels do intervalo [0, 255] a [0, 1]. No Capítulo 14, Classificando imagens
com redes neurais convolucionais profundas, veremos alguns
métodos adicionais de transformação de dados quando trabalharmos com
conjuntos de dados de imagens mais complexos. Os rótulos são inteiros de 0
a 9 representando dez dígitos. Portanto, não precisamos fazer nenhum
dimensionamento ou conversão adicional. Observe que podemos acessar os
pixels brutos usando o atributo e não se esqueça de dimensioná-los para o
intervalo [0, 1].torchvision.transforms.ComposeToTensor()ToTensor()data

Construiremos o modelo na próxima etapa, uma vez que os dados sejam pré-
processados.
21. Construa o modelo NN:
22. >>> hidden_units = [32, 16]

23. >>> image_size = mnist_train_dataset[0][0].shape

24. >>> input_size = image_size[0] * image_size[1] * image_size[2]

25. >>> all_layers = [nn.Flatten()]

26. >>> for hidden_unit in hidden_units:

27. ... layer = nn.Linear(input_size, hidden_unit)

28. ... all_layers.append(layer)

29. ... all_layers.append(nn.ReLU())

30. ... input_size = hidden_unit

31. >>> all_layers.append(nn.Linear(hidden_units[-1], 10))

32. >>> model = nn.Sequential(*all_layers)

33. >>> model

34. Sequential(

35. (0): Flatten(start_dim=1, end_dim=-1)

36. (1): Linear(in_features=784, out_features=32, bias=True)

37. (2): ReLU()

38. (3): Linear(in_features=32, out_features=16, bias=True)

39. (4): ReLU()

40. (5): Linear(in_features=16, out_features=10, bias=True)

41. )
CopyExplain

Observe que o modelo começa com uma camada achatada que achata uma
imagem de entrada em um tensor unidimensional. Isso ocorre porque as
imagens de entrada estão na forma de [1, 28, 28]. O modelo tem duas
camadas ocultas, com 32 e 16 unidades, respectivamente. E termina com
uma camada de saída de dez unidades representando dez classes, ativadas
por uma função softmax. Na próxima etapa, treinaremos o modelo no
conjunto de trens e o avaliaremos no conjunto de testes.

42. Use o modelo para treinamento, avaliação e previsão:


43. >>> loss_fn = nn.CrossEntropyLoss()

44. >>> optimizer = torch.optim.Adam(model.parameters(), lr=0.001)

45. >>> torch.manual_seed(1)

46. >>> num_epochs = 20

47. >>> for epoch in range(num_epochs):

48. ... accuracy_hist_train = 0

49. ... for x_batch, y_batch in train_dl:

50. ... pred = model(x_batch)

51. ... loss = loss_fn(pred, y_batch)

52. ... loss.backward()

53. ... optimizer.step()

54. ... optimizer.zero_grad()

55. ... is_correct = (

56. ... torch.argmax(pred, dim=1) == y_batch

57. ... ).float()

58. ... accuracy_hist_train += is_correct.sum()

59. ... accuracy_hist_train /= len(train_dl.dataset)

60. ... print(f'Epoch {epoch} Accuracy '

61. ... f'{accuracy_hist_train:.4f}')

62. Epoch 0 Accuracy 0.8531

63. ...

64. Epoch 9 Accuracy 0.9691

65. ...

66. Epoch 19 Accuracy 0.9813


CopyExplain

Usamos a função de perda de entropia cruzada para classificação multiclasse


e o otimizador de Adam para descida de gradiente. Falaremos sobre o
otimizador Adam no Capítulo 14. Treinamos o modelo por 20 épocas e
mostramos a precisão do trem para cada época. O modelo treinado atingiu
uma precisão de 96,3% no conjunto de treinamento e vamos avaliá-lo no
conjunto de testes:

>>> pred = model(mnist_test_dataset.data / 255.)

>>> is_correct = (

... torch.argmax(pred, dim=1) ==

... mnist_test_dataset.targets

... ).float()

>>> print(f'Test accuracy: {is_correct.mean():.4f}')

Test accuracy: 0.9645


CopyExplain

A precisão do teste é de 95,6%. Você aprendeu como resolver um problema de


classificação usando o PyTorch.

APIs PyTorch de nível superior: uma


breve introdução ao PyTorch-Lightning
Nos últimos anos, a comunidade PyTorch desenvolveu várias bibliotecas e APIs
diferentes em cima do PyTorch. Exemplos notáveis incluem fastai
(https://docs.fast.ai/), Catalyst (https://github.com/catalyst-team/catalyst), PyTorch
Lightning (https://www.pytorchlightning.ai), (https://lightning-flash.readthedocs.io/
en/latest/quickstart.html) e PyTorch-Ignite (https://github.com/pytorch/ignite).

Nesta seção, exploraremos o PyTorch Lightning (Relâmpago), que é uma


biblioteca PyTorch amplamente utilizada que simplifica o treinamento de redes
neurais profundas, removendo grande parte do código clichê. No entanto, embora
o foco do Lightning esteja na simplicidade e flexibilidade, ele também nos permite
usar muitos recursos avançados, como suporte a várias GPUs e treinamento
rápido de baixa precisão, que você pode aprender na documentação oficial
em https://pytorch-lightning.rtfd.io/en/latest/.

Há também uma introdução bônus ao PyTorch-Ignite


no https://github.com/rasbt/machine-learning-book/blob/main/ch13/ch13_part4_igni
te.ipynb.

Em uma seção anterior, Projeto dois – classificando dígitos manuscritos MNIST,


implementamos um perceptron multicamadas para classificar dígitos manuscritos
no conjunto de dados MNIST. Nas próximas subseções, reimplementaremos esse
classificador usando o Lightning.

Instalando o PyTorch Lightning

O Lightning pode ser instalado via pip ou conda, dependendo da sua preferência.
Por exemplo, o comando para instalar o Lightning via pip é o seguinte:

pip install pytorch-lightning


CopyExplain

Veja a seguir o comando para instalar o Lightning via conda:

conda install pytorch-lightning -c conda-forge


CopyExplain

O código nas subseções a seguir é baseado no PyTorch Lightning versão 1.5, que
você pode instalar substituindo por esses comandos. pytorch-lightningpytorch-
lightning==1.5

Configurando o modelo PyTorch Lightning


Começamos implementando o modelo, que treinaremos nas próximas subseções.
Definir um modelo para o Lightning é relativamente simples, pois é baseado em
código Python e PyTorch regulares. Tudo o que é necessário para implementar
um modelo Lightning é usar em vez do módulo PyTorch regular. Para aproveitar
as funções de conveniência do PyTorch, como a API do treinador e o registro
automático, definimos apenas alguns métodos especificamente nomeados, que
veremos no código a seguir:LightningModule

import pytorch_lightning as pl

import torch

import torch.nn as nn

from torchmetrics import Accuracy

class MultiLayerPerceptron(pl.LightningModule):

def __init__(self, image_shape=(1, 28, 28), hidden_units=(32, 16)):

super().__init__()

# new PL attributes:

self.train_acc = Accuracy()

self.valid_acc = Accuracy()

self.test_acc = Accuracy()

# Model similar to previous section:

input_size = image_shape[0] * image_shape[1] * image_shape[2]

all_layers = [nn.Flatten()]

for hidden_unit in hidden_units:

layer = nn.Linear(input_size, hidden_unit)

all_layers.append(layer)

all_layers.append(nn.ReLU())

input_size = hidden_unit

all_layers.append(nn.Linear(hidden_units[-1], 10))

self.model = nn.Sequential(*all_layers)

def forward(self, x):


x = self.model(x)

return x

def training_step(self, batch, batch_idx):

x, y = batch

logits = self(x)

loss = nn.functional.cross_entropy(self(x), y)

preds = torch.argmax(logits, dim=1)

self.train_acc.update(preds, y)

self.log("train_loss", loss, prog_bar=True)

return loss

def training_epoch_end(self, outs):

self.log("train_acc", self.train_acc.compute())

def validation_step(self, batch, batch_idx):

x, y = batch

logits = self(x)

loss = nn.functional.cross_entropy(self(x), y)

preds = torch.argmax(logits, dim=1)

self.valid_acc.update(preds, y)

self.log("valid_loss", loss, prog_bar=True)

self.log("valid_acc", self.valid_acc.compute(), prog_bar=True)

return loss

def test_step(self, batch, batch_idx):

x, y = batch

logits = self(x)
loss = nn.functional.cross_entropy(self(x), y)

preds = torch.argmax(logits, dim=1)

self.test_acc.update(preds, y)

self.log("test_loss", loss, prog_bar=True)

self.log("test_acc", self.test_acc.compute(), prog_bar=True)

return loss

def configure_optimizers(self):

optimizer = torch.optim.Adam(self.parameters(), lr=0.001)

return optimizer
CopyExplain

Vamos agora discutir os diferentes métodos, um por um. Como você pode ver, o
construtor contém o mesmo código de modelo que usamos em uma subseção
anterior. A novidade é que adicionamos os atributos de precisão, como . Isso nos
permitirá acompanhar a precisão durante o treinamento. foi importado do módulo,
que deve ser instalado automaticamente com o Lightning. Se você não pode
importar , você pode tentar instalá-lo via . Mais informações podem ser
encontradas
em https://torchmetrics.readthedocs.io/en/latest/pages/quickstart.html.__init__self
.train_acc = Accuracy()Accuracytorchmetricstorchmetricspip install torchmetrics

O método implementa um simples forward pass que retorna os logits (saídas da


última camada totalmente conectada de nossa rede antes da camada softmax)
quando chamamos nosso modelo nos dados de entrada. Os logits, calculados por
meio do método por chamada , são usados para as etapas de treinamento,
validação e teste, que descreveremos a seguir.forwardforwardself(x)

Os métodos , , , e são métodos especificamente reconhecidos pelo Lightning. Por


exemplo, define um único passe para frente durante o treinamento, onde também
acompanhamos a precisão e a perda para que possamos analisá-las
posteriormente. Observe que calculamos a precisão por meio dela, mas ainda não
a registramos. O método é executado em cada lote individual durante o
treinamento, e através do método, que é executado no final de cada época de
treinamento, calculamos a precisão do conjunto de treinamento a partir dos
valores de precisão que acumulamos através do
treinamento.training_steptraining_epoch_endvalidation_steptest_stepconfigure_opt
imizerstraining_stepself.train_acc.update(preds,
y)training_steptraining_epoch_end

Os métodos e definem, de forma análoga ao método, como o processo de


validação e avaliação de testes deve ser computado. Semelhante ao , cada um e
recebe um único lote, e é por isso que registramos a precisão através dos
respectivos atributos de precisão derivados de . No entanto, note que só é
chamado em determinados intervalos, por exemplo, após cada época de
treinamento. É por isso que registramos a precisão de validação dentro da etapa
de validação, enquanto que com a precisão de treinamento, registramos após
cada época de treinamento, caso contrário, o gráfico de precisão que
inspecionamos mais tarde parecerá muito
barulhento.validation_steptest_steptraining_steptraining_stepvalidation_steptest
_stepAccuracytorchmetricvalidation_step

Finalmente, através do método, especificamos o otimizador usado para o


treinamento. As duas subseções a seguir discutirão como podemos configurar o
conjunto de dados e como podemos treinar o modelo. configure_optimizers

Configurando os carregadores de dados para o


Lightning
Há três maneiras principais de preparar o conjunto de dados para o Lightning.
Podemos:

 Tornar o conjunto de dados parte do modelo

 Configure os carregadores de dados como de costume e alimente-os com o


método de um Lightning Trainer — o Treinador é apresentado na próxima
subseçãofit
 Crie um LightningDataModule
Aqui, vamos usar um , que é a abordagem mais organizada. O consiste em cinco
métodos principais, como podemos ver a
seguir: LightningDataModuleLightningDataModule

from torch.utils.data import DataLoader

from torch.utils.data import random_split

from torchvision.datasets import MNIST

from torchvision import transforms

class MnistDataModule(pl.LightningDataModule):

def __init__(self, data_path='./'):

super().__init__()

self.data_path = data_path

self.transform = transforms.Compose([transforms.ToTensor()])

def prepare_data(self):

MNIST(root=self.data_path, download=True)

def setup(self, stage=None):

# stage is either 'fit', 'validate', 'test', or 'predict'

# here note relevant

mnist_all = MNIST(

root=self.data_path,

train=True,

transform=self.transform,

download=False

self.train, self.val = random_split(

mnist_all, [55000, 5000], generator=torch.Generator().manual_seed(1)

)
self.test = MNIST(

root=self.data_path,

train=False,

transform=self.transform,

download=False

def train_dataloader(self):

return DataLoader(self.train, batch_size=64, num_workers=4)

def val_dataloader(self):

return DataLoader(self.val, batch_size=64, num_workers=4)

def test_dataloader(self):

return DataLoader(self.test, batch_size=64, num_workers=4)


CopyExplain

In the method, we define general steps, such as downloading the dataset. In


the method, we define the datasets used for training, validation, and testing. Note
that MNIST does not have a dedicated validation split, which is why we use
the function to divide the 60,000-example training set into 55,000 examples
for training and 5,000 examples for validation.prepare_datasetuprandom_split

The data loader methods are self-explanatory and define how the respective
datasets are loaded. Now, we can initialize the data module and use it for training,
validation, and testing in the next subsections:

torch.manual_seed(1)

mnist_dm = MnistDataModule()
CopyExplain

Training the model using the PyTorch Lightning


Trainer class
Now we can reap the rewards from setting up the model with the specifically
named methods, as well as the Lightning data module. Lightning implements
a class that makes the training model super convenient by taking care of all the
intermediate steps, such as calling , , and for us. Also, as a bonus, it lets us easily
specify one or more GPUs to use (if
available):Trainerzero_grad()backward()optimizer.step()

mnistclassifier = MultiLayerPerceptron()

if torch.cuda.is_available(): # if you have GPUs

trainer = pl.Trainer(max_epochs=10, gpus=1)

else:

trainer = pl.Trainer(max_epochs=10)

trainer.fit(model=mnistclassifier, datamodule=mnist_dm)
CopyExplain

Via the preceding code, we train our multilayer perceptron for 10 epochs. During
training, we see a handy progress bar that keeps track of the epoch and core
metrics such as the training and validation losses:

Epoch 9: 100% 939/939 [00:07<00:00, 130.42it/s, loss=0.1, v_num=0, train_loss=0.260,

valid_loss=0.166, valid_acc=0.949]
CopyExplain

After the training has finished, we can also inspect the metrics we logged in more
detail, as we will see in the next subsection.

Evaluating the model using TensorBoard


In the previous section, we experienced the convenience of the class. Another nice
feature of Lightning is its logging capabilities. Recall that we specified several steps
in our Lightning model earlier. After, and even during training, we can visualize
them in TensorBoard. (Note that Lightning supports other loggers as well; for more
information, please see the official documentation at https://pytorch-
lightning.readthedocs.io/en/latest/common/loggers.html.)Trainerself.log
Installing TensorBoard

TensorBoard can be installed via pip or conda, depending on your preference. For
instance, the command for installing TensorBoard via pip is as follows:

pip install tensorboard


CopyExplain

The following is the command for installing Lightning via conda:

conda install tensorboard -c conda-forge


CopyExplain

The code in the following subsection is based on TensorBoard version 2.4, which
you can install by replacing with in these commands.tensorboardtensorboard==2.4

By default, Lightning tracks the training in a subfolder named . To visualize the


training runs, you can execute the following code in the command-line terminal,
which will open TensorBoard in your browser:lightning_logs

tensorboard --logdir lightning_logs/


CopyExplain

Alternatively, if you are running the code in a Jupyter notebook, you can add the
following code to a Jupyter notebook cell to show the TensorBoard dashboard in
the notebook directly:

%load_ext tensorboard

%tensorboard --logdir lightning_logs/


CopyExplain

Figure 13.9 shows the TensorBoard dashboard with the logged training and


validation accuracy. Note that there is a toggle shown in the lower-left corner. If
you run the training code multiple times, Lightning will track them as separate
subfolders: , , , and so forth:version_0version_0version_1version_2
Figure 13.9: TensorBoard dashboard

By looking at the training and validation accuracies in Figure 13.9, we can


hypothesize that training the model for a few additional epochs can improve
performance.

Lightning allows us to load a trained model and train it for additional epochs
conveniently. As mentioned previously, Lightning tracks the individual training runs
via subfolders. In Figure 13.10, we see the contents of the subfolder, which
contains log files and a model checkpoint for reloading the model: version_0
Figure 13.10: PyTorch Lightning log files

For instance, we can use the following code to load the latest model checkpoint
from this folder and train the model via :fit

if torch.cuda.is_available(): # if you have GPUs

trainer = pl.Trainer(max_epochs=15,

resume_from_checkpoint='./lightning_logs/version_0/checkpoints/epoch=8-step=7739.ckpt',

gpus=1)

else:
trainer = pl.Trainer(max_epochs=15,

resume_from_checkpoint='./lightning_logs/version_0/checkpoints/epoch=8-step=7739.ckpt')

trainer.fit(model=mnistclassifier, datamodule=mnist_dm)
CopyExplain

Here, we set to , which trained the model for 5 additional epochs (previously, we


trained it for 10 epochs).max_epochs15

Now, let’s take a look at the TensorBoard dashboard in Figure 13.11 and see
whether training the model for a few additional epochs was worthwhile:
Figure 13.11: TensorBoard dashboard after training for five more epochs

Como podemos ver na Figura 13.11, o TensorBoard nos permite mostrar os


resultados das épocas de treinamento adicionais () ao lado das anteriores (), o que
é muito conveniente. De fato, podemos ver que o treinamento para mais cinco
épocas melhorou a precisão da validação. Neste ponto, podemos decidir treinar o
modelo para mais épocas, o que deixamos como um exercício para
você.version_1version_0

Uma vez terminado o treinamento, podemos avaliar o modelo no conjunto de


testes usando o seguinte código:

trainer.test(model=mnistclassifier, datamodule=mnist_dm)
CopyExplain

O desempenho resultante do conjunto de testes, após o treinamento por 15


épocas no total, é de aproximadamente 95 por cento:

[{'test_loss': 0.14912301301956177, 'test_acc': 0.9499600529670715}]


CopyExplain

Observe que o PyTorch Lightning também salva o modelo automaticamente para


nós. Se você quiser reutilizar o modelo mais tarde, você pode carregá-lo
convenientemente através do seguinte código:

model = MultiLayerPerceptron.load_from_checkpoint("path/to/checkpoint.ckpt")
CopyExplain

Saiba mais sobre o PyTorch Lightning

Para saber mais sobre o Lightning, visite o site oficial, que contém tutoriais e
exemplos, em https://pytorch-lightning.readthedocs.io.

O Lightning também tem uma comunidade ativa no Slack que dá as boas-vindas a


novos usuários e colaboradores. Para saber mais, visite o site oficial do Lightning
em https://www.pytorchlightning.ai.
Classificando Imagens com Redes
Neurais Convolucionais Profundas
No capítulo anterior, analisamos em profundidade diferentes aspectos da rede
neural PyTorch e módulos de diferenciação automática, você se familiarizou com
tensores e funções de decoração e aprendeu a trabalhar com . Neste capítulo,
você aprenderá sobre redes neurais convolucionais (CNNs) para classificação
de imagens. Começaremos discutindo os blocos de construção básicos das
CNNs, usando uma abordagem de baixo para cima. Em seguida, vamos dar um
mergulho mais profundo na arquitetura da CNN e explorar como implementar
CNNs no PyTorch. Neste capítulo, abordaremos os seguintes tópicos:torch.nn
 Operações de convolução em uma e duas dimensões
 Os blocos de construção das arquiteturas da CNN
 Implementando CNNs profundas no PyTorch
 Técnicas de aumento de dados para melhorar o desempenho de
generalização
 Implementar um classificador facial da CNN para reconhecer se alguém
está sorrindo ou não

Os blocos de construção das CNNs


As CNNs são uma família de modelos que foram originalmente inspirados em
como o córtex visual do cérebro humano funciona ao reconhecer objetos. O
desenvolvimento das CNNs remonta à década de 1990, quando Yann LeCun e
seus colegas propuseram uma nova arquitetura NN para classificar dígitos
manuscritos a partir de imagens (Handwritten Digit Recognition with a Back-
Propagation Network por Y. LeCun, e colegas, 1989, publicado na
conferência Neural Information Processing Systems (NeurIPS)).
O córtex visual humano

A descoberta original de como o córtex visual do nosso cérebro funciona foi feita
por David H. Hubel e Torsten Wiesel em 1959, quando inseriram um microeletrodo
no córtex visual primário de um gato anestesiado. Eles observaram que os
neurônios respondem de forma diferente após projetar diferentes padrões de luz
na frente do gato. Isso acabou levando à descoberta das diferentes camadas do
córtex visual. Enquanto a camada primária detecta principalmente bordas e linhas
retas, as camadas de ordem superior se concentram mais na extração de formas
e padrões complexos.

Devido ao excelente desempenho das CNNs para tarefas de classificação de


imagens, esse tipo específico de NN feedforward ganhou muita atenção e levou a
enormes melhorias no aprendizado de máquina para visão computacional. Vários
anos depois, em 2019, Yann LeCun recebeu o prêmio Turing (o mais prestigioso
prêmio em ciência da computação) por suas contribuições ao campo
da inteligência artificial (IA), junto com outros dois pesquisadores,
Yoshua Bengio e Geoffrey Hinton, cujos nomes você encontrou nos capítulos
anteriores.

Nas seções a seguir, discutiremos os conceitos mais amplos de CNNs e por que
arquiteturas convolucionais são frequentemente descritas como "camadas de
extração de recursos". Em seguida, vamos nos aprofundar na definição teórica do
tipo de operação de convolução que é comumente usado em CNNs e percorrer
exemplos de convoluções computacionais em uma e duas dimensões.

Noções básicas sobre CNNs e hierarquias de


recursos
Extrair com sucesso recursos salientes (relevantes) é a chave para o
desempenho de qualquer algoritmo de aprendizado de máquina, e os modelos
tradicionais de aprendizado de máquina dependem de recursos de entrada que
podem vir de um especialista em domínio ou são baseados em técnicas de
extração de recursos computacionais.
Certos tipos de NNs, como CNNs, podem aprender automaticamente os recursos
de dados brutos que são mais úteis para uma tarefa específica. Por esse motivo, é
comum considerar as camadas CNN como extratores de feição: as camadas
iniciais (aquelas logo após a camada de entrada) extraem recursos de baixo
nível de dados brutos, e as camadas posteriores (geralmente camadas
totalmente conectadas, como em um perceptron multicamada (MLP)) usam
esses recursos para prever um valor de destino contínuo ou rótulo de classe.
Certos tipos de NNs multicamadas e, em particular, CNNs profundas, constroem a
chamada hierarquia de recursos combinando os recursos de baixo nível de forma
em camadas para formar recursos de alto nível. Por exemplo, se estivermos
lidando com imagens, os recursos de baixo nível, como bordas e blobs, serão
extraídos das camadas anteriores, que são combinadas para formar recursos de
alto nível. Essas características de alto nível podem formar formas mais
complexas, como os contornos gerais de objetos como edifícios, gatos ou cães.
Como você pode ver na Figura 14.1, um CNN calcula mapas de recursos a partir
de uma imagem de entrada, onde cada elemento vem de um patch local de pixels
na imagem de entrada:

Figura 14.1: Criando mapas de recursos a partir de uma imagem (foto de


Alexander Dummer no Unsplash)

Esse patch local de pixels é conhecido como o campo receptivo local. As CNNs
geralmente têm um desempenho muito bom em tarefas relacionadas à imagem, e
isso se deve em grande parte a duas ideias importantes:
 Conectividade esparsa: um único elemento no mapa de recursos é
conectado a apenas um pequeno pedaço de pixels. (Isso é muito diferente
de se conectar à imagem de entrada inteira, como no caso de MLPs. Você
pode achar útil olhar para trás e comparar como implementamos uma rede
totalmente conectada que se conectou à imagem inteira no Capítulo
11, Implementando uma Rede Neural Artificial Multicamada do Zero.)
 Compartilhamento de parâmetros: os mesmos pesos são usados para
patches diferentes da imagem de entrada.
Como consequência direta dessas duas ideias, substituir um MLP convencional
totalmente conectado por uma camada de convolução diminui substancialmente o
número de pesos (parâmetros) na rede, e veremos uma melhoria na capacidade
de capturar características salientes. No contexto dos dados de imagem, faz
sentido assumir que os pixels próximos são normalmente mais relevantes entre si
do que os pixels que estão longe uns dos outros.
Normalmente, as CNNs são compostas por várias camadas convolucionais e de
subamostragem que são seguidas por uma ou mais camadas totalmente
conectadas no final. As camadas totalmente conectadas são essencialmente um
MLP, onde cada unidade de entrada, i, é conectada a cada unidade de saída, j,
com peso wIj (que abordamos com mais detalhes no Capítulo 11).
Observe que as camadas de subamostragem, comumente conhecidas
como camadas de pooling, não têm nenhum parâmetro aprendível; por exemplo,
não há pesos ou unidades de polarização nas camadas de pooling. No entanto,
tanto a camada convolucional quanto a totalmente conectada possuem pesos e
vieses que são otimizados durante o treinamento.

Nas seções a seguir, estudaremos as camadas convolucionais e de agrupamento


com mais detalhes e veremos como elas funcionam. Para entender como as
operações de convolução funcionam, vamos começar com uma convolução em
uma dimensão, que às vezes é usada para trabalhar com certos tipos de dados de
sequência, como texto. Depois de discutir as convoluções unidimensionais,
trabalharemos as típicas bidimensionais que são comumente aplicadas a imagens
bidimensionais.

Executando convoluções discretas


Uma convolução discreta (ou simplesmente convolução) é uma operação
fundamental em uma CNN. Por isso, é importante entender como funciona essa
operação. Nesta seção, abordaremos a definição matemática e discutiremos
alguns dos algoritmos ingênuos para calcular convoluções de tensores
unidimensionais (vetores) e tensores bidimensionais (matrizes).

Observe que as fórmulas e descrições nesta seção são exclusivamente para


entender como as operações de convolução em CNNs funcionam. De fato,
implementações muito mais eficientes de operações convolucionais já existem em
pacotes como o PyTorch, como você verá mais adiante neste capítulo.

Notação matemática
Neste capítulo, usaremos subscrito para denotar o tamanho de uma matriz

multidimensional (tensor); por exemplo,   é uma matriz bidimensional


de tamanho n1×n2. Usamos colchetes, [ ], para denotar a indexação de uma matriz
multidimensional. Por exemplo, A[i, j] refere-se ao elemento no índice i, j da
matriz A. Além disso, note que usamos um símbolo especial, , para denotar a
operação de convolução entre dois vetores ou matrizes, que não deve ser

confundido com o operador de multiplicação, ,  em Python.*

Convoluções discretas em uma dimensão


Vamos começar com algumas definições e notações básicas que vamos usar.
Uma convolução discreta para dois vetores, x e w, é denotada

por  , na qual o vetor x é nossa entrada (às vezes chamado


de sinal) e w é chamado de filtro ou kernel. Uma convolução discreta é
matematicamente definida da seguinte forma:

As mentioned earlier, the brackets, [ ], are used to denote the indexing for vector
elements. The index, i, runs through each element of the output vector, y. There
are two odd things in the preceding formula that we need to clarify: –∞ to +∞
indices and negative indexing for x.
The fact that the sum runs through indices from –∞ to +∞ seems odd, mainly
because in machine learning applications, we always deal with finite feature
vectors. For example, if x has 10 features with indices 0, 1, 2, ..., 8, 9, then indices
–∞: –1 and 10: +∞ are out of bounds for x. Therefore, to correctly compute the
summation shown in the preceding formula, it is assumed that x and w are filled
with zeros. This will result in an output vector, y, that also has infinite size, with lots
of zeros as well. Since this is not useful in practical situations, x is padded only
with a finite number of zeros.
This process is called zero-padding or simply padding. Here, the number of zeros
padded on each side is denoted by p. An example padding of a one-dimensional
vector, x, is shown in Figure 14.2:

Figure 14.2: An example of padding

Let’s assume that the original input, x, and filter, w, have n and m elements,

respectively, where  . Therefore, the padded vector, xp, has


size n + 2p. The practical formula for computing a discrete convolution will change
to the following:

Now that we have solved the infinite index issue, the second issue is
indexing x with i + m – k. The important point to notice here is that x and w are
indexed in different directions in this summation. Computing the sum with one
index going in the reverse direction is equivalent to computing the sum with both
indices in the forward direction after flipping one of those vectors, x or w, after they
are padded. Then, we can simply compute their dot product. Let’s assume we flip
(rotate) the filter, w, to get the rotated filter, wr. Then, the dot product, x[i: i + m].wr,
is computed to get one element, y[i], where x[i: i + m] is a patch of x with size m.
This operation is repeated like in a sliding window approach to get all the output
elements.
The following figure provides an example with x = [3 2 1 7 1 2 5 4]

and   so that the first three output elements are


computed:

Figure 14.3: The steps for computing a discrete convolution

You can see in the preceding example that the padding size is zero (p =  0). Notice
that the rotated filter, wr, is shifted by two cells each time we shift. This shift is
another hyperparameter of a convolution, the stride, s. In this example, the stride
is two, s = 2. Note that the stride has to be a positive number smaller than the size
of the input vector. We will talk more about padding and strides in the next section.
Cross-correlation
Cross-correlation (or simply correlation) between an input vector and a filter is

denoted by   and is very much like a sibling of a convolution,


with a small difference: in cross-correlation, the multiplication is performed in the
same direction. Therefore, it is not a requirement to rotate the filter matrix, w, in
each dimension. Mathematically, cross-correlation is defined as follows:

The same rules for padding and stride may be applied to cross-correlation as well.
Note that most deep learning frameworks (including PyTorch) implement cross-
correlation but refer to it as convolution, which is a common convention in the deep
learning field.

Padding inputs to control the size of the output feature maps


So far, we’ve only used zero-padding in convolutions to compute finite-sized output

vectors. Technically, padding can be applied with any  . Depending on


the choice of p, boundary cells may be treated differently than the cells located in
the middle of x.
Now, consider an example where n = 5 and m = 3. Then, with p = 0, x[0] is only
used in computing one output element (for instance, y[0]), while x[1] is used in the
computation of two output elements (for instance, y[0] and y[1]). So, you can see
that this different treatment of elements of x can artificially put more emphasis on
the middle element, x[2], since it has appeared in most computations. We can
avoid this issue if we choose p = 2, in which case, each element of x will be
involved in computing three elements of y.
Furthermore, the size of the output, y, also depends on the choice of the padding
strategy we use.
There are three modes of padding that are commonly used in practice: full, same,
and valid.
In full mode, the padding parameter, p, is set to p = m – 1. Full padding increases
the dimensions of the output; thus, it is rarely used in CNN architectures.
The same padding mode is usually used to ensure that the output vector has the
same size as the input vector, x. In this case, the padding parameter, p, is
computed according to the filter size, along with the requirement that the input size
and output size are the same.
Finally, computing a convolution in valid mode refers to the case where p = 0 (no
padding).
Figure 14.4 illustrates the three different padding modes for a simple 5×5 pixel
input with a kernel size of 3×3 and a stride of 1:

Figure 14.4: The three modes of padding

The most commonly used padding mode in CNNs is same padding. One of its
advantages over the other padding modes is that same padding preserves the size
of the vector—or the height and width of the input images when we are working on
image-related tasks in computer vision—which makes designing a network
architecture more convenient.

One big disadvantage of valid padding versus full and same padding is that the
volume of the tensors will decrease substantially in NNs with many layers, which
can be detrimental to the network’s performance. In practice, you should preserve
the spatial size using same padding for the convolutional layers and decrease the
spatial size via pooling layers or convolutional layers with stride 2 instead, as
described in Striving for Simplicity: The All Convolutional Net ICLR (workshop
track), by Jost Tobias Springenberg, Alexey Dosovitskiy, and others, 2015
(https://arxiv.org/abs/1412.6806).

As for full padding, its size results in an output larger than the input size. Full
padding is usually used in signal processing applications where it is important to
minimize boundary effects. However, in a deep learning context, boundary effects
are usually not an issue, so we rarely see full padding being used in practice.
Determining the size of the convolution output
The output size of a convolution is determined by the total number of times that we
shift the filter, w, along the input vector. Let’s assume that the input vector is of
size n and the filter is of size m. Then, the size of the output resulting

from  , with padding p and stride s, would be determined as


follows:

Here,   denotes the floor operation.


The floor operation

The floor operation returns the largest integer that is equal to or smaller than the
input, for example:

Consider the following two cases:

 Compute the output size for an input vector of size 10 with a convolution


kernel of size 5, padding 2, and stride 1:

(Note that in this case, the output size turns out to be the same as the input;
therefore, we can conclude this to be same padding mode.)
 How does the output size change for the same input vector when we have a
kernel of size 3 and stride 2?

If you are interested in learning more about the size of the convolution output, we
recommend the manuscript A guide to convolution arithmetic for deep
learning by Vincent Dumoulin and Francesco Visin, which is freely available
at https://arxiv.org/abs/1603.07285.
Finally, in order to learn how to compute convolutions in one dimension, a naive
implementation is shown in the following code block, and the results are compared
with the  function. The code is as follows:numpy.convolve
>>> import numpy as np

>>> def conv1d(x, w, p=0, s=1):

... w_rot = np.array(w[::-1])

... x_padded = np.array(x)

... if p > 0:

... zero_pad = np.zeros(shape=p)

... x_padded = np.concatenate([

... zero_pad, x_padded, zero_pad

... ])

... res = []

... for i in range(0, int((len(x_padded) - len(w_rot))) + 1, s):

... res.append(np.sum(x_padded[i:i+w_rot.shape[0]] * w_rot))

... return np.array(res)

>>> ## Testing:

>>> x = [1, 3, 2, 4, 5, 6, 1, 3]

>>> w = [1, 0, 3, 1, 2]

>>> print('Conv1d Implementation:',


... conv1d(x, w, p=2, s=1))

Conv1d Implementation: [ 5. 14. 16. 26. 24. 34. 19. 22.]

>>> print('NumPy Results:',

... np.convolve(x, w, mode='same'))

NumPy Results: [ 5 14 16 26 24 34 19 22]


CopyExplain

So far, we have mostly focused on convolutions for vectors (1D convolutions). We


started with the 1D case to make the concepts easier to understand. In the next
section, we will cover 2D convolutions in more detail, which are the building blocks
of CNNs for image-related tasks.

Performing a discrete convolution in 2D


The concepts you learned in the previous sections are easily extendible to 2D.

When we deal with 2D inputs, such as a matrix,  , and the filter

matrix,  , where   and  ,

then the matrix   is the result of a 2D convolution


between X and W. This is defined mathematically as follows:

Notice that if you omit one of the dimensions, the remaining formula is exactly the
same as the one we used previously to compute the convolution in 1D. In fact, all
the previously mentioned techniques, such as zero padding, rotating the filter
matrix, and the use of strides, are also applicable to 2D convolutions, provided that
they are extended to both dimensions independently. Figure 14.5 demonstrates the
2D convolution of an input matrix of size 8×8, using a kernel of size 3×3. The input
matrix is padded with zeros with p = 1. As a result, the output of the 2D convolution
will have a size of 8×8:

Figure 14.5: The output of a 2D convolution

The following example illustrates the computation of a 2D convolution between an


input matrix, X3×3, and a kernel matrix, W3×3, using padding p = (1, 1) and
stride s = (2, 2). According to the specified padding, one layer of zeros is added on

each side of the input matrix, which results in the padded matrix  ,


as follows:
Figure 14.6: Computing a 2D convolution between an input and kernel matrix

With the preceding filter, the rotated filter will be:

Note that this rotation is not the same as the transpose matrix. To get the rotated
filter in NumPy, we can write . Next, we can shift the rotated filter matrix along the
padded input matrix, XW_rot=W[::-1,::-1]padded, like a sliding window, and compute

the sum of the element-wise product, which is denoted by the   operator


in Figure 14.7:
Figure 14.7: Computing the sum of the element-wise product

The result will be the 2×2 matrix, Y.


Let’s also implement the 2D convolution according to the naive algorithm
described. The package provides a way to compute 2D convolution via
the function:scipy.signalscipy.signal.convolve2d
>>> import numpy as np

>>> import scipy.signal

>>> def conv2d(X, W, p=(0, 0), s=(1, 1)):

... W_rot = np.array(W)[::-1,::-1]

... X_orig = np.array(X)

... n1 = X_orig.shape[0] + 2*p[0]

... n2 = X_orig.shape[1] + 2*p[1]

... X_padded = np.zeros(shape=(n1, n2))

... X_padded[p[0]:p[0]+X_orig.shape[0],

... p[1]:p[1]+X_orig.shape[1]] = X_orig

...

... res = []

... for i in range(0,

... int((X_padded.shape[0] - \
... W_rot.shape[0])/s[0])+1, s[0]):

... res.append([])

... for j in range(0,

... int((X_padded.shape[1] - \

... W_rot.shape[1])/s[1])+1, s[1]):

... X_sub = X_padded[i:i+W_rot.shape[0],

... j:j+W_rot.shape[1]]

... res[-1].append(np.sum(X_sub * W_rot))

... return(np.array(res))

>>> X = [[1, 3, 2, 4], [5, 6, 1, 3], [1, 2, 0, 2], [3, 4, 3, 2]]

>>> W = [[1, 0, 3], [1, 2, 1], [0, 1, 1]]

>>> print('Conv2d Implementation:\n',

... conv2d(X, W, p=(1, 1), s=(1, 1)))

Conv2d Implementation:

[[ 11. 25. 32. 13.]

[ 19. 25. 24. 13.]

[ 13. 28. 25. 17.]

[ 11. 17. 14. 9.]]

>>> print('SciPy Results:\n',

... scipy.signal.convolve2d(X, W, mode='same'))

SciPy Results:

[[11 25 32 13]

[19 25 24 13]

[13 28 25 17]

[11 17 14 9]]
CopyExplain
Efficient algorithms for computing convolution
We provided a naive implementation to compute a 2D convolution for the purpose
of understanding the concepts. However, this implementation is very inefficient in
terms of memory requirements and computational complexity. Therefore, it should
not be used in real-world NN applications.

One aspect is that the filter matrix is actually not rotated in most tools like PyTorch.
Moreover, in recent years, much more efficient algorithms have been developed
that use the Fourier transform to compute convolutions. It is also important to note
that in the context of NNs, the size of a convolution kernel is usually much smaller
than the size of the input image.

For example, modern CNNs usually use kernel sizes such as 1×1, 3×3, or 5×5, for
which efficient algorithms have been designed that can carry out the convolutional
operations much more efficiently, such as Winograd’s minimal filtering algorithm.
These algorithms are beyond the scope of this book, but if you are interested in
learning more, you can read the manuscript Fast Algorithms for Convolutional
Neural Networks by Andrew Lavin and Scott Gray, 2015, which is freely available
at https://arxiv.org/abs/1509.09308.

In the next section, we will discuss subsampling or pooling, which is another


important operation often used in CNNs.

Subsampling layers
Subsampling is typically applied in two forms of pooling operations in CNNs: max-
pooling and mean-pooling (also known as average-pooling). The pooling layer

is usually denoted by  . Here, the subscript determines the size of the


neighborhood (the number of adjacent pixels in each dimension) where the max or
mean operation is performed. We refer to such a neighborhood as the pooling
size.
The operation is described in Figure 14.8. Here, max-pooling takes the maximum
value from a neighborhood of pixels, and mean-pooling computes their average:
Figure 14.8: An example of max-pooling and mean-pooling

The advantage of pooling is twofold:

 Pooling (max-pooling) introduces a local invariance. This means that small


changes in a local neighborhood do not change the result of max-pooling.
Therefore, it helps with generating features that are more robust to noise in
the input data. Refer to the following example, which shows that the max-
pooling of two different input matrices, X1 and X2, results in the same output:
 Pooling decreases the size of features, which results in higher
computational efficiency. Furthermore, reducing the number of features may
reduce the degree of overfitting as well.
Overlapping versus non-overlapping pooling
Traditionally, pooling is assumed to be non-overlapping. Pooling is typically
performed on non-overlapping neighborhoods, which can be done by setting the
stride parameter equal to the pooling size. For example, a non-overlapping pooling

layer,  , requires a stride parameter s = (n1, n2). On the other hand,


overlapping pooling occurs if the stride is smaller than the pooling size. An
example where overlapping pooling is used in a convolutional network is described
in ImageNet Classification with Deep Convolutional Neural Networks by A.
Krizhevsky, I. Sutskever, and G. Hinton, 2012, which is freely available as a
manuscript at https://papers.nips.cc/paper/4824-imagenet-classification-with-deep-
convolutional-neural-networks.

While pooling is still an essential part of many CNN architectures, several CNN
architectures have also been developed without using pooling layers. Instead of
using pooling layers to reduce the feature size, researchers use convolutional
layers with a stride of 2.

De certa forma, você pode pensar em uma camada convolucional com passada 2
como uma camada de agrupamento com pesos aprendíveis. Se você está
interessado em uma comparação empírica de diferentes arquiteturas CNN
desenvolvidas com e sem camadas de pooling, recomendamos a leitura do artigo
de pesquisa Striving for Simplicity: The All Convolutional Net de Jost Tobias
Springenberg, Alexey Dosovitskiy, Thomas Brox e Martin Riedmiller. Este artigo
está disponível gratuitamente em https://arxiv.org/abs/1412.6806.

Juntando tudo – implementando uma


CNN
Até agora, você aprendeu sobre os blocos de construção básicos das CNNs. Os
conceitos ilustrados neste capítulo não são realmente mais difíceis do que os NNs
multicamadas tradicionais. Podemos dizer que a operação mais importante em um
NN tradicional é a multiplicação matricial. Por exemplo, usamos multiplicações
matriciais para calcular as pré-ativações (ou entradas líquidas), como
em z = Wx + b. Aqui, x é um vetor de coluna (matriz) que representa pixels, e W é

a  matriz de peso que conecta as entradas de pixel a cada unidade


oculta.
Em uma CNN, essa operação é substituída por uma operação de convolução,
como em , onde X é uma matriz que representa os pixels

em  um arranjo altura×largura. Em ambos os


casos, as pré-ativações são passadas para uma função de ativação para obter a

ativação de uma unidade oculta,  onde   está a função de


ativação. Além disso, você vai lembrar que a subamostragem é outro bloco de
construção de uma CNN, que pode aparecer na forma de pooling, como foi
descrito na seção anterior.

Trabalhando com vários canais de entrada ou


cores
Uma entrada para uma camada convolucional pode conter uma ou mais matrizes
ou matrizes 2D com dimensões N1×N2 (por exemplo, a altura e a largura da
imagem em pixels). Estes n1×N2 As matrizes são chamadas de canais.
Implementações convencionais de camadas convolucionais esperam uma
representação de tensor rank-3 como entrada, por exemplo, uma matriz

tridimensional,  onde Cem é o número de canais de


entrada. Por exemplo, vamos considerar imagens como entrada para a primeira
camada de uma CNN. Se a imagem for colorida e usar o modo de cor RGB,
então Cem = 3 (para os canais de cores vermelho, verde e azul em RGB). No
entanto, se a imagem estiver em tons de cinza, então temos Cem = 1, porque há
apenas um canal com os valores de intensidade de pixel em tons de cinza.
Lendo um arquivo de imagem
Quando trabalhamos com imagens, podemos ler imagens em matrizes NumPy
usando o tipo de dados (inteiro de 8 bits não assinado) para reduzir o uso de
memória em comparação com tipos inteiros de 16 bits, 32 bits ou 64 bits, por
exemplo.uint8

Inteiros de 8 bits não assinados recebem valores no intervalo [0, 255], que são
suficientes para armazenar as informações de pixel em imagens RGB, que
também recebem valores no mesmo intervalo.

No Capítulo 12, Paralelizando o treinamento de redes neurais com o PyTorch,


você viu que o PyTorch fornece um módulo para carregar/armazenar e manipular
imagens via . Vamos recapitular como ler uma imagem (este exemplo de imagem
RGB está localizado na pasta do pacote de código fornecida com este
capítulo):torchvision
>>> import torch

>>> from torchvision.io import read_image

>>> img = read_image('example-image.png')

>>> print('Image shape:', img.shape)

Image shape: torch.Size([3, 252, 221])

>>> print('Number of channels:', img.shape[0])

Number of channels: 3

>>> print('Image data type:', img.dtype)

Image data type: torch.uint8

>>> print(img[:, 100:102, 100:102])

tensor([[[179, 182],

[180, 182]],

[[134, 136],

[135, 137]],

[[110, 112],

[111, 113]]], dtype=torch.uint8)


CopyExplain
Observe que com , os tensores de imagem de entrada e saída estão no formato
de .torchvisionTensor[channels, image_height, image_width]
Agora que você está familiarizado com a estrutura dos dados de entrada, a
próxima pergunta é: como podemos incorporar vários canais de entrada na
operação de convolução que discutimos nas seções anteriores? A resposta é
muito simples: realizamos a operação de convolução para cada canal
separadamente e, em seguida, adicionamos os resultados usando a soma
matricial. A convolução associada a cada canal (c) tem sua própria matriz de
kernel como W[:, :, c].

O resultado total da pré-ativação é calculado na seguinte fórmula:

O resultado final, A, é um mapa de feições. Normalmente, uma camada


convolucional de uma CNN tem mais de um mapa de feição. Se usarmos vários
mapas de feição, o tensor do kernel se torna
quadridimensional: width×height×Cem×Cfora. Aqui, largura×altura é o tamanho do
kernel, Cem é o número de canais de entrada, e Cfora é o número de mapas de
recursos de saída. Então, agora vamos incluir o número de mapas de recursos de
saída na fórmula anterior e atualizá-la, da seguinte maneira:

Para concluir nossa discussão sobre convoluções computacionais no contexto de


NNs, vejamos o exemplo na Figura 14.9, que mostra uma camada convolucional,
seguida por uma camada de pooling. Neste exemplo, há três canais de entrada. O
tensor do kernel é quadridimensional. Cada matriz do kernel é denotada
como m1×2, e há três deles, um para cada canal de entrada. Além disso, existem
cinco desses kernels, contabilizando cinco mapas de recursos de saída.
Finalmente, há uma camada de agrupamento para subamostragem dos mapas de
feição:

Figura 14.9: Implementando uma CNN

Quantos parâmetros treináveis existem no exemplo anterior?


Para ilustrar as vantagens da convolução, do compartilhamento de parâmetros e
da conectividade esparsa, vamos trabalhar com um exemplo. A camada
convolucional na rede mostrada na Figura 14.9 é um tensor quadridimensional.
Então, existem m1×2×3×5 parâmetros associados ao kernel. Além disso, há um
vetor de viés para cada mapa de feição de saída da camada convolucional. Assim,
o tamanho do vetor de viés é 5. As camadas de agrupamento não têm nenhum
parâmetro (treinável); Portanto, podemos escrever o seguinte:
m1 × m2 × 3 × 5 + 5
Se o tensor de entrada for de tamanho n1×n2×3, supondo que a convolução seja
realizada com o mesmo modo de preenchimento, então o tamanho dos mapas de
recursos de saída seria n1 × n2 × 5.

Observe que se usarmos uma camada totalmente conectada em vez de uma


camada convolucional, esse número será muito maior. No caso de uma camada
totalmente conectada, o número de parâmetros para que a matriz de peso atinja o
mesmo número de unidades de saída teria sido o seguinte:

(n1 × n2 × 3) × (n1 × n2 × 5) = (n1 × n2)2 × 3 × 5
Além disso, o tamanho do vetor de viés é n1 × n2 × 5 (um elemento de polarização
para cada unidade de saída). Dado que m1 < n1 e m2 < n2, podemos ver que a
diferença no número de parâmetros treináveis é significativa.
Por fim, como já foi mencionado, as operações de convolução normalmente são
realizadas tratando uma imagem de entrada com múltiplos canais de cores como
uma pilha de matrizes; ou seja, realizamos a convolução em cada matriz
separadamente e depois somamos os resultados, como foi ilustrado na figura
anterior. No entanto, as convoluções também podem ser estendidas para volumes
3D se você estiver trabalhando com conjuntos de dados 3D, por exemplo, como
mostrado no artigo VoxNet: A 3D Convolutional Neural Network for Real-Time
Object Recognition de Daniel Maturana e Sebastian Scherer, 2015, que pode ser
acessado
em https://www.ri.cmu.edu/pub_files/2015/9/voxnet_maturana_scherer_iros15.pdf.

Na próxima seção, falaremos sobre como regularizar um NN.

Regularização de um NN com regularização e


abandono L2
Escolher o tamanho de uma rede, se estamos lidando com um NN tradicional
(totalmente conectado) ou uma CNN, sempre foi um problema desafiador. Por
exemplo, o tamanho de uma matriz de peso e o número de camadas precisam ser
ajustados para alcançar um desempenho razoavelmente bom.

Você vai se lembrar do Capítulo 13, Indo mais fundo – A mecânica de PyTorch,


que uma rede simples sem uma camada oculta só poderia capturar um limite de
decisão linear, o que não é suficiente para lidar com um problema exclusivo ou (ou
XOR) ou similar. A capacidade de uma rede refere-se ao nível de complexidade
da função que ela pode aprender a aproximar. Redes pequenas, ou redes com um
número relativamente pequeno de parâmetros, têm uma baixa capacidade e,
portanto, são propensas a subajustar, resultando em baixo desempenho, uma vez
que não podem aprender a estrutura subjacente de conjuntos de dados
complexos. No entanto, redes muito grandes podem resultar em overfitting, onde a
rede memorizará os dados de treinamento e se sairá extremamente bem no
conjunto de dados de treinamento, enquanto alcança um desempenho ruim no
conjunto de dados de teste mantido. Quando lidamos com problemas de
aprendizado de máquina do mundo real, não sabemos quão grande a rede deve
ser a priori.

Uma maneira de resolver esse problema é construir uma rede com uma
capacidade relativamente grande (na prática, queremos escolher uma capacidade
um pouco maior do que a necessária) para se sair bem no conjunto de dados de
treinamento. Então, para evitar o overfitting, podemos aplicar um ou vários
esquemas de regularização para alcançar um bom desempenho de generalização
em novos dados, como o conjunto de dados de teste retido.

Nos capítulos 3 e 4, abordamos a regularização L1 e L2. Ambas as técnicas


podem prevenir ou reduzir o efeito do overfitting, adicionando uma penalidade à
perda que resulta na redução dos parâmetros de peso durante o treinamento.
Embora tanto a regularização L1 quanto a L2 também possam ser usadas para
NNs, sendo L2 a escolha mais comum das duas, existem outros métodos para
regularizar NNs, como o abandono, que discutimos nesta seção. Mas antes de
passarmos a discutir o abandono, para usar a regularização L2 dentro de uma
rede convolucional ou totalmente conectada (lembre-se, camadas totalmente
conectadas são implementadas via no PyTorch), você pode simplesmente
adicionar a penalidade L2 de uma camada específica à função de perda no
PyTorch, da seguinte maneira:torch.nn.Linear
>>> import torch.nn as nn

>>> loss_func = nn.BCELoss()

>>> loss = loss_func(torch.tensor([0.9]), torch.tensor([1.0]))

>>> l2_lambda = 0.001

>>> conv_layer = nn.Conv2d(in_channels=3,

... out_channels=5,

... kernel_size=5)

>>> l2_penalty = l2_lambda * sum(

... [(p**2).sum() for p in conv_layer.parameters()]

... )
>>> loss_with_penalty = loss + l2_penalty

>>> linear_layer = nn.Linear(10, 16)

>>> l2_penalty = l2_lambda * sum(

... [(p**2).sum() for p in linear_layer.parameters()]

... )

>>> loss_with_penalty = loss + l2_penalty


CopyExplain
Decadência de peso versus regularização L2
Uma maneira alternativa de usar a regularização L2 é definindo o parâmetro em
um otimizador PyTorch para um valor positivo, por exemplo: weight_decay
optimizer = torch.optim.SGD(

model.parameters(),

weight_decay=l2_lambda,

...

)
CopyExplain
Embora a regularização L2 e não sejam estritamente idênticas, pode-se mostrar
que elas são equivalentes quando se usam otimizadores de descida de gradiente
estocástico (SGD). Os leitores interessados podem encontrar mais informações
no artigo Decoupled Weight Decay
Regularization de Ilya Loshchilov e Frank Hutter, 2019, que está disponível
gratuitamente em https://arxiv.org/abs/1711.05101.weight_decay
Nos últimos anos, o abandono surgiu como uma técnica popular para regularizar
NNs (profundos) para evitar o overfitting, melhorando assim o desempenho de
generalização (Dropout: A Simple Way to Prevent Neural Networks from
Overfitting por N. Srivastava, G. Hinton, A. Krizhevsky, I. Sutskever, e
R. Salakhutdinov, Journal of Machine Learning Research 15.1, páginas 1929-
1958,
2014, http://www.jmlr.org/papers/volume15/srivastava14a/srivastava14a.pdf). O
abandono é geralmente aplicado às unidades ocultas das camadas superiores e
funciona da seguinte forma: durante a fase de treinamento de um NN, uma fração
das unidades ocultas é descartada aleatoriamente a cada iteração com
probabilidade pdeixar cair (ou manter probabilidade pguardar = 1 – pdeixar cair). Essa
probabilidade de desistência é determinada pelo usuário e a escolha comum é p =
0,5, conforme discutido no artigo mencionado anteriormente por Nitish
Srivastava e outros, 2014. Ao soltar uma certa fração de neurônios de entrada, os
pesos associados aos neurônios restantes são redimensionados para dar conta
dos neurônios ausentes (caídos).

O efeito dessa desistência aleatória é que a rede é forçada a aprender uma


representação redundante dos dados. Portanto, a rede não pode contar com a
ativação de qualquer conjunto de unidades ocultas, uma vez que elas podem ser
desligadas a qualquer momento durante o treinamento, e é forçada a aprender
padrões mais gerais e robustos a partir dos dados.

Essa desistência aleatória pode efetivamente evitar o overfitting. A figura


14.10 mostra um exemplo de aplicação de dropout com probabilidade p = 0,5
durante a fase de treinamento, em que metade dos neurônios se tornará inativa
aleatoriamente (unidades soltas são selecionadas aleatoriamente em cada passe
avançado de treinamento). No entanto, durante a previsão, todos os neurônios
contribuirão para calcular as pré-ativações da próxima camada:

Figura 14.10: Aplicação do abandono durante a fase de formação

Como mostrado aqui, um ponto importante a ser lembrado é que as unidades


podem cair aleatoriamente apenas durante o treinamento, enquanto para a fase
de avaliação (inferência), todas as unidades ocultas devem estar ativas (por
exemplo, pdeixar cair = 0 ou pguardar = 1). Para garantir que as ativações globais
estejam na mesma escala durante o treinamento e a previsão, as ativações dos
neurônios ativos devem ser dimensionadas adequadamente (por exemplo,
reduzindo pela metade a ativação se a probabilidade de abandono foi definida
como p = 0,5).
No entanto, como é inconveniente sempre escalar ativações ao fazer previsões, o
PyTorch e outras ferramentas dimensionam as ativações durante o treinamento
(por exemplo, dobrando as ativações se a probabilidade de desistência foi definida
como p = 0,5). Essa abordagem é comumente referida como abandono inverso.
Embora a relação não seja imediatamente óbvia, a desistência pode ser
interpretada como o consenso (média) de um conjunto de modelos. Como
discutido no Capítulo 7, Combinando Diferentes Modelos para a Aprendizagem
em Conjunto, na aprendizagem em conjunto, treinamos vários modelos de forma
independente. Durante a previsão, usamos o consenso de todos os modelos
treinados. Já sabemos que os conjuntos de modelos são conhecidos por terem um
desempenho melhor do que os modelos individuais. No aprendizado profundo, no
entanto, tanto treinar vários modelos quanto coletar e calcular a média da saída
de vários modelos é computacionalmente caro. Aqui, a desistência oferece uma
solução alternativa, com uma maneira eficiente de treinar muitos modelos ao
mesmo tempo e calcular suas previsões médias no momento do teste ou da
previsão.

Como mencionado anteriormente, a relação entre conjuntos de modelos e


desistência não é imediatamente óbvia. No entanto, considere que, no dropout,
temos um modelo diferente para cada mini-lote (devido à definição dos pesos para
zero aleatoriamente durante cada passagem para frente).

Então, através da iteração sobre os mini-lotes, nós essencialmente amostramos


sobre M = 2h modelos, onde h é o número de unidades ocultas.

A restrição e o aspecto que distingue a evasão do agrupamento regular, no


entanto, é que dividimos os pesos sobre esses "diferentes modelos", o que pode
ser visto como uma forma de regularização. Então, durante a "inferência" (por
exemplo, prevendo os rótulos no conjunto de dados de teste), podemos fazer uma
média de todos esses diferentes modelos que amostramos durante o treinamento.
Isso é muito caro, no entanto.
Então, calculando a média dos modelos, isto é, calculando-se a média geométrica
da probabilidade de associação de classe que é retornada por um modelo, i, pode
ser calculada da seguinte forma:

Agora, o truque por trás da desistência é que essa média geométrica dos
conjuntos de modelos (aqui, modelos M) pode ser aproximada escalando as
previsões do último (ou final) modelo amostrado durante o treinamento por um
fator de 1/(1 – p), o que é muito mais barato do que calcular a média geométrica
explicitamente usando a equação anterior. (Na verdade, a aproximação é
exatamente equivalente à média geométrica verdadeira se considerarmos
modelos lineares.)

Funções de perda para classificação


No Capítulo 12, Paralelizando o Treinamento de Redes Neurais com o PyTorch,
vimos diferentes funções de ativação, como ReLU, sigmoide e tanh. Algumas
dessas funções de ativação, como ReLU, são usadas principalmente nas
camadas intermediárias (ocultas) de um NN para adicionar não-linearidades ao
nosso modelo. Mas outros, como sigmoid (para binário) e softmax (para
multiclasse), são adicionados na última camada (saída), o que resulta em
probabilidades de associação de classe como a saída do modelo. Se as ativações
sigmoid ou softmax não forem incluídas na camada de saída, o modelo calculará
os logits em vez das probabilidades de associação de classe.
Focando em problemas de classificação aqui, dependendo do tipo de problema
(binário versus multiclasse) e do tipo de saída (logits versus probabilidades),
devemos escolher a função de perda apropriada para treinar nosso modelo.
Entropia cruzada binária é a função de perda para uma classificação binária (com
uma única unidade de saída), e entropia cruzada categórica é a função de perda
para classificação multiclasse. No módulo, a perda categórica de entropia cruzada
recebe rótulos de verdade fundamental como inteiros (por exemplo, y=2, de três
classes, 0, 1 e 2).torch.nn
A figura 14.11 descreve duas funções de perda disponíveis para lidar com ambos
os casos: classificação binária e multiclasse com rótulos inteiros. Cada uma
dessas duas funções de perda também tem a opção de receber as previsões na
forma de logits ou probabilidades de associação de classe: torch.nn

Figura 14.11: Dois exemplos de funções de perda no PyTorch

Observe que calcular a perda de entropia cruzada fornecendo os logits, e não as


probabilidades de associação de classe, é geralmente preferido devido a razões
de estabilidade numérica. Para classificação binária, podemos fornecer logits
como entradas para a função de perda , ou calcular as probabilidades com base
nos logits e alimentá-los para a função de perda . Para classificação multiclasse,
podemos fornecer logits como entradas para a função de perda , ou calcular as
probabilidades de log com base nos logits e alimentá-los para a função de perda
de log-probabilidade
negativa .nn.BCEWithLogitsLoss()nn.BCELoss()nn.CrossEntropyLoss()nn.NLLLoss()

O código a seguir mostrará como usar essas funções de perda com dois formatos
diferentes, onde os logits ou probabilidades de associação de classe são dados
como entradas para as funções de perda:

>>> ####### Binary Cross-entropy

>>> logits = torch.tensor([0.8])


>>> probas = torch.sigmoid(logits)

>>> target = torch.tensor([1.0])

>>> bce_loss_fn = nn.BCELoss()

>>> bce_logits_loss_fn = nn.BCEWithLogitsLoss()

>>> print(f'BCE (w Probas): {bce_loss_fn(probas, target):.4f}')

BCE (w Probas): 0.3711

>>> print(f'BCE (w Logits): '

... f'{bce_logits_loss_fn(logits, target):.4f}')

BCE (w Logits): 0.3711

>>> ####### Categorical Cross-entropy

>>> logits = torch.tensor([[1.5, 0.8, 2.1]])

>>> probas = torch.softmax(logits, dim=1)

>>> target = torch.tensor([2])

>>> cce_loss_fn = nn.NLLLoss()

>>> cce_logits_loss_fn = nn.CrossEntropyLoss()

>>> print(f'CCE (w Logits): '

... f'{cce_logits_loss_fn(logits, target):.4f}')

CCE (w Probas): 0.5996

>>> print(f'CCE (w Probas): '

... f'{cce_loss_fn(torch.log(probas), target):.4f}')

CCE (w Logits): 0.5996


CopyExplain
Observe que, às vezes, você pode se deparar com uma implementação onde uma
perda de entropia cruzada categórica é usada para classificação binária.
Normalmente, quando temos uma tarefa de classificação binária, o modelo retorna
um único valor de saída para cada exemplo. Interpretamos essa saída de modelo
único como a probabilidade da classe positiva (por exemplo, classe 1), P(classe =
1|x). Em um problema de classificação binária, está implícito que P(classe = 0|x)=
1 – P(classe = 1|x); portanto, não precisamos de uma segunda unidade de saída
para obter a probabilidade da classe negativa. No entanto, às vezes os praticantes
optam por retornar duas saídas para cada exemplo de treinamento e interpretá-las
como probabilidades de cada classe: P(classe = 0|x) versus P(classe = 1|x).
Então, nesse caso, recomenda-se o uso de uma função softmax (em vez do
sigmoide logístico) para normalizar as saídas (de modo que elas somem 1), e a
entropia cruzada categórica é a função de perda apropriada.

Implementando uma CNN profunda


usando PyTorch
No Capítulo 13, como você deve se lembrar, resolvemos o problema de
reconhecimento de dígitos manuscritos usando o módulo. Você também deve se
lembrar que alcançamos cerca de 95,6% de precisão usando um NN com duas
camadas ocultas lineares.torch.nn
Agora, vamos implementar uma CNN e ver se ela pode alcançar um melhor
desempenho preditivo em comparação com o modelo anterior para classificar
dígitos manuscritos. Observe que as camadas totalmente conectadas que vimos
no Capítulo 13 foram capazes de ter um bom desempenho nesse problema. No
entanto, em alguns aplicativos, como a leitura de números de contas bancárias a
partir de dígitos manuscritos, mesmo pequenos erros podem custar muito caro.
Portanto, é crucial reduzir esse erro o máximo possível.

A arquitetura CNN multicamada


A arquitetura da rede que vamos implementar é mostrada na Figura 14.12. As
entradas são 28×28 imagens em tons de cinza. Considerando o número de canais
(que é 1 para imagens em tons de cinza) e um lote de imagens de entrada, as
dimensões do tensor de entrada serão batchsize×28×28×1.
Os dados de entrada passam por duas camadas convolucionais que têm um
tamanho de kernel de 5×5. A primeira convolução tem 32 mapas de recursos de
saída, e a segunda tem 64 mapas de recursos de saída. Cada camada de
convolução é seguida por uma camada de subamostragem na forma de uma
operação de agrupamento máximo, P2×2. Em seguida, uma camada totalmente
conectada passa a saída para uma segunda camada totalmente conectada, que
atua como a camada de saída softmax final. A arquitetura da rede que vamos
implementar é mostrada na Figura 14.12:

Figura 14.12: Uma CNN profunda

As dimensões dos tensores em cada camada são as seguintes:

 Entrada: [tamanho do lote×28×28×1]


 Conv_1: [tamanho do lote×28×28×32]
 Pooling_1: [tamanho do lote×14×14×32]
 Conv_2: [tamanho do lote×14×14×64]
 Pooling_2: [tamanho do lote×7×7×64]
 FC_1: [tamanho do lote×1024]
 FC_2 e camada softmax: [batchsize×10]
Para os núcleos convolucionais, estamos usando tal que as dimensões de entrada
são preservadas nos mapas de feição resultantes. Para as camadas de pooling,
estamos usando para subamostrar a imagem e reduzir o tamanho dos mapas de
feição de saída. Vamos implementar esta rede usando o módulo PyTorch
NN.stride=1kernel_size=2

Carregamento e pré-processamento dos dados


Primeiro, carregaremos o conjunto de dados MNIST usando o módulo e
construiremos os conjuntos de treinamento e teste, como fizemos no Capítulo
13:torchvision
>>> import torchvision

>>> from torchvision import transforms

>>> image_path = './'


>>> transform = transforms.Compose([

... transforms.ToTensor()

... ])

>>> mnist_dataset = torchvision.datasets.MNIST(

... root=image_path, train=True,

... transform=transform, download=True

... )

>>> from torch.utils.data import Subset

>>> mnist_valid_dataset = Subset(mnist_dataset,

... torch.arange(10000))

>>> mnist_train_dataset = Subset(mnist_dataset,

... torch.arange(

... 10000, len(mnist_dataset)

... ))

>>> mnist_test_dataset = torchvision.datasets.MNIST(

... root=image_path, train=False,

... transform=transform, download=False

... )
CopyExplain

O conjunto de dados MNIST vem com um esquema de particionamento de


conjunto de dados de treinamento e teste pré-especificado, mas também
queremos criar uma divisão de validação a partir da partição train. Assim,
utilizamos os primeiros 10.000 exemplos de treinamento para validação. Observe
que as imagens não são classificadas por rótulo de classe, portanto, não
precisamos nos preocupar se essas imagens do conjunto de validação são das
mesmas classes.

Em seguida, construiremos o carregador de dados com lotes de 64 imagens para


o conjunto de treinamento e conjunto de validação, respectivamente:
>>> from torch.utils.data import DataLoader

>>> batch_size = 64

>>> torch.manual_seed(1)

>>> train_dl = DataLoader(mnist_train_dataset,

... batch_size,

... shuffle=True)

>>> valid_dl = DataLoader(mnist_valid_dataset,

... batch_size,

... shuffle=False)
CopyExplain

As características que lemos são de valores no intervalo [0, 1]. Além disso, já
convertemos as imagens em tensores. Os rótulos são inteiros de 0 a 9,
representando dez dígitos. Portanto, não precisamos fazer nenhum
dimensionamento ou conversão adicional.

Agora, depois de preparar o conjunto de dados, estamos prontos para


implementar a CNN que acabamos de descrever.

Implementando uma CNN usando o módulo


torch.nn
Para implementar um CNN no PyTorch, usamos a classe para empilhar diferentes
camadas, como convolução, agrupamento e dropout, bem como as camadas
totalmente conectadas. O módulo fornece classes para cada um: para uma
camada de convolução bidimensional; e para subamostragem (max-pooling e
average-pooling); e para regularização com recurso à evasão. Vamos analisar
cada uma dessas classes com mais
detalhes.torch.nnSequentialtorch.nnnn.Conv2dnn.MaxPool2dnn.AvgPool2dnn.Dropout
Configurando camadas CNN no PyTorch
Construir uma camada com a classe requer que especifiquemos o número de
canais de saída (que é equivalente ao número de mapas de feição de saída, ou o
número de filtros de saída) e tamanhos de kernel.Conv2d
Além disso, existem parâmetros opcionais que podemos usar para configurar uma
camada convolucional. Os mais usados são os passos (com um valor padrão de 1
em ambas as dimensões x, y) e o preenchimento, que controla a quantidade de
preenchimento implícito em ambas as dimensões. Parâmetros de configuração
adicionais estão listados na documentação
oficial: https://pytorch.org/docs/stable/generated/torch.nn.Conv2d.html.
Vale ressaltar que, geralmente, quando lemos uma imagem, a dimensão padrão
para os canais é a primeira dimensão da matriz tensorial (ou a segunda dimensão
considerando a dimensão do lote). Isso é chamado de formato NCHW,
onde N representa o número de imagens dentro do lote, C significa canais
e H e W representam altura e largura, respectivamente.
Observe que a classe assume que as entradas estão no formato NCHW por
padrão. (Outras ferramentas, como TensorFlow, usam o formato NHWC.) No
entanto, se você se deparar com alguns dados cujos canais são colocados na
última dimensão, você precisaria trocar os eixos em seus dados para mover os
canais para a primeira dimensão (ou a segunda dimensão considerando a
dimensão do lote). Depois que a camada é construída, ela pode ser chamada
fornecendo um tensor de quatro dimensões, com a primeira dimensão reservada
para um lote de exemplos; a segunda dimensão corresponde ao canal; e as outras
duas dimensões são as dimensões espaciais.Conv2D
Como mostrado na arquitetura do modelo CNN que queremos construir, cada
camada de convolução é seguida por uma camada de agrupamento para
subamostragem (reduzindo o tamanho dos mapas de feição). As classes e
constroem as camadas max-pooling e average-pooling, respectivamente. O
argumento determina o tamanho da janela (ou vizinhança) que será usada para
calcular as operações max ou média. Além disso, o parâmetro pode ser usado
para configurar a camada de pooling, como discutimos
anteriormente.MaxPool2dAvgPool2dkernel_sizestride
Por fim, a turma construirá a camada de desistência para regularização, com o
argumento que denota a probabilidade de queda pDropoutpdeixar cair, que é usado
para determinar a probabilidade de queda das unidades de entrada durante o
treinamento, como discutimos anteriormente. Ao chamar essa camada, seu
comportamento pode ser controlado via e , para especificar se essa chamada será
feita durante o treinamento ou durante a inferência. Ao usar o dropout, alternar
entre esses dois modos é crucial para garantir que ele se comporte corretamente;
por exemplo, nós são descartados apenas aleatoriamente durante o treinamento,
não avaliação ou inferência.model.train()model.eval()

Construindo uma CNN em PyTorch


Agora que você aprendeu sobre essas classes, podemos construir o modelo da
CNN que foi mostrado na figura anterior. No código a seguir, usaremos a classe e
adicionaremos as camadas de convolução e pooling: Sequential
>>> model = nn.Sequential()

>>> model.add_module(

... 'conv1',

... nn.Conv2d(

... in_channels=1, out_channels=32,

... kernel_size=5, padding=2

... )

... )

>>> model.add_module('relu1', nn.ReLU())

>>> model.add_module('pool1', nn.MaxPool2d(kernel_size=2))

>>> model.add_module(

... 'conv2',

... nn.Conv2d(

... in_channels=32, out_channels=64,

... kernel_size=5, padding=2

... )

... )

>>> model.add_module('relu2', nn.ReLU())

>>> model.add_module('pool2', nn.MaxPool2d(kernel_size=2))


CopyExplain
Até agora, adicionamos duas camadas de convolução ao modelo. Para cada
camada convolucional, utilizou-se um kernel de tamanho 5×5 e . Como discutido
anteriormente, o uso do mesmo modo de preenchimento preserva as dimensões
espaciais (dimensões verticais e horizontais) dos mapas de feição, de modo que
as entradas e saídas tenham a mesma altura e largura (e o número de canais só
pode diferir em termos do número de filtros usados). Como mencionado
anteriormente, a dimensão espacial do mapa de feição de saída é calculada
por:padding=2

onde n é a dimensão espacial do mapa de feição de entrada, e p, m e s denotam


o preenchimento, o tamanho do kernel e a passada, respectivamente.
Obtemos p = 2 para atingir o = i.
As camadas de agrupamento máximo com tamanho de agrupamento 2×2 e passo
de 2 reduzirão as dimensões espaciais pela metade. (Observe que, se o
parâmetro não for especificado no , por padrão, ele será definido como igual ao
tamanho do kernel do pool.)strideMaxPool2D

Embora possamos calcular o tamanho dos mapas de recursos neste estágio


manualmente, o PyTorch fornece um método conveniente para calcular isso para
nós:

>>> x = torch.ones((4, 1, 28, 28))

>>> model(x).shape

torch.Size([4, 64, 7, 7])


CopyExplain
Ao fornecer a forma de entrada como uma tupla (4 imagens dentro do lote, 1 canal
e tamanho de imagem 28×28), especificada neste exemplo, calculamos a saída
para ter uma forma, indicando mapas de feição com 64 canais e um tamanho
espacial de 7×7. A primeira dimensão corresponde à dimensão do lote, para a
qual usamos 4 arbitrariamente.(4, 1, 28, 28)(4, 64, 7, 7)
A próxima camada que queremos adicionar é uma camada totalmente conectada
para implementar um classificador em cima de nossas camadas convolucionais e
de pooling. A entrada para essa camada deve ter rank 2, ou seja, shape
[batchsize × input_units]. Assim, precisamos achatar a saída das camadas
anteriores para atender a esse requisito para a camada totalmente conectada:
>>> model.add_module('flatten', nn.Flatten())

>>> x = torch.ones((4, 1, 28, 28))

>>> model(x).shape

torch.Size([4, 3136])
CopyExplain

Como a forma de saída indica, as dimensões de entrada para a camada


totalmente conectada estão configuradas corretamente. Em seguida,
adicionaremos duas camadas totalmente conectadas com uma camada de
dropout entre elas:

>>> model.add_module('fc1', nn.Linear(3136, 1024))

>>> model.add_module('relu3', nn.ReLU())

>>> model.add_module('dropout', nn.Dropout(p=0.5))

>>> model.add_module('fc2', nn.Linear(1024, 10))


CopyExplain
A última camada totalmente conectada, chamada , tem 10 unidades de saída para
os rótulos de 10 classes no conjunto de dados MNIST. Na prática, geralmente
usamos a ativação sofmax para obter as probabilidades de associação de classe
de cada exemplo de entrada, assumindo que as classes são mutuamente
exclusivas, de modo que as probabilidades para cada exemplo somam 1. No
entanto, a função softmax já é usada internamente dentro da implementação do
PyTorch, e é por isso que não precisa adicioná-la explicitamente como uma
camada após a camada de saída acima. O código a seguir criará a função de
perda e o otimizador para o modelo:'fc2'CrossEntropyLoss
>>> loss_fn = nn.CrossEntropyLoss()

>>> optimizer = torch.optim.Adam(model.parameters(), lr=0.001)


CopyExplain
O otimizador Adam
Note que nesta implementação, utilizamos a classe para treinamento do modelo
CNN. O otimizador Adam é um método de otimização robusto, baseado em
gradiente, adequado para otimização não convexa e problemas de aprendizado de
máquina. Dois métodos populares de otimização inspiraram Adam:
e .torch.optim.AdamRMSPropAdaGrad
A principal vantagem de Adam está na escolha do tamanho da etapa de
atualização derivado da média de execução dos momentos de gradiente. Por
favor, sinta-se livre para ler mais sobre o otimizador Adam no manuscrito, Adam:
A Method for Stochastic Optimization por Diederik P. Kingma e Jimmy Lei Ba,
2014. O artigo está disponível gratuitamente em https://arxiv.org/abs/1412.6980.

Agora podemos treinar o modelo definindo a seguinte função:

>>> def train(model, num_epochs, train_dl, valid_dl):

... loss_hist_train = [0] * num_epochs

... accuracy_hist_train = [0] * num_epochs

... loss_hist_valid = [0] * num_epochs

... accuracy_hist_valid = [0] * num_epochs

... for epoch in range(num_epochs):

... model.train()

... for x_batch, y_batch in train_dl:

... pred = model(x_batch)

... loss = loss_fn(pred, y_batch)

... loss.backward()

... optimizer.step()

... optimizer.zero_grad()

... loss_hist_train[epoch] += loss.item()*y_batch.size(0)

... is_correct = (

... torch.argmax(pred, dim=1) == y_batch

... ).float()
... accuracy_hist_train[epoch] += is_correct.sum()

... loss_hist_train[epoch] /= len(train_dl.dataset)

... accuracy_hist_train[epoch] /= len(train_dl.dataset)

...

... model.eval()

... with torch.no_grad():

... for x_batch, y_batch in valid_dl:

... pred = model(x_batch)

... loss = loss_fn(pred, y_batch)

... loss_hist_valid[epoch] += \

... loss.item()*y_batch.size(0)

... is_correct = (

... torch.argmax(pred, dim=1) == y_batch

... ).float()

... accuracy_hist_valid[epoch] += is_correct.sum()

... loss_hist_valid[epoch] /= len(valid_dl.dataset)

... accuracy_hist_valid[epoch] /= len(valid_dl.dataset)

...

... print(f'Epoch {epoch+1} accuracy: '

... f'{accuracy_hist_train[epoch]:.4f} val_accuracy: '

... f'{accuracy_hist_valid[epoch]:.4f}')

... return loss_hist_train, loss_hist_valid, \

... accuracy_hist_train, accuracy_hist_valid


CopyExplain
Note that using the designated settings for training and evaluation will
automatically set the mode for the dropout layer and rescale the hidden units
appropriately so that we do not have to worry about that at all. Next, we will train
this CNN model and use the validation dataset that we created for monitoring the
learning progress:model.train()model.eval()
>>> torch.manual_seed(1)

>>> num_epochs = 20

>>> hist = train(model, num_epochs, train_dl, valid_dl)

Epoch 1 accuracy: 0.9503 val_accuracy: 0.9802

...

Epoch 9 accuracy: 0.9968 val_accuracy: 0.9892

...

Epoch 20 accuracy: 0.9979 val_accuracy: 0.9907


CopyExplain

Once the 20 epochs of training are finished, we can visualize the learning curves:

>>> import matplotlib.pyplot as plt

>>> x_arr = np.arange(len(hist[0])) + 1

>>> fig = plt.figure(figsize=(12, 4))

>>> ax = fig.add_subplot(1, 2, 1)

>>> ax.plot(x_arr, hist[0], '-o', label='Train loss')

>>> ax.plot(x_arr, hist[1], '--<', label='Validation loss')

>>> ax.legend(fontsize=15)

>>> ax = fig.add_subplot(1, 2, 2)

>>> ax.plot(x_arr, hist[2], '-o', label='Train acc.')

>>> ax.plot(x_arr, hist[3], '--<',

... label='Validation acc.')

>>> ax.legend(fontsize=15)

>>> ax.set_xlabel('Epoch', size=15)

>>> ax.set_ylabel('Accuracy', size=15)

>>> plt.show()
CopyExplain

Figure 14.13: Loss and accuracy graphs for the training and validation data

Now, we evaluate the trained model on the test dataset:

>>> pred = model(mnist_test_dataset.data.unsqueeze(1) / 255.)

>>> is_correct = (

... torch.argmax(pred, dim=1) == mnist_test_dataset.targets

... ).float()

>>> print(f'Test accuracy: {is_correct.mean():.4f}')

Test accuracy: 0.9914


CopyExplain
The CNN model achieves an accuracy of 99.07 percent. Remember that
in Chapter 13, we got approximately 95 percent accuracy using only fully
connected (instead of convolutional) layers.
Finally, we can get the prediction results in the form of class-
membership probabilities and convert them to predicted labels by using
the function to find the element with the maximum probability. We will do this for a
batch of 12 examples and visualize the input and predicted labels: torch.argmax
>>> fig = plt.figure(figsize=(12, 4))

>>> for i in range(12):

... ax = fig.add_subplot(2, 6, i+1)

... ax.set_xticks([]); ax.set_yticks([])


... img = mnist_test_dataset[i][0][0, :, :]

... pred = model(img.unsqueeze(0).unsqueeze(1))

... y_pred = torch.argmax(pred)

... ax.imshow(img, cmap='gray_r')

... ax.text(0.9, 0.1, y_pred.item(),

... size=15, color='blue',

... horizontalalignment='center',

... verticalalignment='center',

... transform=ax.transAxes)

>>> plt.show()
CopyExplain
Figure 14.14 shows the handwritten inputs and their predicted labels:

Figure 14.14: Predicted labels for handwritten digits

In this set of plotted examples, all the predicted labels are correct.

Deixamos a tarefa de mostrar alguns dos dígitos mal classificados, como fizemos
no Capítulo 11, Implementando uma Rede Neural Artificial Multicamadas do Zero,
como um exercício para o leitor.

Classificação do sorriso a partir de


imagens de rosto usando uma CNN
Nesta seção, vamos implementar uma CNN para classificação de sorriso a partir
de imagens de rosto usando o conjunto de dados CelebA. Como você viu
no Capítulo 12, o conjunto de dados CelebA contém 202.599 imagens de rostos
de celebridades. Além disso, 40 atributos faciais binários estão disponíveis para
cada imagem, incluindo se uma celebridade está sorrindo (ou não) e sua idade
(jovem ou velha).
Com base no que você aprendeu até agora, o objetivo desta seção é construir e
treinar um modelo da CNN para prever o atributo sorriso a partir dessas imagens
de rosto. Aqui, para simplificar, usaremos apenas uma pequena parte dos dados
de treinamento (16.000 exemplos de treinamento) para acelerar o processo de
treinamento. No entanto, para melhorar o desempenho de generalização e reduzir
o overfitting em um conjunto de dados tão pequeno, usaremos uma técnica
chamada aumento de dados.

Carregando o conjunto de dados CelebA


Primeiro, vamos carregar os dados de forma semelhante ao que fizemos na seção
anterior para o conjunto de dados MNIST. Os dados CelebA vêm em três
partições: um conjunto de dados de treinamento, um conjunto de dados de
validação e um conjunto de dados de teste. Em seguida, contaremos o número de
exemplos em cada partição:

>>> image_path = './'

>>> celeba_train_dataset = torchvision.datasets.CelebA(

... image_path, split='train',

... target_type='attr', download=True

... )

>>> celeba_valid_dataset = torchvision.datasets.CelebA(

... image_path, split='valid',

... target_type='attr', download=True

... )

>>> celeba_test_dataset = torchvision.datasets.CelebA(


... image_path, split='test',

... target_type='attr', download=True

... )

>>>

>>> print('Train set:', len(celeba_train_dataset))

Train set: 162770

>>> print('Validation set:', len(celeba_valid_dataset))

Validation: 19867

>>> print('Test set:', len(celeba_test_dataset))

Test set: 19962


CopyExplain
Maneiras alternativas de baixar o conjunto de dados CelebA
O conjunto de dados CelebA é relativamente grande (aproximadamente 1,5 GB) e
o link de download é notoriamente instável. Se você encontrar problemas para
executar o código anterior, você pode baixar os arquivos do site oficial do CelebA
manualmente (https://mmlab.ie.cuhk.edu.hk/projects/CelebA.html) ou usar nosso
link de download: https://drive.google.com/file/d/1m8-
EBPgi5MRubrm6iQjafK2QMHDBMSfJ/view?usp=sharing. Se você usar nosso link
de download, ele baixará um arquivo, que você precisa descompactar no diretório
atual onde você está executando o código. Além disso, depois de baixar e
descompactar a pasta, você precisa executar novamente o código acima com a
configuração em vez de . Caso você esteja encontrando problemas com essa
abordagem, não hesite em abrir uma nova edição ou iniciar uma discussão
no https://github.com/rasbt/machine-learning-book para que possamos fornecer
informações adicionais.torchvisionceleba.zipcelebadownload=Falsedownload=True

Em seguida, discutiremos o aumento de dados como uma técnica para aumentar


o desempenho de NNs profundos.

Transformação de imagens e aumento de dados


O aumento de dados resume um amplo conjunto de técnicas para lidar com casos
em que os dados de treinamento são limitados. Por exemplo, certas técnicas de
aumento de dados nos permitem modificar ou até mesmo sintetizar artificialmente
mais dados e, assim, aumentar o desempenho de um modelo de aprendizado de
máquina ou profundo, reduzindo o overfitting. Embora o aumento de dados não
seja apenas para dados de imagem, há um conjunto de transformações
exclusivamente aplicáveis aos dados de imagem, como cortar partes de uma
imagem, inverter e alterar o contraste, o brilho e a saturação. Vamos ver algumas
dessas transformações que estão disponíveis através do módulo. No bloco de
código a seguir, primeiro obteremos cinco exemplos do conjunto de dados e
aplicaremos cinco tipos diferentes de transformação: 1) cortar uma imagem para
uma caixa delimitadora, 2) inverter uma imagem horizontalmente, 3) ajustar o
contraste, 4) ajustar o brilho e 5) centralizar uma imagem e redimensionar a
imagem resultante de volta ao seu tamanho original, (218, 178). No código a
seguir, visualizaremos os resultados dessas transformações, mostrando cada uma
delas em uma coluna separada para
comparação:torchvision.transformsceleba_train_dataset
>>> fig = plt.figure(figsize=(16, 8.5))

>>> ## Column 1: cropping to a bounding-box

>>> ax = fig.add_subplot(2, 5, 1)

>>> img, attr = celeba_train_dataset[0]

>>> ax.set_title('Crop to a \nbounding-box', size=15)

>>> ax.imshow(img)

>>> ax = fig.add_subplot(2, 5, 6)

>>> img_cropped = transforms.functional.crop(img, 50, 20, 128, 128)

>>> ax.imshow(img_cropped)

>>>

>>> ## Column 2: flipping (horizontally)

>>> ax = fig.add_subplot(2, 5, 2)

>>> img, attr = celeba_train_dataset[1]

>>> ax.set_title('Flip (horizontal)', size=15)

>>> ax.imshow(img)
>>> ax = fig.add_subplot(2, 5, 7)

>>> img_flipped = transforms.functional.hflip(img)

>>> ax.imshow(img_flipped)

>>>

>>> ## Column 3: adjust contrast

>>> ax = fig.add_subplot(2, 5, 3)

>>> img, attr = celeba_train_dataset[2]

>>> ax.set_title('Adjust constrast', size=15)

>>> ax.imshow(img)

>>> ax = fig.add_subplot(2, 5, 8)

>>> img_adj_contrast = transforms.functional.adjust_contrast(

... img, contrast_factor=2

... )

>>> ax.imshow(img_adj_contrast)

>>>

>>> ## Column 4: adjust brightness

>>> ax = fig.add_subplot(2, 5, 4)

>>> img, attr = celeba_train_dataset[3]

>>> ax.set_title('Adjust brightness', size=15)

>>> ax.imshow(img)

>>> ax = fig.add_subplot(2, 5, 9)

>>> img_adj_brightness = transforms.functional.adjust_brightness(

... img, brightness_factor=1.3

... )

>>> ax.imshow(img_adj_brightness)

>>>
>>> ## Column 5: cropping from image center

>>> ax = fig.add_subplot(2, 5, 5)

>>> img, attr = celeba_train_dataset[4]

>>> ax.set_title('Center crop\nand resize', size=15)

>>> ax.imshow(img)

>>> ax = fig.add_subplot(2, 5, 10)

>>> img_center_crop = transforms.functional.center_crop(

... img, [0.7*218, 0.7*178]

... )

>>> img_resized = transforms.functional.resize(

... img_center_crop, size=(218, 178)

... )

>>> ax.imshow(img_resized)

>>> plt.show()
CopyExplain
A figura 14.15 mostra os resultados:

Figura 14.15: Diferentes transformações de imagem


Na Figura 14.15, as imagens originais são mostradas na primeira linha e suas
versões transformadas na segunda linha. Observe que para a primeira
transformação (coluna mais à esquerda), a caixa delimitadora é especificada por
quatro números: a coordenada do canto superior esquerdo da caixa delimitadora
(aqui x=20, y=50) e a largura e altura da caixa (width=128, height=128). Observe
também que a origem (as coordenadas no local indicadas como (0, 0)) para
imagens carregadas pelo PyTorch (bem como outros pacotes, como ) é o canto
superior esquerdo da imagem.imageio
As transformações no bloco de código anterior são determinísticas. No entanto,
todas essas transformações também podem ser randomizadas, o que é
recomendado para o aumento dos dados durante o treinamento do modelo. Por
exemplo, uma caixa delimitadora aleatória (onde as coordenadas do canto
superior esquerdo são selecionadas aleatoriamente) pode ser cortada de uma
imagem, uma imagem pode ser invertida aleatoriamente ao longo dos eixos
horizontal ou vertical com uma probabilidade de 0,5, ou o contraste de uma
imagem pode ser alterado aleatoriamente, onde o é selecionado aleatoriamente,
mas com distribuição uniforme, a partir de uma faixa de valores. Além disso,
podemos criar um pipeline dessas transformações.contrast_factor

Por exemplo, podemos primeiro cortar aleatoriamente uma imagem, depois


invertê-la aleatoriamente e, finalmente, redimensioná-la para o tamanho desejado.
O código é o seguinte (como temos elementos aleatórios, definimos a semente
aleatória para reprodutibilidade):

>>> torch.manual_seed(1)

>>> fig = plt.figure(figsize=(14, 12))

>>> for i, (img, attr) in enumerate(celeba_train_dataset):

... ax = fig.add_subplot(3, 4, i*4+1)

... ax.imshow(img)

... if i == 0:

... ax.set_title('Orig.', size=15)

...

... ax = fig.add_subplot(3, 4, i*4+2)


... img_transform = transforms.Compose([

... transforms.RandomCrop([178, 178])

... ])

... img_cropped = img_transform(img)

... ax.imshow(img_cropped)

... if i == 0:

... ax.set_title('Step 1: Random crop', size=15)

...

... ax = fig.add_subplot(3, 4, i*4+3)

... img_transform = transforms.Compose([

... transforms.RandomHorizontalFlip()

... ])

... img_flip = img_transform(img_cropped)

... ax.imshow(img_flip)

... if i == 0:

... ax.set_title('Step 2: Random flip', size=15)

...

... ax = fig.add_subplot(3, 4, i*4+4)

... img_resized = transforms.functional.resize(

... img_flip, size=(128, 128)

... )

... ax.imshow(img_resized)

... if i == 0:

... ax.set_title('Step 3: Resize', size=15)

... if i == 2:

... break
>>> plt.show()
CopyExplain
A figura 14.16 mostra transformações aleatórias em três imagens de exemplo:

Figure 14.16: Random image transformations

Note that each time we iterate through these three examples, we get slightly
different images due to random transformations.

For convenience, we can define transform functions to use this pipeline for data
augmentation during dataset loading. In the following code, we will define the
function , which will extract the smile label from the list:get_smile'attributes'
>>> get_smile = lambda attr: attr[31]
CopyExplain
We will define the function that will produce the transformed image (where we will
first randomly crop the image, then flip it randomly, and finally, resize it to the
desired size 64×64):transform_train
>>> transform_train = transforms.Compose([

... transforms.RandomCrop([178, 178]),

... transforms.RandomHorizontalFlip(),

... transforms.Resize([64, 64]),

... transforms.ToTensor(),

... ])
CopyExplain

We will only apply data augmentation to the training examples, however, and not to
the validation or test images. The code for the validation or test set is as follows
(where we will first simply crop the image and then resize it to the desired size
64×64):

>>> transform = transforms.Compose([

... transforms.CenterCrop([178, 178]),

... transforms.Resize([64, 64]),

... transforms.ToTensor(),

... ])
CopyExplain
Now, to see data augmentation in action, let’s apply the function to our training
dataset and iterate over the dataset five times:transform_train
>>> from torch.utils.data import DataLoader

>>> celeba_train_dataset = torchvision.datasets.CelebA(

... image_path, split='train',

... target_type='attr', download=False,

... transform=transform_train, target_transform=get_smile

... )

>>> torch.manual_seed(1)
>>> data_loader = DataLoader(celeba_train_dataset, batch_size=2)

>>> fig = plt.figure(figsize=(15, 6))

>>> num_epochs = 5

>>> for j in range(num_epochs):

... img_batch, label_batch = next(iter(data_loader))

... img = img_batch[0]

... ax = fig.add_subplot(2, 5, j + 1)

... ax.set_xticks([])

... ax.set_yticks([])

... ax.set_title(f'Epoch {j}:', size=15)

... ax.imshow(img.permute(1, 2, 0))

...

... img = img_batch[1]

... ax = fig.add_subplot(2, 5, j + 6)

... ax.set_xticks([])

... ax.set_yticks([])

... ax.imshow(img.permute(1, 2, 0))

>>> plt.show()
CopyExplain
Figure 14.17 shows the five resulting transformations for data augmentation on two
example images:
Figure 14.17: The result of five image transformations

Next, we will apply the function to our validation and test datasets:transform


>>> celeba_valid_dataset = torchvision.datasets.CelebA(

... image_path, split='valid',

... target_type='attr', download=False,

... transform=transform, target_transform=get_smile

... )

>>> celeba_test_dataset = torchvision.datasets.CelebA(

... image_path, split='test',

... target_type='attr', download=False,

... transform=transform, target_transform=get_smile

... )
CopyExplain

Furthermore, instead of using all the available training and validation data, we will
take a subset of 16,000 training examples and 1,000 examples for validation, as
our goal here is to intentionally train our model with a small dataset:

>>> from torch.utils.data import Subset

>>> celeba_train_dataset = Subset(celeba_train_dataset,


... torch.arange(16000))

>>> celeba_valid_dataset = Subset(celeba_valid_dataset,

... torch.arange(1000))

>>> print('Train set:', len(celeba_train_dataset))

Train set: 16000

>>> print('Validation set:', len(celeba_valid_dataset))

Validation set: 1000


CopyExplain

Now, we can create data loaders for three datasets:

>>> batch_size = 32

>>> torch.manual_seed(1)

>>> train_dl = DataLoader(celeba_train_dataset,

... batch_size, shuffle=True)

>>> valid_dl = DataLoader(celeba_valid_dataset,

... batch_size, shuffle=False)

>>> test_dl = DataLoader(celeba_test_dataset,

... batch_size, shuffle=False)


CopyExplain

Now that the data loaders are ready, we will develop a CNN model, and train and
evaluate it in the next section.

Training a CNN smile classifier


By now, building a model with module and training it should be straightforward. The
design of our CNN is as follows: the CNN model receives input images of size
3×64×64 (the images have three color channels).torch.nn
The input data goes through four convolutional layers to make 32, 64, 128, and
256 feature maps using filters with a kernel size of 3×3 and padding of 1 for same
padding. The first three convolution layers are followed by max-pooling, P2×2. Two
dropout layers are also included for regularization:
>>> model = nn.Sequential()

>>> model.add_module(

... 'conv1',

... nn.Conv2d(

... in_channels=3, out_channels=32,

... kernel_size=3, padding=1

... )

... )

>>> model.add_module('relu1', nn.ReLU())

>>> model.add_module('pool1', nn.MaxPool2d(kernel_size=2))

>>> model.add_module('dropout1', nn.Dropout(p=0.5))

>>>

>>> model.add_module(

... 'conv2',

... nn.Conv2d(

... in_channels=32, out_channels=64,

... kernel_size=3, padding=1

... )

... )

>>> model.add_module('relu2', nn.ReLU())

>>> model.add_module('pool2', nn.MaxPool2d(kernel_size=2))

>>> model.add_module('dropout2', nn.Dropout(p=0.5))

>>>

>>> model.add_module(

... 'conv3',
... nn.Conv2d(

... in_channels=64, out_channels=128,

... kernel_size=3, padding=1

... )

... )

>>> model.add_module('relu3', nn.ReLU())

>>> model.add_module('pool3', nn.MaxPool2d(kernel_size=2))

>>>

>>> model.add_module(

... 'conv4',

... nn.Conv2d(

... in_channels=128, out_channels=256,

... kernel_size=3, padding=1

... )

... )

>>> model.add_module('relu4', nn.ReLU())


CopyExplain

Let’s see the shape of the output feature maps after applying these layers using a
toy batch input (four images arbitrarily):

>>> x = torch.ones((4, 3, 64, 64))

>>> model(x).shape

torch.Size([4, 256, 8, 8])


CopyExplain
There are 256 feature maps (or channels) of size 8×8. Now, we can add a fully
connected layer to get to the output layer with a single unit. If we reshape (flatten)
the feature maps, the number of input units to this fully connected layer will be
8 × 8 × 256 = 16,384. Alternatively, let’s consider a new layer,
called global average-pooling, which computes the average of each feature map
separately, thereby reducing the hidden units to 256. We can then add a fully
connected layer. Although we have not discussed global average-pooling explicitly,
it is conceptually very similar to other pooling layers. Global average-pooling can
be viewed, in fact, as a special case of average-pooling when the pooling size is
equal to the size of the input feature maps.
To understand this, consider Figure 14.18, showing an example of input feature
maps of shape batchsize×8×64×64. The channels are numbered k =0, 1,  ..., 7.
The global average-pooling operation calculates the average of each channel so
that the output will have the shape [batchsize×8]. After this, we will squeeze the
output of the global average-pooling layer.
Without squeezing the output, the shape would be [batchsize×8×1×1], as the
global average-pooling would reduce the spatial dimension of 64×64 to 1×1:

Figure 14.18: Input feature maps

Therefore, given that, in our case, the shape of the feature maps prior to this layer
is [batchsize×256×8×8], we expect to get 256 units as output, that is, the shape of
the output will be [batchsize×256]. Let’s add this layer and recompute the output
shape to verify that this is true:
>>> model.add_module('pool4', nn.AvgPool2d(kernel_size=8))

>>> model.add_module('flatten', nn.Flatten())

>>> x = torch.ones((4, 3, 64, 64))

>>> model(x).shape

torch.Size([4, 256])
CopyExplain
Finally, we can add a fully connected layer to get a single output unit. In this case,
we can specify the activation function to be :'sigmoid'
>>> model.add_module('fc', nn.Linear(256, 1))

>>> model.add_module('sigmoid', nn.Sigmoid())

>>> x = torch.ones((4, 3, 64, 64))

>>> model(x).shape

torch.Size([4, 1])

>>> model

Sequential(

(conv1): Conv2d(3, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))

(relu1): ReLU()

(pool1): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)

(dropout1): Dropout(p=0.5, inplace=False)

(conv2): Conv2d(32, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))

(relu2): ReLU()

(pool2): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)

(dropout2): Dropout(p=0.5, inplace=False)

(conv3): Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))

(relu3): ReLU()

(pool3): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)

(conv4): Conv2d(128, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))

(relu4): ReLU()

(pool4): AvgPool2d(kernel_size=8, stride=8, padding=0)

(flatten): Flatten(start_dim=1, end_dim=-1)

(fc): Linear(in_features=256, out_features=1, bias=True)

(sigmoid): Sigmoid()

)
CopyExplain
The next step is to create a loss function and optimizer (Adam optimizer again).
For a binary classification with a single probabilistic output, we use for the loss
function:BCELoss
>>> loss_fn = nn.BCELoss()

>>> optimizer = torch.optim.Adam(model.parameters(), lr=0.001)


CopyExplain

Now we can train the model by defining the following function:

>>> def train(model, num_epochs, train_dl, valid_dl):

... loss_hist_train = [0] * num_epochs

... accuracy_hist_train = [0] * num_epochs

... loss_hist_valid = [0] * num_epochs

... accuracy_hist_valid = [0] * num_epochs

... for epoch in range(num_epochs):

... model.train()

... for x_batch, y_batch in train_dl:

... pred = model(x_batch)[:, 0]

... loss = loss_fn(pred, y_batch.float())

... loss.backward()

... optimizer.step()

... optimizer.zero_grad()

... loss_hist_train[epoch] += loss.item()*y_batch.size(0)

... is_correct = ((pred>=0.5).float() == y_batch).float()

... accuracy_hist_train[epoch] += is_correct.sum()

... loss_hist_train[epoch] /= len(train_dl.dataset)

... accuracy_hist_train[epoch] /= len(train_dl.dataset)

...
... model.eval()

... with torch.no_grad():

... for x_batch, y_batch in valid_dl:

... pred = model(x_batch)[:, 0]

... loss = loss_fn(pred, y_batch.float())

... loss_hist_valid[epoch] += \

... loss.item() * y_batch.size(0)

... is_correct = \

... ((pred>=0.5).float() == y_batch).float()

... accuracy_hist_valid[epoch] += is_correct.sum()

... loss_hist_valid[epoch] /= len(valid_dl.dataset)

... accuracy_hist_valid[epoch] /= len(valid_dl.dataset)

...

... print(f'Epoch {epoch+1} accuracy: '

... f'{accuracy_hist_train[epoch]:.4f} val_accuracy: '

... f'{accuracy_hist_valid[epoch]:.4f}')

... return loss_hist_train, loss_hist_valid, \

... accuracy_hist_train, accuracy_hist_valid


CopyExplain

Next, we will train this CNN model for 30 epochs and use the validation dataset
that we created for monitoring the learning progress:

>>> torch.manual_seed(1)

>>> num_epochs = 30

>>> hist = train(model, num_epochs, train_dl, valid_dl)

Epoch 1 accuracy: 0.6286 val_accuracy: 0.6540

...
Epoch 15 accuracy: 0.8544 val_accuracy: 0.8700

...

Epoch 30 accuracy: 0.8739 val_accuracy: 0.8710


CopyExplain

Let’s now visualize the learning curve and compare the training and validation loss
and accuracies after each epoch:

>>> x_arr = np.arange(len(hist[0])) + 1

>>> fig = plt.figure(figsize=(12, 4))

>>> ax = fig.add_subplot(1, 2, 1)

>>> ax.plot(x_arr, hist[0], '-o', label='Train loss')

>>> ax.plot(x_arr, hist[1], '--<', label='Validation loss')

>>> ax.legend(fontsize=15)

>>> ax = fig.add_subplot(1, 2, 2)

>>> ax.plot(x_arr, hist[2], '-o', label='Train acc.')

>>> ax.plot(x_arr, hist[3], '--<',

... label='Validation acc.')

>>> ax.legend(fontsize=15)

>>> ax.set_xlabel('Epoch', size=15)

>>> ax.set_ylabel('Accuracy', size=15)

>>> plt.show()
CopyExplain
Figure 14.19: A comparison of the training and validation results

Once we are happy with the learning curves, we can evaluate the model on the
hold-out test dataset:

>>> accuracy_test = 0

>>> model.eval()

>>> with torch.no_grad():

... for x_batch, y_batch in test_dl:

... pred = model(x_batch)[:, 0]

... is_correct = ((pred>=0.5).float() == y_batch).float()

... accuracy_test += is_correct.sum()

>>> accuracy_test /= len(test_dl.dataset)

>>> print(f'Test accuracy: {accuracy_test:.4f}')

Test accuracy: 0.8446


CopyExplain
Finally, we already know how to get the prediction results on some test examples.
In the following code, we will take a small subset of 10 examples from the last
batch of our pre-processed test dataset (). Then, we will compute the probabilities
of each example being from class 1 (which corresponds to smile based on the
labels provided in CelebA) and visualize the examples along with their ground truth
label and the predicted probabilities:test_dl
>>> pred = model(x_batch)[:, 0] * 100
>>> fig = plt.figure(figsize=(15, 7))

>>> for j in range(10, 20):

... ax = fig.add_subplot(2, 5, j-10+1)

... ax.set_xticks([]); ax.set_yticks([])

... ax.imshow(x_batch[j].permute(1, 2, 0))

... if y_batch[j] == 1:

... label='Smile'

... else:

... label = 'Not Smile'

... ax.text(

... 0.5, -0.15,

... f'GT: {label:s}\nPr(Smile)={pred[j]:.0f}%',

... size=16,

... horizontalalignment='center',

... verticalalignment='center',

... transform=ax.transAxes

... )

>>> plt.show()
CopyExplain
Na Figura 14.20, você pode ver 10 imagens de exemplo junto com seus rótulos de
verdade de base e as probabilidades de que pertençam à classe 1, sorria:
Figura 14.20: Rótulos de imagem e suas probabilidades de pertencerem à classe
1

As probabilidades de classe 1 (ou seja, sorrir de acordo com o CelebA) são


fornecidas abaixo de cada imagem. Como você pode ver, nosso modelo treinado é
completamente preciso neste conjunto de 10 exemplos de teste.

Como um exercício opcional, você é encorajado a tentar usar todo o conjunto de


dados de treinamento em vez do pequeno subconjunto que criamos. Além disso,
você pode alterar ou modificar a arquitetura da CNN. Por exemplo, você pode
alterar as probabilidades de abandono e o número de filtros nas diferentes
camadas convolucionais. Além disso, você pode substituir o pool de média global
por uma camada totalmente conectada. Se você estiver usando todo o conjunto de
dados de treinamento com a arquitetura da CNN que treinamos neste capítulo,
você deve ser capaz de alcançar mais de 90% de precisão.

Modelando dados sequenciais usando


redes neurais recorrentes
No capítulo anterior, focamos em redes neurais convolucionais (CNNs). Nós
cobrimos os blocos de construção das arquiteturas da CNN e como implementar
CNNs profundas no PyTorch. Finalmente, você aprendeu a usar CNNs para
classificação de imagens. Neste capítulo, exploraremos redes neurais
recorrentes (RNNs) e veremos sua aplicação na modelagem de dados
sequenciais.

Abordaremos os seguintes tópicos:

 Introdução a dados sequenciais

 RNNs para modelagem de sequências

 Memória de curto prazo longa

 Retropropagação truncada ao longo do tempo

 Implementando um RNN multicamada para modelagem de sequência no


PyTorch

 Projeto um: análise de sentimento RNN do conjunto de dados de revisão de


filmes do IMDb

 Projeto dois: modelagem de linguagem em nível de caractere RNN com


células LSTM, usando dados de texto de A Ilha Misteriosa de Júlio Verne
 Usando o recorte de gradiente para evitar gradientes explosivos

Introdução a dados sequenciais


Vamos começar nossa discussão sobre RNNs observando a natureza dos dados
sequenciais, que é mais comumente conhecida como dados de sequência
ou sequências. Examinaremos as propriedades únicas das sequências que as
tornam diferentes de outros tipos de dados. Em seguida, veremos como
representar dados sequenciais e explorar as várias categorias de modelos para
dados sequenciais, que são baseados na entrada e saída de um modelo. Isso nos
ajudará a explorar a relação entre RNNs e sequências neste capítulo.

Modelagem de dados sequenciais – ordem


importa
O que torna as sequências únicas, em comparação com outros tipos de dados, é
que os elementos em uma sequência aparecem em uma determinada ordem e
não são independentes uns dos outros. Algoritmos típicos de aprendizado de
máquina para aprendizado supervisionado assumem que a
entrada é independente e dados identicamente distribuídos (IID), o que
significa que os exemplos de treinamento são mutuamente independentes e têm a
mesma distribuição subjacente. A este respeito, com base no pressuposto da
independência mútua, a ordem em que os exemplos de formação são dados ao
modelo é irrelevante. Por exemplo, se tivermos uma amostra composta
por n exemplos de treinamento, x(1), x(2), ..., x(n), a ordem em que usamos os dados
para treinar nosso algoritmo de aprendizado de máquina não importa. Um
exemplo desse cenário seria o conjunto de dados Iris com o qual trabalhamos
anteriormente. No conjunto de dados de Iris, cada flor foi medida
independentemente, e as medidas de uma flor não influenciam as medidas de
outra flor.

No entanto, essa suposição não é válida quando lidamos com sequências – por
definição, a ordem importa. Prever o valor de mercado de uma determinada ação
seria um exemplo desse cenário. Por exemplo, suponha que temos uma amostra
de n exemplos de treinamento, onde cada exemplo de treinamento representa o
valor de mercado de uma determinada ação em um determinado dia. Se nossa
tarefa é prever o valor do mercado de ações para os próximos três dias, faria
sentido considerar os preços das ações anteriores em uma ordem ordenada por
data para derivar tendências, em vez de utilizar esses exemplos de treinamento
em uma ordem aleatória.

Dados sequenciais versus dados de séries


temporais
Dados de séries temporais são um tipo especial de dados sequenciais em que
cada exemplo é associado a uma dimensão para o tempo. Em dados de séries
temporais, as amostras são coletadas em carimbos de data/hora sucessivos e,
portanto, a dimensão temporal determina a ordem entre os pontos de dados. Por
exemplo, os preços das ações e os registros de voz ou fala são dados de séries
temporais.
Por outro lado, nem todos os dados sequenciais têm a dimensão temporal.
Por exemplo, em dados de texto ou sequências de DNA, os exemplos são
ordenados, mas o texto ou o DNA não se qualificam como dados de séries
temporais. Como você verá, neste capítulo, nos concentraremos em exemplos de
processamento de linguagem natural (PNL) e modelagem de texto que não são
dados de séries temporais. No entanto, observe que os RNNs também podem ser
usados para dados de séries temporais, o que está além do escopo deste livro.

Representação de sequências
Estabelecemos que a ordem entre pontos de dados é importante em dados
sequenciais, portanto, precisamos encontrar uma maneira de aproveitar essas
informações de ordenação em um modelo de aprendizado de máquina. Ao longo
deste capítulo, representaremos sequências

como  . Os índices sobrescritos indicam


a ordem das ocorrências e o comprimento da sequência é T. Para um exemplo
sensato de sequências, considere dados de séries temporais, onde cada ponto de
exemplo, x(t), pertence a um determinado tempo, t. A figura 15.1 mostra um
exemplo de dados de séries temporais em que tanto os recursos de entrada (x's)
quanto os rótulos de destino (y's) seguem naturalmente a ordem de acordo com
seu eixo temporal; portanto, tanto o x's quanto o y's são sequências.

Figura 15.1: Um exemplo de dados de séries temporais


Como já mencionamos, os modelos NN padrão que cobrimos até agora,
como perceptrons multicamadas (MLPs) e CNNs para dados de imagem,
assumem que os exemplos de treinamento são independentes uns dos outros e,
portanto, não incorporam informações de ordenação. Podemos dizer que tais
modelos não possuem memória de exemplos de treinamento vistos anteriormente.
Por exemplo, as amostras são passadas pelas etapas de feedforward e
backpropagation, e os pesos são atualizados independentemente da ordem em
que os exemplos de treinamento são processados.

Os RNNs, por outro lado, são projetados para modelar sequências e são capazes
de lembrar informações passadas e processar novos eventos de acordo, o que é
uma vantagem clara ao trabalhar com dados de sequência.

As diferentes categorias de modelagem de


sequência
A modelagem de sequência tem muitas aplicações fascinantes, como tradução de
idiomas (por exemplo, tradução de texto do inglês para o alemão), legendagem de
imagens e geração de texto. No entanto, para escolher uma arquitetura e
abordagem apropriadas, temos que entender e ser capazes de distinguir entre
essas diferentes tarefas de modelagem de sequência. A Figura 15.2, baseada nas
explicações do excelente artigo The Unreasonable Effectiveness of Recurrent
Neural Networks, de Andrej Karpathy, 2015
(http://karpathy.github.io/2015/05/21/rnn-effectiveness/), resume as tarefas de
modelagem de sequência mais comuns, que dependem das categorias de
relacionamento de dados de entrada e saída.
Figura 15.2: As tarefas de sequenciamento mais comuns

Vamos discutir as diferentes categorias de relacionamento entre dados de entrada


e saída, que foram descritas na figura anterior, com mais detalhes. Se nem os
dados de entrada nem os dados de saída representam sequências, então estamos
lidando com dados padrão, e poderíamos simplesmente usar um perceptron
multicamadas (ou outro modelo de classificação anteriormente abordado neste
livro) para modelar tais dados. No entanto, se a entrada ou saída for uma
sequência, a tarefa de modelagem provavelmente se enquadra em uma destas
categorias:

 Muitos para um: Os dados de entrada são uma sequência, mas a saída é um


vetor de tamanho fixo ou escalar, não uma sequência. Por exemplo, na
análise de sentimento, a entrada é baseada em texto (por exemplo, uma
revisão de filme) e a saída é um rótulo de classe (por exemplo, um rótulo que
indica se um revisor gostou do filme).
 Um-para-muitos: Os dados de entrada estão no formato padrão e não uma
sequência, mas a saída é uma sequência. Um exemplo dessa categoria é a
legenda de imagem — a entrada é uma imagem e a saída é uma frase em
inglês que resume o conteúdo dessa imagem.
 Muitos para muitos: As matrizes de entrada e saída são sequências. Essa
categoria pode ser dividida com base em se a entrada e a saída estão
sincronizadas. Um exemplo de uma tarefa de modelagem de muitos para
muitos sincronizada é a classificação de vídeo, em que cada quadro em um
vídeo é rotulado. Um exemplo de uma tarefa de modelagem atrasada de
muitos para muitos seria traduzir um idioma para outro. Por exemplo, uma
frase inteira em inglês deve ser lida e processada por uma máquina antes de
sua tradução para o alemão ser produzida.

Agora, depois de resumir as três grandes categorias de modelagem de sequência,


podemos avançar para a discussão da estrutura de uma RNN.

RNNs para modelagem de sequências


Nesta seção, antes de começarmos a implementar RNNs no PyTorch,
discutiremos os principais conceitos de RNNs. Começaremos observando a
estrutura típica de um RNN, que inclui um componente recursivo para modelar
dados de sequência. Em seguida, examinaremos como as ativações de neurônios
são computadas em uma RNN típica. Isso criará um contexto para discutirmos os
desafios comuns no treinamento de RNNs e, em seguida, discutiremos soluções
para esses desafios, como LSTM e unidades recorrentes fechadas (GRUs).

Entendendo o fluxo de dados em RNNs


Vamos começar com a arquitetura de um RNN. A figura 15.3 mostra o fluxo de
dados em um NN feedforward padrão e em um RNN lado a lado para
comparação:
Figura 15.3: O fluxo de dados de um NN de feedforward padrão e de um RNN

Ambas as redes têm apenas uma camada oculta. Nessa representação, as


unidades não são exibidas, mas assumimos que a camada de entrada (x), a
camada oculta (h) e a camada de saída (o) são vetores que contêm muitas
unidades.

Determinando o tipo de saída de um RNN

Esta arquitetura RNN genérica poderia corresponder às duas categorias de


modelagem de sequência onde a entrada é uma sequência. Normalmente, uma
camada recorrente pode retornar uma sequência como saída, ou simplesmente

retornar a última saída (em t = T, ou seja, 


o(T)). Assim, poderia ser muitos-para-muitos, ou poderia ser muitos-para-um se,
por exemplo, usarmos apenas o último elemento, o(T), como saída final.

Veremos mais adiante como isso é tratado no módulo PyTorch, quando


examinamos detalhadamente o comportamento de uma camada recorrente em
relação ao retorno de uma sequência como saída. torch.nn

Em uma rede feedforward padrão, as informações fluem da camada de entrada


para a camada oculta e, em seguida, da camada oculta para a camada de saída.
Por outro lado, em um RNN, a camada oculta recebe sua entrada da camada de
entrada da etapa de tempo atual e da camada oculta da etapa de tempo anterior.
O fluxo de informações em etapas de tempo adjacentes na camada oculta permite
que a rede tenha uma memória de eventos passados. Esse fluxo de informações
geralmente é exibido como um loop, também conhecido como uma borda
recorrente na notação gráfica, que é como essa arquitetura geral de RNN
recebeu seu nome.

Semelhante aos perceptrons multicamadas, os RNNs podem consistir de várias


camadas ocultas. Note que é uma convenção comum referir-se a RNNs com uma
camada oculta como um RNN de camada única, que não deve ser confundido
com NNs de camada única sem uma camada oculta, como Adaline ou regressão
logística. A figura 15.4 ilustra um RNN com uma camada oculta (superior) e um
RNN com duas camadas ocultas (inferior):

Figura 15.4: Exemplos de um RNN com uma e duas camadas ocultas


Para examinar a arquitetura dos RNNs e o fluxo de informações, uma
representação compacta com uma borda recorrente pode ser desdobrada, que
você pode ver na Figura 15.4.

Como sabemos, cada unidade oculta em um NN padrão recebe apenas uma


entrada — a pré-ativação líquida associada à camada de entrada. Em contraste,
cada unidade oculta em um RNN recebe dois conjuntos distintos de entrada — a
pré-ativação da camada de entrada e a ativação da mesma camada oculta da
etapa de tempo anterior, t – 1.

Na primeira etapa, t = 0, as unidades ocultas são inicializadas para zeros ou


pequenos valores aleatórios. Em seguida, em uma etapa de tempo em que t > 0,
as unidades ocultas recebem sua entrada do ponto de dados no momento
atual, x(t), e os valores anteriores de unidades ocultas em t – 1, indicados como h(t–
1)
.

Da mesma forma, no caso de um RNN multicamada, podemos resumir o fluxo de


informações da seguinte forma:

 layer = 1: Aqui, a camada oculta é representada como   e recebe sua


entrada do ponto de dados, x , e os valores ocultos na mesma camada, mas
(t)

na etapa de tempo anterior,  .


 layer = 2: A segunda camada oculta, , recebe suas entradas das saídas da

camada abaixo na etapa de tempo atual ( ) e seus próprios valores

ocultos da etapa de tempo anterior,  .

Como, neste caso, cada camada recorrente deve receber uma sequência como
entrada, todas as camadas recorrentes, exceto a última, devem retornar uma
sequência como saída (ou seja, mais tarde teremos que definir ). O
comportamento da última camada recorrente depende do tipo de
problema.return_sequences=True

Ativações computacionais em um RNN


Agora que você entende a estrutura e o fluxo geral de informações em um RNN,
vamos ser mais específicos e calcular as ativações reais das camadas ocultas,
bem como a camada de saída. Para simplificar, vamos considerar apenas uma
única camada oculta; no entanto, o mesmo conceito se aplica a RNNs
multicamadas.

Cada aresta direcionada (as conexões entre caixas) na representação de um RNN


que acabamos de observar está associada a uma matriz de peso. Esses pesos
não dependem do tempo, t; portanto, eles são compartilhados em todo o eixo de
tempo. As diferentes matrizes de peso em um RNN de camada única são as
seguintes:

 Wxh: A matriz de peso entre a entrada, x(t), e a camada oculta, h


 WHh: A matriz de peso associada à borda recorrente
 Wprostituta: A matriz de peso entre a camada oculta e a camada de saída

Estas matrizes de peso estão representadas na figura 15.5:

Figura 15.5: Aplicação de pesos a um RNN de camada única


Em certas implementações, você pode observar que as matrizes de
peso, Wxh e WHh, são concatenados a uma matriz combinada, Wh = [Wxh; WHh].
Mais adiante nesta seção, também faremos uso dessa notação.

A computação das ativações é muito semelhante aos perceptrons multicamadas


padrão e outros tipos de NNs feedforward. Para a camada oculta, a entrada
net, zh (pré-ativação), é computado através de uma combinação linear; ou seja,
calculamos a soma das multiplicações das matrizes de peso com os vetores
correspondentes e adicionamos a unidade de viés:

Em seguida, as ativações das unidades ocultas na etapa de tempo, t, são


calculadas da seguinte maneira:

Aqui, bh é o vetor de viés para as unidades ocultas e   é a função de


ativação da camada oculta.

Caso queira utilizar a matriz de peso concatenada, Wh = [Wxh; WHh], a fórmula para


calcular unidades ocultas será alterada da seguinte forma:

Uma vez que as ativações das unidades ocultas na etapa de tempo atual são
computadas, então as ativações das unidades de saída serão computadas, da
seguinte maneira:
Para ajudar a esclarecer melhor isso, a Figura 15.6 mostra o processo de
computação dessas ativações com ambas as formulações:

Figura 15.6: Calculando as ativações

Treinamento de RNNs usando backpropagation through time (BPTT)

O algoritmo de aprendizagem para RNNs foi introduzido em


1990: Backpropagation Through Time: What It Does and How to Do It (Paul
Werbos, Proceedings of IEEE, 78(10): 1550-1560, 1990).

A derivação dos gradientes pode ser um pouco complicada, mas a ideia básica é
que a perda geral, L, é a soma de todas as funções de perda às vezes t = 1
a t = T:

Como a perda no tempo t é dependente das unidades ocultas em todas as etapas


de tempo anteriores 1 : t, o gradiente será calculado da seguinte maneira:
Aqui,   é calculado como uma multiplicação de etapas de tempo
adjacentes:

Recorrência oculta versus recorrência de saída


Até agora, você viu redes recorrentes nas quais a camada oculta tem
a propriedade recorrente. No entanto, observe que existe um modelo alternativo
em que a conexão recorrente vem da camada de saída. Nesse caso, as ativações
líquidas da camada de saída na etapa de tempo anterior, ot–1, pode ser adicionado
de duas maneiras:

 Para a camada oculta na etapa de tempo atual, ht (mostrado na Figura


15.7 como recorrência de saída para oculto)
 Para a camada de saída na etapa de tempo atual, ot (mostrado na figura
15.7 como recorrência de saída para saída)
Figure 15.7: Different recurrent connection models

As shown in Figure 15.7, the differences between these architectures can be


clearly seen in the recurring connections. Following our notation, the weights
associated with the recurrent connection will be denoted for the hidden-to-hidden
recurrence by Whh, for the output-to-hidden recurrence by Woh, and for the output-
to-output recurrence by Woo. In some articles in literature, the weights associated
with the recurrent connections are also denoted by Wrec.

To see how this works in practice, let’s manually compute the forward pass for one
of these recurrent types. Using the module, a recurrent layer can be defined via ,
which is similar to the hidden-to-hidden recurrence. In the following code, we will
create a recurrent layer from and perform a forward pass on an input sequence of
length 3 to compute the output. We will also manually compute the forward pass
and compare the results with those of .torch.nnRNNRNNRNN
First, let’s create the layer and assign the weights and biases for our manual
computations:

>>> import torch

>>> import torch.nn as nn

>>> torch.manual_seed(1)

>>> rnn_layer = nn.RNN(input_size=5, hidden_size=2,

... num_layers=1, batch_first=True)

>>> w_xh = rnn_layer.weight_ih_l0

>>> w_hh = rnn_layer.weight_hh_l0

>>> b_xh = rnn_layer.bias_ih_l0

>>> b_hh = rnn_layer.bias_hh_l0

>>> print('W_xh shape:', w_xh.shape)

>>> print('W_hh shape:', w_hh.shape)

>>> print('b_xh shape:', b_xh.shape)

>>> print('b_hh shape:', b_hh.shape)

W_xh shape: torch.Size([2, 5])

W_hh shape: torch.Size([2, 2])

b_xh shape: torch.Size([2])

b_hh shape: torch.Size([2])


CopyExplain

The input shape for this layer is , where the first dimension is the batch dimension
(as we set ), the second dimension corresponds to the sequence, and the last
dimension corresponds to the features. Notice that we will output a sequence,
which, for an input sequence of length 3, will result in the output

sequence  . Also, uses one layer by default, and


you can set to stack multiple RNN layers together to form a stacked RNN.
(batch_size, sequence_length, 5)batch_first=TrueRNNnum_layers
Now, we will call the forward pass on the and manually compute the outputs at
each time step and compare them:rnn_layer

>>> x_seq = torch.tensor([[1.0]*5, [2.0]*5, [3.0]*5]).float()

>>> ## output of the simple RNN:

>>> output, hn = rnn_layer(torch.reshape(x_seq, (1, 3, 5)))

>>> ## manually computing the output:

>>> out_man = []

>>> for t in range(3):

... xt = torch.reshape(x_seq[t], (1, 5))

... print(f'Time step {t} =>')

... print(' Input :', xt.numpy())

...

... ht = torch.matmul(xt, torch.transpose(w_xh, 0, 1)) + b_xh

... print(' Hidden :', ht.detach().numpy())

...

... if t > 0:

... prev_h = out_man[t-1]

... else:

... prev_h = torch.zeros((ht.shape))

... ot = ht + torch.matmul(prev_h, torch.transpose(w_hh, 0, 1)) \

... + b_hh

... ot = torch.tanh(ot)

... out_man.append(ot)

... print(' Output (manual) :', ot.detach().numpy())

... print(' RNN output :', output[:, t].detach().numpy())

... print()
Time step 0 =>

Input : [[1. 1. 1. 1. 1.]]

Hidden : [[-0.4701929 0.5863904]]

Output (manual) : [[-0.3519801 0.52525216]]

RNN output : [[-0.3519801 0.52525216]]

Time step 1 =>

Input : [[2. 2. 2. 2. 2.]]

Hidden : [[-0.88883156 1.2364397 ]]

Output (manual) : [[-0.68424344 0.76074266]]

RNN output : [[-0.68424344 0.76074266]]

Time step 2 =>

Input : [[3. 3. 3. 3. 3.]]

Hidden : [[-1.3074701 1.886489 ]]

Output (manual) : [[-0.8649416 0.90466356]]

RNN output : [[-0.8649416 0.90466356]]


CopyExplain

In our manual forward computation, we used the hyperbolic tangent (tanh)


activation function since it is also used in (the default activation). As you can see
from the printed results, the outputs from the manual forward computations exactly
match the output of the layer at each time step. Hopefully, this hands-on task has
enlightened you on the mysteries of recurrent networks. RNNRNN

The challenges of learning long-range interactions


BPTT, which was briefly mentioned earlier, introduces some new challenges.

Because of the multiplicative factor,  , in computing the gradients of a


loss function, the so-called vanishing and exploding gradient problems arise.
These problems are explained by the examples in Figure 15.8, which shows an
RNN with only one hidden unit for simplicity:

Figure 15.8: Problems in computing the gradients of the loss function

Basically,   has t – k multiplications; therefore, multiplying the


weight, w, by itself t – k times results in a factor, wt–k. As a result, if |w| < 1, this
factor becomes very small when t – k is large. On the other hand, if the weight of
the recurrent edge is |w| > 1, then wt–k becomes very large when t – k is large. Note
that a large t – k refers to long-range dependencies. We can see that a naive
solution to avoid vanishing or exploding gradients can be reached by ensuring |
w| = 1. If you are interested and would like to investigate this in more detail,
read On the difficulty of training recurrent neural networks by R. Pascanu, T.
Mikolov, and Y. Bengio, 2012 (https://arxiv.org/pdf/1211.5063.pdf).

In practice, there are at least three solutions to this problem:

 Gradient clipping

 Truncated backpropagation through time (TBPTT)


 LSTM
Using gradient clipping, we specify a cut-off or threshold value for the gradients,
and we assign this cut-off value to gradient values that exceed this value. In
contrast, TBPTT simply limits the number of time steps that the signal can
backpropagate after each forward pass. For example, even if the sequence has
100 elements or steps, we may only backpropagate the most recent 20 time steps.

While both gradient clipping and TBPTT can solve the exploding gradient problem,
the truncation limits the number of steps that the gradient can effectively flow back
and properly update the weights. On the other hand, LSTM, designed in 1997 by
Sepp Hochreiter and Jürgen Schmidhuber, has been more successful in vanishing
and exploding gradient problems while modeling long-range dependencies through
the use of memory cells. Let’s discuss LSTM in more detail.

Long short-term memory cells


As stated previously, LSTMs were first introduced to overcome the vanishing
gradient problem (Long Short-Term Memory by S. Hochreiter and J.
Schmidhuber, Neural Computation, 9(8): 1735-1780, 1997). The building block of
an LSTM is a memory cell, which essentially represents or replaces the hidden
layer of standard RNNs.

In each memory cell, there is a recurrent edge that has the desirable weight, w = 1,
as we discussed, to overcome the vanishing and exploding gradient problems. The
values associated with this recurrent edge are collectively called the cell state.
The unfolded structure of a modern LSTM cell is shown in Figure 15.9:
Figure 15.9: The structure of an LSTM cell

Notice that the cell state from the previous time step, C(t–1), is modified to get the
cell state at the current time step, C(t), without being multiplied directly by any
weight factor. The flow of information in this memory cell is controlled by several
computation units (often called gates) that will be described here. In the

figure,   refers to the element-wise product (element-wise multiplication)

and   means element-wise summation (element-wise addition).


Furthermore, x(t) refers to the input data at time t, and h(t–1) indicates the hidden
units at time t – 1. Four boxes are indicated with an activation function, either the

sigmoid function ( ) or tanh, and a set of weights; these boxes apply a linear
combination by performing matrix-vector multiplications on their inputs (which
are h(t–1) and x(t)). These units of computation with sigmoid activation functions,

whose output units are passed through  , are called gates.

In an LSTM cell, there are three different types of gates, which are known as the
forget gate, the input gate, and the output gate:
The forget gate (ft) allows the memory cell to reset the cell state without growing
indefinitely. In fact, the forget gate decides which information is allowed to go
through and which information to suppress. Now, ft is computed as follows:

Note that the forget gate was not part of the original LSTM cell; it was added a few
years later to improve the original model (Learning to Forget: Continual Prediction
with LSTM by F. Gers, J. Schmidhuber, and F. Cummins, Neural Computation 12,
2451-2471, 2000).

The input gate (it) and candidate value ( ) are responsible for updating the


cell state. They are computed as follows:

The cell state at time t is computed as follows:

The output gate (ot) decides how to update the values of hidden units:

Given this, the hidden units at the current time step are computed as follows:
The structure of an LSTM cell and its underlying computations might seem very
complex and hard to implement. However, the good news is that PyTorch has
already implemented everything in optimized wrapper functions, which allows us to
define our LSTM cells easily and efficiently. We will apply RNNs and LSTMs to
real-world datasets later in this chapter.

Other advanced RNN models

LSTMs provide a basic approach for modeling long-range dependencies in


sequences. Yet, it is important to note that there are many variations of LSTMs
described in literature (An Empirical Exploration of Recurrent Network
Architectures by Rafal Jozefowicz, Wojciech Zaremba, and Ilya
Sutskever, Proceedings of ICML, 2342-2350, 2015). Also worth noting is a more
recent approach, gated recurrent unit (GRU), which was proposed in 2014.
GRUs have a simpler architecture than LSTMs; therefore, they are computationally
more efficient, while their performance in some tasks, such as polyphonic music
modeling, is comparable to LSTMs. If you are interested in learning more about
these modern RNN architectures, refer to the paper, Empirical Evaluation of Gated
Recurrent Neural Networks on Sequence Modeling by Junyoung Chung and
others, 2014 (https://arxiv.org/pdf/1412.3555v1.pdf).

Implementando RNNs para modelagem


de sequência em PyTorch
Agora que cobrimos a teoria subjacente por trás dos RNNs, estamos prontos para
passar para a parte mais prática deste capítulo: implementar RNNs no PyTorch.
Durante o restante deste capítulo, aplicaremos RNNs a duas tarefas de problemas
comuns:

1. Análise de sentimento
2. Modelagem de linguagem

Esses dois projetos, que apresentaremos juntos nas próximas páginas, são
fascinantes, mas também bastante envolvidos. Assim, em vez de fornecer o
código de uma só vez, dividiremos a implementação em várias etapas e
discutiremos o código em detalhes. Se você gosta de ter uma visão geral e quer
ver todo o código de uma só vez antes de mergulhar na discussão, dê uma olhada
na implementação do código primeiro.

Projeto um – prevendo o sentimento das críticas


de filmes do IMDb
Você deve se lembrar do Capítulo 8, Aplicando Machine Learning à Análise de
Sentimento, que a análise de sentimento se preocupa em analisar a opinião
expressa de uma frase ou de um documento de texto. Nesta seção e nas
subseções a seguir, implementaremos um RNN multicamada para análise de
sentimento usando uma arquitetura de muitos para um.

Na próxima seção, implementaremos um RNN de muitos para muitos para uma


aplicação de modelagem de linguagem. Embora os exemplos escolhidos sejam
propositalmente simples para introduzir os principais conceitos de RNNs, a
modelagem de linguagem tem uma ampla gama de aplicações interessantes,
como a construção de chatbots — dando aos computadores a capacidade de falar
e interagir diretamente com humanos.

Preparando os dados de revisão do filme

No Capítulo 8, pré-processamos e limpamos o conjunto de dados da revisão. E


faremos o mesmo agora. Primeiro, vamos importar os módulos necessários e ler
os dados de (que vamos instalar via ; a versão 0.10.0 foi usada no final de 2021)
da seguinte forma:torchtextpip install torchtext

>>> from torchtext.datasets import IMDB

>>> train_dataset = IMDB(split='train')

>>> test_dataset = IMDB(split='test')


CopyExplain

Cada conjunto tem 25.000 amostras. E cada amostra dos conjuntos de dados
consiste em dois elementos, o rótulo de sentimento que representa o rótulo de
destino que queremos prever (refere-se ao sentimento negativo e refere-se ao
sentimento positivo) e o texto de revisão do filme (os recursos de entrada). O
componente de texto dessas resenhas de filmes são sequências de palavras, e o
modelo RNN classifica cada sequência como uma revisão positiva () ou negativa
().negpos10

No entanto, antes de podermos alimentar os dados em um modelo RNN,


precisamos aplicar várias etapas de pré-processamento:

1. Divida o conjunto de dados de treinamento em partições de treinamento e


validação separadas.
2. Identificar as palavras exclusivas no conjunto de dados de treinamento
3. Mapeie cada palavra única para um inteiro exclusivo e codifique o texto de
revisão em inteiros codificados (um índice de cada palavra única)
4. Divida o conjunto de dados em minilotes como entrada para o modelo

Vamos prosseguir com o primeiro passo: criar uma partição de treinamento e


validação a partir do que lemos anteriormente:train_dataset

>>> ## Step 1: create the datasets

>>> from torch.utils.data.dataset import random_split

>>> torch.manual_seed(1)

>>> train_dataset, valid_dataset = random_split(

... list(train_dataset), [20000, 5000])


CopyExplain

O conjunto de dados de treinamento original contém 25.000 exemplos. 20.000


exemplos são escolhidos aleatoriamente para treinamento e 5.000 para validação.

Para preparar os dados para entrada em um NN, precisamos codificá-los em


valores numéricos, como foi mencionado nas etapas 2 e 3. Para fazer isso,
primeiro encontraremos as palavras exclusivas (tokens) no conjunto de dados de
treinamento. Embora encontrar tokens exclusivos seja um processo para o qual
podemos usar conjuntos de dados Python, pode ser mais eficiente usar a classe
do pacote, que faz parte da biblioteca padrão do Python. Countercollections
No código a seguir, instanciaremos um novo objeto () que coletará as frequências
exclusivas das palavras. Observe que neste aplicativo em particular (e em
contraste com o modelo de saco de palavras), estamos interessados apenas no
conjunto de palavras exclusivas e não exigiremos as contagens de palavras, que
são criadas como um produto secundário. Para dividir o texto em palavras (ou
tokens), vamos reutilizar a função que desenvolvemos no Capítulo 8, que também
remove marcações HTML, bem como pontuação e outros caracteres que não
sejam letras:Countertoken_countstokenizer

O código para coletar tokens exclusivos é o seguinte:

>>> ## Step 2: find unique tokens (words)

>>> import re

>>> from collections import Counter, OrderedDict

>>>

>>> def tokenizer(text):

... text = re.sub('<[^>]*>', '', text)

... emoticons = re.findall(

... '(?::|;|=)(?:-)?(?:\)|\(|D|P)', text.lower()

... )

... text = re.sub('[\W]+', ' ', text.lower()) +\

... ' '.join(emoticons).replace('-', '')

... tokenized = text.split()

... return tokenized

>>>

>>> token_counts = Counter()

>>> for label, line in train_dataset:

... tokens = tokenizer(line)

... token_counts.update(tokens)

>>> print('Vocab-size:', len(token_counts))


Vocab-size: 69023
CopyExplain

Se você quiser saber mais sobre o , consulte sua documentação


em https://docs.python.org/3/library/collections.html#collections.Counter.Counter

Em seguida, vamos mapear cada palavra única para um inteiro único. Isso pode
ser feito manualmente usando um dicionário Python, onde as chaves são os
tokens exclusivos (palavras) e o valor associado a cada chave é um inteiro
exclusivo. No entanto, o pacote já fornece uma classe, , que podemos usar para
criar esse mapeamento e codificar todo o conjunto de dados. Primeiro, criaremos
um objeto passando os tokens de mapeamento de dicionário ordenado para suas
frequências de ocorrência correspondentes (o dicionário ordenado é o classificado
). Em segundo lugar, vamos preceder dois tokens especiais para o vocabulário - o
preenchimento e o token desconhecido:torchtextVocabvocabtoken_counts

>>> ## Step 3: encoding each unique token into integers

>>> from torchtext.vocab import vocab

>>> sorted_by_freq_tuples = sorted(

... token_counts.items(), key=lambda x: x[1], reverse=True

... )

>>> ordered_dict = OrderedDict(sorted_by_freq_tuples)

>>> vocab = vocab(ordered_dict)

>>> vocab.insert_token("<pad>", 0)

>>> vocab.insert_token("<unk>", 1)

>>> vocab.set_default_index(1)
CopyExplain

Para demonstrar como usar o objeto, converteremos um texto de entrada de


exemplo em uma lista de valores inteiros: vocab

>>> print([vocab[token] for token in ['this', 'is',

... 'an', 'example']])


[11, 7, 35, 457]
CopyExplain

Observe que pode haver alguns tokens nos dados de validação ou teste que não
estão presentes nos dados de treinamento e, portanto, não estão incluídos no
mapeamento. Se tivermos tokens q (ou seja, o tamanho de passado para , que
neste caso é 69.023), então todos os tokens que não foram vistos antes e,
portanto, não estão incluídos no , receberão o inteiro 1 (um espaço reservado para
o token desconhecido). Em outras palavras, o índice 1 é reservado para palavras
desconhecidas. Outro valor reservado é o inteiro 0, que serve como um espaço
reservado, o chamado token de preenchimento, para ajustar o comprimento da
sequência. Mais tarde, quando estivermos construindo um modelo RNN no
PyTorch, consideraremos esse espaço reservado, 0, com mais
detalhes.token_countsVocabtoken_counts

Podemos definir a função para transformar cada texto no conjunto de dados de


acordo e a função para converter cada rótulo em 1 ou
0:text_pipelinelabel_pipeline

>>> ## Step 3-A: define the functions for transformation

>>> text_pipeline =\

... lambda x: [vocab[token] for token in tokenizer(x)]

>>> label_pipeline = lambda x: 1. if x == 'pos' else 0.


CopyExplain

Vamos gerar lotes de amostras usando e passar os pipelines de processamento


de dados declarados anteriormente para o argumento . Vamos encapsular a
codificação de texto e a função de transformação de rótulo na
função:DataLoadercollate_fncollate_batch

>>> ## Step 3-B: wrap the encode and transformation function

... def collate_batch(batch):

... label_list, text_list, lengths = [], [], []

... for _label, _text in batch:


... label_list.append(label_pipeline(_label))

... processed_text = torch.tensor(text_pipeline(_text),

... dtype=torch.int64)

... text_list.append(processed_text)

... lengths.append(processed_text.size(0))

... label_list = torch.tensor(label_list)

... lengths = torch.tensor(lengths)

... padded_text_list = nn.utils.rnn.pad_sequence(

... text_list, batch_first=True)

... return padded_text_list, label_list, lengths

>>>

>>> ## Take a small batch

>>> from torch.utils.data import DataLoader

>>> dataloader = DataLoader(train_dataset, batch_size=4,

... shuffle=False, collate_fn=collate_batch)


CopyExplain

Até agora, convertemos sequências de palavras em sequências de inteiros e


rótulos de ou em 1 ou 0. No entanto, há um problema que precisamos resolver: as
sequências atualmente têm comprimentos diferentes (como mostrado no resultado
da execução do código a seguir para quatro exemplos). Embora, em geral, os
RNNs possam lidar com sequências com comprimentos diferentes, ainda
precisamos garantir que todas as sequências em um mini-lote tenham o mesmo
comprimento para armazená-las eficientemente em um tensor. posneg

O PyTorch fornece um método eficiente, , que preencherá automaticamente os


elementos consecutivos que devem ser combinados em um lote com valores de
espaço reservado (0s) para que todas as sequências dentro de um lote tenham a
mesma forma. No código anterior, já criamos um carregador de dados de um
pequeno tamanho de lote a partir do conjunto de dados de treinamento e
aplicamos a função, que por si só incluía uma
chamada.pad_sequence()collate_batchpad_sequence()

No entanto, para ilustrar como o preenchimento funciona, pegaremos o primeiro


lote e imprimiremos os tamanhos dos elementos individuais antes de combiná-los
em mini-lotes, bem como as dimensões dos mini-lotes resultantes:

>>> text_batch, label_batch, length_batch = next(iter(dataloader))

>>> print(text_batch)

tensor([[ 35, 1742, 7, 449, 723, 6, 302, 4,

...

0, 0, 0, 0, 0, 0, 0, 0]],

>>> print(label_batch)

tensor([1., 1., 1., 0.])

>>> print(length_batch)

tensor([165, 86, 218, 145])

>>> print(text_batch.shape)

torch.Size([4, 218])
CopyExplain

Como você pode observar nas formas de tensor impressas, o número de colunas
no primeiro lote é 218, o que resultou da combinação dos quatro primeiros
exemplos em um único lote e usando o tamanho máximo desses exemplos. Isso
significa que os outros três exemplos (cujos comprimentos são 165, 86 e 145,
respectivamente) neste lote são acolchoados tanto quanto necessário para
corresponder a esse tamanho.

Finalmente, vamos dividir todos os três conjuntos de dados em carregadores de


dados com um tamanho de lote de 32:

>>> batch_size = 32

>>> train_dl = DataLoader(train_dataset, batch_size=batch_size,


... shuffle=True, collate_fn=collate_batch)

>>> valid_dl = DataLoader(valid_dataset, batch_size=batch_size,

... shuffle=False, collate_fn=collate_batch)

>>> test_dl = DataLoader(test_dataset, batch_size=batch_size,

... shuffle=False, collate_fn=collate_batch)


CopyExplain

Agora, os dados estão em um formato adequado para um modelo RNN, que


vamos implementar nas subseções a seguir. Na próxima subseção, no entanto,
discutiremos primeiro a incorporação de recursos, que é uma etapa de pré-
processamento opcional, mas altamente recomendada, usada para reduzir a
dimensionalidade dos vetores de palavras.

Incorporando camadas para codificação de sentenças

Durante a preparação dos dados na etapa anterior, foram geradas sequências de


mesmo comprimento. Os elementos dessas sequências eram
números inteiros que correspondiam aos índices de palavras únicas. Esses
índices de palavras podem ser convertidos em recursos de entrada de várias
maneiras diferentes. Uma maneira ingênua é aplicar uma codificação a quente
para converter os índices em vetores de zeros e uns. Em seguida, cada palavra
será mapeada para um vetor cujo tamanho é o número de palavras exclusivas em
todo o conjunto de dados. Dado que o número de palavras únicas (o tamanho do
vocabulário) pode ser da ordem de 104 – 105, que também será o número de
nossos recursos de entrada, um modelo treinado em tais recursos pode sofrer
da maldição da dimensionalidade. Além disso, esses recursos são muito
escassos, já que todos são zero, exceto um.

Uma abordagem mais elegante é mapear cada palavra para um vetor de tamanho
fixo com elementos de valor real (não necessariamente inteiros). Em contraste
com os vetores codificados em um quente, podemos usar vetores de tamanho
finito para representar um número infinito de números reais. (Em teoria, podemos
extrair números reais infinitos de um determinado intervalo, por exemplo [–1, 1].)
Essa é a ideia por trás da incorporação, que é uma técnica de aprendizado de
recursos que podemos utilizar aqui para aprender automaticamente os recursos
salientes para representar as palavras em nosso conjunto de dados. Dado o
número de palavras únicas, nPalavras, podemos selecionar o tamanho dos vetores de
incorporação (também conhecido como dimensão de incorporação) para ser muito
menor do que o número de palavras únicas (embedding_dim << nPalavras) para
representar todo o vocabulário como recursos de entrada.

As vantagens da incorporação em relação à codificação one-hot são as seguintes:

 Uma redução na dimensionalidade do espaço de feição para diminuir o efeito


da maldição da dimensionalidade

 A extração de características salientes, uma vez que a camada de


incorporação em um NN pode ser otimizada (ou aprendida)

A representação esquemática a seguir mostra como a incorporação funciona


mapeando índices de token para uma matriz de incorporação treinável:
Figura 15.10: Detalhamento de como funciona a incorporação

Dado um conjunto de tokens de tamanho n + 2 (n é o tamanho do conjunto de


tokens, mais o índice 0 é reservado para o espaço reservado de preenchimento e
1 é para as palavras não presentes no conjunto de tokens), uma matriz de
incorporação de tamanho (n + 2) × embedding_dim será criada onde cada linha
dessa matriz representa recursos numéricos associados a um token. Portanto,
quando um índice inteiro, i, é dado como entrada para a incorporação, ele
procurará a linha correspondente da matriz no índice i e retornará os recursos
numéricos. A matriz de incorporação serve como a camada de entrada para
nossos modelos NN. Na prática, a criação de uma camada de incorporação pode
ser feita simplesmente usando o . Vamos ver um exemplo em que criaremos uma
camada de incorporação e a aplicaremos a um lote de duas amostras, da seguinte
maneira:nn.Embedding

>>> embedding = nn.Embedding(

... num_embeddings=10,

... embedding_dim=3,

... padding_idx=0)

>>> # a batch of 2 samples of 4 indices each

>>> text_encoded_input = torch.LongTensor([[1,2,4,5],[4,3,2,0]])

>>> print(embedding(text_encoded_input))

tensor([[[-0.7027, 0.3684, -0.5512],

[-0.4147, 1.7891, -1.0674],

[ 1.1400, 0.1595, -1.0167],

[ 0.0573, -1.7568, 1.9067]],

[[ 1.1400, 0.1595, -1.0167],

[-0.8165, -0.0946, -0.1881],

[-0.4147, 1.7891, -1.0674],

[ 0.0000, 0.0000, 0.0000]]], grad_fn=<EmbeddingBackward>)


CopyExplain
A entrada para esse modelo (camada de incorporação) deve ter a classificação 2
com o tamanho do lote de dimensionalidade × input_length, onde input_length é o
comprimento das sequências (aqui, 4). Por exemplo, uma sequência de entrada
no mini-lote poderia ser <1, 5, 9, 2>, onde cada elemento dessa sequência é o
índice das palavras únicas. A saída terá o tamanho do lote de dimensionalidade
× input_length × embedding_dim, onde embedding_dim é o tamanho dos recursos
de incorporação (aqui, definido como 3). O outro argumento fornecido para a
camada de incorporação, , corresponde aos valores inteiros exclusivos que o
modelo receberá como entrada (por exemplo, n + 2, definido aqui como 10).
Portanto, a matriz de incorporação, neste caso, tem o tamanho
10×3.num_embeddings

padding_idx indicao índice de token para preenchimento (aqui, 0), que, se


especificado, não contribuirá para as atualizações de gradiente durante o
treinamento. Em nosso exemplo, o comprimento da sequência original da segunda
amostra é 3, e nós a preenchemos com mais 1 elemento 0. A saída de
incorporação do elemento acolchoado é [0, 0, 0].

Construindo um modelo RNN

Agora estamos prontos para construir um modelo RNN. Usando a classe,


podemos combinar a camada de incorporação, as camadas recorrentes do RNN e
as camadas não recorrentes totalmente conectadas. Para as camadas
recorrentes, podemos usar qualquer uma das seguintes implementações: nn.Module

 RNN: uma camada RNN regular, ou seja, uma camada recorrente totalmente
conectada
 LSTM: um RNN de memória de curto prazo longo, que é útil para capturar as
dependências de longo prazo
 GRU: uma camada recorrente com uma unidade recorrente fechada, como
proposto em Learning Phrase Representations Using RNN Encoder–Decoder
for Statistical Machine Translation por K. Cho et al., 2014
(https://arxiv.org/abs/1406.1078v3), como uma alternativa aos LSTMs

Para ver como um modelo de RNN multicamada pode ser construído usando uma
dessas camadas recorrentes, no exemplo a seguir, criaremos um modelo de RNN
com duas camadas recorrentes do tipo . Finalmente, adicionaremos uma camada
totalmente conectada não recorrente como a camada de saída, que retornará um
único valor de saída como previsão:RNN

>>> class RNN(nn.Module):

... def __init__(self, input_size, hidden_size):

... super().__init__()

... self.rnn = nn.RNN(input_size, hidden_size, num_layers=2,

... batch_first=True)

... # self.rnn = nn.GRU(input_size, hidden_size, num_layers,

... # batch_first=True)

... # self.rnn = nn.LSTM(input_size, hidden_size, num_layers,

... # batch_first=True)

... self.fc = nn.Linear(hidden_size, 1)

...

... def forward(self, x):

... _, hidden = self.rnn(x)

... out = hidden[-1, :, :] # we use the final hidden state

... # from the last hidden layer as

... # the input to the fully connected

... # layer

... out = self.fc(out)

... return out

>>>

>>> model = RNN(64, 32)

>>> print(model)

>>> model(torch.randn(5, 3, 64))

RNN(
(rnn): RNN(64, 32, num_layers=2, batch_first=True)

(fc): Linear(in_features=32, out_features=1, bias=True)

tensor([[ 0.0010],

[ 0.2478],

[ 0.0573],

[ 0.1637],

[-0.0073]], grad_fn=<AddmmBackward>)
CopyExplain

As you can see, building an RNN model using these recurrent layers is pretty
straightforward. In the next subsection, we will go back to our sentiment analysis
task and build an RNN model to solve that.

Building an RNN model for the sentiment analysis task

Since we have very long sequences, we are going to use an LSTM layer to


account for long-range effects. We will create an RNN model for sentiment
analysis, starting with an embedding layer producing word embeddings of feature
size 20 (). Then, a recurrent layer of type LSTM will be added. Finally, we will add
a fully connected layer as a hidden layer and another fully connected layer as the
output layer, which will return a single class-membership probability value via the
logistic sigmoid activation as the prediction:embed_dim=20

>>> class RNN(nn.Module):

... def __init__(self, vocab_size, embed_dim, rnn_hidden_size,

... fc_hidden_size):

... super().__init__()

... self.embedding = nn.Embedding(vocab_size,

... embed_dim,

... padding_idx=0)
... self.rnn = nn.LSTM(embed_dim, rnn_hidden_size,

... batch_first=True)

... self.fc1 = nn.Linear(rnn_hidden_size, fc_hidden_size)

... self.relu = nn.ReLU()

... self.fc2 = nn.Linear(fc_hidden_size, 1)

... self.sigmoid = nn.Sigmoid()

...

... def forward(self, text, lengths):

... out = self.embedding(text)

... out = nn.utils.rnn.pack_padded_sequence(

... out, lengths.cpu().numpy(), enforce_sorted=False, batch_first=True

... )

... out, (hidden, cell) = self.rnn(out)

... out = hidden[-1, :, :]

... out = self.fc1(out)

... out = self.relu(out)

... out = self.fc2(out)

... out = self.sigmoid(out)

... return out

>>>

>>> vocab_size = len(vocab)

>>> embed_dim = 20

>>> rnn_hidden_size = 64

>>> fc_hidden_size = 64

>>> torch.manual_seed(1)

>>> model = RNN(vocab_size, embed_dim,


rnn_hidden_size, fc_hidden_size)

>>> model

RNN(

(embedding): Embedding(69025, 20, padding_idx=0)

(rnn): LSTM(20, 64, batch_first=True)

(fc1): Linear(in_features=64, out_features=64, bias=True)

(relu): ReLU()

(fc2): Linear(in_features=64, out_features=1, bias=True)

(sigmoid): Sigmoid()

)
CopyExplain

Now we will develop the function to train the model on the given dataset for one
epoch and return the classification accuracy and loss: train

>>> def train(dataloader):

... model.train()

... total_acc, total_loss = 0, 0

... for text_batch, label_batch, lengths in dataloader:

... optimizer.zero_grad()

... pred = model(text_batch, lengths)[:, 0]

... loss = loss_fn(pred, label_batch)

... loss.backward()

... optimizer.step()

... total_acc += (

... (pred >= 0.5).float() == label_batch

... ).float().sum().item()

... total_loss += loss.item()*label_batch.size(0)


... return total_acc/len(dataloader.dataset), \

... total_loss/len(dataloader.dataset)
CopyExplain

Similarly, we will develop the function to measure the model’s performance on a


given dataset:evaluate

>>> def evaluate(dataloader):

... model.eval()

... total_acc, total_loss = 0, 0

... with torch.no_grad():

... for text_batch, label_batch, lengths in dataloader:

... pred = model(text_batch, lengths)[:, 0]

... loss = loss_fn(pred, label_batch)

... total_acc += (

... (pred>=0.5).float() == label_batch

... ).float().sum().item()

... total_loss += loss.item()*label_batch.size(0)

... return total_acc/len(dataloader.dataset), \

... total_loss/len(dataloader.dataset)
CopyExplain

The next step is to create a loss function and optimizer (Adam optimizer). For a
binary classification with a single class-membership probability output, we use the
binary cross-entropy loss () as the loss function:BCELoss

>>> loss_fn = nn.BCELoss()

>>> optimizer = torch.optim.Adam(model.parameters(), lr=0.001)


CopyExplain

Now we will train the model for 10 epochs and display the training and validation
performances:
>>> num_epochs = 10

>>> torch.manual_seed(1)

>>> for epoch in range(num_epochs):

... acc_train, loss_train = train(train_dl)

... acc_valid, loss_valid = evaluate(valid_dl)

... print(f'Epoch {epoch} accuracy: {acc_train:.4f}'

... f' val_accuracy: {acc_valid:.4f}')

Epoch 0 accuracy: 0.5843 val_accuracy: 0.6240

Epoch 1 accuracy: 0.6364 val_accuracy: 0.6870

Epoch 2 accuracy: 0.8020 val_accuracy: 0.8194

Epoch 3 accuracy: 0.8730 val_accuracy: 0.8454

Epoch 4 accuracy: 0.9092 val_accuracy: 0.8598

Epoch 5 accuracy: 0.9347 val_accuracy: 0.8630

Epoch 6 accuracy: 0.9507 val_accuracy: 0.8636

Epoch 7 accuracy: 0.9655 val_accuracy: 0.8654

Epoch 8 accuracy: 0.9765 val_accuracy: 0.8528

Epoch 9 accuracy: 0.9839 val_accuracy: 0.8596


CopyExplain

Depois de treinar este modelo por 10 épocas, vamos avaliá-lo nos dados do teste:

>>> acc_test, _ = evaluate(test_dl)

>>> print(f'test_accuracy: {acc_test:.4f}')

test_accuracy: 0.8512
CopyExplain

Mostrou 85% de precisão. (Observe que esse resultado não é o melhor quando
comparado aos métodos de última geração usados no conjunto de dados IMDb. O
objetivo era simplesmente mostrar como um RNN funciona no PyTorch.)
Mais sobre o RNN bidirecional

Além disso, definiremos a configuração do to , que fará com que a camada


recorrente passe pelas sequências de entrada de ambas as direções, do início ao
fim, bem como no sentido inverso:bidirectionalLSTMTrue

>>> class RNN(nn.Module):

... def __init__(self, vocab_size, embed_dim,

... rnn_hidden_size, fc_hidden_size):

... super().__init__()

... self.embedding = nn.Embedding(

... vocab_size, embed_dim, padding_idx=0

... )

... self.rnn = nn.LSTM(embed_dim, rnn_hidden_size,

... batch_first=True, bidirectional=True)

... self.fc1 = nn.Linear(rnn_hidden_size*2, fc_hidden_size)

... self.relu = nn.ReLU()

... self.fc2 = nn.Linear(fc_hidden_size, 1)

... self.sigmoid = nn.Sigmoid()

...

... def forward(self, text, lengths):

... out = self.embedding(text)

... out = nn.utils.rnn.pack_padded_sequence(

... out, lengths.cpu().numpy(), enforce_sorted=False, batch_first=True

... )

... _, (hidden, cell) = self.rnn(out)

... out = torch.cat((hidden[-2, :, :],

... hidden[-1, :, :]), dim=1)


... out = self.fc1(out)

... out = self.relu(out)

... out = self.fc2(out)

... out = self.sigmoid(out)

... return out

>>>

>>> torch.manual_seed(1)

>>> model = RNN(vocab_size, embed_dim,

... rnn_hidden_size, fc_hidden_size)

>>> model

RNN(

(embedding): Embedding(69025, 20, padding_idx=0)

(rnn): LSTM(20, 64, batch_first=True, bidirectional=True)

(fc1): Linear(in_features=128, out_features=64, bias=True)

(relu): ReLU()

(fc2): Linear(in_features=64, out_features=1, bias=True)

(sigmoid): Sigmoid()

)
CopyExplain

A camada RNN bidirecional faz duas passagens sobre cada sequência de


entrada: uma passagem para frente e uma passagem para trás ou para trás (note
que isso não deve ser confundido com as passagens para frente e para trás no
contexto de retropropagação). Os estados ocultos resultantes dessas passagens
para frente e para trás são geralmente concatenados em um único estado oculto.
Outros modos de mesclagem incluem soma, multiplicação (multiplicando os
resultados das duas passagens) e média (tomando a média das duas).

Também podemos tentar outros tipos de camadas recorrentes, como as regulares.


No entanto, como se vê, um modelo construído com camadas recorrentes
regulares não será capaz de alcançar um bom desempenho preditivo (mesmo nos
dados de treinamento). Por exemplo, se você tentar substituir a camada LSTM
bidirecional no código anterior por uma camada unidirecional (em vez de ) e treinar
o modelo em sequências completas, poderá observar que a perda nem mesmo
diminuirá durante o treinamento. O motivo é que as sequências nesse conjunto de
dados são muito longas, portanto, um modelo com uma camada não pode
aprender as dependências de longo prazo e pode sofrer problemas de gradiente
de desaparecimento ou explosão.RNNnn.RNNnn.LSTMRNN

Projeto dois – modelagem de linguagem em nível


de caractere no PyTorch
A modelagem de linguagem é uma aplicação fascinante que permite que
máquinas executem tarefas relacionadas à linguagem humana, como gerar frases
em inglês. Um dos estudos interessantes nessa área é Generating Text with
Recurrent Neural Networks, de Ilya Sutskever, James Martens e Geoffrey E.
Hinton, Proceedings of the 28th International Conference on Machine Learning
(ICML-11), 2011
(https://pdfs.semanticscholar.org/93c2/0e38c85b69fc2d2eb314b3c1217913f7db11.
pdf).

No modelo que vamos construir agora, a entrada é um documento de texto, e


nosso objetivo é desenvolver um modelo que possa gerar um novo texto que seja
semelhante em estilo ao documento de entrada. Exemplos de tais entradas são
um livro ou um programa de computador em uma linguagem de programação
específica.

Na modelagem de linguagem em nível de caractere, a entrada é dividida em uma


sequência de caracteres que são alimentados em nossa rede um caractere de
cada vez. A rede processará cada novo personagem em conjunto com a memória
dos personagens vistos anteriormente para prever o próximo.

A figura 15.11 mostra um exemplo de modelagem de linguagem em nível de


caractere (observe que EOS significa "fim da sequência"):
Figura 15.11: Modelagem de linguagem em nível de caractere

Podemos dividir essa implementação em três etapas separadas: preparar os


dados, construir o modelo RNN e executar a previsão e amostragem de próximo
caractere para gerar novo texto.

Pré-processamento do conjunto de dados

Nesta seção, prepararemos os dados para modelagem de linguagem em nível de


caractere.

Para obter os dados de entrada, visite o site do Project Gutenberg


em https://www.gutenberg.org/, que fornece milhares de e-books gratuitos. Para o
nosso exemplo, você pode baixar o livro A Ilha Misteriosa, de Júlio Verne
(publicado em 1874) em formato de texto simples
de https://www.gutenberg.org/files/1268/1268-0.txt.

Observe que este link o levará diretamente para a página de download. Se você
estiver usando o macOS ou um sistema operacional Linux, você pode baixar o
arquivo com o seguinte comando no terminal:

curl -O https://www.gutenberg.org/files/1268/1268-0.txt
CopyExplain

Se esse recurso ficar indisponível no futuro, uma cópia deste texto também será
incluída no diretório de códigos deste capítulo no repositório de códigos do livro
em https://github.com/rasbt/machine-learning-book.
Depois de baixarmos o conjunto de dados, podemos lê-lo em uma sessão Python
como texto simples. Usando o código a seguir, leremos o texto diretamente do
arquivo baixado e removeremos partes do início e do fim (estas contêm certas
descrições do projeto Gutenberg). Em seguida, criaremos uma variável Python, ,
que representa o conjunto de caracteres únicos observados neste texto:char_set

>>> import numpy as np

>>> ## Reading and processing text

>>> with open('1268-0.txt', 'r', encoding="utf8") as fp:

... text=fp.read()

>>> start_indx = text.find('THE MYSTERIOUS ISLAND')

>>> end_indx = text.find('End of the Project Gutenberg')

>>> text = text[start_indx:end_indx]

>>> char_set = set(text)

>>> print('Total Length:', len(text))

Total Length: 1112350

>>> print('Unique Characters:', len(char_set))

Unique Characters: 80
CopyExplain

Após o download e pré-processamento do texto, temos uma sequência composta


por 1.112.350 caracteres no total e 80 caracteres únicos. No entanto, a maioria
das bibliotecas NN e implementações RNN não podem lidar com dados de
entrada em formato de cadeia de caracteres, e é por isso que temos que converter
o texto em um formato numérico. Para fazer isso, vamos criar um dicionário
Python simples que mapeia cada caractere para um inteiro, . Também
precisaremos de um mapeamento reverso para converter os resultados do nosso
modelo de volta em texto. Embora o inverso possa ser feito usando um dicionário
que associa chaves inteiras a valores de caracteres, usar uma matriz NumPy e
indexar a matriz para mapear índices para esses caracteres exclusivos é mais
eficiente. A figura 15.12 mostra um exemplo de conversão de caracteres em
inteiros e o inverso para as palavras e : char2int"Hello""world"
Figura 15.12: Mapeamentos de caracteres e inteiros

A criação do dicionário para mapear caracteres para inteiros e o mapeamento


reverso por meio da indexação de uma matriz NumPy, como foi mostrado na
figura anterior, é o seguinte:

>>> chars_sorted = sorted(char_set)

>>> char2int = {ch:i for i,ch in enumerate(chars_sorted)}

>>> char_array = np.array(chars_sorted)

>>> text_encoded = np.array(

... [char2int[ch] for ch in text],

... dtype=np.int32

... )

>>> print('Text encoded shape:', text_encoded.shape)

Text encoded shape: (1112350,)

>>> print(text[:15], '== Encoding ==>', text_encoded[:15])

>>> print(text_encoded[15:21], '== Reverse ==>',

... ''.join(char_array[text_encoded[15:21]]))

THE MYSTERIOUS == Encoding ==> [44 32 29 1 37 48 43 44 29 42 33 39 45 43 1]

[33 43 36 25 38 28] == Reverse ==> ISLAND


CopyExplain
A matriz NumPy contém os valores codificados para todos os caracteres no texto.
Agora, vamos imprimir os mapeamentos dos cinco primeiros caracteres dessa
matriz:text_encoded

>>> for ex in text_encoded[:5]:

... print('{} -> {}'.format(ex, char_array[ex]))

44 -> T

32 -> H

29 -> E

1 ->

37 -> M
CopyExplain

Agora, vamos dar um passo atrás e olhar para o quadro geral do que estamos
tentando fazer. Para a tarefa de geração de texto, podemos formular o problema
como uma tarefa de classificação.

Suponha que tenhamos um conjunto de sequências de caracteres de texto


incompletos, como mostrado na Figura 15.13:
Figura 15.13: Prevendo o próximo caractere para uma sequência de texto

Na Figura 15.13, podemos considerar as sequências mostradas na caixa à


esquerda como entrada. Para gerar um novo texto, nosso objetivo é projetar um
modelo que possa prever o próximo caractere de uma dada sequência de entrada,
onde a sequência de entrada representa um texto incompleto. Por exemplo,
depois de ver "Deep Learn", o modelo deve prever "i" como o próximo caractere.
Dado que temos 80 caracteres únicos, esse problema se torna uma tarefa de
classificação multiclasse.

Começando com uma sequência de comprimento 1 (ou seja, uma única letra),
podemos gerar iterativamente um novo texto com base nessa abordagem de
classificação multiclasse, como ilustrado na Figura 15.14:

Figura 15.14: Gerando o próximo texto com base nesta abordagem de


classificação multiclasse
Para implementar a tarefa de geração de texto no PyTorch, vamos primeiro cortar
o comprimento da sequência para 40. Isso significa que o tensor de entrada, x,
consiste em 40 tokens. Na prática, o comprimento da sequência impacta na
qualidade do texto gerado. Sequências mais longas podem resultar em frases
mais significativas. Para sequências mais curtas, no entanto, o modelo pode se
concentrar em capturar palavras individuais corretamente, ignorando o contexto na
maior parte. Embora sequências mais longas geralmente resultem em sentenças
mais significativas, como mencionado, para sequências longas, o modelo RNN
terá problemas para capturar dependências de longo alcance. Assim, na prática,
encontrar um ponto ideal e um bom valor para o comprimento da sequência é um
problema de otimização de hiperparâmetros, que temos que avaliar
empiricamente. Aqui, vamos escolher 40, pois oferece um bom trade-off.

Como você pode ver na figura anterior, as entradas, x, e os destinos, y, são


deslocados por um caractere. Assim, dividiremos o texto em partes de tamanho
41: os primeiros 40 caracteres formarão a sequência de entrada, x, e os últimos
40 elementos formarão a sequência de destino, y.

Já armazenamos todo o texto codificado em sua ordem original em . Primeiro,


criaremos blocos de texto compostos por 41 caracteres cada. Vamos nos livrar
ainda mais do último pedaço se ele for menor do que 41 caracteres. Como
resultado, o novo conjunto de dados em partes, chamado , sempre conterá
sequências de tamanho 41. Os blocos de 41 caracteres serão usados para
construir a sequência x (ou seja, a entrada), bem como a sequência y (ou seja, o
alvo), ambos com 40 elementos. Por exemplo, a sequência x será constituída
pelos elementos com índices [0, 1, ..., 39]. Além disso, como a sequência y será
deslocada por uma posição em relação a x, seus índices correspondentes serão
[1, 2, ..., 40]. Em seguida, transformaremos o resultado em um objeto aplicando
uma classe autodefinida:text_encodedtext_chunksDatasetDataset

>>> import torch

>>> from torch.utils.data import Dataset

>>> seq_length = 40

>>> chunk_size = seq_length + 1

>>> text_chunks = [text_encoded[i:i+chunk_size]


... for i in range(len(text_encoded)-chunk_size+1)]

>>> from torch.utils.data import Dataset

>>> class TextDataset(Dataset):

... def __init__(self, text_chunks):

... self.text_chunks = text_chunks

...

... def __len__(self):

... return len(self.text_chunks)

...

... def __getitem__(self, idx):

... text_chunk = self.text_chunks[idx]

... return text_chunk[:-1].long(), text_chunk[1:].long()

>>>

>>> seq_dataset = TextDataset(torch.tensor(text_chunks))


CopyExplain

Vamos dar uma olhada em algumas sequências de exemplo desse conjunto de


dados transformado:

>>> for i, (seq, target) in enumerate(seq_dataset):

... print(' Input (x): ',

... repr(''.join(char_array[seq])))

... print('Target (y): ',

... repr(''.join(char_array[target])))

... print()

... if i == 1:

... break

Input (x): 'THE MYSTERIOUS ISLAND ***\n\n\n\n\nProduced b'


Target (y): 'HE MYSTERIOUS ISLAND ***\n\n\n\n\nProduced by'

Input (x): 'HE MYSTERIOUS ISLAND ***\n\n\n\n\nProduced by'

Target (y): 'E MYSTERIOUS ISLAND ***\n\n\n\n\nProduced by '


CopyExplain

Finally, the last step in preparing the dataset is to transform this dataset into mini-
batches:

>>> from torch.utils.data import DataLoader

>>> batch_size = 64

>>> torch.manual_seed(1)

>>> seq_dl = DataLoader(seq_dataset, batch_size=batch_size,

... shuffle=True, drop_last=True)


CopyExplain

Building a character-level RNN model

Now that the dataset is ready, building the model will be relatively straightforward:

>>> import torch.nn as nn

>>> class RNN(nn.Module):

... def __init__(self, vocab_size, embed_dim, rnn_hidden_size):

... super().__init__()

... self.embedding = nn.Embedding(vocab_size, embed_dim)

... self.rnn_hidden_size = rnn_hidden_size

... self.rnn = nn.LSTM(embed_dim, rnn_hidden_size,

... batch_first=True)

... self.fc = nn.Linear(rnn_hidden_size, vocab_size)

...

... def forward(self, x, hidden, cell):


... out = self.embedding(x).unsqueeze(1)

... out, (hidden, cell) = self.rnn(out, (hidden, cell))

... out = self.fc(out).reshape(out.size(0), -1)

... return out, hidden, cell

...

... def init_hidden(self, batch_size):

... hidden = torch.zeros(1, batch_size, self.rnn_hidden_size)

... cell = torch.zeros(1, batch_size, self.rnn_hidden_size)

... return hidden, cell


CopyExplain

Notice that we will need to have the logits as outputs of the model so that we can
sample from the model predictions in order to generate new text. We will get to this
sampling part later.

Then, we can specify the model parameters and create an RNN model:

>>> vocab_size = len(char_array)

>>> embed_dim = 256

>>> rnn_hidden_size = 512

>>> torch.manual_seed(1)

>>> model = RNN(vocab_size, embed_dim, rnn_hidden_size)

>>> model

RNN(

(embedding): Embedding(80, 256)

(rnn): LSTM(256, 512, batch_first=True)

(fc): Linear(in_features=512, out_features=80, bias=True)

)
CopyExplain
The next step is to create a loss function and optimizer (Adam optimizer). For a
multiclass classification (we have classes) with a single logits output for each target
character, we use as the loss function:vocab_size=80CrossEntropyLoss

>>> loss_fn = nn.CrossEntropyLoss()

>>> optimizer = torch.optim.Adam(model.parameters(), lr=0.005)


CopyExplain

Now we will train the model for 10,000 epochs. In each epoch, we will use only one
batch randomly chosen from the data loader, . We will also display the training loss
for every 500 epochs:seq_dl

>>> num_epochs = 10000

>>> torch.manual_seed(1)

>>> for epoch in range(num_epochs):

... hidden, cell = model.init_hidden(batch_size)

... seq_batch, target_batch = next(iter(seq_dl))

... optimizer.zero_grad()

... loss = 0

... for c in range(seq_length):

... pred, hidden, cell = model(seq_batch[:, c], hidden, cell)

... loss += loss_fn(pred, target_batch[:, c])

... loss.backward()

... optimizer.step()

... loss = loss.item()/seq_length

... if epoch % 500 == 0:

... print(f'Epoch {epoch} loss: {loss:.4f}')

Epoch 0 loss: 1.9689

Epoch 500 loss: 1.4064

Epoch 1000 loss: 1.3155


Epoch 1500 loss: 1.2414

Epoch 2000 loss: 1.1697

Epoch 2500 loss: 1.1840

Epoch 3000 loss: 1.1469

Epoch 3500 loss: 1.1633

Epoch 4000 loss: 1.1788

Epoch 4500 loss: 1.0828

Epoch 5000 loss: 1.1164

Epoch 5500 loss: 1.0821

Epoch 6000 loss: 1.0764

Epoch 6500 loss: 1.0561

Epoch 7000 loss: 1.0631

Epoch 7500 loss: 0.9904

Epoch 8000 loss: 1.0053

Epoch 8500 loss: 1.0290

Epoch 9000 loss: 1.0133

Epoch 9500 loss: 1.0047


CopyExplain

Next, we can evaluate the model to generate new text, starting with a given short
string. In the next section, we will define a function to evaluate the trained model.

Evaluation phase – generating new text passages

The RNN model we trained in the previous section returns the logits of size 80 for
each unique character. These logits can be readily converted to probabilities, via
the softmax function, that a particular character will be encountered as the next
character. To predict the next character in the sequence, we can simply select the
element with the maximum logit value, which is equivalent to selecting the
character with the highest probability. However, instead of always selecting the
character with the highest likelihood, we want to (randomly) sample from the
outputs; otherwise, the model will always produce the same text. PyTorch already
provides a class, , which we can use to draw random samples from a categorical
distribution. To see how this works, let’s generate some random samples from
three categories [0, 1, 2], with input logits
[1, 1, 1]:torch.distributions.categorical.Categorical

>>> from torch.distributions.categorical import Categorical

>>> torch.manual_seed(1)

>>> logits = torch.tensor([[1.0, 1.0, 1.0]])

>>> print('Probabilities:',

... nn.functional.softmax(logits, dim=1).numpy()[0])

Probabilities: [0.33333334 0.33333334 0.33333334]

>>> m = Categorical(logits=logits)

>>> samples = m.sample((10,))

>>> print(samples.numpy())

[[0]

[0]

[0]

[0]

[1]

[0]

[1]

[2]

[1]

[1]]
CopyExplain

As you can see, with the given logits, the categories have the same probabilities
(that is, equiprobable categories). Therefore, if we use a large sample size
(num_samples → ∞), we would expect the number of occurrences of each
category to reach ≈ 1/3 of the sample size. If we change the logits to [1, 1, 3], then
we would expect to observe more occurrences for category 2 (when a very large
number of examples are drawn from this distribution):

>>> torch.manual_seed(1)

>>> logits = torch.tensor([[1.0, 1.0, 3.0]])

>>> print('Probabilities:', nn.functional.softmax(logits, dim=1).numpy()[0])

Probabilities: [0.10650698 0.10650698 0.78698605]

>>> m = Categorical(logits=logits)

>>> samples = m.sample((10,))

>>> print(samples.numpy())

[[0]

[2]

[2]

[1]

[2]

[1]

[2]

[2]

[2]

[2]]
CopyExplain

Using , we can generate examples based on the logits computed by our


model.Categorical

We will define a function, , that receives a short starting string, , and generate a
new string, , which is initially set to the input string. is encoded to a sequence of
integers, . is passed to the RNN model one character at a time to update the
hidden states. The last character of is passed to the model to generate a new
character. Note that the output of the RNN model represents the logits (here, a
vector of size 80, which is the total number of possible characters) for the next
character after observing the input sequence by the
model.sample()starting_strgenerated_strstarting_strencoded_inputencoded_inputen
coded_input

Here, we only use the output (that is, ologits(T)), which is passed to the class to


generate a new sample. This new sample is converted to a character, which is
then appended to the end of the generated string, , increasing its length by 1.
Then, this process is repeated until the length of the generated string reaches the
desired value. The process of consuming the generated sequence as input for
generating new elements is called autoregression.Categoricalgenerated_text

The code for the function is as follows:sample()

>>> def sample(model, starting_str,

... len_generated_text=500,

... scale_factor=1.0):

... encoded_input = torch.tensor(

... [char2int[s] for s in starting_str]

... )

... encoded_input = torch.reshape(

... encoded_input, (1, -1)

... )

... generated_str = starting_str

...

... model.eval()

... hidden, cell = model.init_hidden(1)

... for c in range(len(starting_str)-1):

... _, hidden, cell = model(

... encoded_input[:, c].view(1), hidden, cell

... )
...

... last_char = encoded_input[:, -1]

... for i in range(len_generated_text):

... logits, hidden, cell = model(

... last_char.view(1), hidden, cell

... )

... logits = torch.squeeze(logits, 0)

... scaled_logits = logits * scale_factor

... m = Categorical(logits=scaled_logits)

... last_char = m.sample()

... generated_str += str(char_array[last_char])

...

... return generated_str


CopyExplain

Let’s now generate some new text:

>>> torch.manual_seed(1)

>>> print(sample(model, starting_str='The island'))

The island had been made

and ovylore with think, captain?" asked Neb; "we do."

It was found, they full to time to remove. About this neur prowers, perhaps ended? It is might be

rather rose?"

"Forward!" exclaimed Pencroft, "they were it? It seems to me?"

"The dog Top--"

"What can have been struggling sventy."

Pencroft calling, themselves in time to try them what proves that the sailor and Neb bounded this

tenarvan's feelings, and then


still hid head a grand furiously watched to the dorner nor his only
CopyExplain

As you can see, the model generates mostly correct words, and, in some cases,
the sentences are partially meaningful. You can further tune the training
parameters, such as the length of input sequences for training, and the model
architecture.

Furthermore, to control the predictability of the generated samples (that is,


generating text following the learned patterns from the training text versus adding
more randomness), the logits computed by the RNN model can be scaled before

being passed to for sampling. The scaling factor,  , can be interpreted as an


analog to the temperature in physics. Higher temperatures result in more entropy
or randomness versus more predictable behavior at lower temperatures. By scaling

the logits with  , the probabilities computed by the softmax function


become more uniform, as shown in the following code: Categorical

>>> logits = torch.tensor([[1.0, 1.0, 3.0]])

>>> print('Probabilities before scaling: ',

... nn.functional.softmax(logits, dim=1).numpy()[0])

>>> print('Probabilities after scaling with 0.5:',

... nn.functional.softmax(0.5*logits, dim=1).numpy()[0])

>>> print('Probabilities after scaling with 0.1:',

... nn.functional.softmax(0.1*logits, dim=1).numpy()[0])

Probabilities before scaling: [0.10650698 0.10650698 0.78698604]

Probabilities after scaling with 0.5: [0.21194156 0.21194156 0.57611688]

Probabilities after scaling with 0.1: [0.31042377 0.31042377 0.37915245]


CopyExplain
As you can see, scaling the logits by   results in near-uniform
probabilities [0.31, 0.31, 0.38]. Now, we can compare the generated text

with   and  , as shown in the following points:

 :
 >>> torch.manual_seed(1)

 >>> print(sample(model, starting_str='The island',

 ... scale_factor=2.0))

 The island is one of the colony?" asked the sailor, "there is not to be able to come to the shores

of the Pacific."

 "Yes," replied the engineer, "and if it is not the position of the forest, and the marshy way have

been said, the dog was not first on the shore, and

 found themselves to the corral.

 The settlers had the sailor was still from the surface of the sea, they were not received for the

sea. The shore was to be able to inspect the windows of Granite House.

 The sailor turned the sailor was the hor


CopyExplain

 :
 >>> torch.manual_seed(1)

 >>> print(sample(model, starting_str='The island',

 ... scale_factor=0.5))

 The island

 deep incomele.

 Manyl's', House, won's calcon-sglenderlessly," everful ineriorouins., pyra" into

 truth. Sometinivabes, iskumar gave-zen."


 Bleshed but what cotch quadrap which little cedass

 fell oprely

 by-andonem. Peditivall--"i dove Gurgeon. What resolt-eartnated to him

 ran trail.

 Withinhe)tiny turns returned, after owner plan bushelsion lairs; they were

 know? Whalerin branch I

 pites, Dougg!-iteun," returnwe aid masses atong thoughts! Dak,

 Hem-arches yone, Veay wantzer? Woblding,

 Herbert, omep
CopyExplain

Os resultados mostram que dimensionar os logits com   


(aumentando a temperatura) gera mais texto aleatório. Há um trade-off entre a
novidade do texto gerado e sua correção.

Nesta seção, trabalhamos com a geração de texto em nível de caractere, que é


uma tarefa de modelagem sequência a seq (seq2seq). Embora este exemplo
possa não ser muito útil por si só, é fácil pensar em várias aplicações úteis para
esses tipos de modelos; por exemplo, um modelo RNN semelhante pode ser
treinado como um chatbot para ajudar os usuários com consultas simples.

7.
Production machine learning combines which two key disciplines?

Modern software development

Feature selection and engineering

Software testing

Machine learning development


8.
What are the unique challenges to overcome in a production-grade ML system? (Check
all that apply)
Handling continuously changing data.

Building integrated ML systems.

Optimizing computational resources and costs.

Assessing model performance.

 Deploying the model to serve requests.

Continually operating while in production.

Training the model on real world data.


9.
Production grade machine learning challenges are addressed by implementing an
important concept:

Machine learning pipelines

Directed Acyclic Graphs (DAGs)

Orchestrators

Tensorflow Extended (TFX) 

ransformers – Melhorando o
Processamento de Linguagem Natural
com Mecanismos de Atenção
No capítulo anterior, aprendemos sobre redes neurais recorrentes (RNNs) e
suas aplicações no processamento de linguagem natural (NLP) por meio de um
projeto de análise de sentimento. No entanto, recentemente surgiu uma nova
arquitetura que demonstrou superar os modelos seqüência-a-
seqüência (seq2seq) baseados em RNN em várias tarefas de PNL. Essa é a
chamada arquitetura de transformadores.
Os transformadores revolucionaram o processamento de linguagem natural e
estiveram na vanguarda de muitas aplicações impressionantes, que vão desde a
tradução automatizada de linguagem (https://ai.googleblog.com/2020/06/recent-
advances-in-google-translate.html) e a modelagem de propriedades fundamentais
de sequências de proteínas
(https://www.pnas.org/content/118/15/e2016239118.short) até a criação de uma IA
que ajuda as pessoas a escrever código (https://github.blog/2021-06-29-
introducing-github-copilot-ai-pair-programmer).

Neste capítulo, você aprenderá sobre os mecanismos básicos


de atenção e autoatenção e verá como eles são usados na arquitetura original do
transformador. Em seguida, munidos de uma compreensão de como os
transformadores funcionam, exploraremos alguns dos modelos de PNL mais
influentes que surgiram dessa arquitetura e aprenderemos a usar um modelo de
linguagem em larga escala, o chamado modelo BERT, no PyTorch.

Abordaremos os seguintes tópicos:

 Melhorando RNNs com um mecanismo de atenção

 Introdução ao mecanismo autônomo de autoatenção

 Entendendo a arquitetura original do transformador

 Comparando modelos de linguagem em larga escala baseados em


transformadores

 Ajuste fino do BERT para classificação de sentimento

Adicionando um mecanismo de atenção


aos RNNs
Nesta seção, discutimos a motivação por trás do desenvolvimento de um
mecanismo de atenção, que ajuda os modelos preditivos a se concentrarem em
certas partes da sequência de entrada mais do que outras, e como ele foi
originalmente usado no contexto de RNNs. Observe que esta seção fornece uma
perspectiva histórica explicando por que o mecanismo de atenção foi
desenvolvido. Se detalhes matemáticos individuais parecerem complicados, você
pode se sentir livre para pulá-los, pois eles não são necessários para a próxima
seção, explicando o mecanismo de autoatenção para transformadores, que é o
foco deste capítulo.

Atenção ajuda RNNs a acessar informações


Para entender o desenvolvimento de um mecanismo de atenção, considere o
modelo RNN tradicional para uma tarefa seq2seq como a tradução de linguagem,
que analisa toda a sequência de entrada (por exemplo, uma ou mais frases) antes
de produzir a tradução, como mostrado na Figura 16.1:

Figura 16.1: Uma arquitetura tradicional de codificador-decodificador RNN para


uma tarefa de modelagem seq2seq

Por que o RNN está analisando toda a sentença de entrada antes de produzir a
primeira saída? Isso é motivado pelo fato de que traduzir uma frase palavra por
palavra provavelmente resultaria em erros gramaticais, como ilustrado na Figura
16.2:
Figura 16.2: Traduzir uma frase palavra por palavra pode levar a erros gramaticais

No entanto, como ilustrado na Figura 16.2, uma limitação dessa abordagem


seq2seq é que o RNN está tentando lembrar toda a sequência de entrada através
de uma única unidade oculta antes de traduzi-la. Comprimir todas as informações
em uma unidade oculta pode causar perda de informações, especialmente para
sequências longas. Assim, semelhante a como os humanos traduzem frases, pode
ser benéfico ter acesso a toda a sequência de entrada em cada etapa de tempo.

Em contraste com um RNN regular, um mecanismo de atenção permite que o


RNN acesse todos os elementos de entrada em cada etapa de tempo dada. No
entanto, ter acesso a todos os elementos de sequência de entrada em cada etapa
de tempo pode ser esmagador. Assim, para ajudar o RNN a se concentrar nos
elementos mais relevantes da sequência de entrada, o mecanismo de atenção
atribui diferentes pesos de atenção a cada elemento de entrada. Esses pesos de
atenção designam o quão importante ou relevante um determinado elemento de
sequência de entrada é em uma determinada etapa de tempo. Por exemplo,
revisitando a Figura 16.2, as palavras "mir, helfen, zu" podem ser mais relevantes
para produzir a palavra de saída "help" do que as palavras "kannst, du, Satz".

A próxima subseção apresenta uma arquitetura RNN que foi equipada com um
mecanismo de atenção para ajudar a processar longas sequências para tradução
de idiomas.

O mecanismo de atenção original para RNNs


Nesta subseção, resumiremos a mecânica do mecanismo de atenção que foi
originalmente desenvolvido para tradução de linguagem e apareceu pela primeira
vez no seguinte artigo: Neural Machine Translation by Together Learning to Align
and Translate de Bahdanau, D., Cho, K. e Bengio, Y.,
2014, https://arxiv.org/abs/1409.0473.

Dada uma sequência  de

entrada, o mecanismo de atenção atribui um peso a cada elemento   (ou,


para ser mais específico, sua representação oculta) e ajuda o modelo a identificar
em qual parte da entrada ele deve se concentrar. Por exemplo, suponha que
nossa entrada seja uma frase, e uma palavra com um peso maior contribua mais
para nossa compreensão de toda a frase. A RNN com o mecanismo de atenção
mostrado na Figura 16.3 (modelada após o artigo mencionado anteriormente)
ilustra o conceito geral de geração da segunda palavra de saída:

Figura 16.3: RNN com mecanismo de atenção


A arquitetura baseada na atenção representada na figura consiste em dois
modelos RNN, que explicaremos nas próximas subseções.

Processando as entradas usando um RNN


bidirecional
O primeiro RNN (RNN #1) do RNN baseado em atenção na Figura 16.3 é um RNN

bidirecional que gera vetores de contexto,  . Você pode pensar em um vetor de

contexto como uma versão aumentada do vetor de entrada,  . Em outras

palavras, o   vetor de entrada também incorpora informações de todos os


outros elementos de entrada por meio de um mecanismo de atenção. Como
podemos ver na Figura 16.3, a RNN #2 usa esse vetor de contexto, preparado
pela RNN #1, para gerar as saídas. No restante desta subseção, discutiremos
como a RNN #1 funciona, e revisitaremos a RNN #2 na próxima subseção.

O RNN bidirecional #1 processa a sequência de entrada x na direção regular para

frente (), bem como para trás ( ). Analisar uma sequência


na direção inversa tem o mesmo efeito que reverter a sequência de entrada
original — pense em ler uma frase em ordem inversa. A lógica por trás disso é
capturar informações adicionais, uma vez que as entradas atuais podem ter uma
dependência de elementos de sequência que vieram antes ou depois dela em
uma frase, ou ambos.

Consequentemente, a partir da leitura da sequência de entrada duas vezes (isto é,


para frente e para trás), temos dois estados ocultos para cada elemento

de sequência de entrada. Por exemplo, para o segundo elemento  de


sequência de entrada, obtemos o estado oculto da passagem para frente e o

estado   oculto da passagem para trás. Esses dois estados


ocultos são então concatenados para formar o estado  oculto . Por

exemplo, se ambos   e   são vetores de 128 dimensões, o

estado   oculto concatenado consistirá de 256 elementos. Podemos


considerar esse estado oculto concatenado como a "anotação" da palavra fonte,
uma vez que contém a informação da jésima palavra em ambas as direções.

Na próxima seção, veremos como esses estados ocultos concatenados são


processados e usados pelo segundo RNN para gerar as saídas.

Gerando saídas a partir de vetores de contexto


Na Figura 16.3, podemos considerar o RNN #2 como o RNN principal que
está gerando as saídas. Além dos estados ocultos, ele recebe os chamados

vetores de contexto como entrada. Um vetor de contexto   é uma versão

ponderada dos estados ocultos concatenados,  que


obtivemos da RNN #1 na subseção anterior. Podemos calcular o vetor de contexto
da i-ésima entrada como uma soma ponderada:

Aqui,   representa os pesos de atenção sobre a sequência de entrada no

contexto do i-ésimo elemento de sequência   de


entrada. Observe que cada i-ésimo elemento de sequência de entrada tem um
conjunto exclusivo de pesos de atenção. Discutiremos o cálculo dos pesos   
de atenção na próxima subseção.

Para o restante desta subseção, vamos discutir como os vetores de contexto são
usados através da segunda RNN na figura anterior (RNN #2). Assim como um
RNN de baunilha (regular), o RNN #2 também usa estados ocultos. Considerando
a camada oculta entre a "anotação" acima mencionada e a saída final, vamos

denotar o estado oculto no momento   como  . Agora, o RNN #2 recebe o

vetor de contexto mencionado   acima em cada etapa i como entrada.

Na Figura 16.3, vimos que o estado oculto depende do estado   

oculto anterior, da palavra de destino anterior e do vetor de contexto

, que são usados para gerar a saída   prevista para a

palavra    de destino no tempo i. Observe que o vetor de

sequência   refere-se ao vetor de sequência que representa a tradução correta

da sequência   de entrada que está disponível durante o treinamento. Durante o

treinamento, o rótulo verdadeiro (palavra)   é alimentado no próximo estado

; Como essa informação verdadeira do rótulo não está disponível para

previsão (inferência), alimentamos a saída   prevista, como descrito na


figura anterior.

Para resumir o que acabamos de discutir acima, o RNN baseado em atenção


consiste em dois RNNs. O RNN #1 prepara vetores de contexto a partir dos
elementos de sequência de entrada, e o RNN #2 recebe os vetores de contexto
como entrada. Os vetores de contexto são calculados por meio de uma soma
ponderada sobre as entradas, onde os pesos são os pesos de atenção A próxima

subseção discute como calculamos esses pesos de  atenção.

Calculando os pesos de atenção


Finalmente, vamos visitar a última peça que falta em nosso quebra-cabeça: os
pesos da atenção. Como esses pesos conectam em pares as entradas

(anotações) e as saídas (contextos), cada peso   de atenção tem dois


subscritos: j refere-se à posição do índice da entrada e i corresponde à posição do

índice de saída. O peso   da atenção é uma versão normalizada do escore

de alinhamento, onde o escore  de alinhamento avalia quão bem a entrada


em torno da posição j coincide com a saída na posição i. Para ser mais específico,
o peso da atenção é calculado normalizando as pontuações de alinhamento da
seguinte maneira:

Note que esta equação é semelhante à função softmax, que discutimos


no Capítulo 12, Parallelizing Neural Network Training with PyTorch, na
seção Estimando probabilidades de classe na classificação multiclasse através da

função softmax. Consequentemente, a atenção pesa ...   soma até 1.

Agora, para resumir, podemos estruturar o modelo de RNN baseado em atenção


em três partes. A primeira parte calcula anotações bidirecionais da entrada. A
segunda parte consiste no bloco recorrente, que é muito parecido com o RNN
original, exceto que ele usa vetores de contexto em vez da entrada original. A
última parte diz respeito ao cálculo dos pesos de atenção e vetores de contexto,
que descrevem a relação entre cada par de elementos de entrada e saída.

A arquitetura do transformador também utiliza um mecanismo de atenção, mas ao


contrário do RNN baseado em atenção, ele depende exclusivamente do
mecanismo de auto-atenção e não inclui o processo recorrente encontrado no
RNN. Em outras palavras, um modelo transformador processa toda a sequência
de entrada de uma só vez, em vez de ler e processar a sequência um elemento de
cada vez. Na próxima seção, apresentaremos uma forma básica do mecanismo de
auto-atenção antes de discutirmos a arquitetura do transformador em mais
detalhes na seção seguinte.

Introduzindo o mecanismo de
autoatenção
Na seção anterior, vimos que mecanismos de atenção podem ajudar RNNs a
lembrar o contexto ao trabalhar com sequências longas. Como veremos na
próxima seção, podemos ter uma arquitetura inteiramente baseada na atenção,
sem as partes recorrentes de um RNN. Essa arquitetura baseada em atenção é
conhecida como transformador, e discutiremos isso com mais detalhes mais
adiante.

Na verdade, os transformadores podem parecer um pouco complicados à primeira


vista. Então, antes de discutirmos os transformadores na próxima seção, vamos
mergulhar no mecanismo de autoatenção usado em transformadores. Na
verdade, como veremos, esse mecanismo de autoatenção é apenas um sabor
diferente do mecanismo de atenção que discutimos na seção anterior. Podemos
pensar no mecanismo de atenção discutido anteriormente como uma operação
que conecta dois módulos diferentes, ou seja, o codificador e o decodificador da
RNN. Como veremos, a autoatenção se concentra apenas na entrada e captura
apenas dependências entre os elementos de entrada. sem conectar dois módulos.

Na primeira subseção, apresentaremos uma forma básica de autoatenção sem


nenhum parâmetro de aprendizado, que é muito parecido com uma etapa de pré-
processamento para a entrada. Em seguida, na segunda subseção,
apresentaremos a versão comum da autoatenção que é usada na arquitetura do
transformador e envolve parâmetros aprendíveis.

Começando com uma forma básica de auto-


atenção
Para introduzir a autoatenção, vamos supor que temos uma sequência de entrada
de comprimento T, bem como uma sequência de

saída,    . Para

evitar confusão, usaremos   como saída final de todo o modelo do transformador

e   como saída da camada de autoatenção, pois é uma etapa intermediária no


modelo.

Cada i-ésimo elemento nessas sequências, e  , são vetores de


tamanho d (isto é, ) representando a informação de feição para a entrada na

posição i,    que é semelhante aos RNNs. Então, para


uma tarefa seq2seq, o objetivo da autoatenção é modelar as dependências do
elemento de entrada atual para todos os outros elementos de entrada. Para tanto,
os mecanismos de autoatenção são compostos por três etapas. Primeiro,
derivamos pesos de importância com base na semelhança entre o elemento atual
e todos os outros elementos na sequência. Em segundo lugar, normalizamos os
pesos, o que geralmente envolve o uso da já conhecida função softmax. Terceiro,
usamos esses pesos em combinação com os elementos de sequência
correspondentes para calcular o valor de atenção.

Mais formalmente, a saída de auto-atenção, é a soma ponderada de todas as

sequências de entrada T,     (onde  ). Por


exemplo, para o i-ésimo elemento de entrada, o valor de saída correspondente é
calculado da seguinte forma:
Assim, podemos pensar como um vetor de incorporação sensível ao contexto no

vetor de entrada   que envolve todos os outros elementos de sequência de

entrada ponderados por seus respectivos pesos de   atenção. Aqui, os


pesos de atenção, , são calculados com base na semelhança entre o elemento de

entrada atual, , e todos os outros elementos na sequência de entrada, 

. Mais concretamente, essa semelhança é


computada em duas etapas explicadas nos próximos parágrafos.

Primeiro, calculamos o produto de ponto entre o elemento de entrada atual, , e

outro elemento na sequência de entrada,  :

Antes de normalizarmos os valores para obter os pesos de atenção,  vamos

ilustrar como calculamos os     valores com um exemplo de código.


Aqui, vamos supor que temos uma frase de entrada "você pode me ajudar a
traduzir esta frase" que já foi mapeada para uma representação inteira por meio
de um dicionário, conforme explicado no Capítulo 15, Modelando dados
sequenciais usando redes neurais recorrentes:
>>> import torch

>>> sentence = torch.tensor(

>>> [0, # can

>>> 7, # you

>>> 1, # help

>>> 2, # me

>>> 5, # to

>>> 6, # translate

>>> 4, # this

>>> 3] # sentence

>>> )

>>> sentence

tensor([0, 7, 1, 2, 5, 6, 4, 3])
CopyExplain

Vamos supor também que já codificamos essa frase em uma representação


vetorial de número real por meio de uma camada de incorporação. Aqui, nosso
tamanho de incorporação é 16 e assumimos que o tamanho do dicionário é 10. O
código a seguir produzirá as incorporações de palavras de nossas oito palavras:

>>> torch.manual_seed(123)

>>> embed = torch.nn.Embedding(10, 16)

>>> embedded_sentence = embed(sentence).detach()

>>> embedded_sentence.shape

torch.Size([8, 16])
CopyExplain
Agora, podemos calcular   como o produto de ponto entre as

incorporações de ie jésima palavra. Podemos fazer isso para todos os   


valores da seguinte maneira:

>>> omega = torch.empty(8, 8)

>>> for i, x_i in enumerate(embedded_sentence):

>>> for j, x_j in enumerate(embedded_sentence):

>>> omega[i, j] = torch.dot(x_i, x_j)


CopyExplain

Embora o código anterior seja fácil de ler e entender, os loops podem ser muito
ineficientes, então vamos calcular isso usando a multiplicação de matrizes: for

>>> omega_mat = embedded_sentence.matmul(embedded_sentence.T)


CopyExplain

Podemos usar a função para verificar se essa multiplicação da matriz produz os


resultados esperados. Se dois tensores contiverem os mesmos valores, retorna ,
como podemos ver aqui:torch.allclosetorch.allcloseTrue

>>> torch.allclose(omega_mat, omega)

True
CopyExplain

Aprendemos a calcular os pesos baseados em similaridade para a i-ésima entrada

e todas as entradas na sequência ( para ), os pesos "brutos" (   

para ).  Podemos obter os pesos de atenção, , normalizando

os   valores através da função softmax familiar,  da seguinte forma:


Observe que o denominador envolve uma soma sobre todos os elementos de

entrada ( ). Assim, devido à aplicação desta função softmax, os pesos


somarão 1 após esta normalização, ou seja,

Podemos calcular os pesos de atenção usando a função softmax do PyTorch da


seguinte maneira:

>>> import torch.nn.functional as F

>>> attention_weights = F.softmax(omega, dim=1)

>>> attention_weights.shape

torch.Size([8, 8])
CopyExplain

Note que é uma   matriz, onde cada elemento representa um peso de

atenção,  . Por exemplo, se estamos processando a i-ésima palavra de


entrada, a i-ésimalinha dessa matriz contém os pesos de atenção
correspondentes para todas as palavras da frase. Esses pesos de atenção
indicam o quão relevante cada palavra é para a i-ésima palavra. Assim, as colunas
nesta matriz de atenção devem somar 1, o que podemos confirmar através do
seguinte código:attention_weights
>>> attention_weights.sum(dim=1)

tensor([1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000])


CopyExplain

Agora que vimos como calcular os pesos de atenção, vamos recapitular e resumir
os três principais passos por trás da operação de autoatenção:

1. Para um dado elemento de entrada, , e cada jésimo elemento no conjunto

{1, ..., T}, calcule o produto ponto,   

2. Obter o peso de atenção, ,  normalizando os produtos de ponto usando


a função softmax

3. Calcule a saída,  , como a soma ponderada sobre toda a sequência de

entrada: 

Estas etapas são ilustradas na Figura 16.4:

Figura 16.4: Um processo básico de auto-atenção para fins de ilustração


Por fim, vejamos um exemplo de código para calcular os vetores de

contexto,  , como a soma ponderada pela atenção das entradas (etapa 3


na Figura 16.4). Em particular, vamos supor que estamos computando o vetor de

contexto para a segunda palavra de entrada, ou seja,  :

>>> x_2 = embedded_sentence[1, :]

>>> context_vec_2 = torch.zeros(x_2.shape)

>>> for j in range(8):

... x_j = embedded_sentence[j, :]

... context_vec_2 += attention_weights[1, j] * x_j

>>> context_vec_2

tensor([-9.3975e-01, -4.6856e-01, 1.0311e+00, -2.8192e-01, 4.9373e-01, -1.2896e-02, -2.7327e-01,

-7.6358e-01, 1.3958e+00, -9.9543e-01,

-7.1288e-04, 1.2449e+00, -7.8077e-02, 1.2765e+00, -1.4589e+00,

-2.1601e+00])
CopyExplain

Novamente, podemos conseguir isso de forma mais eficiente usando a


multiplicação matricial. Usando o código a seguir, estamos computando os vetores
de contexto para todas as oito palavras de entrada:

>>> context_vectors = torch.matmul(

... attention_weights, embedded_sentence)


CopyExplain

Semelhante às incorporações de palavras de entrada armazenadas no , a matriz

tem dimensionalidade . A segunda linha nesta matriz contém o


vetor de contexto para a segunda palavra de entrada, e podemos verificar a
implementação usando
novamente:embedded_sentencecontext_vectorstorch.allclose()
>>> torch.allclose(context_vec_2, context_vectors[1])

True
CopyExplain

Como podemos ver, os cálculos de loop manual e matriz do segundo vetor de


contexto produziram os mesmos resultados.for

Esta seção implementou uma forma básica de autoatenção e, na próxima seção,


modificaremos essa implementação usando matrizes de parâmetros aprendíveis
que podem ser otimizadas durante o treinamento de redes neurais.

Parametrizando o mecanismo de autoatenção:


atenção ao produto em escala
Agora que você foi apresentado ao conceito básico por trás da autoatenção, esta
subseção resume o mecanismo de autoatenção mais avançado chamado atenção
ao produto de ponto em escala que é usado na arquitetura do transformador.
Observe que, na subseção anterior, não envolvemos nenhum parâmetro
aprendível ao calcular as saídas. Em outras palavras, usando o mecanismo básico
de auto-atenção previamente introduzido, o modelo do transformador é bastante
limitado em relação a como ele pode atualizar ou alterar os valores de atenção
durante a otimização do modelo para uma dada sequência. Para tornar o
mecanismo de auto-atenção mais flexível e passível de otimização do modelo,
apresentaremos três matrizes de peso adicionais que podem ser ajustadas como
parâmetros do modelo durante o treinamento do modelo. Denotamos essas três

matrizes de peso como  ,  e  . Eles são usados para projetar as


entradas em elementos de sequência de consulta, chave e valor, da seguinte
maneira:

 Sequência de consulta:   

para 
 Sequência de teclas:  

para 

 Sequência de valores:   

para 

A figura 16.5 ilustra como esses componentes individuais são usados para


calcular o vetor de incorporação com reconhecimento de contexto correspondente
ao segundo elemento de entrada:

Figura 16.5: Calculando o vetor de incorporação com reconhecimento de contexto


do segundo elemento de sequência

Terminologia de consulta, chave e valor

Os termos consulta, chave e valor que foram usados no artigo original são
inspirados em sistemas de recuperação de informação e bancos de dados. Por
exemplo, se inserirmos uma consulta, ela será comparada com os valores de
chave para os quais determinados valores são recuperados.

Aqui, ambos   e   são vetores de tamanho . Portanto, as

matrizes   de projeção e   tem a forma, enquanto   tem a forma

. (Observe que   é a dimensionalidade de cada

vetor de palavra,  .) Para simplificar, podemos projetar esses vetores para

ter a mesma forma, por exemplo, usando  . Para


fornecer intuição adicional via código, podemos inicializar essas matrizes de
projeção da seguinte maneira:

>>> torch.manual_seed(123)

>>> d = embedded_sentence.shape[1]

>>> U_query = torch.rand(d, d)

>>> U_key = torch.rand(d, d)

>>> U_value = torch.rand(d, d)


CopyExplain

Usando a matriz de projeção de consulta, podemos então calcular a sequência de


consulta. Para este exemplo, considere o segundo elemento de entrada, , como

nossa consulta,  conforme ilustrado na Figura 16.5:

>>> x_2 = embedded_sentence[1]

>>> query_2 = U_query.matmul(x_2)


CopyExplain

De maneira semelhante, podemos calcular as sequências de chave e valor e


>>> key_2 = U_key.matmul(x_2)

>>> value_2 = U_value.matmul(x_2)


CopyExplain

No entanto, como podemos ver na Figura 16.5, também precisamos das


sequências de chave e valor para todos os outros elementos de entrada, que
podemos calcular da seguinte maneira:

>>> keys = U_key.matmul(embedded_sentence.T).T

>>> values = U_value.matmul(embedded_sentence.T).T


CopyExplain

Na matriz de chaves, a i-ésima linha corresponde à sequência de chaves do i-


ésimo elemento de entrada, e o mesmo se aplica à matriz de valores. Podemos
confirmar isso usando novamente, que deve retornar: torch.allclose()True

>>> keys = U_key.matmul(embedded_sentence.T).T

>>> torch.allclose(key_2, keys[1])

>>> values = U_value.matmul(embedded_sentence.T).T

>>> torch.allclose(value_2, values[1])


CopyExplain

Na seção anterior, calculamos os pesos não normalizados, , como o produto de


ponto par a par entre o elemento de sequência de entrada dado, e o jésimo

elemento de sequência,  . Agora, nesta versão

parametrizada da autoatenção, calculamos   como o produto de ponto entre


a consulta e a chave:
Por exemplo, o código a seguir calcula o peso de atenção não normalizado, ou

seja,  o produto de ponto entre nossa consulta e o terceiro elemento de


sequência de entrada:

>>> omega_23 = query_2.dot(keys[2])

>>> omega_23

tensor(14.3667)
CopyExplain

Como precisaremos deles mais tarde, podemos escalar essa computação para
todas as chaves:

>>> omega_2 = query_2.matmul(keys.T)

>>> omega_2

tensor([-25.1623, 9.3602, 14.3667, 32.1482, 53.8976, 46.6626, -1.2131, -32.9391])


CopyExplain

O próximo passo na auto-atenção é passar dos pesos de atenção não

normalizados, , para os pesos de atenção normalizados,  usando a

função softmax. Podemos então usar   ainda mais para

dimensionar   antes de normalizá-lo através da função softmax, da seguinte


maneira:
Observe que a   escala por  , onde

normalmente  , garante que o comprimento euclidiano dos


vetores de peso estará aproximadamente na mesma faixa.

O código a seguir é para implementar essa normalização para calcular os pesos


de atenção para toda a sequência de entrada em relação ao segundo elemento de
entrada como a consulta:

>>> attention_weights_2 = F.softmax(omega_2 / d**0.5, dim=0)

>>> attention_weights_2

tensor([2.2317e-09, 1.2499e-05, 4.3696e-05, 3.7242e-03, 8.5596e-01, 1.4025e-01, 8.8896e-07,

3.1936e-10])
CopyExplain

Finalmente, a saída é uma média ponderada de sequências de valores: , que pode

ser implementada da seguinte maneira: 

>>> context_vector_2 = attention_weights_2.matmul(values)

>>> context_vector_2

tensor([-1.2226, -3.4387, -4.3928, -5.2125, -1.1249, -3.3041,

-1.4316, -3.2765, -2.5114, -2.6105, -1.5793, -2.8433, -2.4142,

-0.3998, -1.9917, -3.3499])


CopyExplain

Nesta seção, introduzimos um mecanismo de autoatenção com parâmetros


treináveis que nos permite calcular vetores de incorporação sensíveis ao contexto
envolvendo todos os elementos de entrada, que são ponderados por suas
respectivas pontuações de atenção. Na próxima seção, aprenderemos sobre a
arquitetura de transformadores, uma arquitetura de rede neural centrada em torno
do mecanismo de autoatenção introduzido nesta seção.
Atenção é tudo o que precisamos:
apresentar a arquitetura original do
transformador
Curiosamente, a arquitetura original do transformador é baseada em um
mecanismo de atenção que foi usado pela primeira vez em uma RNN.
Originalmente, a intenção por trás do uso de um mecanismo de atenção era
melhorar as capacidades de geração de texto de RNNs ao trabalhar com frases
longas. No entanto, apenas alguns anos depois de experimentar mecanismos de
atenção para RNNs, os pesquisadores descobriram que um modelo de linguagem
baseado em atenção era ainda mais poderoso quando as camadas recorrentes
eram excluídas. Isso levou ao desenvolvimento da arquitetura do transformador,
que é o tópico principal deste capítulo e das demais seções.

A arquitetura do transformador foi proposta pela primeira vez no artigo Attention Is


All You Need da NeurIPS 2017 por A. Vaswani e colegas
(https://arxiv.org/abs/1706.03762). Graças ao mecanismo de autoatenção, um
modelo de transformador pode capturar dependências de longo alcance entre os
elementos em uma sequência de entrada — em um contexto de PNL; Por
exemplo, isso ajuda o modelo a "entender" melhor o significado de uma frase de
entrada.

Embora essa arquitetura transformadora tenha sido originalmente projetada para


tradução de idiomas, ela pode ser generalizada para outras tarefas, como análise
de circunscrição em inglês, geração de texto e classificação de texto. Mais
adiante, discutiremos modelos de linguagem popular, como BERT e GPT, que
foram derivados dessa arquitetura original de transformadores. A figura 16.6, que
adaptamos do artigo original do transformador, ilustra a arquitetura principal e os
componentes que discutiremos nesta seção:
Figura 16.6: A arquitetura original do transformador

Nas subseções seguintes, analisamos passo a passo esse modelo original de


transformador, decompondo-o em dois blocos principais: um codificador e um
decodificador. O codificador recebe a entrada sequencial original e codifica as
incorporações usando um módulo de autoatenção de várias cabeças. O
decodificador recebe a entrada processada e produz a sequência resultante (por
exemplo, a frase traduzida) usando uma forma mascarada de autoatenção.

Incorporações de contexto de codificação por meio


de atenção de várias cabeças
O objetivo geral do bloco codificador é receber uma

entrada   sequencial e
mapeá-la em uma

representação   contínua que


é então passada para o decodificador.

O codificador é uma pilha de seis camadas idênticas. Seis não é um número


mágico aqui, mas apenas uma escolha de hiperparâmetro feita no papel original
do transformador. Você pode ajustar o número de camadas de acordo com o
desempenho do modelo. Dentro de cada uma dessas camadas idênticas, existem
duas subcamadas: uma calcula a autoatenção multi-cabeça, que discutiremos
abaixo, e a outra é uma camada totalmente conectada, que você já encontrou nos
capítulos anteriores.

Vamos falar primeiro sobre a autoatenção multi-cabeça, que é uma simples


modificação da atenção do produto ponto em escala abordada no início deste
capítulo. Na atenção ponto-produto dimensionada, usamos três matrizes
(correspondentes a consulta, valor e chave) para transformar a sequência de
entrada. No contexto da atenção multi-cabeça, podemos pensar nesse conjunto
de três matrizes como uma única cabeça de atenção. Como indicado pelo nome,
na atenção multi-cabeça, agora temos várias dessas cabeças (conjuntos de
consulta, valor e matrizes de chave) semelhantes a como as redes neurais
convolucionais podem ter vários núcleos.
Para explicar o conceito de autoatenção multi-cabeça com   cabeças em mais
detalhes, vamos dividi-lo nas etapas a seguir.

Primeiro, lemos na entrada 


sequencial. Suponha que cada elemento seja incorporado por um vetor de

comprimento d. Aqui, a entrada pode ser incorporada em uma   

matriz. Em seguida, criamos   conjuntos das matrizes de parâmetros de


aprendizagem de consulta, chave e valor:

 ...

Porque estamos usando essas matrizes de peso para projetar cada

elemento   para a correspondência de dimensão necessária nas

multiplicações de matriz, ambos   e têm a forma , e     tem a

forma  . Como resultado, ambas as sequências


resultantes, consulta e chave, têm comprimento e a sequência de valores

resultante tem comprimento  . Na prática, as pessoas muitas vezes

optam   pela simplicidade.


Para ilustrar a pilha de autoatenção de várias cabeças no código, primeiro
considere como criamos a matriz de projeção de consulta única na subseção
anterior, Parametrizando o mecanismo de autoatenção: Atenção dimensionada do
produto ponto:

>>> torch.manual_seed(123)

>>> d = embedded_sentence.shape[1]

>>> one_U_query = torch.rand(d, d)


CopyExplain

Agora, suponhamos que temos oito cabeças de atenção semelhantes ao

transformador original, ou seja: 

>>> h = 8

>>> multihead_U_query = torch.rand(h, d, d)

>>> multihead_U_key = torch.rand(h, d, d)

>>> multihead_U_value = torch.rand(h, d, d)


CopyExplain

Como podemos ver no código, várias cabeças de atenção podem ser adicionadas
simplesmente adicionando uma dimensão adicional.

Dividindo dados em várias cabeças de atenção

Na prática, em vez de ter uma matriz separada para cada cabeça de atenção, as
implementações de transformadores usam uma única matriz para todas as
cabeças de atenção. As cabeças de atenção são então organizadas em regiões
logicamente separadas nessa matriz, que podem ser acessadas por meio de
máscaras booleanas. Isso torna possível implementar a atenção de várias
cabeças de forma mais eficiente, porque várias multiplicações de matriz podem
ser implementadas como uma única multiplicação de matriz. No entanto, para
simplificar, estamos omitindo esse detalhe de implementação nesta seção.

Depois de inicializar as matrizes de projeção, podemos calcular as sequências


projetadas de forma semelhante a como é feito na atenção do produto de ponto
em escala. Agora, em vez de computar um conjunto de sequências de consulta,
chave e valor, precisamos computar conjuntos h delas. Mais formalmente, por
exemplo, o cálculo envolvendo a projeção de consulta para o i-ésimo ponto de
dados no cabeçalho jpode ser escrito da seguinte forma:

Em seguida, repetimos esse cálculo para todas as cabeças

No código, isso se parece com o seguinte para a segunda palavra de entrada


como a consulta:

>>> multihead_query_2 = multihead_U_query.matmul(x_2)

>>> multihead_query_2.shape

torch.Size([8, 16])
CopyExplain

A matriz tem oito linhas, onde cada linha corresponde à jésima cabeça de
atenção.multihead_query_2

Da mesma forma, podemos calcular sequências de chaves e valores para cada


cabeça:

>>> multihead_key_2 = multihead_U_key.matmul(x_2)

>>> multihead_value_2 = multihead_U_value.matmul(x_2)

>>> multihead_key_2[2]

tensor([-1.9619, -0.7701, -0.7280, -1.6840, -1.0801, -1.6778, 0.6763, 0.6547,

1.4445, -2.7016, -1.1364, -1.1204, -2.4430, -0.5982, -0.8292, -1.4401])


CopyExplain
A saída do código mostra o vetor chave do segundo elemento de entrada através
da terceira cabeça de atenção.

No entanto, lembre-se de que precisamos repetir os cálculos de chave e valor


para todos os elementos de sequência de entrada, não apenas — precisamos
disso para calcular a autoatenção mais tarde. Uma maneira simples e ilustrativa
de fazer isso é expandindo as incorporações de sequência de entrada para o
tamanho 8 como a primeira dimensão, que é o número de cabeças de atenção.
Usamos o método para isso:x_2.repeat()

>>> stacked_inputs = embedded_sentence.T.repeat(8, 1, 1)

>>> stacked_inputs.shape

torch.Size([8, 16, 8])


CopyExplain

Então, podemos ter uma multiplicação de matriz em lote, via , com as cabeças de
atenção para calcular todas as chaves:torch.bmm()

>>> multihead_keys = torch.bmm(multihead_U_key, stacked_inputs)

>>> multihead_keys.shape

torch.Size([8, 16, 8])


CopyExplain

Neste código, temos agora um tensor que se refere às oito cabeças de atenção
em sua primeira dimensão. A segunda e a terceira dimensões referem-se ao
tamanho da incorporação e ao número de palavras, respectivamente. Vamos
trocar a segunda e terceira dimensões para que as teclas tenham uma
representação mais intuitiva, ou seja, a mesma dimensionalidade da sequência de
entrada original:embedded_sentence

>>> multihead_keys = multihead_keys.permute(0, 2, 1)

>>> multihead_keys.shape

torch.Size([8, 8, 16])
CopyExplain
Após a reorganização, podemos acessar o segundo valor de chave na segunda
cabeça de atenção da seguinte maneira:

>>> multihead_keys[2, 1]

tensor([-1.9619, -0.7701, -0.7280, -1.6840, -1.0801, -1.6778, 0.6763, 0.6547,

1.4445, -2.7016, -1.1364, -1.1204, -2.4430, -0.5982, -0.8292, -1.4401])


CopyExplain

Podemos ver que este é o mesmo valor-chave que obtivemos anteriormente, o


que indica que nossas manipulações e cálculos de matriz complexa estão
corretos. Então, vamos repeti-lo para as sequências de valores: multihead_key_2[2]

>>> multihead_values = torch.matmul(

multihead_U_value, stacked_inputs)

>>> multihead_values = multihead_values.permute(0, 2, 1)


CopyExplain

Seguimos as etapas do cálculo de atenção de cabeça única para calcular os


vetores de contexto, conforme descrito na seção Parametrizando o mecanismo de
autoatenção: atenção ao produto em escala de pontos. Ignoraremos as etapas
intermediárias por brevidade e assumiremos que computamos os vetores de
contexto para o segundo elemento de entrada como a consulta e as oito cabeças
de atenção diferentes, que representamos como via dados
aleatórios:multihead_z_2

>>> multihead_z_2 = torch.rand(8, 16)


CopyExplain

Observe que os índices de primeira dimensão sobre as oito cabeças de atenção e


os vetores de contexto, semelhantes às sentenças de entrada, são vetores de 16

dimensões. Se isso parecer complicado, pense em oito cópias do   

mostrado na Figura 16.5; ou seja, temos uma para cada uma   das oito
cabeças de atenção.multihead_z_2
Em seguida, concatenamos esses vetores em um vetor longo de comprimento e
usamos uma projeção linear (através de uma camada totalmente conectada) para

mapeá-lo de volta a um vetor de comprimento    . Este


processo é ilustrado na figura 16.7:

Figura 16.7: Concatenação dos vetores de atenção ponto-produto dimensionados


em um vetor e passando-o através de uma projeção linear

No código, podemos implementar a concatenação e o esmagamento da seguinte


maneira:

>>> linear = torch.nn.Linear(8*16, 16)

>>> context_vector_2 = linear(multihead_z_2.flatten())

>>> context_vector_2.shape

torch.Size([16])
CopyExplain

Para resumir, a autoatenção de várias cabeças está repetindo o cálculo de


atenção do produto de ponto em escala várias vezes em paralelo e combinando
os resultados. Ele funciona muito bem na prática porque os múltiplos cabeçotes
ajudam o modelo a capturar informações de diferentes partes da entrada, o que é
muito semelhante a como os múltiplos núcleos produzem múltiplos canais em uma
rede convolucional, onde cada canal pode capturar informações de recursos
diferentes. Por fim, embora a atenção de várias cabeças pareça
computacionalmente cara, observe que a computação pode ser feita em paralelo
porque não há dependências entre as várias cabeças.

Aprendendo um modelo de linguagem:


decodificador e atenção mascarada multi-cabeça
Semelhante ao codificador, o decodificador também contém várias camadas
repetidas. Além das duas subcamadas que já introduzimos na seção anterior do
codificador (a camada de autoatenção multi-cabeça e a camada totalmente
conectada), cada camada repetida também contém uma subcamada de atenção
multi-cabeça mascarada.

A atenção mascarada é uma variação do mecanismo de atenção original, onde a


atenção mascarada apenas passa uma sequência de entrada limitada para o
modelo, "mascarando" um certo número de palavras. Por exemplo, se estamos
construindo um modelo de tradução de idiomas com um conjunto de dados
rotulado, na posição de sequência i durante o procedimento de treinamento,
alimentamos apenas as palavras de saída corretas das posições 1,...,i-1. Todas as
outras palavras (por exemplo, aquelas que vêm depois da posição atual) são
ocultadas do modelo para evitar que o modelo "trapaceie". Isso também é
consistente com a natureza da geração de texto: embora as verdadeiras palavras
traduzidas sejam conhecidas durante o treinamento, não sabemos nada sobre a
verdade fundamental na prática. Assim, só podemos alimentar o modelo com as
soluções para o que ele já gerou, na posição i.

A figura 16.8 ilustra como as camadas são organizadas no bloco decodificador:


Figura 16.8: Disposição das camadas na parte do descodificador

Primeiro, as palavras de saída anteriores (incorporações de saída) são passadas


para a camada de atenção multi-cabeça mascarada. Em seguida, a segunda
camada recebe as entradas codificadas do bloco codificador e a saída da camada
de atenção multi-cabeça mascarada em uma camada de atenção multi-cabeça.
Finalmente, passamos as saídas de atenção multi-cabeça para uma camada
totalmente conectada que gera a saída geral do modelo: um vetor de
probabilidade correspondente às palavras de saída.

Note que podemos usar uma função argmax para obter as palavras previstas a
partir dessas probabilidades de palavras semelhantes à abordagem geral que
adotamos na rede neural recorrente no Capítulo 15, Modelando dados
sequenciais usando redes neurais recorrentes.

Comparando o decodificador com o bloco codificador, a principal diferença é a


gama de elementos de sequência que o modelo pode atender. No codificador,
para cada palavra dada, a atenção é calculada em todas as palavras de uma
frase, o que pode ser considerado como uma forma de análise de entrada
bidirecional. O decodificador também recebe as entradas analisadas
bidirecionalmente do codificador. No entanto, quando se trata da sequência de
saída, o decodificador considera apenas os elementos que estão precedendo a
posição de entrada atual, o que pode ser interpretado como uma forma de análise
de entrada unidirecional.
Detalhes da implementação: codificações
posicionais e normalização de camadas
Nesta subseção, discutiremos alguns dos detalhes de implementação de
transformadores que analisamos até agora, mas que merecem ser mencionados.

Primeiro, vamos considerar as codificações posicionais que faziam parte da


arquitetura original do transformador da Figura 16.6. As codificações posicionais
ajudam na captura de informações sobre a ordenação da sequência de entrada e
são uma parte crucial dos transformadores, pois tanto as camadas de atenção do
produto ponto em escala quanto as camadas totalmente conectadas são
invariantes por permutação. Isso significa que, sem codificação posicional, a
ordem das palavras é ignorada e não faz diferença para as codificações baseadas
em atenção. No entanto, sabemos que a ordem das palavras é essencial para a
compreensão de uma frase. Por exemplo, considere as duas frases a seguir:

1. Maria dá uma flor a João


2. João dá uma flor a Maria

As palavras que ocorrem nas duas frases são exatamente as mesmas; Os


significados, no entanto, são muito diferentes.

Os transformadores permitem que as mesmas palavras em posições diferentes


tenham codificações ligeiramente diferentes, adicionando um vetor de pequenos
valores às incorporações de entrada no início dos blocos codificador e
decodificador. Em particular, a arquitetura original do transformador usa a
chamada codificação senoidal:
Aqui   está a posição da palavra e k denota o comprimento do vetor de
codificação, onde escolhemos k para ter a mesma dimensão que as incorporações
de palavras de entrada para que a codificação posicional e as incorporações de
palavras possam ser adicionadas. As funções senoidais são usadas para evitar
que as codificações posicionais se tornem muito grandes. Por exemplo, se
usássemos a posição absoluta 1,2,3..., n para serem codificações posicionais,
elas dominariam a codificação da palavra e tornariam os valores de incorporação
da palavra insignificantes.

Em geral, existem dois tipos de codificações posicionais, uma absoluta (como


mostrado na fórmula anterior) e uma relativa. O primeiro registrará posições
absolutas de palavras e é sensível a mudanças de palavras em uma frase. Ou
seja, codificações posicionais absolutas são vetores fixos para cada posição dada.
Por outro lado, as codificações relativas apenas mantêm a posição relativa das
palavras e são invariantes ao deslocamento de frases.

Em seguida, vamos examinar o mecanismo de normalização de camadas, que


foi introduzido pela primeira vez por J. Ba, J.R. Kiros e G.E. Hinton em 2016 no
artigo de mesmo nome Layer
Normalization (URL: https://arxiv.org/abs/1607.06450). Enquanto a normalização
em lote, que discutiremos em mais detalhes no Capítulo 17, Generative
Adversarial Networks for Synthesizing New Data, é uma escolha popular em
contextos de visão computacional, a normalização de camadas é a escolha
preferida em contextos de PNL, onde os comprimentos das sentenças podem
variar. A figura 16.9 ilustra as principais diferenças de normalização de camada e
lote lado a lado:
Figura 16.9: Uma comparação da normalização de lote e camada

Enquanto a normalização de camada é tradicionalmente executada em todos os


elementos em um determinado recurso para cada recurso independentemente, a
normalização de camada usada em transformadores estende esse conceito e
calcula as estatísticas de normalização em todos os valores de recurso
independentemente para cada exemplo de treinamento.

Como a normalização de camada calcula a média e o desvio padrão para cada


exemplo de treinamento, ela relaxa as restrições ou dependências de tamanho de
minilote. Em contraste com a normalização em lote, a normalização de camada é,
portanto, capaz de aprender com dados com tamanhos de minilote pequenos e
comprimentos variados. No entanto, observe que a arquitetura original do
transformador não tem entradas de comprimento variável (as sentenças são
acolchoadas quando necessário) e, ao contrário das RNNs, não há recorrência no
modelo. Então, como podemos então justificar o uso da normalização de camada
sobre a normalização de lote? Os transformadores são geralmente treinados em
corpora de texto muito grandes, o que requer computação paralela; Isso pode ser
um desafio para alcançar com a normalização em lote, que tem uma dependência
entre os exemplos de treinamento. A normalização de camadas não tem essa
dependência e, portanto, é uma escolha mais natural para transformadores.
Criando modelos de linguagem em
grande escala aproveitando dados não
rotulados
Nesta seção, discutiremos modelos populares de transformadores em grande
escala que surgiram do transformador original. Um tema comum entre esses
transformadores é que eles são pré-treinados em conjuntos de dados muito
grandes e não rotulados e, em seguida, ajustados para suas respectivas tarefas-
alvo. Primeiro, apresentaremos o procedimento comum de treinamento de
modelos baseados em transformadores e explicaremos como ele é diferente do
transformador original. Em seguida, nos concentraremos em modelos populares
de linguagem em larga escala, incluindo Transformador Pré-treinado
Gerativo (GPT), Representações de Codificador Bidirecional de Transformadores
(BERT) e Transformadores Bidirecionais e Auto-Regressivos (BART).

Pré-treinamento e ajuste fino de modelos de


transformadores
Em uma seção anterior, Atenção é tudo o que precisamos: introduzindo a
arquitetura original do transformador, discutimos como a arquitetura original do
transformador pode ser usada para tradução de idiomas. A tradução de idiomas é
uma tarefa supervisionada e requer um conjunto de dados rotulado, o que pode
ser muito caro de obter. A falta de grandes conjuntos de dados rotulados é
um problema duradouro no aprendizado profundo, especialmente para modelos
como o transformer, que são ainda mais famintos por dados do que outras
arquiteturas de aprendizado profundo. No entanto, dado que grandes quantidades
de texto (livros, sites e postagens em mídias sociais) são geradas todos os dias,
uma questão interessante é como podemos usar esses dados não rotulados para
melhorar o treinamento do modelo.

A resposta para saber se podemos aproveitar dados não rotulados em


transformadores é sim, e o truque é um processo chamado aprendizagem auto-
supervisionada: podemos gerar "rótulos" a partir do aprendizado supervisionado
a partir do próprio texto simples. Por exemplo, dado um corpus de texto grande e
não rotulado, treinamos o modelo para realizar a predição da próxima palavra, o
que permite que o modelo aprenda a distribuição de probabilidade das palavras e
pode formar uma base forte para se tornar um modelo de linguagem poderoso.

A aprendizagem auto-supervisionada é tradicionalmente também referida


como pré-treinamento não supervisionado e é essencial para o sucesso dos
modelos modernos baseados em transformadores. O "não supervisionado" no pré-
treinamento não supervisionado supostamente se refere ao fato de usarmos
dados não rotulados; No entanto, como usamos a estrutura dos dados para gerar
rótulos (por exemplo, a tarefa de previsão de próxima palavra mencionada
anteriormente), ainda é um processo de aprendizado supervisionado.

Para elaborar um pouco mais sobre como funciona o pré-treinamento não


supervisionado e a previsão de próxima palavra, se tivermos uma frase
contendo n palavras, o procedimento de pré-treinamento pode ser decomposto
nas três etapas a seguir:

1. No passo 1, alimente as palavras terra-verdade 1, ..., i-1.


2. Peça ao modelo para prever a palavra na posição i e compare-a com a
palavra verdade-base i.
3. Atualize o modelo e a etapa de tempo, i:= i+1. Volte para a etapa 1 e repita
até que todas as palavras sejam processadas.

Devemos notar que, na próxima iteração, sempre alimentamos o modelo com as


palavras terra-verdade (corretas) em vez do que o modelo gerou na rodada
anterior.

A ideia principal do pré-treinamento é fazer uso de texto sem formatação e, em


seguida, transferir e ajustar o modelo para executar algumas tarefas específicas
para as quais um conjunto de dados rotulado (menor) está disponível. Agora,
existem muitos tipos diferentes de técnicas de pré-treinamento. Por exemplo, a
tarefa de predição de próxima palavra mencionada anteriormente pode ser
considerada como uma abordagem unidirecional de pré-treinamento.
Posteriormente, apresentaremos técnicas adicionais de pré-treinamento que são
utilizadas em diferentes modelos de linguagem para alcançar várias
funcionalidades.

Um procedimento de treinamento completo de um modelo baseado em


transformador consiste em duas partes: (1) pré-treinamento em um grande
conjunto de dados não rotulado e (2) treinamento (isto é, ajuste fino) do modelo
para tarefas específicas a jusante usando um conjunto de dados rotulado. Na
primeira etapa, o modelo pré-treinado não é projetado para nenhuma tarefa
específica, mas sim treinado como um modelo de linguagem "geral". Depois, por
meio da segunda etapa, ele pode ser generalizado para qualquer tarefa
personalizada por meio de aprendizado supervisionado regular em um conjunto de
dados rotulado.

Com as representações que podem ser obtidas a partir do modelo pré-treinado,


existem principalmente duas estratégias para transferir e adotar um modelo para
uma tarefa específica: (1) uma abordagem baseada em recursos e (2)
uma abordagem de ajuste fino. (Aqui, podemos pensar nessas representações
como as ativações de camada oculta das últimas camadas de um modelo.)

A abordagem baseada em recursos usa as representações pré-treinadas como


recursos adicionais a um conjunto de dados rotulado. Isso exige que aprendamos
a extrair características de sentenças do modelo pré-treinado. Um modelo
inicial que é bem conhecido por essa abordagem de extração de recursos é o
ELMo (Embeddings from Language Models) proposto por Peters e colegas em
2018 no artigo Deep Contextualized Word
Representations (URL: https://arxiv.org/abs/1802.05365). ELMo é um modelo de
linguagem bidirecional pré-treinado que mascara palavras em uma determinada
taxa. Em particular, ele mascara aleatoriamente 15% das palavras de entrada
durante o pré-treinamento, e a tarefa de modelagem é preencher esses espaços
em branco, ou seja, prever as palavras ausentes (mascaradas). Isso é diferente da
abordagem unidirecional que introduzimos anteriormente, que esconde todas as
palavras futuras no passo i. O mascaramento bidirecional permite que um modelo
aprenda com ambas as extremidades e, assim, pode capturar informações mais
holísticas sobre uma frase. O modelo ELMo pré-treinado pode gerar
representações de sentenças de alta qualidade que, posteriormente, servem como
recursos de entrada para tarefas específicas. Em outras palavras, podemos
pensar na abordagem baseada em recursos como uma técnica de extração de
recursos baseada em modelo semelhante à análise de componentes principais,
que abordamos no Capítulo 5, Compactando dados via redução de
dimensionalidade.

A abordagem de ajuste fino, por outro lado, atualiza os parâmetros do modelo pré-
treinado de forma supervisionada regular via backpropagation. Ao contrário do
método baseado em recursos, geralmente também adicionamos outra camada
totalmente conectada ao modelo pré-treinado, para realizar determinadas tarefas,
como classificação, e atualizar todo o modelo com base no desempenho de
previsão no conjunto de treinamento rotulado. Um modelo popular que segue essa
abordagem é o BERT, um modelo de transformador em larga escala pré-treinado
como um modelo de linguagem bidirecional. Discutiremos o BERT com mais
detalhes nas subseções a seguir. Além disso, na última seção deste capítulo,
veremos um exemplo de código mostrando como ajustar um modelo BERT pré-
treinado para classificação de sentimento usando o conjunto de dados de revisão
de filmes com o qual trabalhamos no Capítulo 8, Aplicando Machine Learning à
Análise de Sentimento, e no Capítulo 15, Modelando Dados Sequenciais Usando
Redes Neurais Recorrentes.

Antes de passarmos para a próxima seção e começarmos nossa discussão sobre


modelos populares de linguagem baseada em transformadores, a figura a seguir
resume os dois estágios dos modelos de transformadores de treinamento e ilustra
a diferença entre as abordagens baseadas em recursos e de ajuste fino:
Figura 16.10: As duas principais formas de adoptar um transformador pré-treinado
para tarefas a jusante

Aproveitando dados sem rótulo com GPT


O Generative Pre-trained Transformer (GPT) é uma série popular de modelos
de linguagem em larga escala para geração de texto desenvolvida pela OpenAI. O
modelo mais recente, o GPT-3, lançado em maio de 2020 (Language Models are
Few-Shot Learners), está produzindo resultados surpreendentes. A qualidade do
texto gerado pelo GPT-3 é muito difícil de distinguir dos textos gerados por
humanos. Nesta seção, vamos discutir como o modelo GPT funciona em alto nível
e como ele evoluiu ao longo dos anos.

Conforme listado na Tabela 16.1, uma evolução óbvia dentro da série de modelos
GPT é o número de parâmetros:

Núm
Ano
ero
Mo de
de
del lanç Título Link do papel
parâ
o ame
metr
nto
os
Melhorar
a
compree
nsão da
110 língua https://www.cs.ubc.ca/~amuham01/
GP
2018 milh através LING530/papers/
T-1
ões de pré- radford2018improving.pdf
formaçã
o
generativ
a

Modelos
de
linguage
https://www.semanticscholar.org/paper/
m são
1,5 Language-Models-are-Unsupervised-
GP aprendiz
2019 bilhã Multitask-Learners-Radford-Wu/
T-2 es
o 9405cc0d6169988371b2755e573cc28650d
multitare
14dfe
fa não
supervisi
onados

Modelos
de
175 linguage
GP
2020 bilhõ m são https://arxiv.org/pdf/2005.14165.pdf
T-3
es poucos
aprendiz
es

Quadro 16.1: Visão geral dos modelos GPT


Mas não vamos nos antecipar e dar uma olhada mais de perto no modelo GPT-1
primeiro, que foi lançado em 2018. Seu procedimento de treinamento pode ser
decomposto em duas etapas:

1. Pré-treinamento em uma grande quantidade de texto sem formatação sem


rótulo
2. Ajuste fino supervisionado

Como ilustra a Figura 16.11 (adaptada do artigo GPT-1), podemos considerar o


GPT-1 como um transformador composto por (1) um decodificador (e sem um
bloco codificador) e (2) uma camada adicional que é adicionada posteriormente
para o ajuste fino supervisionado para realizar tarefas específicas:

Figura 16.11: O transformador GPT-1

Na figura, observe que se nossa tarefa for Previsão de Texto (prevendo a próxima


palavra), o modelo estará pronto após a etapa de pré-treinamento. Caso contrário,
por exemplo, se nossa tarefa estiver relacionada à classificação ou regressão,
então o ajuste fino supervisionado será necessário.

Durante o pré-treinamento, o GPT-1 utiliza uma estrutura decodificadora de


transformadores, onde, em uma dada posição de palavra, o modelo depende
apenas de palavras anteriores para prever a próxima palavra. O GPT-1 utiliza um
mecanismo de autoatenção unidirecional, em oposição a um bidirecional como no
BERT (que abordaremos mais adiante neste capítulo), porque o GPT-1 é focado
na geração de texto e não na classificação. Durante a geração de texto, ele
produz palavras uma a uma com uma direção natural da esquerda para a direita.
Há um outro aspecto que merece destaque aqui: durante o procedimento de
treinamento, para cada posição, sempre alimentamos as palavras corretas das
posições anteriores para o modelo. No entanto, durante a inferência, apenas
alimentamos o modelo com as palavras que ele gerou para poder gerar novos
textos.

Depois de obter o modelo pré-treinado (o bloco na figura anterior rotulado


como Transformer), nós então o inserimos entre o bloco de pré-processamento de
entrada e uma camada linear, onde a camada linear serve como uma camada de
saída (semelhante aos modelos anteriores de redes neurais profundas que
discutimos anteriormente neste livro). Para tarefas de classificação, o ajuste fino é
tão simples quanto primeiro tokenizar a entrada e, em seguida, alimentá-la no
modelo pré-treinado e na camada linear recém-adicionada, que é seguida por uma
função de ativação softmax. No entanto, para tarefas mais complicadas, como
responder a perguntas, as entradas são organizadas em um determinado formato
que não corresponde necessariamente ao modelo pré-treinado, o que requer uma
etapa de processamento extra personalizada para cada tarefa. Os leitores
interessados em modificações específicas são encorajados a ler o artigo do GPT-1
para obter detalhes adicionais (o link é fornecido na tabela anterior).

O GPT-1 também tem um desempenho surpreendentemente bom em tarefas


de tiro zero, o que prova sua capacidade de ser um modelo de linguagem geral
que pode ser personalizado para diferentes tipos de tarefas com ajuste fino
mínimo específico de tarefa. O aprendizado zero-shot geralmente descreve uma
circunstância especial no aprendizado de máquina em que, durante o teste e a
inferência, o modelo é necessário para classificar amostras de classes que não
foram observadas durante o treinamento. No contexto do GPT, a configuração
zero-shot refere-se a tarefas invisíveis.

A adaptabilidade do GPT inspirou os pesquisadores a se livrarem da entrada


específica da tarefa e da configuração do modelo, o que levou ao desenvolvimento
do GPT-2. Ao contrário de seu antecessor, o GPT-2 não requer mais nenhuma
modificação adicional durante os estágios de entrada ou ajuste fino. Em vez de
reorganizar as sequências para corresponder ao formato necessário, o GPT-2
pode distinguir entre diferentes tipos de entradas e executar as tarefas
correspondentes a jusante com pequenas dicas, os chamados "contextos". Isso é
obtido modelando probabilidades de saída condicionadas à entrada e ao tipo de

tarefa,  em vez de apenas


condicionar a entrada. Por exemplo, espera-se que o modelo reconheça uma
tarefa de tradução se o contexto incluir . translate to French, English text, French
text

Isso soa muito mais "artificialmente inteligente" do que o GPT e é de fato a


melhoria mais perceptível além do tamanho do modelo. Assim como o título de
seu artigo correspondente indica (Language Models are Unsupervised Multitask
Learners), um modelo de linguagem não supervisionado pode ser a chave para o
aprendizado zero-shot, e o GPT-2 faz pleno uso da transferência de tarefas zero-
shot para construir esse aprendiz multitarefa.

Em comparação com o GPT-2, o GPT-3 é menos "ambicioso" no sentido de que


muda o foco do aprendizado de zero para um tiro e poucos por meio do
aprendizado em contexto. Embora não fornecer exemplos de treinamento
específicos de tarefas pareça ser muito rigoroso, o aprendizado de poucas fotos
não é apenas mais realista, mas também mais semelhante ao ser humano: os
humanos geralmente precisam ver alguns exemplos para serem capazes de
aprender uma nova tarefa. Assim como o próprio nome sugere, o aprendizado de
poucos tiros significa que o modelo vê alguns exemplos da tarefa, enquanto o
aprendizado de um tiro é restrito a exatamente um exemplo.

A figura 16.12 ilustra a diferença entre os procedimentos zero-shot, one-shot, few-


shot e fine-tuning:
Figura 16.12: Uma comparação entre a aprendizagem de zero-shot, one-shot e
few-shot

A arquitetura do modelo do GPT-3 é praticamente a mesma do GPT-2, exceto


pelo aumento de 100 vezes no tamanho do parâmetro e pelo uso de um
transformador esparso. No mecanismo de atenção original (denso) que discutimos
anteriormente, cada elemento atende a todos os outros elementos na entrada, que

se dimensiona com   complexidade. A atenção esparsa melhora a


eficiência atendendo apenas a um subconjunto de elementos com tamanho

limitado, normalmente proporcional ao  . Os leitores interessados podem


aprender mais sobre a seleção de subconjuntos específicos visitando o artigo
sobre transformadores esparsos: Gerando sequências longas com
transformadores esparsos de Rewon Child et al. 2019
(URL: https://arxiv.org/abs/1904.10509).
Usando GPT-2 para gerar novo texto
Antes de passarmos para a próxima arquitetura de transformador, vamos dar uma
olhada em como podemos usar os modelos GPT mais recentes para gerar novo
texto. Observe que o GPT-3 ainda é relativamente novo e atualmente só está
disponível como uma versão beta através da API OpenAI
em https://openai.com/blog/openai-api/. No entanto, uma implementação do GPT-
2 foi disponibilizada pela Hugging Face (uma popular empresa de PNL e
aprendizado de máquina; http://huggingface.co), que utilizaremos.

Estaremos acessando o GPT-2 via , que é uma biblioteca Python


muito abrangente criada pela Hugging Face que fornece vários modelos baseados
em transformadores para pré-treinamento e ajuste fino. Os usuários também
podem discutir e compartilhar seus modelos personalizados no fórum. Sinta-se à
vontade para conferir e se envolver com a comunidade se estiver
interessado: https://discuss.huggingface.co.transformers

Instalando transformadores versão 4.9.1

Como esse pacote está evoluindo rapidamente, talvez não seja possível replicar
os resultados nas subseções a seguir. Para referência, este tutorial usa a versão
4.9.1 lançada em junho de 2021. Para instalar a versão que usamos neste livro,
você pode executar o seguinte comando em seu terminal para instalá-lo a partir do
PyPI:

pip install transformers==4.9.1


CopyExplain

Também recomendamos verificar as instruções mais recentes na página oficial de


instalação:

https://huggingface.co/transformers/installation.html

Depois de instalar a biblioteca, podemos executar o seguinte código para importar


um modelo GPT pré-treinado que pode gerar novo texto: transformers

>>> from transformers import pipeline, set_seed


>>> generator = pipeline('text-generation', model='gpt2')
CopyExplain

Em seguida, podemos solicitar ao modelo um trecho de texto e pedir que ele gere
um novo texto com base nesse trecho de entrada:

>>> set_seed(123)

>>> generator("Hey readers, today is",

... max_length=20,

... num_return_sequences=3)

[{'generated_text': "Hey readers, today is not the last time we'll be seeing one of our favorite indie

rock bands"},

{'generated_text': 'Hey readers, today is Christmas. This is not Christmas, because Christmas is so

long and I hope'},

{'generated_text': "Hey readers, today is CTA Day!\n\nWe're proud to be hosting a special event"}]
CopyExplain

Como podemos ver na saída, o modelo gerou três frases razoáveis com base em
nosso trecho de texto. Se você quiser explorar mais exemplos, sinta-se à vontade
para alterar a semente aleatória e o comprimento máximo da sequência.

Além disso, como ilustrado anteriormente na Figura 16.10, podemos usar


um modelo de transformador para gerar recursos para treinamento de outros
modelos. O código a seguir ilustra como podemos usar o GPT-2 para gerar
recursos com base em um texto de entrada:

>>> from transformers import GPT2Tokenizer

>>> tokenizer = GPT2Tokenizer.from_pretrained('gpt2')

>>> text = "Let us encode this sentence"

>>> encoded_input = tokenizer(text, return_tensors='pt')

>>> encoded_input

{'input_ids': tensor([[ 5756, 514, 37773, 428, 6827]]), 'attention_mask': tensor([[1, 1, 1, 1, 1]])}
CopyExplain

Esse código codificou o texto da frase de entrada em um formato tokenizado para


o modelo GPT-2. Como podemos ver, ele mapeou as cadeias de caracteres para
uma representação inteira e definiu a máscara de atenção para todos os 1s, o que
significa que todas as palavras serão processadas quando passarmos a entrada
codificada para o modelo, como mostrado aqui:

>>> from transformers import GPT2Model

>>> model = GPT2Model.from_pretrained('gpt2')

>>> output = model(**encoded_input)


CopyExplain

A variável armazena o último estado oculto, ou seja, nossa codificação de recurso


baseada em GPT-2 da sentença de entrada:output

>>> output['last_hidden_state'].shape

torch.Size([1, 5, 768])
CopyExplain

Para suprimir a saída detalhada, mostramos apenas a forma do tensor. Sua


primeira dimensão é o tamanho do lote (temos apenas um texto de entrada), que é
seguido pelo comprimento da frase e tamanho da codificação do recurso. Aqui,
cada uma das cinco palavras é codificada como um vetor de 768 dimensões.

Agora, poderíamos aplicar essa codificação de recurso a um determinado conjunto


de dados e treinar um classificador downstream com base na representação de
recurso baseada em GPT-2 em vez de usar um modelo de saco de palavras,
como discutido no Capítulo 8, Aplicando Machine Learning à Análise de
Sentimento.

Além disso, uma abordagem alternativa ao uso de grandes modelos de linguagem


pré-treinados é o ajuste fino, como discutimos anteriormente. Veremos um
exemplo de ajuste fino mais adiante neste capítulo.

Se você estiver interessado em detalhes adicionais sobre o uso do GPT-2,


recomendamos as seguintes páginas de documentação:
 https://huggingface.co/gpt2
 https://huggingface.co/docs/transformers/model_doc/gpt2

Pré-treinamento bidirecional com BERT


O BERT, cujo nome completo é Bidirectional Encoder Representations
from Transformers, foi criado por uma equipe de pesquisa do Google em 2018
(BERT: Pre-training of Deep Bidirectional Transformers for Language
Understanding por J. Devlin, M. Chang, K. Lee e K.
Toutanova, https://arxiv.org/abs/1810.04805). Para referência, embora não
possamos comparar GPT e BERT diretamente, pois são arquiteturas diferentes, o
BERT tem 345 milhões de parâmetros (o que o torna apenas um pouco maior que
o GPT-1, e seu tamanho é de apenas 1/5 do GPT-2).

Como o próprio nome sugere, o BERT possui uma estrutura de modelo baseada


em transformador-codificador que utiliza um procedimento de treinamento
bidirecional. (Ou, mais precisamente, podemos pensar no BERT como usando
treinamento "não direcional" porque ele lê em todos os elementos de entrada de
uma só vez.) Nessa configuração, a codificação de uma determinada palavra
depende tanto das palavras anteriores quanto das seguintes. Lembre-se de que,
no GPT, os elementos de entrada são lidos com uma ordem natural da esquerda
para a direita, o que ajuda a formar um poderoso modelo de linguagem generativa.
O treinamento bidirecional desabilita a capacidade do BERT de gerar uma frase
palavra por palavra, mas fornece codificações de entrada de maior qualidade para
outras tarefas, como classificação, uma vez que o modelo agora pode processar
informações em ambas as direções.

Lembre-se de que, no codificador de um transformador, a codificação de token é


uma soma de codificações posicionais e incorporações de token. No codificador
BERT, há uma incorporação de segmento adicional indicando a qual segmento
esse token pertence. Isso significa que cada representação simbólica contém três
ingredientes, como ilustra a Figura 16.13:
Figura 16.13: Preparando as entradas para o codificador BERT

Por que precisamos dessas informações adicionais de segmento no BERT? A


necessidade de informações desse segmento originou-se da tarefa especial de
pré-treinamento do BERT chamada predição da próxima frase. Nesta tarefa de
pré-treinamento, cada exemplo de treinamento inclui duas sentenças e, portanto,
requer notação de segmento especial para indicar se pertence à primeira ou à
segunda frase.

Agora, vamos examinar as tarefas de pré-treinamento do BERT com mais


detalhes. Semelhante a todos os outros modelos de linguagem baseados em
transformadores, o BERT tem duas etapas de treinamento: pré-treinamento e
ajuste fino. E o pré-treinamento inclui duas tarefas não
supervisionadas: modelagem de linguagem mascarada e previsão da próxima
frase.

No modelo de linguagem mascarada (MLM), os tokens são substituídos


aleatoriamente pelos chamados tokens de máscara, e o modelo é necessário para
prever essas palavras ocultas. Em comparação com a previsão da próxima
palavra no GPT, o MLM no BERT é mais parecido com "preencher os espaços em
branco" porque o modelo pode atender a todos os tokens na frase (exceto os
mascarados). No entanto, simplesmente mascarar palavras pode resultar em
inconsistências entre o pré-treinamento e o ajuste fino, já que os tokens não
aparecem em textos regulares. Para aliviar isso, há outras modificações nas
palavras que são selecionadas para mascaramento. Por exemplo, 15% das
palavras no BERT são marcadas para mascaramento. Esses 15% de palavras
selecionadas aleatoriamente são tratados da seguinte forma: [MASK][MASK]
1. Mantenha a palavra inalterada 10% do tempo
2. Substitua o token de palavra original por uma palavra aleatória 10% do tempo
3. Substitua o token de palavra original por um token de máscara, , 80 por cento
do tempo[MASK]

Além de evitar a já mencionada inconsistência entre o pré-treinamento e o ajuste


fino ao introduzir tokens no procedimento de treinamento, essas modificações
também têm outros benefícios. Em primeiro lugar, as palavras inalteradas incluem
a possibilidade de manter as informações do token original; caso contrário, o
modelo só pode aprender com o contexto e nada com as palavras mascaradas.
Em segundo lugar, os 10% de palavras aleatórias impedem que o modelo se torne
preguiçoso, por exemplo, não aprendendo nada além de devolver o que está
sendo dado. As probabilidades de mascarar, randomizar e deixar as palavras
inalteradas foram escolhidas por um estudo de ablação (ver o artigo do GPT-2);
Por exemplo, os autores testaram diferentes configurações e descobriram que
essa combinação funcionava melhor.[MASK]

A figura 16.14 ilustra um exemplo em que a palavra raposa é mascarada e, com


uma certa probabilidade, permanece inalterada ou é substituída por ou café. O
modelo é então necessário para prever qual é a palavra mascarada (destacada),
conforme ilustrado na Figura 16.14:[MASK]

Figura 16.14: Um exemplo de MLM

A predição da próxima frase é uma modificação natural da tarefa de predição da


próxima palavra, considerando a codificação bidirecional do BERT. Na verdade,
muitas tarefas importantes de PNL, como responder a perguntas, dependem da
relação de duas frases no documento. Esse tipo de relacionamento é difícil de
capturar por meio de modelos de linguagem regulares porque o treinamento de
previsão da próxima palavra geralmente ocorre em um nível de frase única devido
a restrições de comprimento de entrada.

Na tarefa de predição da próxima frase, o modelo recebe duas frases, A e B, no


seguinte formato:

[CLS] A [SET] B [SET]

[CLS] é um token de classificação, que serve como um espaço reservado para o


rótulo previsto na saída do decodificador, bem como um token que denota o início
das frases. O token [SEP], por outro lado, é anexado para indicar o final de cada
frase. O modelo é então necessário para classificar se B é a próxima sentença
("IsNext") de A ou não. Para fornecer ao modelo um conjunto de dados
balanceado, 50% das amostras são rotuladas como "IsNext", enquanto as
amostras restantes são rotuladas como "NotNext".

O BERT é pré-treinado nessas duas tarefas, frases mascaradas e predição da


próxima frase, ao mesmo tempo. Aqui, o objetivo do treinamento do BERT é
minimizar a função de perda combinada de ambas as tarefas.

A partir do modelo pré-treinado, modificações específicas são necessárias para


diferentes tarefas a jusante no estágio de ajuste fino. Cada exemplo de entrada
precisa corresponder a um determinado formato; por exemplo, ele deve começar
com um token [CLS] e ser separado usando tokens [SEP] se consistir em mais de
uma frase.

Grosso modo, o BERT pode ser afinado em quatro categorias de tarefas: (a)


classificação de pares de frases; b) Classificação de frase única; c) Resposta a
perguntas; d) Marcação de frase única.

Entre elas, (a) e (b) estão tarefas de classificação em nível de sequência, que
exigem apenas uma camada softmax adicional a ser adicionada à representação
de saída do token [CLS]. (c) e (d), por outro lado, são tarefas de classificação em
nível de token. Isso significa que o modelo passa representações de saída de
todos os tokens relacionados para a camada softmax para prever um rótulo de
classe para cada token individual.
Resposta a perguntas

A tarefa (c), resposta a perguntas, parece ser menos frequentemente discutida em


comparação com outras tarefas de classificação popular, como classificação de
sentimentos ou marcação de fala. Na resposta à pergunta, cada exemplo de
entrada pode ser dividido em duas partes, a pergunta e o parágrafo que ajuda a
responder à pergunta. O modelo é necessário para apontar o token inicial e final
no parágrafo que forma uma resposta adequada para a pergunta. Isso significa
que o modelo precisa gerar uma tag para cada token no parágrafo, indicando se
esse token é um token inicial ou final, ou nenhum dos dois. Como uma nota
lateral, vale mencionar que a saída pode conter um token final que aparece antes
do token inicial, o que levará a um conflito ao gerar a resposta. Esse tipo de saída
será reconhecido como "Sem resposta" para a pergunta.

Como a Figura 16.15 indica, a configuração de ajuste fino do modelo tem uma


estrutura muito simples: um codificador de entrada é anexado a um BERT pré-
treinado e uma camada softmax é adicionada para classificação. Uma vez
configurada a estrutura do modelo, todos os parâmetros serão ajustados ao longo
do processo de aprendizagem.
Figura 16.15: Usando o BERT para ajustar tarefas de idiomas diferentes

O melhor dos dois mundos: BART


O Transformador Bidirecional e Auto-Regressivo, abreviado como BART, foi
desenvolvido por pesquisadores da Facebook AI Research em 2019: BART:
Denoising Sequence-to-Sequence Pre-training for Natural Language Generation,
Translation, and Comprehension, Lewis e
colegas, https://arxiv.org/abs/1910.13461 . Lembre-se que nas seções anteriores
argumentamos que o GPT utiliza a estrutura do decodificador de um
transformador, enquanto o BERT utiliza a estrutura do codificador de um
transformador. Esses dois modelos são, portanto, capazes de executar bem
diferentes tarefas: a especialidade do GPT é gerar texto, enquanto o BERT tem
melhor desempenho em tarefas de classificação. BART pode ser visto como uma
generalização de GPT e BERT. Como o título desta seção sugere, o BART é
capaz de realizar ambas as tarefas, gerando e classificando texto. A razão pela
qual ele pode lidar bem com ambas as tarefas é que o modelo vem com um
codificador bidirecional, bem como um decodificador autorregressivo da esquerda
para a direita.

Você pode se perguntar como isso é diferente do transformador original. Há


algumas alterações no tamanho do modelo, juntamente com algumas pequenas
alterações, como opções de função de ativação. No entanto, uma das mudanças
mais interessantes é que o BART trabalha com diferentes entradas de modelo. O
modelo original do transformador foi projetado para tradução de idiomas para que
haja duas entradas: o texto a ser traduzido (sequência fonte) para o codificador e
a tradução (sequência alvo) para o decodificador. Além disso, o decodificador
também recebe a sequência de origem codificada, conforme ilustrado
anteriormente na Figura 16.6. No entanto, no BART, o formato de entrada foi
generalizado de tal forma que ele usa apenas a sequência de origem como
entrada. O BART pode executar uma gama mais ampla de tarefas, incluindo
tradução de idiomas, onde uma sequência de destino ainda é necessária para
calcular a perda e ajustar o modelo, mas não é necessário alimentá-la diretamente
no decodificador.

Agora vamos dar uma olhada mais de perto na estrutura do modelo BART. Como
mencionado anteriormente, o BART é composto por um codificador bidirecional e
um decodificador autorregressivo. Ao receber um exemplo de treinamento como
texto sem formatação, a entrada será primeiro "corrompida" e, em seguida,
codificada pelo codificador. Essas codificações de entrada serão então passadas
para o decodificador, juntamente com os tokens gerados. A perda de entropia
cruzada entre a saída do codificador e o texto original será calculada e, em
seguida, otimizada através do processo de aprendizagem. Pense em um
transformador onde temos dois textos em idiomas diferentes como entrada para o
decodificador: o texto inicial a ser traduzido (texto fonte) e o texto gerado no
idioma de destino. BART pode ser entendido como a substituição do primeiro por
texto corrompido e o segundo pelo próprio texto de entrada.
Figura 16.16: Estrutura do modelo BART

Para explicar a etapa de corrupção com um pouco mais de detalhes, lembre-se de


que BERT e GPT são pré-treinados reconstruindo palavras mascaradas: BERT
está "preenchendo os espaços em branco" e GPT está "prevendo a próxima
palavra". Essas tarefas de pré-treinamento também podem ser reconhecidas como
reconstruir frases corrompidas, porque mascarar palavras é uma maneira de
corromper uma frase. O BART fornece os seguintes métodos de corrupção que
podem ser aplicados ao texto limpo:

 Mascaramento de token

 Exclusão de token

 Preenchimento de texto

 Permutação de sentenças

 Rotação de documentos

Uma ou mais das técnicas listadas acima podem ser aplicadas à mesma frase; No
pior cenário, onde todas as informações são contaminadas e corrompidas, o texto
se torna inútil. Assim, o codificador tem utilidade limitada, e com apenas o módulo
decodificador funcionando corretamente, o modelo se tornará essencialmente
mais semelhante a uma linguagem unidirecional.
O BART pode ser ajustado em uma ampla gama de tarefas downstream, incluindo
(a) classificação de sequência, (b) classificação de token, (c) geração de
sequência e (d) tradução automática. Assim como no BERT, pequenas alterações
nas entradas precisam ser feitas para executar diferentes tarefas.

Na tarefa de classificação de sequência, um token adicional precisa ser anexado à


entrada para servir como o token de rótulo gerado, que é semelhante ao token
[CLS] no BERT. Além disso, em vez de perturbar a entrada, a entrada não
corrompida é alimentada no codificador e no decodificador para que o modelo
possa fazer pleno uso da entrada.

Para a classificação de token, tokens adicionais se tornam desnecessários, e o


modelo pode usar diretamente a representação gerada para cada token para
classificação.

A geração de sequência no BART difere um pouco do GPT por causa da


existência do codificador. Em vez de gerar texto do zero, as tarefas de geração de
sequência via BART são mais comparáveis à sumarização, onde o modelo recebe
um corpus de contextos e é solicitado a gerar um resumo ou uma resposta
abstrata para certas perguntas. Para este fim, sequências de entrada inteiras são
alimentadas no codificador enquanto o decodificador gera saída
autorregressivamente.

Finalmente, é natural que o BART realize a tradução automática considerando a


semelhança entre o BART e o transformador original. No entanto, em vez de
seguir exatamente o mesmo procedimento do treinamento do transformador
original, os pesquisadores consideraram a possibilidade de incorporar todo o
modelo BART como um decodificador pré-treinado. Para concluir o modelo de
tradução, um novo conjunto de parâmetros inicializados aleatoriamente é
adicionado como um novo codificador adicional. Em seguida, o estágio de ajuste
fino pode ser realizado em duas etapas:

1. Primeiro, congele todos os parâmetros, exceto o codificador


2. Em seguida, atualize todos os parâmetros no modelo

O BART foi avaliado em vários conjuntos de dados de benchmark para várias


tarefas, e obteve resultados muito competitivos em comparação com outros
modelos de linguagem famosos, como o BERT. Em particular, para tarefas de
geração, incluindo respostas abstratas a perguntas, resposta a diálogos e tarefas
de resumo, o BART alcançou resultados de última geração.

Ajustando um modelo BERT no PyTorch


Agora que introduzimos e discutimos todos os conceitos necessários e a teoria por
trás dos modelos originais baseados em transformadores e transformadores
populares, é hora de dar uma olhada na parte mais prática! Nesta seção, você
aprenderá a ajustar um modelo BERT para classificação de sentimento no
PyTorch.

Observe que, embora existam muitos outros modelos baseados em


transformadores para escolher, o BERT fornece um bom equilíbrio entre a
popularidade do modelo e ter um tamanho de modelo gerenciável para que ele
possa ser ajustado em uma única GPU. Observe também que o pré-treinamento
de um BERT do zero é doloroso e bastante desnecessário, considerando a
disponibilidade do pacote Python fornecido pelo Hugging Face, que inclui um
monte de modelos pré-treinados que estão prontos para ajuste fino. transformers

Nas seções a seguir, você verá como preparar e tokenizar o conjunto de dados de
revisão de filmes do IMDb e ajustar o modelo BERT destilado para executar a
classificação de sentimento. Escolhemos deliberadamente a classificação de
sentimentos como um exemplo simples, mas clássico, embora existam muitas
outras aplicações fascinantes de modelos de linguagem. Além disso, usando o
conhecido conjunto de dados de revisão de filmes do IMDb, podemos ter uma boa
ideia do desempenho preditivo do modelo BERT comparando-o com o modelo de
regressão logística no Capítulo 8, Aplicando Machine Learning à Análise de
Sentimento, e o RNN no Capítulo 15, Modelando Dados Sequenciais Usando
Redes Neurais Recorrentes.

Carregando o conjunto de dados de revisão de


filme do IMDb
Nesta subseção, começaremos carregando os pacotes necessários e o conjunto
de dados, divididos em conjuntos de trem, validação e teste.

Para as partes relacionadas ao BERT deste tutorial, usaremos principalmente a


biblioteca de código aberto (https://huggingface.co/transformers/) criada pelo
Hugging Face, que instalamos na seção anterior, Usando GPT-2 para gerar novo
texto.transformers

O modelo DistilBERT que estamos usando neste capítulo é um modelo de


transformador leve criado pela destilação de um modelo básico BERT pré-
treinado. O modelo base BERT original sem caixa contém mais de 110 milhões de
parâmetros, enquanto o DistilBERT tem 40% menos parâmetros. Além disso, o
DistilBERT é executado 60% mais rápido e ainda preserva 95% do desempenho
do BERT no benchmark de compreensão da linguagem GLUE.

O código a seguir importa todos os pacotes que usaremos neste capítulo para
preparar os dados e ajustar o modelo DistilBERT:

>>> import gzip

>>> import shutil

>>> import time

>>> import pandas as pd

>>> import requests

>>> import torch

>>> import torch.nn.functional as F

>>> import torchtext

>>> import transformers

>>> from transformers import DistilBertTokenizerFast

>>> from transformers import DistilBertForSequenceClassification


CopyExplain

Em seguida, especificamos algumas configurações gerais, incluindo o número de


épocas em que treinamos a rede, a especificação do dispositivo e a semente
aleatória. Para reproduzir os resultados, certifique-se de definir uma semente
aleatória específica, como : 123

>>> torch.backends.cudnn.deterministic = True

>>> RANDOM_SEED = 123

>>> torch.manual_seed(RANDOM_SEED)

>>> DEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

>>> NUM_EPOCHS = 3
CopyExplain

Estaremos trabalhando no conjunto de dados de revisão de filmes do IMDb, que


você já viu nos capítulos 8 e 15. O código a seguir busca o conjunto de dados
compactado e o descompacta:

>>> url = ("https://github.com/rasbt/"

... "machine-learning-book/raw/"

... "main/ch08/movie_data.csv.gz")

>>> filename = url.split("/")[-1]

>>> with open(filename, "wb") as f:

... r = requests.get(url)

... f.write(r.content)

>>> with gzip.open('movie_data.csv.gz', 'rb') as f_in:

... with open('movie_data.csv', 'wb') as f_out:

... shutil.copyfileobj(f_in, f_out)


CopyExplain

Se você tiver o arquivo do Capítulo 8 ainda em seu disco rígido, você pode ignorar
este procedimento de download e descompactar. movie_data.csv

Em seguida, carregamos os dados em um pandas e nos certificamos de que eles


estejam bem:DataFrame
>>> df = pd.read_csv('movie_data.csv')

>>> df.head(3)
CopyExplain

Figura 16.17: As três primeiras linhas do conjunto de dados de revisão de filmes


do IMDb

A próxima etapa é dividir o conjunto de dados em conjuntos de treinamento,


validação e teste separados. Aqui, usamos 70% das revisões para o conjunto de
treinamento, 10% para o conjunto de validação e os 20% restantes para testes:

>>> train_texts = df.iloc[:35000]['review'].values

>>> train_labels = df.iloc[:35000]['sentiment'].values

>>> valid_texts = df.iloc[35000:40000]['review'].values

>>> valid_labels = df.iloc[35000:40000]['sentiment'].values

>>> test_texts = df.iloc[40000:]['review'].values

>>> test_labels = df.iloc[40000:]['sentiment'].values


CopyExplain

Tokenizando o conjunto de dados


Até agora, obtivemos os textos e rótulos para os conjuntos de treinamento,
validação e teste. Agora, vamos tokenizar os textos em tokens de palavras
individuais usando a implementação do tokenizador herdada da classe de modelo
pré-treinada:
>>> tokenizer = DistilBertTokenizerFast.from_pretrained(

... 'distilbert-base-uncased'

... )

>>> train_encodings = tokenizer(list(train_texts), truncation=True, padding=True)

>>> valid_encodings = tokenizer(list(valid_texts), truncation=True, padding=True)

>>> test_encodings = tokenizer(list(test_texts), truncation=True, padding=True)


CopyExplain

Escolhendo tokenizadores diferentes

Se você está interessado em aplicar diferentes tipos de tokenizadores, sinta-se


livre para explorar o pacote (https://huggingface.co/docs/tokenizers/python/latest/),
que também é construído e mantido pelo Hugging Face. No entanto, os
tokenizadores herdados mantêm a consistência entre o modelo pré-treinado e o
conjunto de dados, o que nos poupa o esforço extra de encontrar o tokenizador
específico correspondente ao modelo. Em outras palavras, usar um tokenizador
herdado é a abordagem recomendada se você quiser ajustar um modelo pré-
treinado.tokenizers

Finalmente, vamos empacotar tudo em uma classe chamada e criar os


carregadores de dados correspondentes. Essa classe de conjunto de dados
autodefinida nos permite personalizar todos os recursos e funções relacionados
para nosso conjunto de dados de revisão de filme personalizado no
formato:IMDbDatasetDataFrame

>>> class IMDbDataset(torch.utils.data.Dataset):

... def __init__(self, encodings, labels):

... self.encodings = encodings

... self.labels = labels

>>> def __getitem__(self, idx):

... item = {key: torch.tensor(val[idx])

... for key, val in self.encodings.items()}


... item['labels'] = torch.tensor(self.labels[idx])

... return item

>>> def __len__(self):

... return len(self.labels)

>>> train_dataset = IMDbDataset(train_encodings, train_labels)

>>> valid_dataset = IMDbDataset(valid_encodings, valid_labels)

>>> test_dataset = IMDbDataset(test_encodings, test_labels)

>>> train_loader = torch.utils.data.DataLoader(

... train_dataset, batch_size=16, shuffle=True)

>>> valid_loader = torch.utils.data.DataLoader(

... valid_dataset, batch_size=16, shuffle=False)

>>> test_loader = torch.utils.data.DataLoader(

... test_dataset, batch_size=16, shuffle=False)


CopyExplain

While the overall data loader setup should be familiar from previous chapters, one
noteworthy detail is the variable in the method. The encodings we produced
previously store a lot of information about the tokenized texts. Via the
dictionary comprehension that we use to assign the dictionary to the variable, we
are only extracting the most relevant information. For instance, the resulting
dictionary entries include (unique integers from the vocabulary corresponding to
the tokens), (the class labels), and . Here, is a tensor with binary values (0s and
1s) that denotes which tokens the model should attend to. In particular, 0s
correspond to tokens used for padding the sequence to equal lengths and are
ignored by the model; the 1s correspond to the actual text
tokens.item__getitem__iteminput_idslabelsattention_maskattention_mask

Loading and fine-tuning a pre-trained BERT model


Having taken care of the data preparation, in this subsection, you will see how to
load the pre-trained DistilBERT model and fine-tune it using the dataset we just
created. The code for loading the pre-trained model is as follows:
>>> model = DistilBertForSequenceClassification.from_pretrained(

... 'distilbert-base-uncased')

>>> model.to(DEVICE)

>>> model.train()

>>> optim = torch.optim.Adam(model.parameters(), lr=5e-5)


CopyExplain

DistilBertForSequenceClassification  specifiesthe downstream task we want to


fine-tune the model on, which is sequence classification in this case. As mentioned
before, is a lightweight version of a BERT uncased base model with manageable
size and good performance. Note that “uncased” means that the model does not
distinguish between upper- and lower-case letters. 'distilbert-base-uncased'

Using other pre-trained transformers

The transformers package also provides many other pre-trained models and


various downstream tasks for fine-tuning. Check them out
at https://huggingface.co/transformers/.

Now, it’s time to train the model. We can break this up into two parts. First, we
need to define an accuracy function to evaluate the model performance. Note that
this accuracy function computes the conventional classification accuracy. Why is it
so verbose? Here, we are loading the dataset batch by batch to work around RAM
or GPU memory (VRAM) limitations when working with a large deep learning
model:

>>> def compute_accuracy(model, data_loader, device):

... with torch.no_grad():

... correct_pred, num_examples = 0, 0

... for batch_idx, batch in enumerate(data_loader):

... ### Prepare data

... input_ids = batch['input_ids'].to(device)

... attention_mask = \
... batch['attention_mask'].to(device)

... labels = batch['labels'].to(device)

... outputs = model(input_ids,

... attention_mask=attention_mask)

... logits = outputs['logits']

... predicted_labels = torch.argmax(logits, 1)

... num_examples += labels.size(0)

... correct_pred += \

... (predicted_labels == labels).sum()

... return correct_pred.float()/num_examples * 100


CopyExplain

In the function, we load a given batch and then obtain the predicted labels from the
outputs. While doing this, we keep track of the total number of examples via .
Similarly, we keep track of the number of correct predictions via the variable.
Finally, after we iterate over the complete dataset, we compute the accuracy as the
proportion of correctly predicted labels.compute_accuracynum_examplescorrect_pred

Overall, via the function, you can already get a glimpse at how we can use the
transformer model to obtain the class labels. That is, we feed the model the along
with the information that, here, denotes whether a token is an actual text token or a
token for padding the sequences to equal length. The call then returns the outputs,
which is a transformer library-specific object. From this object, we then obtain the
logits that we convert into class labels via the function as we have done in previous
chapters.compute_accuracyinput_idsattention_maskmodelSequenceClassifierOutputarg
max

Finally, let us get to the main part: the training (or rather, fine-tuning) loop. As you
will notice, fine-tuning a model from the transformers library is very similar to
training a model in pure PyTorch from scratch:

>>> start_time = time.time()


>>> for epoch in range(NUM_EPOCHS):

... model.train()

... for batch_idx, batch in enumerate(train_loader):

... ### Prepare data

... input_ids = batch['input_ids'].to(DEVICE)

... attention_mask = batch['attention_mask'].to(DEVICE)

... labels = batch['labels'].to(DEVICE)

... ### Forward pass

... outputs = model(input_ids,

... attention_mask=attention_mask,

... labels=labels)

... loss, logits = outputs['loss'], outputs['logits']

... ### Backward pass

... optim.zero_grad()

... loss.backward()

... optim.step()

... ### Logging

... if not batch_idx % 250:

... print(f'Epoch: {epoch+1:04d}/{NUM_EPOCHS:04d}'

... f' | Batch'

... f'{batch_idx:04d}/'

... f'{len(train_loader):04d} | '

... f'Loss: {loss:.4f}')


... model.eval()

... with torch.set_grad_enabled(False):

... print(f'Training accuracy: '

... f'{compute_accuracy(model, train_loader, DEVICE):.2f}%'

... f'\nValid accuracy: '

... f'{compute_accuracy(model, valid_loader, DEVICE):.2f}%')

... print(f'Time elapsed: {(time.time() - start_time)/60:.2f} min')

... print(f'Total Training Time: {(time.time() - start_time)/60:.2f} min')

... print(f'Test accuracy: {compute_accuracy(model, test_loader, DEVICE):.2f}%')


CopyExplain

The output produced by the preceding code is as follows (note that the code is not
fully deterministic, which is why the results you are getting may be slightly
different):

Epoch: 0001/0003 | Batch 0000/2188 | Loss: 0.6771

Epoch: 0001/0003 | Batch 0250/2188 | Loss: 0.3006

Epoch: 0001/0003 | Batch 0500/2188 | Loss: 0.3678

Epoch: 0001/0003 | Batch 0750/2188 | Loss: 0.1487

Epoch: 0001/0003 | Batch 1000/2188 | Loss: 0.6674

Epoch: 0001/0003 | Batch 1250/2188 | Loss: 0.3264

Epoch: 0001/0003 | Batch 1500/2188 | Loss: 0.4358

Epoch: 0001/0003 | Batch 1750/2188 | Loss: 0.2579

Epoch: 0001/0003 | Batch 2000/2188 | Loss: 0.2474

Training accuracy: 96.32%

Valid accuracy: 92.34%

Time elapsed: 20.67 min


Epoch: 0002/0003 | Batch 0000/2188 | Loss: 0.0850

Epoch: 0002/0003 | Batch 0250/2188 | Loss: 0.3433

Epoch: 0002/0003 | Batch 0500/2188 | Loss: 0.0793

Epoch: 0002/0003 | Batch 0750/2188 | Loss: 0.0061

Epoch: 0002/0003 | Batch 1000/2188 | Loss: 0.1536

Epoch: 0002/0003 | Batch 1250/2188 | Loss: 0.0816

Epoch: 0002/0003 | Batch 1500/2188 | Loss: 0.0786

Epoch: 0002/0003 | Batch 1750/2188 | Loss: 0.1395

Epoch: 0002/0003 | Batch 2000/2188 | Loss: 0.0344

Training accuracy: 98.35%

Valid accuracy: 92.46%

Time elapsed: 41.41 min

Epoch: 0003/0003 | Batch 0000/2188 | Loss: 0.0403

Epoch: 0003/0003 | Batch 0250/2188 | Loss: 0.0036

Epoch: 0003/0003 | Batch 0500/2188 | Loss: 0.0156

Epoch: 0003/0003 | Batch 0750/2188 | Loss: 0.0114

Epoch: 0003/0003 | Batch 1000/2188 | Loss: 0.1227

Epoch: 0003/0003 | Batch 1250/2188 | Loss: 0.0125

Epoch: 0003/0003 | Batch 1500/2188 | Loss: 0.0074

Epoch: 0003/0003 | Batch 1750/2188 | Loss: 0.0202

Epoch: 0003/0003 | Batch 2000/2188 | Loss: 0.0746

Training accuracy: 99.08%

Valid accuracy: 91.84%

Time elapsed: 62.15 min

Total Training Time: 62.15 min

Test accuracy: 92.50%


CopyExplain
In this code, we iterate over multiple epochs. In each epoch we perform the
following steps:

1. Load the input into the device we are working on (GPU or CPU)
2. Compute the model output and loss
3. Adjust the weight parameters by backpropagating the loss
4. Evaluate the model performance on both the training and validation set

Note that the training time may vary on different devices. After three epochs,
accuracy on the test dataset reaches around 93 percent, which is a substantial
improvement compared to the 85 percent test accuracy that the RNN achieved
in Chapter 15.

Fine-tuning a transformer more conveniently using


the Trainer API
Na subseção anterior, implementamos o loop de treinamento no PyTorch
manualmente para ilustrar que ajustar um modelo de transformador não é muito
diferente de treinar um modelo RNN ou CNN do zero. No entanto, observe que a
biblioteca contém vários recursos extras interessantes para conveniência
adicional, como a API do Treinador, que apresentaremos nesta
subseção.transformers

A API do Trainer fornecida pela Hugging Face é otimizada para modelos de


transformadores com uma ampla gama de opções de treinamento e vários
recursos integrados. Ao usar a API do Trainer, podemos ignorar o esforço de
escrever loops de treinamento por conta própria, e treinar ou ajustar um modelo de
transformador é tão simples quanto uma chamada de função (ou método). Vamos
ver como isso funciona na prática.

Após o carregamento do modelo pré-treinado via

>>> model = DistilBertForSequenceClassification.from_pretrained(

... 'distilbert-base-uncased')
>>> model.to(DEVICE)

>>> model.train();
CopyExplain

O loop de treinamento da seção anterior pode ser substituído pelo seguinte


código:

>>> optim = torch.optim.Adam(model.parameters(), lr=5e-5)

>>> from transformers import Trainer, TrainingArguments

>>> training_args = TrainingArguments(

... output_dir='./results',

... num_train_epochs=3,

... per_device_train_batch_size=16,

... per_device_eval_batch_size=16,

... logging_dir='./logs',

... logging_steps=10,

... )

>>> trainer = Trainer(

... model=model,

... args=training_args,

... train_dataset=train_dataset,

... optimizers=(optim, None) # optim and learning rate scheduler

... )
CopyExplain

Nos trechos de código anteriores, primeiro definimos os argumentos de


treinamento, que são configurações relativamente autoexplicativas em relação aos
locais de entrada e saída, número de épocas e tamanhos de lote. Tentamos
manter as configurações o mais simples possível; No entanto, há muitas
configurações adicionais disponíveis, e recomendamos consultar a página de
documentação para obter detalhes
adicionais: https://huggingface.co/transformers/main_classes/trainer.html#traininga
rguments.TrainingArguments

Em seguida, passamos essas configurações para a classe para instanciar um


novo objeto. Depois de iniciar o com as configurações, o modelo a ser ajustado e
os conjuntos de treinamento e avaliação, podemos treinar o modelo chamando o
método (usaremos esse método mais adiante). É isso, usar a API do Trainer é tão
simples quanto mostrado no código anterior, e nenhum código clichê adicional é
necessário.TrainingArgumentsTrainertrainertrainertrainer.train()

No entanto, você pode ter notado que o conjunto de dados de teste não estava
envolvido nesses trechos de código e não especificamos nenhuma métrica de
avaliação nesta subseção. Isso ocorre porque a API do Trainer mostra apenas a
perda de treinamento e não fornece avaliação de modelo ao longo do processo de
treinamento por padrão. Há duas maneiras de exibir o desempenho final do
modelo, que ilustraremos a seguir.

O primeiro método para avaliar o modelo final é definir uma função de avaliação
como argumento para outra instância. A função opera nas previsões de teste dos
modelos como logits (que é a saída padrão do modelo) e os rótulos de teste. Para
instanciar essa função, recomendamos instalar a biblioteca do Hugging Face via e
usá-la da seguinte maneira:compute_metricsTrainercompute_metricsdatasetspip
install datasets

>>> from datasets import load_metric

>>> import numpy as np

>>> metric = load_metric("accuracy")

>>> def compute_metrics(eval_pred):

... logits, labels = eval_pred

... # note: logits are a numpy array, not a pytorch tensor

... predictions = np.argmax(logits, axis=-1)

... return metric.compute(

... predictions=predictions, references=labels)


CopyExplain
A instanciação atualizada (agora incluindo ) é então a
seguinte:Trainercompute_metrics

>>> trainer=Trainer(

... model=model,

... args=training_args,

... train_dataset=train_dataset,

... eval_dataset=test_dataset,

... compute_metrics=compute_metrics,

... optimizers=(optim, None) # optim and learning rate scheduler

... )
CopyExplain

Agora, vamos treinar o modelo (novamente, observe que o código não é


totalmente determinístico, e é por isso que você pode estar obtendo resultados
ligeiramente diferentes):

>>> start_time = time.time()

>>> trainer.train()

***** Running training *****

Num examples = 35000

Num Epochs = 3

Instantaneous batch size per device = 16

Total train batch size (w. parallel, distributed & accumulation) = 16

Gradient Accumulation steps = 1

Total optimization steps = 6564

Step Training Loss

10 0.705800

20 0.684100
30 0.681500

40 0.591600

50 0.328600

60 0.478300

...

>>> print(f'Total Training Time: '

... f'{(time.time() - start_time)/60:.2f} min')

Total Training Time: 45.36 min


CopyExplain

Após a conclusão do treinamento, que pode levar até uma hora, dependendo da
sua GPU, podemos ligar para obter o desempenho do modelo no conjunto de
testes:trainer.evaluate()

>>> print(trainer.evaluate())

***** Running Evaluation *****

Num examples = 10000

Batch size = 16

100%|█████████████████████████████████████████| 625/625 [10:59<00:00,

1.06s/it]

{'eval_loss': 0.30534815788269043,

'eval_accuracy': 0.9327,

'eval_runtime': 87.1161,

'eval_samples_per_second': 114.789,

'eval_steps_per_second': 7.174,

'epoch': 3.0}
CopyExplain

Como podemos ver, a precisão da avaliação é de cerca de 94%, semelhante ao


nosso próprio ciclo de treinamento PyTorch usado anteriormente. (Observe que
pulamos a etapa de treinamento, porque o já está ajustado após a chamada
anterior.) Há uma pequena discrepância entre nossa abordagem de treinamento
manual e o uso da classe, porque a classe usa algumas configurações diferentes
e algumas adicionais.modeltrainer.train()TrainerTrainer

O segundo método que poderíamos empregar para calcular a precisão do


conjunto de teste final é reutilizar nossa função que definimos na seção anterior.
Podemos avaliar diretamente o desempenho do modelo ajustado no conjunto de
dados de teste executando o seguinte código: compute_accuracy

>>> model.eval()

>>> model.to(DEVICE)

>>> print(f'Test accuracy: {compute_accuracy(model, test_loader, DEVICE):.2f}%')

Test accuracy: 93.27%


CopyExplain

Na verdade, se você quiser verificar o desempenho do modelo regularmente


durante o treinamento, você pode exigir que o treinador imprima a avaliação do
modelo após cada época, definindo os argumentos de treinamento da seguinte
maneira:

>>> from transformers import TrainingArguments

>>> training_args = TrainingArguments("test_trainer",

... evaluation_strategy="epoch", ...)


CopyExplain

No entanto, se você estiver planejando alterar ou otimizar hiperparâmetros e


repetir o procedimento de ajuste fino várias vezes, recomendamos usar o conjunto
de validação para essa finalidade, a fim de manter o conjunto de teste
independente. Podemos conseguir isso instanciando o uso : Trainervalid_dataset

>>> trainer=Trainer(

... model=model,

... args=training_args,
... train_dataset=train_dataset,

... eval_dataset=valid_dataset,

... compute_metrics=compute_metrics,

... )
CopyExplain

Nesta seção, vimos como podemos ajustar um modelo BERT para classificação.


Isso é diferente de usar outras arquiteturas de aprendizado profundo, como RNNs,
que geralmente treinamos do zero. No entanto, a menos que estejamos fazendo
pesquisa e tentando desenvolver novas arquiteturas de transformadores – um
esforço muito caro – modelos de transformadores de pré-treinamento não são
necessários. Como os modelos de transformadores são treinados em recursos
gerais e não rotulados do conjunto de dados, pré-treiná-los nós mesmos pode não
ser um bom uso de nosso tempo e recursos; O ajuste fino é o caminho a seguir.

edes adversárias generativas para


sintetizar novos dados
No capítulo anterior, focamos em redes neurais recorrentes para modelagem de
sequências. Neste capítulo, exploraremos as redes adversárias
generativas (GANs) e veremos sua aplicação na síntese de novas amostras de
dados. As GANs são consideradas um dos avanços mais importantes no
aprendizado profundo, permitindo que os computadores gerem novos dados
(como novas imagens).

Neste capítulo, abordaremos os seguintes tópicos:

 Introdução de modelos generativos para síntese de novos dados


 Autoencoders, autoencoders variacionais e sua relação com GANs
 Entendendo os blocos de construção das GANs
 Implementando um modelo GAN simples para gerar dígitos manuscritos
 Entendendo a convolução transposta e a normalização em lote
 Melhorando as GANs: GANs convolucionais profundas e GANs usando a
distância de Wasserstein
Redes neurais de grafo para capturar
dependências em dados estruturados de
grafos
Neste capítulo, apresentaremos uma classe de modelos de aprendizagem
profunda que opera em dados de grafos, ou seja, redes neurais de
grafos (GNNs). Os GNNs têm sido uma área de rápido desenvolvimento nos
últimos anos. De acordo com o relatório State of AI de 2021
(https://www.stateof.ai/2021-report-launch.html), os GNNs evoluíram "de nicho
para um dos campos mais quentes da pesquisa de IA".

Os GNNs têm sido aplicados em uma variedade de áreas, incluindo as seguintes:

 Classificação de texto (https://arxiv.org/abs/1710.10903)


 Sistemas de recomendação (https://arxiv.org/abs/1704.06803)
 Previsão de tráfego (https://arxiv.org/abs/1707.01926)
 Descoberta de drogas (https://arxiv.org/abs/1806.02473)
Embora não possamos cobrir todas as novas ideias neste espaço em rápido
desenvolvimento, forneceremos uma base para entender como os GNNs
funcionam e como eles podem ser implementados. Além disso, apresentaremos a
biblioteca Geométrica PyTorch, que fornece recursos para gerenciar dados de
gráficos para aprendizado profundo, bem como implementações de muitos tipos
diferentes de camadas de gráficos que você pode usar em seus modelos de
aprendizado profundo.

Os tópicos que serão abordados neste capítulo são os seguintes:

 Uma introdução aos dados gráficos e como eles podem ser representados
para uso em redes neurais profundas
 Uma explicação das convoluções de grafos, um dos principais blocos de
construção dos GNNs comuns
 Um tutorial mostrando como implementar GNNs para predição de
propriedades moleculares usando PyTorch Geometric
 Uma visão geral dos métodos na vanguarda do campo GNN
 Introdução de redes adversárias
generativas
 Vamos primeiro olhar para os fundamentos dos modelos GAN. O objetivo
geral de um GAN é sintetizar novos dados que tenham a mesma
distribuição que seu conjunto de dados de treinamento. Portanto, as GANs,
em sua forma original, são consideradas na categoria de aprendizado não
supervisionado de tarefas de aprendizado de máquina, uma vez que
nenhum dado rotulado é necessário. Vale ressaltar, no entanto, que as
extensões feitas no GAN original podem estar tanto no domínio semi-
supervisionado quanto no supervisionado.
 O conceito geral de GAN foi proposto pela primeira vez em 2014 por Ian
Goodfellow e seus colegas como um método para sintetizar novas imagens
usando redes neurais profundas (NNs) (Generative Adversarial Nets,
in Advances in Neural Information Processing Systems por I. Goodfellow, J.
Pouget-Abadie, M. Mirza, B. Xu, D. Warde-Farley, S. Ozair,
A. Courville e Y. Bengio, pp. 2672-2680, 2014). Embora a arquitetura GAN
inicial proposta neste artigo tenha sido baseada em camadas totalmente
conectadas, semelhantes às arquiteturas perceptron multicamadas, e
treinada para gerar dígitos manuscritos semelhantes a MNIST, ela serviu
mais como uma prova de conceito para demonstrar a viabilidade dessa
nova abordagem.
 No entanto, desde sua introdução, os autores originais, assim como muitos
outros pesquisadores, propuseram inúmeras melhorias e várias aplicações
em diferentes campos da engenharia e da ciência; por exemplo, em visão
computacional, os GANs são usados para tradução de imagem para
imagem (aprendendo a mapear uma imagem de entrada para uma imagem
de saída), super-resolução de imagem (fazendo uma imagem de alta
resolução a partir de uma versão de baixa resolução), pintura de imagem
(aprendendo a reconstruir as partes ausentes de uma imagem) e muitas
outras aplicações. Por exemplo, avanços recentes na pesquisa de GAN
levaram a modelos que são capazes de gerar novas imagens faciais de alta
resolução. Exemplos dessas imagens de alta resolução podem ser
encontrados no https://www.thispersondoesnotexist.com/, que mostra
imagens de rosto sintético geradas por um GAN.
 Começando com autoencoders
 Antes de discutirmos como as GANs funcionam, começaremos com os
autoencoders, que podem compactar e descompactar dados de
treinamento. Embora os codificadores automáticos padrão não possam
gerar novos dados, entender sua função ajudará você a navegar pelas
GANs na próxima seção.
 Os codificadores automáticos são compostos por duas redes concatenadas
entre si: uma rede codificadora e uma rede decodificadora. A rede
codificadora recebe um vetor de recurso de entrada d-
dimensional associado ao exemplo x (isto é, ) e o codifica em um vetor p-

dimensional, z (isto é,  ).  Em outras palavras,


o papel do codificador é aprender a modelar a função z = f(x). O vetor
codificado, z, também é chamado de vetor latente, ou representação de
feição latente. Normalmente, a dimensionalidade do vetor latente é menor
do que a dos exemplos de entrada; Em outras palavras, p < d. Assim,
podemos dizer que o codificador atua como uma função de compressão de

dados. Então, o decodificador se descomprime   do vetor latente de


dimensão inferior, z, onde podemos pensar no decodificador como uma

função,  . Uma arquitetura simples de autoencoder é


mostrada na Figura 17.1, onde as partes do codificador e do
decodificador consistem em apenas uma camada totalmente conectada
cada:

 Figura 17.1: A arquitetura de um codificador automático


 A conexão entre autoencoders e redução de dimensionalidade
 No Capítulo 5, Compactando dados via redução de dimensionalidade, você
aprendeu sobre técnicas de redução de dimensionalidade, como análise de
componentes principais (PCA) e análise discriminante linear (LDA). Os
autoencoders também podem ser usados como uma técnica de redução
de dimensionalidade. Na verdade, quando não há não-linearidade em
nenhuma das duas sub-redes (codificador e decodificador), então a
abordagem do autoencoder é quase idêntica à PCA.
 Neste caso, se assumirmos que os pesos de um codificador de camada
única (sem camada oculta e sem função de ativação não linear) são
denotados pela matriz U, então os modelos do codificador z = UTx. Da
mesma forma, um decodificador linear de camada única
modela  . Juntando esses dois componentes,

temos  . Isso é exatamente o que a PCA faz, com a


exceção de que a PCA tem uma restrição ortonormal
adicional: UUT = Eun×n.
 Embora a Figura 17.1 represente um autoencoder sem camadas ocultas
dentro do codificador e do decodificador, podemos, é claro, adicionar várias
camadas ocultas com não-linearidades (como em um NN de várias
camadas) para construir um autoencoder profundo que possa aprender
funções de compactação e reconstrução de dados mais eficazes. Além
disso, observe que o codificador automático mencionado nesta seção usa
camadas totalmente conectadas. Quando trabalhamos com imagens, no
entanto, podemos substituir as camadas totalmente conectadas por
camadas convolucionais, como você aprendeu no Capítulo
14, Classificando imagens com redes neurais convolucionais profundas.
 Outros tipos de autoencoders com base no tamanho do espaço latente
 Como mencionado anteriormente, a dimensionalidade do espaço latente de
um autoencoder é tipicamente menor do que a dimensionalidade das
entradas (p < d), o que torna os autoencoders adequados para redução de
dimensionalidade. Por essa razão, o vetor latente também é
frequentemente referido como o "gargalo", e essa configuração específica
de um autoencoder também é chamada de subcompleta. No entanto, há
uma categoria diferente de autoencoders, chamada overcomplete, onde a
dimensionalidade do vetor latente, z, é, de fato, maior do que
a dimensionalidade dos exemplos de entrada (p > d).
 Ao treinar um autoencoder supercompleto, há uma solução trivial onde o
codificador e o decodificador podem simplesmente aprender a copiar
(memorizar) os recursos de entrada para sua camada de saída.
Obviamente, esta solução não é muito útil. No entanto, com algumas
modificações no procedimento de treinamento, autoencoders
supercompletos podem ser usados para redução de ruído.

 In this case, during training, random noise,  , is added to the input


examples and the network learns to reconstruct the clean example, x, from
the noisy signal,  . Then, at evaluation time, we provide the new
examples that are naturally noisy (that is, noise is already present such that

no additional artificial noise,  , is added) in order to remove the existing


noise from these examples. This particular autoencoder architecture and
training method is referred to as a denoising autoencoder.
 If you are interested, you can learn more about it in the research
article Stacked denoising autoencoders: Learning useful representations in
a deep network with a local denoising criterion by Pascal Vincent and
colleagues, 2010 (http://www.jmlr.org/papers/v11/vincent10a.html).

 Generative models for synthesizing new data


 Autoencoders are deterministic models, which means that after an
autoencoder is trained, given an input, x, it will be able to reconstruct the
input from its compressed version in a lower-dimensional space. Therefore,
it cannot generate new data beyond reconstructing its input through the
transformation of the compressed representation.

 A generative model, on the other hand, can generate a new example,  ,


from a random vector, z (corresponding to the latent representation). A
schematic representation of a generative model is shown in the following
figure. The random vector, z, comes from a distribution with fully known
characteristics, so we can easily sample from such a distribution. For
example, each element of z may come from the uniform distribution in the
range [–1, 1] (for which we

write  ) or from a standard


normal distribution (in which case, we

write  ):

 Figure 17.2: A generative model


 As we have shifted our attention from autoencoders to generative models,
you may have noticed that the decoder component of an autoencoder has
some similarities with a generative model. In particular, they both receive a
latent vector, z, as input and return an output in the same space as x. (For

the autoencoder,   is the reconstruction of an input, x, and for the

generative model,   is a synthesized sample.)


 However, the major difference between the two is that we do not know the
distribution of z in the autoencoder, while in a generative model, the
distribution of z is fully characterizable. It is possible to generalize an
autoencoder into a generative model, though. One approach is
the variational autoencoder (VAE).
 In a VAE receiving an input example, x, the encoder network is modified in
such a way that it computes two moments of the distribution of the latent

vector: the mean,  , and variance,  . During the training of a VAE,


the network is forced to match these moments with those of a standard
normal distribution (that is, zero mean and unit variance). Then, after the
VAE model is trained, the encoder is discarded, and we can use the

decoder network to generate new examples,  , by feeding


random z vectors from the “learned” Gaussian distribution.
 Besides VAEs, there are other types of generative models, for
example, autoregressive models and normalizing flow models. However, in
this chapter, we are only going to focus on GAN models, which are among
the most recent and most popular types of generative models in deep
learning.
 What is a generative model?
 Note that generative models are traditionally defined as algorithms that
model data input distributions, p(x), or the joint distributions of the input data
and associated targets, p(x, y). By definition, these models are also capable
of sampling from some feature, xi, conditioned on another feature, xj, which
is known as conditional inference. In the context of deep learning,
however, the term generative model is typically used to refer to models
that generate realistic-looking data. This means that we can sample from
input distributions, p(x), but we are not necessarily able to perform
conditional inference.

 Generating new samples with GANs


 To understand what GANs do in a nutshell, let’s first assume we have a
network that receives a random vector, z, sampled from a known
distribution, and generates an output image, x. We will call

this network generator (G) and use the notation   to


refer to the generated output. Assume our goal is to generate some images,
for example, face images, images of buildings, images of animals, or even
handwritten digits such as MNIST.
 As always, we will initialize this network with random weights. Therefore, the
first output images, before these weights are adjusted, will look like white
noise. Now, imagine there is a function that can assess the quality of
images (let’s call it an assessor function).
 If such a function exists, we can use the feedback from that function to tell
our generator network how to adjust its weights to improve the quality of the
generated images. This way, we can train the generator based on the
feedback from that assessor function, such that the generator learns to
improve its output toward producing realistic-looking images.
 While an assessor function, as described in the previous paragraph, would
make the image generation task very easy, the question is whether such a
universal function to assess the quality of images exists and, if so, how it is
defined. Obviously, as humans, we can easily assess the quality of output
images when we observe the outputs of the network; although, we cannot
(yet) backpropagate the result from our brain to the network. Now, if our
brain can assess the quality of synthesized images, can we design an
NN model to do the same thing? In fact, that’s the general idea of a GAN.
 As shown in Figure 17.3, a GAN model consists of an additional NN
called discriminator (D), which is a classifier that learns to detect a

synthesized image,  , from a real image, x:


 Figure 17.3: The discriminator distinguishes between the real image and the
one created by the generator
 In a GAN model, the two networks, generator and discriminator, are trained
together. At first, after initializing the model weights, the generator creates
images that do not look realistic. Similarly, the discriminator does a poor job
of distinguishing between real images and images synthesized by the
generator. But over time (that is, through training), both networks become
better as they interact with each other. In fact, the two networks play an
adversarial game, where the generator learns to improve its output to be
able to fool the discriminator. At the same time, the discriminator becomes
better at detecting the synthesized images.

 Understanding the loss functions of the


generator and discriminator networks in a GAN
model
 The objective function of GANs, as described in the original
paper Generative Adversarial Nets by I. Goodfellow and colleagues
(https://papers.nips.cc/paper/5423-generative-adversarial-nets.pdf), is as
follows:

 Here,   is called the value function, which can


be interpreted as a payoff: we want to maximize its value with respect to the
discriminator (D), while minimizing its value with respect to the generator

(G), that is,  . D(x) is the


probability that indicates whether the input example, x, is real or fake (that
is, generated). The

expression   refers to the


expected value of the quantity in brackets with respect to the examples from
the data distribution (distribution of the real
examples); 

 refers to
the expected value of the quantity with respect to the distribution of the
input, z, vectors.
 One training step of a GAN model with such a value function requires two
optimization steps: (1) maximizing the payoff for the discriminator and (2)
minimizing the payoff for the generator. A practical way of training GANs is
to alternate between these two optimization steps: (1) fix (freeze) the
parameters of one network and optimize the weights of the other one, and
(2) fix the second network and optimize the first one. This process should be
repeated at each training iteration. Let’s assume that the generator network
is fixed, and we want to optimize the discriminator. Both terms in the value

function   contribute to optimizing the


discriminator, where the first term corresponds to the loss associated with
the real examples, and the second term is the loss for the fake examples.
Therefore, when G is fixed, our objective is

to maximize  , which means making the


discriminator better at distinguishing between real and generated images.
 After optimizing the discriminator using the loss terms for real and fake
samples, we then fix the discriminator and optimize the generator. In this

case, only the second term in   contributes to


the gradients of the generator. As a result, when D is fixed, our objective is

to minimize  , which can be written


as 

. As was mentioned in the original GAN paper by Goodfellow and

colleagues, this function,  ,


suffers from vanishing gradients in the early training stages. The reason for
this is that the outputs, G(z), early in the learning process, look nothing like
real examples, and therefore D(G(z)) will be close to zero with high
confidence. This phenomenon is called saturation. To resolve this issue,
we can reformulate the minimization
objective, 

, by rewriting it

as  .
 Essa substituição significa que, para treinar o gerador, podemos trocar os
rótulos de exemplos reais e falsos e realizar uma minimização regular da
função. Em outras palavras, mesmo que os exemplos sintetizados pelo
gerador sejam falsos e, portanto, rotulados como 0, podemos inverter os
rótulos atribuindo o rótulo 1 a esses exemplos e minimizar a perda de
entropia cruzada binária com esses novos rótulos em vez de
maximizar 

.
 Agora que abordamos o procedimento geral de otimização para treinar
modelos GAN, vamos explorar os vários rótulos de dados que podemos
usar ao treinar GANs. Dado que o discriminador é um classificador binário
(os rótulos de classe são 0 e 1 para imagens falsas e reais,
respectivamente), podemos usar a função de perda de entropia cruzada
binária. Portanto, podemos determinar os rótulos de verdade base para a
perda discriminadora da seguinte maneira:

 E as etiquetas para treinar o gerador? Como queremos que o gerador


sintetize imagens realistas, queremos penalizar o gerador quando suas
saídas não são classificadas como reais pelo discriminador. Isso significa
que assumiremos que as etiquetas de verdade básicas para as saídas do
gerador sejam 1 ao calcular a função de perda para o gerador.
 Juntando tudo isso, a figura a seguir exibe as etapas individuais em um
modelo GAN simples:

 Figura 17.4: As etapas na criação de um modelo GAN


 Na seção seguinte, implementaremos uma GAN do zero para gerar novos
dígitos manuscritos.

Melhorar a qualidade das imagens


sintetizadas usando um GAN
convolucional e Wasserstein
Nesta seção, implementaremos um DCGAN, que nos permitirá melhorar o
desempenho que vimos no exemplo anterior do GAN. Além disso,
falaremos brevemente sobre uma técnica chave extra, Wasserstein
GAN (WGAN).

As técnicas que abordaremos nesta seção incluirão o seguinte:

 Convolução transposta
 Normalização em lote (BatchNorm)
 WGAN
O DCGAN foi proposto em 2016 por A. Radford, L. Metz e S. Chintala em seu
artigo Unsupervised representation learning with deep convolutional generative
adversarial networks, que está disponível gratuitamente
em https://arxiv.org/pdf/1511.06434.pdf. Neste artigo, os pesquisadores
propuseram o uso de camadas convolucionais para as redes geradora e
discriminadora. A partir de um vetor aleatório, z, o DCGAN primeiro usa uma
camada totalmente conectada para projetar z em um novo vetor com um tamanho
adequado para que ele possa ser remodelado..

Redes neurais de grafo para capturar


dependências em dados estruturados de
grafos
Neste capítulo, apresentaremos uma classe de modelos de aprendizagem
profunda que opera em dados de grafos, ou seja, redes neurais de
grafos (GNNs). Os GNNs têm sido uma área de rápido desenvolvimento nos
últimos anos. De acordo com o relatório State of AI de 2021
(https://www.stateof.ai/2021-report-launch.html), os GNNs evoluíram "de nicho
para um dos campos mais quentes da pesquisa de IA".

Os GNNs têm sido aplicados em uma variedade de áreas, incluindo as seguintes:

 Classificação de texto (https://arxiv.org/abs/1710.10903)


 Sistemas de recomendação (https://arxiv.org/abs/1704.06803)
 Previsão de tráfego (https://arxiv.org/abs/1707.01926)
 Descoberta de drogas (https://arxiv.org/abs/1806.02473)
Embora não possamos cobrir todas as novas ideias neste espaço em rápido
desenvolvimento, forneceremos uma base para entender como os GNNs
funcionam e como eles podem ser implementados. Além disso, apresentaremos a
biblioteca Geométrica PyTorch, que fornece recursos para gerenciar dados de
gráficos para aprendizado profundo, bem como implementações de muitos tipos
diferentes de camadas de gráficos que você pode usar em seus modelos de
aprendizado profundo.

Os tópicos que serão abordados neste capítulo são os seguintes:

 Uma introdução aos dados gráficos e como eles podem ser representados
para uso em redes neurais profundas
 Uma explicação das convoluções de grafos, um dos principais blocos de
construção dos GNNs comuns
 Um tutorial mostrando como implementar GNNs para predição de
propriedades moleculares usando PyTorch Geometric
 Uma visão geral dos métodos na vanguarda do campo GNN

 Introdução aos dados do gráfico


 Em linhas gerais, os gráficos representam uma certa maneira como
descrevemos e capturamos relacionamentos em dados. Gráficos são
um tipo particular de estrutura de dados que é não linear e abstrato. E como
os grafos são objetos abstratos, uma representação concreta precisa ser
definida para que os grafos possam ser operados. Além disso, os grafos
podem ser definidos para ter certas propriedades que podem exigir
representações diferentes. A Figura 18.1 resume os tipos comuns de
gráficos, que discutiremos mais detalhadamente nas seguintes subseções:

 Figura 18.1: Tipos comuns de gráficos


 Gráficos não direcionados
 Um grafo não direcionado consiste em nós (na teoria dos grafos também
muitas vezes chamados de vértices) que são conectados através de
arestas onde a ordem dos nós e sua conexão não importa. A figura
18.2 esboça dois exemplos típicos de grafos não direcionados, um grafo
amigo e um gráfico de uma molécula química consistindo de átomos
conectados através de ligações químicas (discutiremos tais grafos
moleculares com muito mais detalhes nas seções posteriores):

 Figura 18.2: Dois exemplos de grafos não dirigidos


 Outros exemplos comuns de dados que podem ser representados como
gráficos não direcionados incluem imagens, redes de interação proteína-
proteína e nuvens de pontos.
 Matematicamente, um grafo não direcionado G é um par (V, E), onde V é
um conjunto de nós do grafo, e E é o conjunto de arestas que compõem os
nós pareados. O gráfico pode então ser codificado como um |V|×|V| matriz
de adjacência A. Cada elemento xIj na matriz A é um 1 ou um 0, com 1
denotando uma aresta entre os nós i e j (vice-versa, 0 denota a ausência
de uma aresta). Como o gráfico não é direcionado, uma propriedade
adicional de A é que xIj = xJi.

 Gráficos direcionados
 Grafos direcionados, em contraste com grafos não direcionados
discutidos na seção anterior, conectam nós através de
arestas direcionadas. Matematicamente eles são definidos da mesma forma
que um grafo não direcionado, exceto que E, o conjunto de arestas, é um
conjunto de pares ordenados. Portanto, o elemento xIj de A não precisa ser
igual a xJi.
 Um exemplo de grafo direcionado é uma rede de citações, onde nós são
publicações e arestas de um nó são direcionadas para os nós de artigos
que um determinado artigo citou.

 Figure 18.3: An example of a directed graph

 Labeled graphs
 Muitos gráficos com os quais estamos interessados em trabalhar têm
informações adicionais associadas a cada um de seus nós e arestas. Por
exemplo, se você considerar a molécula de cafeína mostrada
anteriormente, as moléculas podem ser representadas como gráficos onde
cada nó é um elemento químico (por exemplo, átomos O, C, N ou H) e cada
aresta é o tipo de ligação (por exemplo, ligação simples ou dupla) entre
seus dois nós. Esses recursos de nó e borda precisam ser codificados com
alguma capacidade. Dado o gráfico G, definido pelo conjunto de nós e tupla
do conjunto de arestas (V, E), definimos um |V|×fV matriz de recursos do
nó X, onde fV é o comprimento do vetor de rótulo de cada nó. Para etiquetas
de borda, definimos um |E|×fE matriz de recursos de borda XE, onde fE é o
comprimento do vetor de rótulo de cada aresta.
 As moléculas são um excelente exemplo de dados que podem ser
representados como um gráfico rotulado, e trabalharemos com dados
moleculares ao longo do capítulo. Como tal, aproveitaremos esta
oportunidade para abordar a sua representação em detalhe na próxima
secção.

 Representação de moléculas como gráficos


 Como uma visão geral química, as moléculas podem ser pensadas como
grupos de átomos mantidos juntos por ligações químicas. Existem
diferentes átomos correspondentes a diferentes elementos químicos, por
exemplo, elementos comuns incluem carbono (C), oxigênio (O), nitrogênio
(N) e hidrogênio (H). Além disso, existem diferentes tipos de ligações que
formam a conexão entre átomos, por exemplo, ligações simples ou duplas.
 Podemos representar uma molécula como um grafo não direcionado com
uma matriz de etiqueta de nó, onde cada linha é uma codificação a quente
do tipo de átomo do nó associado. Além disso, há uma matriz de rótulo de
borda onde cada linha é uma codificação a quente do tipo de ligação da
borda associada. Para simplificar essa representação, átomos de
hidrogênio às vezes são tornados implícitos, uma vez que sua localização
pode ser inferida com regras químicas básicas. Considerando a molécula
de cafeína que vimos anteriormente, um exemplo de representação gráfica
com átomos de hidrogênio implícitos é mostrado na Figura 18.4:

 Figura 18.4: Representação gráfica de uma molécula de cafeína

 Implementando uma GAN do zero


 Nesta seção, abordaremos como implementar e treinar um modelo GAN
para gerar novas imagens, como dígitos MNIST. Como o treinamento em
uma unidade de processamento central (CPU) normal pode levar muito
tempo, na subseção a seguir, abordaremos como configurar o ambiente do
Google Colab, o que nos permitirá executar os cálculos em unidades de
processamento gráfico (GPUs).

 Treinamento de modelos de GAN no Google


Colab
 Alguns dos exemplos de código neste capítulo podem exigir
recursos computacionais extensos que vão além de um laptop convencional
ou uma estação de trabalho sem uma GPU. Se você já tiver uma máquina
de computação habilitada para GPU NVIDIA disponível, com bibliotecas
CUDA e cuDNN instaladas, poderá usá-la para acelerar os cálculos.
 No entanto, como muitos de nós não temos acesso a recursos de
computação de alto desempenho, usaremos o ambiente Google
Colaboratory (muitas vezes referido como Google Colab), que é um serviço
gratuito de computação em nuvem (disponível na maioria dos países).
 O Google Colab fornece instâncias do Jupyter Notebook que são
executadas na nuvem; os notebooks podem ser salvos no Google Drive ou
GitHub. Embora a plataforma forneça vários recursos de computação
diferentes, como CPUs, GPUs e até unidades de processamento
tensor (TPUs), é importante destacar que o tempo de execução atualmente
é limitado a 12 horas. Portanto, qualquer notebook com mais de 12 horas
será interrompido.
 Os blocos de código neste capítulo precisarão de um tempo máximo de
computação de duas a três horas, portanto, isso não será um problema. No
entanto, se você decidir usar o Google Colab para outros projetos que
levam mais de 12 horas, use o ponto de verificação e salve os pontos de
verificação intermediários.
 Caderno Jupyter
 Jupyter Notebook é uma interface gráfica do usuário (GUI) para executar
código interativamente e intercalá-lo com texto, documentação e figuras.
Devido à sua versatilidade e facilidade de uso, tornou-se uma das
ferramentas mais populares em ciência de dados.
 Para obter mais informações sobre a GUI geral do Jupyter Notebook,
consulte a documentação oficial em https://jupyter-
notebook.readthedocs.io/en/stable/. Todo o código deste livro também está
disponível na forma de cadernos Jupyter, e uma breve introdução pode ser
encontrada no diretório de códigos do primeiro capítulo.
 Por fim, recomendamos o artigo de Adam Rule et al., Ten simple rules for
writing and sharing computational analyses in Jupyter Notebooks, sobre o
uso efetivo do Jupyter Notebook em projetos de pesquisa científica, que
está disponível gratuitamente
em https://journals.plos.org/ploscompbiol/article?id=10.1371/journal.pcbi.10
07007.
 Acessar o Google Colab é muito simples. Você pode visitar o
https://colab.research.google.com, que o leva automaticamente a uma
janela de prompt onde você pode ver seus blocos de anotações Jupyter
existentes. Nessa janela de prompt, clique na guia Google Drive, como
mostra a Figura 17.5. É aqui que você salvará o bloco de anotações no
Google Drive.
 Em seguida, para criar um novo bloco de anotações, clique no link Novo
bloco de anotações na parte inferior da janela de prompt:

 Figura 17.5: Criando um novo bloco de anotações Python no Google Colab


 Isso criará e abrirá um novo bloco de anotações para você. Todos os
exemplos de código que você escrever neste bloco de anotações serão
salvos automaticamente e, posteriormente, você poderá acessar o bloco de
anotações do seu Google Drive em um diretório chamado Colab
Notebooks.
 Na próxima etapa, queremos utilizar GPUs para executar os exemplos de
código neste notebook. Para fazer isso, na opção Runtime na barra de
menus deste notebook, clique em Change runtime type e
selecione GPU, como mostra a Figura 17.6:

 Figura 17.6: Utilização de GPUs no Google Colab


 Na última etapa, só precisamos instalar os pacotes Python que
precisaremos para este capítulo. O ambiente Colab Notebooks já vem com
alguns pacotes, como NumPy, SciPy e a última versão estável do PyTorch.
No momento em que este artigo foi escrito, a última versão estável no
Google Colab é o PyTorch 1.9.
 Agora, podemos testar a instalação e verificar se a GPU está disponível
usando o seguinte código:
 >>> import torch

 >>> print(torch.__version__)

 1.9.0+cu111

 >>> print("GPU Available:", torch.cuda.is_available())

 GPU Available: True

 >>> if torch.cuda.is_available():

 ... device = torch.device("cuda:0")

 ... else:

 ... device = "cpu"

 >>> print(device)

 cuda:0
 CopyExplain
 Além disso, se você quiser salvar o modelo em seu Google Drive pessoal,
ou transferir ou fazer upload de outros arquivos, você precisa montar o
Google Drive. Para fazer isso, execute o seguinte em uma nova célula do
bloco de anotações:
 >>> from google.colab import drive

 >>> drive.mount('/content/drive/')
 CopyExplain
 Isso fornecerá um link para autenticar o Colab Notebook acessando seu
Google Drive. Depois de seguir as instruções para autenticação, ele
fornecerá um código de autenticação que você precisa copiar e colar
no campo de entrada designado abaixo da célula que você acabou de
executar. Em seguida, seu Google Drive será montado e estará disponível
em . Como alternativa, você pode montá-lo por meio da interface GUI,
conforme destacado na Figura 17.7:/content/drive/My Drive

 Figure 17.7: Mounting your Google Drive

 Implementing the generator and the


discriminator networks
 We will start the implementation of our first GAN model with a generator and
a discriminator as two fully connected networks with one or more hidden
layers, as shown in Figure 17.8:

 Figure 17.8: A GAN model with a generator and discriminator as two fully
connected networks
 Figure 17.8 depicts the original GAN based on fully connected layers, which
we will refer to as a vanilla GAN.
 In this model, for each hidden layer, we will apply the leaky ReLU activation
function. The use of ReLU results in sparse gradients, which may not be
suitable when we want to have the gradients for the full range of input
values. In the discriminator network, each hidden layer is also followed by a
dropout layer. Furthermore, the output layer in the generator uses the
hyperbolic tangent (tanh) activation function. (Using tanh activation is
recommended for the generator network since it helps with the learning.)
 The output layer in the discriminator has no activation function (that is, linear
activation) to get the logits. Alternatively, we can use the sigmoid activation
function to get probabilities as output.
 Leaky rectified linear unit (ReLU) activation function
 In Chapter 12, Parallelizing Neural Network Training with PyTorch, we
covered different nonlinear activation functions that can be used in an NN
model. If you recall, the ReLU activation function was defined

as  , which suppresses the negative


(preactivation) inputs; that is, negative inputs are set to zero. Consequently,
using the ReLU activation function may result in sparse gradients during
backpropagation. Sparse gradients are not always detrimental and can even
benefit models for classification. However, in certain applications, such as
GANs, it can be beneficial to obtain the gradients for the full range of input
values, which we can achieve by making a slight modification to the ReLU
function such that it outputs small values for negative inputs. This modified
version of the ReLU function is also known as leaky ReLU. In short, the
leaky ReLU activation function permits non-zero gradients for negative
inputs as well, and as a result, it makes the networks more expressive
overall.
 The leaky ReLU activation function is defined as follows:

 Figure 17.9: The leaky ReLU activation function

 Here,   determines the slope for the negative (preactivation) inputs.


 We will define two helper functions for each of the two networks, instantiate
a model from the PyTorch class, and add the layers as described. The code
is as follows:nn.Sequential
 >>> import torch.nn as nn

 >>> import numpy as np

 >>> import matplotlib.pyplot as plt

 >>> ## define a function for the generator:

 >>> def make_generator_network(

 ... input_size=20,

 ... num_hidden_layers=1,

 ... num_hidden_units=100,

 ... num_output_units=784):

 ... model = nn.Sequential()

 ... for i in range(num_hidden_layers):

 ... model.add_module(f'fc_g{i}',

 ... nn.Linear(input_size, num_hidden_units))

 ... model.add_module(f'relu_g{i}', nn.LeakyReLU())

 ... input_size = num_hidden_units

 ... model.add_module(f'fc_g{num_hidden_layers}',

 ... nn.Linear(input_size, num_output_units))

 ... model.add_module('tanh_g', nn.Tanh())

 ... return model

 >>>

 >>> ## define a function for the discriminator:

 >>> def make_discriminator_network(

 ... input_size,

 ... num_hidden_layers=1,
 ... num_hidden_units=100,

 ... num_output_units=1):

 ... model = nn.Sequential()

 ... for i in range(num_hidden_layers):

 ... model.add_module(

 ... f'fc_d{i}',

 ... nn.Linear(input_size, num_hidden_units, bias=False)

 ... )

 ... model.add_module(f'relu_d{i}', nn.LeakyReLU())

 ... model.add_module('dropout', nn.Dropout(p=0.5))

 ... input_size = num_hidden_units

 ... model.add_module(f'fc_d{num_hidden_layers}',

 ... nn.Linear(input_size, num_output_units))

 ... model.add_module('sigmoid', nn.Sigmoid())

 ... return model


 CopyExplain
 Next, we will specify the training settings for the model. As you will
remember from previous chapters, the image size in the MNIST dataset is
28×28 pixels. (That is only one color channel because MNIST contains only
grayscale images.) We will further specify the size of the input vector, z, to
be 20. Since we are implementing a very simple GAN model for illustration
purposes only and using fully connected layers, we will only use a single
hidden layer with 100 units in each network. In the following code, we will
specify and initialize the two networks, and print their summary information:
 >>> image_size = (28, 28)

 >>> z_size = 20

 >>> gen_hidden_layers = 1

 >>> gen_hidden_size = 100

 >>> disc_hidden_layers = 1
 >>> disc_hidden_size = 100

 >>> torch.manual_seed(1)

 >>> gen_model = make_generator_network(

 ... input_size=z_size,

 ... num_hidden_layers=gen_hidden_layers,

 ... num_hidden_units=gen_hidden_size,

 ... num_output_units=np.prod(image_size)

 ... )

 >>> print(gen_model)

 Sequential(

 (fc_g0): Linear(in_features=20, out_features=100, bias=False)

 (relu_g0): LeakyReLU(negative_slope=0.01)

 (fc_g1): Linear(in_features=100, out_features=784, bias=True)

 (tanh_g): Tanh()

 )

 >>> disc_model = make_discriminator_network(

 ... input_size=np.prod(image_size),

 ... num_hidden_layers=disc_hidden_layers,

 ... num_hidden_units=disc_hidden_size

 ... )

 >>> print(disc_model)

 Sequential(

 (fc_d0): Linear(in_features=784, out_features=100, bias=False)

 (relu_d0): LeakyReLU(negative_slope=0.01)

 (dropout): Dropout(p=0.5, inplace=False)

 (fc_d1): Linear(in_features=100, out_features=1, bias=True)


 (sigmoid): Sigmoid()

 )
 CopyExplain

 Defining the training dataset


 In the next step, we will load the MNIST dataset from PyTorch and apply the
necessary preprocessing steps. Since the output layer of the generator is
using the tanh activation function, the pixel values of the synthesized
images will be in the range (–1, 1). However, the input pixels of the MNIST
images are within the range [0, 255] (with a data type ). Thus, in the
preprocessing steps, we will use the function to convert the input image
tensors to a tensor. As a result, besides changing the data type, calling this
function will also change the range of input pixel intensities to [0, 1]. Then,
we can shift them by –0.5 and scale them by a factor of 0.5 such that the
pixel intensities will be rescaled to be in the range [–1, 1], which can
improve gradient descent-based
learning:PIL.Image.Imagetorchvision.transforms.ToTensor
 >>> import torchvision

 >>> from torchvision import transforms

 >>> image_path = './'

 >>> transform = transforms.Compose([

 ... transforms.ToTensor(),

 ... transforms.Normalize(mean=(0.5), std=(0.5)),

 ... ])

 >>> mnist_dataset = torchvision.datasets.MNIST(

 ... root=image_path, train=True,

 ... transform=transform, download=False

 ... )

 >>> example, label = next(iter(mnist_dataset))

 >>> print(f'Min: {example.min()} Max: {example.max()}')

 >>> print(example.shape)
 Min: -1.0 Max: 1.0

 torch.Size([1, 28, 28])


 CopyExplain
 Furthermore, we will also create a random vector, z, based on the desired
random distribution (in this code example, uniform or normal, which are the
most common choices):
 >>> def create_noise(batch_size, z_size, mode_z):

 ... if mode_z == 'uniform':

 ... input_z = torch.rand(batch_size, z_size)*2 - 1

 ... elif mode_z == 'normal':

 ... input_z = torch.randn(batch_size, z_size)

 ... return input_z


 CopyExplain
 Let’s inspect the dataset object that we created. In the following code, we
will take one batch of examples and print the array shapes of this sample of
input vectors and images. Furthermore, in order to understand the overall
data flow of our GAN model, in the following code, we will process a forward
pass for our generator and discriminator.
 First, we will feed the batch of input, z, vectors to the generator and get its
output, . This will be a batch of fake examples, which will be fed to the
discriminator model to get the probabilities for the batch of fake examples, .
Furthermore, the processed images that we get from the dataset object will
be fed to the discriminator model, which will result in the probabilities for the
real examples, . The code is as follows:g_outputd_proba_faked_proba_real
 >>> from torch.utils.data import DataLoader

 >>> batch_size = 32

 >>> dataloader = DataLoader(mnist_dataset, batch_size, shuffle=False)

 >>> input_real, label = next(iter(dataloader))

 >>> input_real = input_real.view(batch_size, -1)

 >>> torch.manual_seed(1)

 >>> mode_z = 'uniform' # 'uniform' vs. 'normal'


 >>> input_z = create_noise(batch_size, z_size, mode_z)

 >>> print('input-z -- shape:', input_z.shape)

 >>> print('input-real -- shape:', input_real.shape)

 input-z -- shape: torch.Size([32, 20])

 input-real -- shape: torch.Size([32, 784])

 >>> g_output = gen_model(input_z)

 >>> print('Output of G -- shape:', g_output.shape)

 Output of G -- shape: torch.Size([32, 784])

 >>> d_proba_real = disc_model(input_real)

 >>> d_proba_fake = disc_model(g_output)

 >>> print('Disc. (real) -- shape:', d_proba_real.shape)

 >>> print('Disc. (fake) -- shape:', d_proba_fake.shape)

 Disc. (real) -- shape: torch.Size([32, 1])

 Disc. (fake) -- shape: torch.Size([32, 1])


 CopyExplain
 The two probabilities, and , will be used to compute the loss functions for
training the model.d_proba_faked_proba_real

 Training the GAN model


 As the next step, we will create an instance of as our loss function and use
that to calculate the binary cross-entropy loss for the generator and
discriminator associated with the batches that we just processed. To do this,
we also need the ground truth labels for each output. For the generator, we
will create a vector of 1s with the same shape as the vector containing the
predicted probabilities for the generated images, . For the discriminator loss,
we have two terms: the loss for detecting the fake examples involving and
the loss for detecting the real examples based
on .nn.BCELossd_proba_faked_proba_faked_proba_real
 The ground truth labels for the fake term will be a vector of 0s that we can
generate via the (or ) function. Similarly, we can generate the ground truth
values for the real images via the (or ) function, which creates a vector of
1s:torch.zeros()torch.zeros_like()torch.ones()torch.ones_like()
 >>> loss_fn = nn.BCELoss()

 >>> ## Loss for the Generator

 >>> g_labels_real = torch.ones_like(d_proba_fake)

 >>> g_loss = loss_fn(d_proba_fake, g_labels_real)

 >>> print(f'Generator Loss: {g_loss:.4f}')

 Generator Loss: 0.6863

 >>> ## Loss for the Discriminator

 >>> d_labels_real = torch.ones_like(d_proba_real)

 >>> d_labels_fake = torch.zeros_like(d_proba_fake)

 >>> d_loss_real = loss_fn(d_proba_real, d_labels_real)

 >>> d_loss_fake = loss_fn(d_proba_fake, d_labels_fake)

 >>> print(f'Discriminator Losses: Real {d_loss_real:.4f} Fake {d_loss_fake:.4f}')

 Discriminator Losses: Real 0.6226 Fake 0.7007


 CopyExplain
 The previous code example shows the step-by-step calculation of the
different loss terms for the purpose of understanding the overall concept
behind training a GAN model. The following code will set up the GAN model
and implement the training loop, where we will include these calculations in
a loop.for
 We will start with setting up the data loader for the real dataset, the
generator and discriminator model, as well as a separate Adam optimizer for
each of the two models:
 >>> batch_size = 64

 >>> torch.manual_seed(1)

 >>> np.random.seed(1)

 >>> mnist_dl = DataLoader(mnist_dataset, batch_size=batch_size,

 ... shuffle=True, drop_last=True)


 >>> gen_model = make_generator_network(

 ... input_size=z_size,

 ... num_hidden_layers=gen_hidden_layers,

 ... num_hidden_units=gen_hidden_size,

 ... num_output_units=np.prod(image_size)

 ... ).to(device)

 >>> disc_model = make_discriminator_network(

 ... input_size=np.prod(image_size),

 ... num_hidden_layers=disc_hidden_layers,

 ... num_hidden_units=disc_hidden_size

 ... ).to(device)

 >>> loss_fn = nn.BCELoss()

 >>> g_optimizer = torch.optim.Adam(gen_model.parameters())

 >>> d_optimizer = torch.optim.Adam(disc_model.parameters())


 CopyExplain
 In addition, we will compute the loss gradients with respect to the model
weights and optimize the parameters of the generator and discriminator
using two separate Adam optimizers. We will write two utility functions for
training the discriminator and the generator as follows:
 >>> ## Train the discriminator

 >>> def d_train(x):

 ... disc_model.zero_grad()

 ... # Train discriminator with a real batch

 ... batch_size = x.size(0)

 ... x = x.view(batch_size, -1).to(device)

 ... d_labels_real = torch.ones(batch_size, 1, device=device)

 ... d_proba_real = disc_model(x)

 ... d_loss_real = loss_fn(d_proba_real, d_labels_real)


 ... # Train discriminator on a fake batch

 ... input_z = create_noise(batch_size, z_size, mode_z).to(device)

 ... g_output = gen_model(input_z)

 ... d_proba_fake = disc_model(g_output)

 ... d_labels_fake = torch.zeros(batch_size, 1, device=device)

 ... d_loss_fake = loss_fn(d_proba_fake, d_labels_fake)

 ... # gradient backprop & optimize ONLY D's parameters

 ... d_loss = d_loss_real + d_loss_fake

 ... d_loss.backward()

 ... d_optimizer.step()

 ... return d_loss.data.item(), d_proba_real.detach(), \

 ... d_proba_fake.detach()

 >>>

 >>> ## Train the generator

 >>> def g_train(x):

 ... gen_model.zero_grad()

 ... batch_size = x.size(0)

 ... input_z = create_noise(batch_size, z_size, mode_z).to(device)

 ... g_labels_real = torch.ones(batch_size, 1, device=device)

 ...

 ... g_output = gen_model(input_z)

 ... d_proba_fake = disc_model(g_output)

 ... g_loss = loss_fn(d_proba_fake, g_labels_real)

 ... # gradient backprop & optimize ONLY G's parameters

 ... g_loss.backward()

 ... g_optimizer.step()
 ... return g_loss.data.item()
 CopyExplain
 Next, we will alternate between the training of the generator and the
discriminator for 100 epochs. For each epoch, we will record the loss for the
generator, the loss for the discriminator, and the loss for the real data and
fake data respectively. Furthermore, after each epoch, we will generate
some examples from a fixed noise input using the current generator model
by calling the function. We will store the synthesized images in a Python list.
The code is as follows:create_samples()
 >>> fixed_z = create_noise(batch_size, z_size, mode_z).to(device)

 >>> def create_samples(g_model, input_z):

 ... g_output = g_model(input_z)

 ... images = torch.reshape(g_output, (batch_size, *image_size))

 ... return (images+1)/2.0

 >>>

 >>> epoch_samples = []

 >>> all_d_losses = []

 >>> all_g_losses = []

 >>> all_d_real = []

 >>> all_d_fake = []

 >>> num_epochs = 100

 >>>

 >>> for epoch in range(1, num_epochs+1):

 ... d_losses, g_losses = [], []

 ... d_vals_real, d_vals_fake = [], []

 ... for i, (x, _) in enumerate(mnist_dl):

 ... d_loss, d_proba_real, d_proba_fake = d_train(x)

 ... d_losses.append(d_loss)

 ... g_losses.append(g_train(x))
 ... d_vals_real.append(d_proba_real.mean().cpu())

 ... d_vals_fake.append(d_proba_fake.mean().cpu())

 ...

 ... all_d_losses.append(torch.tensor(d_losses).mean())

 ... all_g_losses.append(torch.tensor(g_losses).mean())

 ... all_d_real.append(torch.tensor(d_vals_real).mean())

 ... all_d_fake.append(torch.tensor(d_vals_fake).mean())

 ... print(f'Epoch {epoch:03d} | Avg Losses >>'

 ... f' G/D {all_g_losses[-1]:.4f}/{all_d_losses[-1]:.4f}'

 ... f' [D-Real: {all_d_real[-1]:.4f}'

 ... f' D-Fake: {all_d_fake[-1]:.4f}]')

 ... epoch_samples.append(

 ... create_samples(gen_model, fixed_z).detach().cpu().numpy()

 ... )

 Epoch 001 | Avg Losses >> G/D 0.9546/0.8957 [D-Real: 0.8074 D-Fake: 0.4687]

 Epoch 002 | Avg Losses >> G/D 0.9571/1.0841 [D-Real: 0.6346 D-Fake: 0.4155]

 Epoch ...

 Epoch 100 | Avg Losses >> G/D 0.8622/1.2878 [D-Real: 0.5488 D-Fake: 0.4518]
 CopyExplain
 Using a GPU on Google Colab, the training process that we implemented in
the previous code block should be completed in less than an hour. (It may
even be faster on your personal computer if you have a recent and capable
CPU and a GPU.) After the model training has completed, it is often helpful
to plot the discriminator and generator losses to analyze the behavior of
both subnetworks and assess whether they converged.
 It is also helpful to plot the average probabilities of the batches of real and
fake examples as computed by the discriminator in each iteration. We
expect these probabilities to be around 0.5, which means that the
discriminator is not able to confidently distinguish between real and fake
images:
 >>> import itertools

 >>> fig = plt.figure(figsize=(16, 6))

 >>> ## Plotting the losses

 >>> ax = fig.add_subplot(1, 2, 1)

 >>> plt.plot(all_g_losses, label='Generator loss')

 >>> half_d_losses = [all_d_loss/2 for all_d_loss in all_d_losses]

 >>> plt.plot(half_d_losses, label='Discriminator loss')

 >>> plt.legend(fontsize=20)

 >>> ax.set_xlabel('Iteration', size=15)

 >>> ax.set_ylabel('Loss', size=15)

 >>>

 >>> ## Plotting the outputs of the discriminator

 >>> ax = fig.add_subplot(1, 2, 2)

 >>> plt.plot(all_d_real, label=r'Real: $D(\mathbf{x})$')

 >>> plt.plot(all_d_fake, label=r'Fake: $D(G(\mathbf{z}))$')

 >>> plt.legend(fontsize=20)

 >>> ax.set_xlabel('Iteration', size=15)

 >>> ax.set_ylabel('Discriminator output', size=15)

 >>> plt.show()
 CopyExplain
 Figure 17.10 shows the results:

 Figure 17.10: The discriminator performance


 As you can see from the discriminator outputs in the previous figure, during
the early stages of the training, the discriminator was able to quickly learn to
distinguish quite accurately between the real and fake examples; that is, the
fake examples had probabilities close to 0, and the real examples had
probabilities close to 1. The reason for that was that the fake examples were
nothing like the real ones; therefore, distinguishing between real and fake
was rather easy. As the training proceeds further, the generator will become
better at synthesizing realistic images, which will result in probabilities of
both real and fake examples that are close to 0.5.
 Furthermore, we can also see how the outputs of the generator, that is, the
synthesized images, change during training. In the following code, we will
visualize some of the images produced by the generator for a selection of
epochs:
 >>> selected_epochs = [1, 2, 4, 10, 50, 100]

 >>> fig = plt.figure(figsize=(10, 14))

 >>> for i,e in enumerate(selected_epochs):

 ... for j in range(5):

 ... ax = fig.add_subplot(6, 5, i*5+j+1)

 ... ax.set_xticks([])

 ... ax.set_yticks([])

 ... if j == 0:
 ... ax.text(

 ... -0.06, 0.5, f'Epoch {e}',

 ... rotation=90, size=18, color='red',

 ... horizontalalignment='right',

 ... verticalalignment='center',

 ... transform=ax.transAxes

 ... )

 ...

 ... image = epoch_samples[e-1][j]

 ... ax.imshow(image, cmap='gray_r')

 ...

 >>> plt.show()
 CopyExplain
 Figure 17.11 shows the produced images:

 Figura 17.11: Imagens produzidas pelo gerador


 Como você pode ver na Figura 17.11, a rede geradora produziu imagens
cada vez mais realistas à medida que o treinamento progredia. No entanto,
mesmo após 100 épocas, as imagens produzidas ainda parecem muito
diferentes dos dígitos manuscritos contidos no conjunto de dados MNIST.
 Nesta seção, projetamos um modelo GAN muito simples com apenas uma
única camada oculta totalmente conectada para o gerador e o
discriminador. Após o treinamento do modelo GAN no conjunto de dados
MNIST, conseguimos obter resultados promissores, embora ainda não
satisfatórios, com os novos dígitos manuscritos.
 Como aprendemos no Capítulo 14, Classificando imagens com redes
neurais convolucionais profundas, as arquiteturas NN com camadas
convolucionais têm várias vantagens sobre as camadas totalmente
conectadas quando se trata de classificação de imagens. Em um sentido
semelhante, adicionar camadas convolucionais ao nosso modelo GAN para
trabalhar com dados de imagem pode melhorar o resultado. Na próxima
seção, implementaremos um GAN convolucional profundo (DCGAN),
que usa camadas convolucionais tanto para o gerador quanto para as redes
discriminadoras.

Melhorar a qualidade das imagens


sintetizadas usando um GAN
convolucional e Wasserstein
Nesta seção, implementaremos um DCGAN, que nos permitirá melhorar o
desempenho que vimos no exemplo anterior do GAN. Além disso,
falaremos brevemente sobre uma técnica chave extra, Wasserstein
GAN (WGAN).

As técnicas que abordaremos nesta seção incluirão o seguinte:

 Convolução transposta
 Normalização em lote (BatchNorm)
 WGAN
O DCGAN foi proposto em 2016 por A. Radford, L. Metz e S. Chintala em seu
artigo Unsupervised representation learning with deep convolutional generative
adversarial networks, que está disponível gratuitamente
em https://arxiv.org/pdf/1511.06434.pdf. Neste artigo, os pesquisadores
propuseram o uso de camadas convolucionais para as redes geradora e
discriminadora. A partir de um vetor aleatório, z, o DCGAN primeiro usa uma
camada totalmente conectada para projetar z em um novo vetor com um tamanho
adequado para que ele possa ser remodelado...

Você também pode gostar