Escolar Documentos
Profissional Documentos
Cultura Documentos
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.
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
self.eta = eta
self.n_iter = n_iter
self.random_state = random_state
CopyExplain
>>> v2 = 0.5 * v1
... np.linalg.norm(v2)))
0.0
CopyExplain
Entre em contato
Feedback de nossos leitores é sempre bem-vindo.
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.
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:
https://packt.link/free-ebook/9781801819312
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.
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.
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.
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:
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.
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
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
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.
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.
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.
python --version
CopyExplain
ou
python3 --version
CopyExplain
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
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:
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.
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.
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.
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.
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
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:
(1)
(2)
(3)
(4)
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.
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.
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.
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
import numpy as np
class Perceptron:
"""Perceptron classifier.
Parameters
------------
eta : float
n_iter : int
random_state : int
initialization.
Attributes
-----------
w_ : 1d-array
b_ : Scalar
errors_ : list
"""
self.eta = eta
self.n_iter = n_iter
self.random_state = random_state
Parameters
----------
Target values.
Returns
-------
self : object
"""
rgen = np.random.RandomState(self.random_state)
size=X.shape[1])
self.b_ = np.float_(0.)
self.errors_ = []
for _ in range(self.n_iter):
errors = 0
self.w_ += update * xi
self.b_ += update
self.errors_.append(errors)
return self
>>> v2 = 0.5 * v1
... np.linalg.norm(v2)))
0.0
CopyExplain
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
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
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
>>> import os
>>> s = '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
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
>>> plt.show()
CopyExplain
Figura 2.6: Gráfico de dispersão das flores setosa e versicolor por comprimento
sépala e pétala
>>> ppn.fit(X, y)
>>> plt.xlabel('Epochs')
>>> plt.show()
CopyExplain
cmap = ListedColormap(colors[:len(np.unique(y))])
lab = lab.reshape(xx1.shape)
plt.xlim(xx1.min(), xx1.max())
plt.ylim(xx2.min(), xx2.max())
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
>>> 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
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.
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):
perda, L(w, b):
para .
class AdalineGD:
------------
eta : float
n_iter : int
random_state : int
Attributes
-----------
w_ : 1d-array
b_ : Scalar
losses_ : list
"""
self.eta = eta
self.n_iter = n_iter
self.random_state = random_state
Parameters
----------
Target values.
Returns
-------
self : object
"""
rgen = np.random.RandomState(self.random_state)
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)
loss = (errors**2).mean()
self.losses_.append(loss)
return self
return X
return np.where(self.activation(self.net_input(X))
>= 0.5, 1, 0)
CopyExplain
self.w_[w_j] += self.eta *
(2.0 * (X[:, w_j]*errors)).mean()
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
Multiplicação matricial
Na prática, muitas vezes requer alguma experimentação para encontrar uma boa
Hiperparâmetros
Let’s now plot the loss against the number of epochs for the two different learning
rates:
>>> ax[0].set_xlabel('Epochs')
>>> ax[1].set_xlabel('Epochs')
>>> 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
Figure 2.12: A comparison of a well-chosen learning rate and a learning rate that is
too large
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
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
After standardization, we will train Adaline again and see that it now converges
>>> ada_gd.fit(X_std, y)
>>> plt.tight_layout()
>>> plt.show()
>>> plt.xlabel('Epochs')
>>> 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.
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.
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:
Parameters
------------
eta : float
n_iter : int
cycles.
random_state : int
initialization.
Attributes
-----------
w_ : 1d-array
b_ : Scalar
losses_ : list
"""
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
Parameters
----------
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 = []
avg_loss = np.mean(losses)
self.losses_.append(avg_loss)
return self
if not self.w_initialized:
self._initialize_weights(X.shape[1])
if y.ravel().shape[0] > 1:
self._update_weights(xi, target)
else:
self._update_weights(X, y)
return self
r = self.rgen.permutation(len(y))
self.rgen = np.random.RandomState(self.random_state)
size=m)
self.b_ = np.float_(0.)
self.w_initialized = True
output = self.activation(self.net_input(xi))
loss = error**2
return loss
return X
return np.where(self.activation(self.net_input(X))
>= 0.5, 1, 0)
CopyExplain
>>> ada_sgd.fit(X_std, y)
>>> plt.tight_layout()
>>> plt.show()
... marker='o')
>>> plt.xlabel('Epochs')
>>> 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.
https://packt.link/MLwPyTorch
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.
>>> 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,'''
>>> df = pd.read_csv(StringIO(csv_data))
>>> df
A B C D
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
>>> df.values
>>> df.dropna(axis=0)
A B C D
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
>>> df.dropna(how='all')
A B C D
>>> df.dropna(thresh=4)
A B C D
>>> # only drop rows where NaN appear in specific columns (here: 'C')
>>> df.dropna(subset=['C'])
A B C D
>>> imputed_data
>>> df.fillna(df.mean())
CopyExplain
Figura 4.1: Substituição dos valores em falta nos dados pela média
>>> df = pd.DataFrame([
>>> df
... 'L': 2,
... 'M': 1}
>>> df
>>> df['size'].map(inv_size_mapping)
0 M
1 L
2 XL
... enumerate(np.unique(df['classlabel']))}
>>> class_mapping
{'class1': 0, 'class2': 1}
CopyExplain
>>> df
1 red 2 13.5 0
2 blue 3 15.3 1
CopyExplain
>>> df
>>> y = class_le.fit_transform(df['classlabel'].values)
>>> y
array([1, 0, 1])
CopyExplain
>>> X
array([[1, 1, 10.1],
[2, 2, 13.5],
blue = 0
green = 1
red = 2
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
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))
... ])
>>> c_transf.fit_transform(X).astype(float)
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
0 10.1 1 0 1 0
1 13.5 2 0 0 1
2 15.3 3 1 0 0
CopyExplain
... drop_first=True)
0 10.1 1 1 0
1 13.5 2 0 1
2 15.3 3 0 0
CopyExplain
... ])
>>> c_transf.fit_transform(X).astype(float)
array([[ 1. , 0. , 1. , 10.1],
[ 0. , 1. , 2. , 13.5],
[ 0. , 0. , 3. , 15.3]])
CopyExplain
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.
... 'class2'],
... ['red', 'L', 13.5,
... 'class1'],
... 'class2']])
... 'classlabel']
>>> df
CopyExplain
>>> df
CopyExplain
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:
... 'ml/machine-learning-databases/'
... 'Proanthocyanins',
... 'Proline']
Class labels [1 2 3]
>>> df_wine.head()
CopyExplain
... train_test_split(X, y,
... test_size=0.3,
... random_state=0,
... stratify=y)
CopyExplain
Entrad
Padronizado Mín-máx normalizado
a
0.87831007 1.46385011]
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.
>>> LogisticRegression(penalty='l1',
... solver='liblinear',
... multi_class='ovr')
CopyExplain
>>> lr = LogisticRegression(penalty='l1',
... C=1.0,
... solver='liblinear',
... multi_class='ovr')
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_
>>> lr.coef_
0. ,0. , 1.16243821, 0. ,
0. , 0. , 0. , 0.55620267,
2.50890638],
-0.05892612, 0. , 0.66710883, 0. ,
0. , -1.9318798 , 1.23775092, 0. ,
-2.23280039],
0. , 0. , -2.43804744, 0. ,
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_
>>> ax = plt.subplot(111)
... solver='liblinear',
... weights.append(lr.coef_[1])
... params.append(10**c)
... color=color)
>>> plt.xscale('log')
>>> 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
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.
onde .
3. Remova o recurso, x , do conjunto de recursos: Xk–1 = Xk – x–; k = k – 1.
–
import numpy as np
class SBS:
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
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_]
self.scores_ = [score]
scores = []
subsets = []
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
return score
CopyExplain
>>> plt.ylabel('Accuracy')
>>> plt.grid()
>>> plt.tight_layout()
>>> plt.show()
CopyExplain
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
... random_state=1)
... feat_labels[indices[f]],
... importances[indices[f]]))
>>> plt.bar(range(X_train.shape[1]),
... importances[indices],
... align='center')
>>> plt.xticks(range(X_train.shape[1]),
>>> plt.tight_layout()
>>> plt.show()
1) Proline 0.185453
2) Flavanoids 0.174751
5) Alcohol 0.118529
6) Hue 0.058739
8) Magnesium 0.031357
... feat_labels[indices[f]],
... importances[indices[f]]))
1) Proline 0.185453
2) Flavanoids 0.174751
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.
https://packt.link/MLwPyTorch
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
transformação, :
xW = z
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.
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.
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
... 'https://archive.ics.uci.edu/ml/'
... 'machine-learning-databases/wine/wine.data',
... header=None
... )
CopyExplain
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
... stratify=y,
... random_state=0)
>>> sc = StandardScaler()
(note que é a letra maiúscula grega sigma, que não deve ser confundida com o
símbolo de soma):
Eigenvalues
Autodecomposição em NumPy
>>> plt.legend(loc='best')
>>> plt.tight_layout()
>>> plt.show()
CopyExplain
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:
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
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 é
x′ = xW
>>> X_train_std[0].dot(w)
array([ 2.38299011, 0.45458499])
CopyExplain
X′ = XW
>>> 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.
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
cmap = ListedColormap(colors[:len(np.unique(y))])
lab = lab.reshape(xx1.shape)
plt.xlim(xx1.min(), xx1.max())
plt.ylim(xx2.min(), xx2.max())
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
>>> lr = LogisticRegression(multi_class='ovr',
... random_state=1,
... solver='lbfgs')
>>> 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:
>>> 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
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
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.
Then, we plot the loadings for the first principal component, , which is the first
column in this matrix:loadings[:, 0]
>>> ax.set_xticks(range(13))
>>> plt.tight_layout()
>>> plt.show()
CopyExplain
To compare the scikit-learn PCA loadings with those we created previously, let us
create a similar bar plot:
>>> ax.set_xticks(range(13))
>>> plt.tight_layout()
>>> plt.show()
CopyExplain
Pescador LDA
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:
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.
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 = []
... mean_vecs.append(np.mean(
... f'{S_W.shape[0]}x{S_W.shape[1]}')
... np.bincount(y_train)[1:])
... f'{S_W.shape[0]}x{S_W.shape[1]}')
... f'{S_B.shape[0]}x{S_B.shape[1]}')
Between-class scatter matrix: 13x13
CopyExplain
... np.linalg.eig(np.linalg.inv(S_W).dot(S_B))
CopyExplain
... print(eigen_val[0])
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
Colinearidade
... reverse=True)]
>>> plt.legend(loc='best')
>>> plt.tight_layout()
>>> plt.show()
CopyExplain
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
X′ = XW
>>> X_train_lda = X_train_std.dot(w)
>>> 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
... solver='lbfgs')
>>> plt.tight_layout()
>>> plt.show()
CopyExplain
>>> 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:
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):
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:
>>> plt.show()
CopyExplain
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
... random_state=123)
... ax = plt.subplot(aspect='equal')
... txt.set_path_effects([
... PathEffects.Normal()])
>>> plt.show()
CopyExplain
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.
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.
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
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
... PCA(n_components=2),
... LogisticRegression())
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.
>>> scores = []
... scores.append(score)
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:
... X=X_train,
... y=y_train,
... cv=10,
... n_jobs=1)
0.95555556]
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.
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.
... LogisticRegression(penalty='l2',
... max_iter=10000))
... learning_curve(estimator=pipe_lr,
... X=X_train,
... y=y_train,
... train_sizes=np.linspace(
... cv=10,
... n_jobs=1)
>>> plt.fill_between(train_sizes,
>>> plt.fill_between(train_sizes,
>>> plt.grid()
>>> plt.ylabel('Accuracy')
>>> plt.show()
CopyExplain
... estimator=pipe_lr,
... X=X_train,
... y=y_train,
... param_name='logisticregression__C',
... param_range=param_range,
... cv=10)
... color='blue')
>>> plt.fill_between(param_range,
>>> plt.grid()
>>> plt.xscale('log')
>>> plt.ylabel('Accuracy')
>>> plt.show()
CopyExplain
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.
... SVC(random_state=1))
>>> gs = GridSearchCV(estimator=pipe_svc,
... param_grid=param_grid,
... scoring='accuracy',
... cv=10,
... refit=True,
... n_jobs=-1)
>>> print(gs.best_score_)
0.9846153846153847
>>> print(gs.best_params_)
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
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
>>> np.random.seed(1)
>>> param_range.rvs(10)
5.98924832e-02, 5.91176467e-01])
CopyExplain
Especificando distribuições
... SVC(random_state=1))
>>> rs = RandomizedSearchCV(estimator=pipe_svc,
... param_distributions=param_grid,
... scoring='accuracy',
... refit=True,
... n_iter=20,
... cv=10,
... random_state=1,
... n_jobs=-1)
>>> print(rs.best_score_)
0.9670531400966184
>>> print(rs.best_params_)
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
>>> hs = HalvingRandomSearchCV(pipe_svc,
... param_distributions=param_grid,
... n_candidates='exhaust',
... resource='n_samples',
... factor=1.5,
... random_state=1,
... n_jobs=-1)
CopyExplain
>>> print(hs.best_score_)
0.9617647058823529
>>> print(hs.best_params_)
... param_grid=param_grid,
... scoring='accuracy',
... cv=2)
>>> gs = GridSearchCV(
... estimator=DecisionTreeClassifier(random_state=0),
... scoring='accuracy',
... cv=2
... )
>>> print(confmat)
[[71 1]
[ 2 40]]
CopyExplain
>>> ax.xaxis.set_ticks_position('bottom')
>>> plt.show()
CopyExplain
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.
Precision: 0.976
Recall: 0.952
F1: 0.964
MCC: 0.943
CopyExplain
>>> gs = GridSearchCV(estimator=pipe_svc,
... param_grid=param_grid,
... scoring=scorer,
... cv=10)
>>> print(gs.best_score_)
0.986202145696
>>> print(gs.best_params_)
... StandardScaler(),
... PCA(n_components=2),
... )
>>> all_tpr = []
... X_train2[train],
... y_train[train]
... ).predict_proba(X_train2[test])
... pos_label=1)
... plt.plot(fpr,
... tpr,
... linestyle='--',
... linestyle=':',
... color='black',
>>> plt.show()
CopyExplain
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.
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
... greater_is_better=True,
... average='micro')
CopyExplain
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
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])
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.
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
... replace=True,
... random_state=123)
... X_upsampled.shape[0])
After resampling, we can then stack the original class samples with the upsampled
class subset to obtain a balanced dataset as follows:01
50
CopyExplain
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.
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.)
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:
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
):
O coeficiente binomial
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
... error**k *
... (1-error)**(n_classifier - k)
0.03432750701904297
CopyExplain
... linewidth=2)
... linewidth=2)
>>> plt.xlabel('Base error')
>>> plt.grid(alpha=0.5)
>>> plt.show()
CopyExplain
aleatória ( ).
Observe que o eixo y representa o erro base (linha pontilhada), bem como o erro
de conjunto (linha contínua):
Voto plural
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:
1
CopyExplain
Usando os mesmos pesos anteriores (0,2, 0,2 e 0,6), podemos então calcular as
probabilidades de classe individuais da seguinte maneira:
>>> p
array([0.58, 0.42])
>>> np.argmax(p)
0
CopyExplain
Juntando tudo, vamos agora implementar em Python: MajorityVoteClassifier
import numpy as np
import operator
self.classifiers = classifiers
self.named_classifiers = {
value in _name_estimators(classifiers)
self.vote = vote
self.weights = weights
f"or 'classlabel'"
if self.weights and
len(self.weights) != len(self.classifiers):
# call in self.predict
self.lablenc_ = LabelEncoder()
self.lablenc_.fit(y)
self.classes_ = self.lablenc_.classes_
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
if self.vote == 'probability':
predictions = np.asarray([
]).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
probas = np.asarray([clf.predict_proba(X)
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()
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
>>> le = LabelEncoder()
>>> y = le.fit_transform(y)
CopyExplain
... train_test_split(X, y,
... test_size=0.5,
... random_state=1,
... stratify=y)
CopyExplain
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:
... C=0.001,
... solver='lbfgs',
... random_state=1)
... random_state=0)
... p=2,
... metric='minkowski')
... X=X_train,
... y=y_train,
... cv=10,
... scoring='roc_auc')
... )
... X=X_train,
... y=y_train,
... cv=10,
... scoring='roc_auc')
... y_train).predict_proba(X_test)[:, 1]
... y_score=y_pred)
... color=clr,
... linestyle=ls,
... color='gray',
... linewidth=2)
>>> plt.grid(alpha=0.5)
>>> 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
>>> sc = StandardScaler()
>>>
... sharex='col',
... sharey='row',
... c='blue',
... marker='^',
... s=50)
... c='green',
... marker='o',
... s=50)
>>> plt.show()
CopyExplain
>>> 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':
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':
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':
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,
'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
... param_grid=params,
... cv=10,
... scoring='roc_auc')
'pipeline-1__clf__C': 0.001}
'pipeline-1__clf__C': 0.1}
'pipeline-1__clf__C': 100.0}
'pipeline-1__clf__C': 0.1}
'pipeline-1__clf__C': 100.0}
'pipeline-1__clf__C': 0.001}
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
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.
... 'machine-learning-databases/'
... 'wine/wine.data',
... header=None)
... 'Proanthocyanins',
... 'Proline']
>>> X = df_wine[['Alcohol',
>>> le = LabelEncoder()
>>> y = le.fit_transform(y)
... train_test_split(X, y,
... test_size=0.2,
... random_state=1,
... stratify=y)
CopyExplain
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
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
... random_state=1,
... max_depth=None)
... n_estimators=500,
... max_samples=1.0,
... max_features=1.0,
... bootstrap=True,
... bootstrap_features=False,
... n_jobs=1,
... random_state=1)
CopyExplain
... f'{tree_train:.3f}/{tree_test:.3f}')
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:
... f'{bag_train:.3f}/{bag_test:.3f}')
... sharex='col',
... sharey='row',
...
... Z = Z.reshape(xx.shape)
... axarr[idx].set_title(tt)
>>> plt.tight_layout()
... 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.
Reconhecimento AdaBoost
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).
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.
ponderada: .
d. Calcule o coeficiente: .
e. Atualize os
pesos:
.
f. Normalize os pesos para somar 1: .
3. Calcule a previsão
final:
>>> yhat = np.array([1, 1, 1, -1, -1, -1, -1, -1, -1, -1])
>>> print(weights)
[0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1]
>>> 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
>>> print(alpha_j)
0.42364893019360184
CopyExplain
>>> print(update_if_correct)
0.06546536707079771
CopyExplain
Da mesma forma, aumentaremos o i-ésimo peso se o rótulo for previsto
incorretamente, assim:
>>> print(update_if_wrong_1)
0.1527525231651947
CopyExplain
Alternativamente, é assim:
>>> print(update_if_wrong_2)
0.1527525231651947
CopyExplain
... update_if_correct,
... update_if_wrong_1)
>>> print(weights)
>>> print(normalized_weights)
... random_state=1,
... max_depth=1)
... n_estimators=500,
... learning_rate=0.1,
... random_state=1)
... f'{tree_train:.3f}/{tree_test:.3f}')
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:
... f'{ada_train:.3f}/{ada_test:.3f}')
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.
... sharex='col',
... sharey='row',
... Z = Z.reshape(xx.shape)
... axarr[idx].contourf(xx, yy, Z, alpha=0.3)
... c='blue',
... marker='^')
... c='green',
... marker='o')
... axarr[idx].set_title(tt)
>>> plt.tight_layout()
... s='Alcohol',
... ha='center',
... va='center',
... fontsize=12,
... transform=axarr[1].transAxes)
>>> plt.show()
CopyExplain
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.
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.
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.
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
taxa de aprendizado:
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.)
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.
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
Instalando o XGBoost
Para este capítulo, usamos o XGBoost versão 1.5.0, que pode ser instalado
através de:
... use_label_encoder=False)
... f'{gbm_train:.3f}/{gbm_test:.3f}')
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
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.
https://packt.link/MLwPyTorch
... 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
>>> import os
>>>
>>> df = pd.DataFrame()
... ignore_index=True)
... pbar.update()
0% 100%
>>> np.random.seed(0)
>>> df = df.reindex(np.random.permutation(df.index))
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.head(3)
CopyExplain
>>> 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:
>>> 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
... norm='l2',
... smooth_idf=True)
>>> np.set_printoptions(precision=2)
>>> print(tfidf.fit_transform(count.fit_transform(docs))
... .toarray())
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
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:
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.
>>> import re
... text)
Expressões regulares
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.
Algoritmos de derivação
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'
>>> nltk.download('stopwords')
CopyExplain
... lowercase=False,
... preprocessor=None)
>>> small_param_grid = [
... {
... },
... {
... 'vect__use_idf':[False],
... 'vect__norm':[None],
... },
... ]
... ])
>>> gs_lr_tfidf = GridSearchCV(lr_tfidf, small_param_grid,
Best parameter set: {'clf__C': 10.0, 'clf__penalty': 'l2', 'vect__ngram_range': (1, 1),
CV Accuracy: 0.897
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 re
... text)
>>> next(stream_docs(path='movie_data.csv'))
... try:
... docs.append(text)
... y.append(label)
... n_features=2**21,
... preprocessor=None,
... tokenizer=tokenizer)
... break
... pbar.update()
0% 100%
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
O modelo word2vec
Dada uma matriz de saco de palavras como entrada, a LDA a decompõe em duas
novas matrizes:
... max_df=.1,
... max_features=5000)
>>> X = count.fit_transform(df['review'].values)
CopyExplain
... 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
>>> lda.components_.shape
(10, 5000)
CopyExplain
>>> n_top_words = 5
>>> feature_names = count.get_feature_names_out()
Topic 1:
Topic 2:
Topic 3:
Topic 4:
Topic 5:
Topic 6:
Topic 7:
Topic 8:
Topic 9:
Topic 10:
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
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 ...
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
...
<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
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.
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).
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
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.
import pandas as pd
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
>>> df.shape
(2930, 6)
CopyExplain
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'
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
Você pode instalar o pacote via ou . Para este capítulo, utilizamos mlxtend versão
0.19.0.mlxtendconda install mlxtendpip install mlxtend
>>> 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
>>> cm = np.corrcoef(df.values.T)
>>> 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:
class LinearRegressionGD:
self.eta = eta
self.n_iter = n_iter
self.random_state = random_state
rgen = np.random.RandomState(self.random_state)
self.b_ = np.array([0.])
self.losses_ = []
for i in range(self.n_iter):
output = self.net_input(X)
errors = (y - output)
loss = (errors**2).mean()
self.losses_.append(loss)
return self
def net_input(self, X):
return self.net_input(X)
CopyExplain
>>> y = df['SalePrice'].values
>>> lr = LinearRegressionGD(eta=0.1)
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.ylabel('MSE')
>>> plt.xlabel('Epoch')
>>> plt.show()
CopyExplain
Agora, vamos usar essa função para plotar a área de estar contra o preço de
venda:lin_regplot
>>> 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.
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:
Slope: 0.707
Intercept: -0.000
CopyExplain
>>> slr.fit(X, y)
Slope: 111.666
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
>>> 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
>>> w = np.zeros(X.shape[1])
Slope: 111.666
Intercept: 13342.979
CopyExplain
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
... LinearRegression(),
... min_samples=0.95,
>>> ransac.fit(X, y)
CopyExplain
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
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:
>>> 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:
Intercept: 20190.093
CopyExplain
>>> median_absolute_deviation(y)
37000.00
CopyExplain
>>> X = df[features].values
>>> y = df[target].values
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:
>>> ax1.scatter(
... edgecolor='white',
>>> ax2.scatter(
>>> ax1.set_ylabel('Residuals')
>>> 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:
Com base no conjunto de testes MAE, podemos dizer que o modelo comete um
erro de aproximadamente US$ 25.000 em média.
Aqui, SSE é a soma dos erros quadrados, que é semelhante ao MSE, mas não
inclui a normalização pelo tamanho da amostra n:
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:
8. >>> lr = LinearRegression()
9. >>> pr = LinearRegression()
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:
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.
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:
>>> y = df['SalePrice'].values
... color='blue',
... lw=2,
... linestyle=':')
... color='red',
... lw=2,
... linestyle='-')
... color='green',
... lw=2,
... linestyle='--')
>>> plt.show()
CopyExplain
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
>>> 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
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
>>> y = df['SalePrice'].values
>>> tree.fit(X, y)
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:
>>> X = df[features].values
>>> y = df[target].values
... criterion='squared_error',
... random_state=1,
... n_jobs=-1)
>>> ax1.set_ylabel('Residuals')
>>> 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
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 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
>>> X, y = make_blobs(n_samples=150,
... n_features=2,
... centers=3,
... cluster_std=0.5,
... shuffle=True,
... random_state=0)
... c='white',
... marker='o',
... edgecolor='black',
... s=50)
>>> plt.grid()
>>> plt.tight_layout()
>>> plt.show()
CopyExplain
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:
>>> km = KMeans(n_clusters=3,
... init='random',
... n_init=10,
... max_iter=300,
... tol=1e-04,
... random_state=0)
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.
... label='Centroids')
>>> 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).
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.
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
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:
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:
):
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.
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 = []
... 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.ylabel('Distortion')
>>> plt.tight_layout()
>>> plt.show()
CopyExplain
>>> km = KMeans(n_clusters=3,
... init='k-means++',
... n_init=10,
... max_iter=300,
... tol=1e-04,
... random_state=0)
... )
>>> yticks = []
... c_silhouette_vals.sort()
... c_silhouette_vals,
... height=1.0,
... edgecolor='none',
... color=color)
... color="red",
... linestyle="--")
>>> plt.ylabel('Cluster')
>>> plt.tight_layout()
>>> plt.show()
CopyExplain
>>> km = KMeans(n_clusters=2,
... init='k-means++',
... n_init=10,
... max_iter=300,
... tol=1e-04,
... random_state=0)
... edgecolor='black',
... marker='s',
... s=50,
... c='orange',
... edgecolor='black',
... marker='o',
... s=250,
... marker='*',
... c='red',
... label='Centroids')
>>> 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.
... )
>>> yticks = []
... c_silhouette_vals.sort()
... y_ax_upper += len(c_silhouette_vals)
... c_silhouette_vals,
... height=1.0,
... edgecolor='none',
... color=color)
>>> plt.ylabel('Cluster')
>>> 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
>>> np.random.seed(123)
>>> 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
>>> help(linkage)
[...]
Parameters:
y : ndarray
an m by n array.
Returns:
Z : ndarray
[...]
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
... 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,
... 'distance',
... 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.
>>> # set_link_color_palette(['black'])
... row_clusters,
... labels=labels,
... # color_threshold=np.inf
... )
>>> plt.tight_layout()
>>> plt.ylabel('Euclidean distance')
>>> plt.show()
CopyExplain
5. ... orientation='left')
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])
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.
>>> ac = AgglomerativeClustering(n_clusters=3,
... affinity='euclidean',
... linkage='complete')
Cluster labels: [1 0 0 2 1]
CopyExplain
>>> ac = AgglomerativeClustering(n_clusters=2,
... affinity='euclidean',
... linkage='complete')
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
especificado, .
especificado,
Um ponto de fronteira é um ponto que tem menos vizinhos do que MinPts
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:
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:
>>> X, y = make_moons(n_samples=200,
... noise=0.05,
... random_state=0)
>>> 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
>>> km = KMeans(n_clusters=2,
... random_state=0)
... c='lightblue',
... edgecolor='black',
... marker='o',
... s=40,
... c='red',
... edgecolor='black',
... marker='s',
... s=40,
>>> ac = AgglomerativeClustering(n_clusters=2,
... affinity='euclidean',
... linkage='complete')
... c='lightblue',
... edgecolor='black',
... marker='o',
... s=40,
... c='red',
... edgecolor='black',
... marker='s',
... s=40,
>>> plt.legend()
>>> plt.tight_layout()
>>> plt.show()
CopyExplain
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:
>>> db = DBSCAN(eps=0.2,
... min_samples=5,
... metric='euclidean')
... c='lightblue',
... edgecolor='black',
... marker='o',
... s=40,
... edgecolor='black',
... marker='s',
... s=40,
>>> plt.legend()
>>> plt.tight_layout()
>>> plt.show()
CopyExplain
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.
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, 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.
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:
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.)
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
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
For example, we can encode the three class labels in the familiar Iris dataset
(0=Setosa, 1=Versicolor, 2=Virginica) as follows:
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
um(h) (onde ).
Z(h) = X(em)W(h)T + b(h)
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):
Por fim, aplicamos a função de ativação sigmoide para obter a saída de valor
contínuo de nossa rede:
https://sebastianraschka.com/blog/2021/dl-course.html#l09-multilayer-
perceptrons-and-backpropration
... return_X_y=True)
>>> X = X.values
>>> y = y.astype(int).values
CopyExplain
>>> print(X.shape)
(70000, 784)
>>> print(y.shape)
(70000,)
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.
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
>>> ax = ax.flatten()
>>> 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:
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 é:
... ncols=5,
... sharex=True,
... sharey=True)
>>> ax = ax.flatten()
>>> ax[0].set_xticks([])
>>> ax[0].set_yticks([])
>>> plt.tight_layout()
>>> plt.show()
CopyExplain
... )
... )
CopyExplain
import numpy as np
def sigmoid(z):
ary[i, val] = 1
return ary
CopyExplain
class NeuralNetMLP:
num_classes, random_seed=123):
super().__init__()
self.num_classes = num_classes
# hidden
rng = np.random.RandomState(random_seed)
self.weight_h = rng.normal(
# output
self.weight_out = rng.normal(
self.bias_out = np.zeros(num_classes)
CopyExplain
# Hidden layer
a_h = sigmoid(z_h)
# Output layer
a_out = sigmoid(z_out)
Por fim, vamos falar sobre o método, que atualiza os parâmetros de peso e viés
da rede neural:backward
#########################
#########################
# one-hot encoding
# Part 1: dLoss/dOutWeights
d_z_out__dw_out = a_h
#################################
# Part 2: dLoss/dHiddenWeights
# * dHiddenNet/dWeight
# [n_classes, n_hidden]
d_z_out__a_h = self.weight_out
# [n_examples, n_hidden]
# [n_examples, n_features]
d_z_h__d_w_h = x
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)
d_loss__d_w_h, d_loss__d_b_h)
CopyExplain
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()
... 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()
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.
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:
>>> num_epochs = 50
... np.random.shuffle(indices)
... + 1, minibatch_size):
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
... break
... break
>>> print(X_train_mini.shape)
(100, 784)
>>> print(y_train_mini.shape)
(100,)
CopyExplain
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:
... )
Let’s test the preceding function and compute the initial validation set MSE and
accuracy of the model we instantiated in the previous section:
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
... )
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:
Let’s now get to the main part and implement the code to train our model:
... learning_rate=0.1):
... epoch_loss = []
... epoch_train_acc = []
... epoch_valid_acc = []
...
... y_train_mini)
...
...
... )
... )
... epoch_train_acc.append(train_acc)
... epoch_valid_acc.append(valid_acc)
... epoch_loss.append(train_mse)
...
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()
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:
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.
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.xlabel('Epoch')
>>> plt.show()
CopyExplain
The preceding code plots the loss over the 50 epochs, as shown in Figure 11.7:
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.
... label='Training')
... label='Validation')
>>> plt.ylabel('Accuracy')
>>> plt.xlabel('Epochs')
>>> plt.show()
CopyExplain
The preceding code examples plot those accuracy values over the 50 training
epochs, as shown in Figure 11.8:
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
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.
Other common tricks that are beyond the scope of the following chapters include:
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:
>>> misclassified_images = \
>>> ax = ax.flatten()
>>> 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.
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:
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.
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:
Em seguida, usamos esse valor para atualizar o peso por meio da atualização de
# Part 1: dLoss/dOutWeights
# placeholder"
# [n_examples, n_hidden]
d_z_out__dw_out = a_h
reuse .
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:
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:
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.
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
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.
Aqui, a entrada líquida, z, é uma combinação linear dos pesos que estão
conectando a camada de entrada à camada de saída:
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
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
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:
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
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:
um(h) (onde ).
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):
Z(fora) = Um(h)W(fora)T + b(fora)
Por fim, aplicamos a função de ativação sigmoide para obter a saída de valor
contínuo de nossa rede:
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
... return_X_y=True)
>>> X = X.values
>>> y = y.astype(int).values
CopyExplain
>>> print(X.shape)
(70000, 784)
>>> print(y.shape)
(70000,)
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.
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
>>> ax = ax.flatten()
>>> 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 é:
... ncols=5,
... sharex=True,
... sharey=True)
>>> ax = ax.flatten()
>>> ax[0].set_xticks([])
>>> ax[0].set_yticks([])
>>> plt.tight_layout()
>>> plt.show()
CopyExplain
... )
... )
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.
import numpy as np
def sigmoid(z):
ary[i, val] = 1
return ary
CopyExplain
class NeuralNetMLP:
num_classes, random_seed=123):
super().__init__()
self.num_classes = num_classes
# hidden
rng = np.random.RandomState(random_seed)
self.weight_h = rng.normal(
self.bias_h = np.zeros(num_hidden)
# output
self.weight_out = rng.normal(
self.bias_out = np.zeros(num_classes)
CopyExplain
# Hidden layer
a_h = sigmoid(z_h)
# Output layer
a_out = sigmoid(z_out)
Por fim, vamos falar sobre o método, que atualiza os parâmetros de peso e viés
da rede neural:backward
#########################
### Output layer weights
#########################
# one-hot encoding
# Part 1: dLoss/dOutWeights
# [n_examples, n_hidden]
d_z_out__dw_out = a_h
#################################
# Part 2: dLoss/dHiddenWeights
# * dHiddenNet/dWeight
# [n_classes, n_hidden]
d_z_out__a_h = self.weight_out
# [n_examples, n_hidden]
# [n_examples, n_features]
d_z_h__d_w_h = x
d_z_h__d_w_h)
d_loss__d_w_h, d_loss__d_b_h)
CopyExplain
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()
... 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()
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.
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:
... np.random.shuffle(indices)
... + 1, minibatch_size):
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
... 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:
... )
Let’s test the preceding function and compute the initial validation set MSE and
accuracy of the model we instantiated in the previous section:
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
... minibatch_size=100):
... )
... loss = np.mean((onehot_targets - probas)**2)
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:
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:
... learning_rate=0.1):
... epoch_loss = []
... epoch_train_acc = []
... epoch_valid_acc = []
...
... y_train_mini)
...
...
... )
... epoch_train_acc.append(train_acc)
... epoch_valid_acc.append(valid_acc)
... epoch_loss.append(train_mse)
...
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()
Let’s now execute this function to train our model for 50 epochs, which may take a
few minutes to finish:
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.
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.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.
... label='Training')
... label='Validation')
>>> plt.ylabel('Accuracy')
>>> plt.xlabel('Epochs')
>>> plt.show()
CopyExplain
The preceding code examples plot those accuracy values over the 50 training
epochs, as shown in Figure 11.8:
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
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.
Other common tricks that are beyond the scope of the following chapters include:
>>> misclassified_images = \
>>> ax = ax.flatten()
>>> 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.
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
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.
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.
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:
Vamos começar com , que é a derivada parcial da perda de MSE (que simplifica
Em seguida, usamos esse valor para atualizar o peso por meio da atualização de
# Part 1: dLoss/dOutWeights
# placeholder"
# [n_examples, n_hidden]
d_z_out__dw_out = a_h
reuse .
Figure 11.13: Computing the partial derivatives of the loss with respect to the first
hidden layer weight
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
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.
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
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.
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.
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
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
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:
https://download.pytorch.org/whl/torch_stable.html
CopyExplain
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.
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:
>>> np.set_printoptions(precision=3)
>>> a = [1, 2, 3]
>>> print(t_a)
>>> print(t_b)
tensor([1, 2, 3])
>>> t_ones.shape
torch.Size([2, 3])
>>> print(t_ones)
>>> print(rand_tensor)
A função pode ser usada para alterar o tipo de dados de um tensor para um tipo
desejado:torch.to()
>>> print(t_a_new.dtype)
torch.int64
CopyExplain
Consulte https://pytorch.org/docs/stable/tensor_attributes.html para todos os
outros tipos de dados.
Transpondo um tensor:
>>> t = torch.rand(3, 5)
>>> 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)
>>> torch.manual_seed(1)
>>> t1 = 2 * torch.rand(5, 2) - 1
Observe isso e tenha o mesmo formato. Agora, para calcular o produto elementar
de e , podemos usar o seguinte:t1t2t1t2
>>> print(t3)
[ 0.0660, -0.5970],
[ 1.1249, 0.0150],
[ 0.1569, 0.7107],
[-0.0451, -0.0352]])
CopyExplain
>>> 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()
>>> print(t5)
>>> print(t6)
[-1.6038, -0.2180]])
CopyExplain
>>> print(norm_t1)
>>> t = torch.rand(6)
>>> print(t)
>>> torch.manual_seed(1)
>>> t = torch.rand(5)
>>> print(t)
>>> A = torch.ones(3)
>>> B = torch.zeros(2)
>>> print(C)
>>> A = torch.ones(3)
>>> B = torch.zeros(3)
>>> 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.
... print(item)
tensor([0.])
tensor([1.])
tensor([2.])
tensor([3.])
tensor([4.])
tensor([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
>>> torch.manual_seed(1)
... self.x = x
... self.y = y
...
...
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.
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.
>>> torch.manual_seed(1)
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:
y: tensor([3, 2])
y: tensor([0, 1])
CopyExplain
epoch 1
y: tensor([1, 2])
y: tensor([3, 0])
epoch 2
y: tensor([2, 0])
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
... imgdir_path.glob('*.jpg')])
>>> print(file_list)
>>> import os
... ax.imshow(img)
>>> plt.tight_layout()
>>> plt.show()
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
>>> 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):
...
...
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
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
... transforms.ToTensor(),
... ])
CopyExplain
...
...
>>>
...
>>> plt.tight_layout()
>>> plt.show()
CopyExplain
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
First, if you haven’t already installed together with PyTorch earlier, you need to
install the library via from the command line:torchvisiontorchvisionpip
In the following paragraphs, we will cover fetching two different datasets: CelebA ()
and the MNIST digit dataset.celeb_a
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
... )
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
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'
>>> print(example)
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'
... ax.imshow(image)
>>> plt.show()
CopyExplain
The examples and their labels that are retrieved from are shown in Figure
12.5:celeba_dataset
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
Now, we can download the partition, convert the elements to tuples, and visualize
10 examples:'train'
>>> print(example)
>>> plt.show()
CopyExplain
The retrieved example handwritten digits from this dataset are shown as follows:
Figure 12.6: Correctly identifying handwritten digits
Finalmente, como você verá nas subseções a seguir, um modelo treinado pode
ser salvo e recarregado para uso futuro.
>>> 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:
>>> batch_size = 1
>>> torch.manual_seed(1)
>>> weight.requires_grad_()
>>> log_epochs = 10
... loss.backward()
... weight.grad.zero_()
... bias.grad.zero_()
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:
>>> ax = fig.add_subplot(1, 2, 1)
>>> plt.show()
CopyExplain
>>> input_size = 1
>>> output_size = 1
Note que aqui usamos a classe para a camada linear em vez de defini-la
manualmente.torch.nn.Linear
... loss.backward()
... optimizer.step()
... optimizer.zero_grad()
>>> X = iris['data']
>>> y = iris['target']
>>> torch.manual_seed(1)
>>> batch_size = 2
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
... super().__init__()
... x = self.layer1(x)
... x = nn.Sigmoid()(x)
... x = self.layer2(x)
... return x
>>> hidden_size = 16
>>> output_size = 3
... loss.backward()
... optimizer.step()
... optimizer.zero_grad()
... loss_hist[epoch] += loss.item()*y_batch.size(0)
>>> ax = fig.add_subplot(1, 2, 1)
>>> ax = fig.add_subplot(1, 2, 2)
>>> plt.show()
CopyExplain
>>> model_new.eval()
Model(
)
CopyExplain
>>> model_new.load_state_dict(torch.load(path))
CopyExplain
... z = net_input(X, w)
P(y=1|x) = 0.888
CopyExplain
>>> # note that the first column are the bias units
Net Input:
Output Units:
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:
Probabilities:
>>> 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
... label='tanh')
... linewidth=3,
... label='logistic')
>>> plt.tight_layout()
>>> plt.show()
CopyExplain
>>> np.tanh(z)
0.99990737, 0.99990829])
>>> torch.tanh(torch.from_numpy(z))
dtype=torch.float64)
CopyExplain
0.99327383])
CopyExplain
>>> torch.sigmoid(torch.from_numpy(z))
dtype=torch.float64)
CopyExplain
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,
e erros numéricos).
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))
dtype=torch.float64)
CopyExplain
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
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.
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.
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.
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.
... r1 = torch.sub(a, b)
... r2 = torch.mul(r1, 2)
... z = torch.add(r2, c)
... return z
CopyExplain
>>> print(a)
tensor(3.1400, requires_grad=True)
>>> print(b)
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.
>>> 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_()
>>> torch.manual_seed(1)
>>> w = torch.empty(2, 3)
>>> nn.init.xavier_normal_(w)
>>> print(w)
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
... super().__init__()
... nn.init.xavier_normal_(self.w1)
... 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.
Uma derivada parcial pode ser entendida como a taxa de mudança de uma função
>>> x = torch.tensor([1.4])
>>> y = torch.tensor([2.1])
>>> loss.backward()
dL/dw : tensor(-0.5600)
dL/db : tensor(-0.4000)
CopyExplain
tensor([-0.5600], grad_fn=<MulBackward0>)
CopyExplain
Deixamos a verificação de b como um exercício para o leitor.
u0 = x
u1 = h(x)
u2 = g(u1)
u3 = f(u2) = y
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
... nn.ReLU(),
... nn.ReLU()
... )
>>> model
Sequential(
(1): ReLU()
(3): ReLU()
)
CopyExplain
>>> nn.init.xavier_uniform_(model[0].weight)
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
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:
>>> torch.manual_seed(1)
>>> np.random.seed(1)
>>> y = np.ones(len(x))
>>> plt.show()
CopyExplain
... nn.Sigmoid()
... )
>>> model
Sequential(
(1): Sigmoid()
)
CopyExplain
>>> batch_size = 2
>>> torch.manual_seed(1)
Agora vamos treinar o modelo para 200 épocas e registrar uma história de épocas
de treinamento:
>>> torch.manual_seed(1)
>>> num_epochs = 200
... loss.backward()
... optimizer.step()
... optimizer.zero_grad()
>>> ax = fig.add_subplot(1, 2, 1)
>>> ax = fig.add_subplot(1, 2, 2)
This results in the following figure, with two separate panels for the losses and
accuracies:
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:
... nn.ReLU(),
... nn.ReLU(),
... nn.Sigmoid()
... )
>>> model
Sequential(
(1): ReLU()
(3): ReLU()
(5): Sigmoid()
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.
... 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()
...
... 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(
(module_list): ModuleList(
(1): ReLU()
(3): ReLU()
(5): Sigmoid()
Next, besides the train history, we will use the mlxtend library to visualize the
validation data and the decision boundary.
The following code will plot the training performance along with the decision region
bias:
>>> ax = fig.add_subplot(1, 3, 1)
>>> ax = fig.add_subplot(1, 3, 2)
>>> ax = fig.add_subplot(1, 3, 3)
>>> plot_decision_regions(X=x_valid.numpy(),
... y=y_valid.numpy().astype(np.integer),
... clf=model)
>>> 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:
... noise_stddev=0.1):
... super().__init__()
... nn.init.xavier_uniform_(self.w)
... b = torch.Tensor(output_size).fill_(0)
...
... if training:
... else:
... x_new = x
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)
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):
21. ...
30. ...
35. ...
39. MyNoisyModule(
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()
91. ... )
... 'machine-learning-databases/auto-mpg/auto-mpg.data'
>>>
>>> df = df.dropna()
>>> df = df.reset_index(drop=True)
>>>
... )
>>>
>>> numeric_column_names = [
... 'Acceleration'
... ]
>>> df_train_norm, df_test_norm = df_train.copy(), df_test.copy()
>>> df_train_norm.tail()
CopyExplain
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
... )
... )
... df_train_norm[numeric_column_names].values)
... df_test_norm[numeric_column_names].values)
>>> batch_size = 8
>>> torch.manual_seed(1)
>>> all_layers = []
... all_layers.append(layer)
... all_layers.append(nn.ReLU())
... input_size = hidden_unit
>>> model
Sequential(
(1): ReLU()
(3): ReLU()
)
CopyExplain
Agora vamos treinar o modelo para 200 épocas e exibir a perda de trem para cada
20 épocas:
>>> torch.manual_seed(1)
>>> log_epochs = 20
... loss_hist_train = 0
... loss.backward()
... optimizer.step()
... optimizer.zero_grad()
... f'{loss_hist_train/len(train_dl):.4f}')
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:
6. ... transforms.ToTensor()
7. ... ])
11. ... )
15. ... )
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]
34. Sequential(
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.
63. ...
65. ...
>>> is_correct = (
... mnist_test_dataset.targets
... ).float()
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:
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
import pytorch_lightning as pl
import torch
import torch.nn as nn
class MultiLayerPerceptron(pl.LightningModule):
super().__init__()
# new PL attributes:
self.train_acc = Accuracy()
self.valid_acc = Accuracy()
self.test_acc = Accuracy()
all_layers = [nn.Flatten()]
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)
return x
x, y = batch
logits = self(x)
loss = nn.functional.cross_entropy(self(x), y)
self.train_acc.update(preds, y)
return loss
self.log("train_acc", self.train_acc.compute())
x, y = batch
logits = self(x)
loss = nn.functional.cross_entropy(self(x), y)
self.valid_acc.update(preds, y)
return loss
x, y = batch
logits = self(x)
loss = nn.functional.cross_entropy(self(x), y)
self.test_acc.update(preds, y)
return loss
def configure_optimizers(self):
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
class MnistDataModule(pl.LightningDataModule):
super().__init__()
self.data_path = data_path
self.transform = transforms.Compose([transforms.ToTensor()])
def prepare_data(self):
MNIST(root=self.data_path, download=True)
mnist_all = MNIST(
root=self.data_path,
train=True,
transform=self.transform,
download=False
)
self.test = MNIST(
root=self.data_path,
train=False,
transform=self.transform,
download=False
def train_dataloader(self):
def val_dataloader(self):
def test_dataloader(self):
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
mnistclassifier = MultiLayerPerceptron()
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:
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.
TensorBoard can be installed via pip or conda, depending on your preference. For
instance, the command for installing TensorBoard via pip is as follows:
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
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
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
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
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
trainer.test(model=mnistclassifier, datamodule=mnist_dm)
CopyExplain
model = MultiLayerPerceptron.load_from_checkpoint("path/to/checkpoint.ckpt")
CopyExplain
Para saber mais sobre o Lightning, visite o site oficial, que contém tutoriais e
exemplos, em https://pytorch-lightning.readthedocs.io.
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.
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.
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.
Notação matemática
Neste capítulo, usaremos subscrito para denotar o tamanho de uma matriz
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:
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]
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
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.
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
The floor operation returns the largest integer that is equal to or smaller than the
input, for example:
(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
... if p > 0:
... ])
... res = []
>>> ## Testing:
>>> x = [1, 3, 2, 4, 5, 6, 1, 3]
>>> w = [1, 0, 3, 1, 2]
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:
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
... X_padded[p[0]:p[0]+X_orig.shape[0],
...
... res = []
... int((X_padded.shape[0] - \
... W_rot.shape[0])/s[0])+1, s[0]):
... res.append([])
... int((X_padded.shape[1] - \
... j:j+W_rot.shape[1]]
... return(np.array(res))
Conv2d Implementation:
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.
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
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.
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.
Number of channels: 3
tensor([[[179, 182],
[180, 182]],
[[134, 136],
[135, 137]],
[[110, 112],
(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.
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.
... out_channels=5,
... kernel_size=5)
... )
>>> loss_with_penalty = loss + l2_penalty
... )
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).
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.)
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:
... transforms.ToTensor()
... ])
... )
... torch.arange(10000))
... torch.arange(
... ))
... )
CopyExplain
>>> batch_size = 64
>>> torch.manual_seed(1)
... batch_size,
... shuffle=True)
... 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.
>>> model.add_module(
... 'conv1',
... nn.Conv2d(
... )
... )
>>> model.add_module(
... 'conv2',
... nn.Conv2d(
... )
... )
>>> model(x).shape
>>> model(x).shape
torch.Size([4, 3136])
CopyExplain
... model.train()
... loss.backward()
... optimizer.step()
... optimizer.zero_grad()
... is_correct = (
... ).float()
... accuracy_hist_train[epoch] += is_correct.sum()
...
... model.eval()
... loss_hist_valid[epoch] += \
... loss.item()*y_batch.size(0)
... is_correct = (
... ).float()
...
... f'{accuracy_hist_valid[epoch]:.4f}')
>>> num_epochs = 20
...
...
Once the 20 epochs of training are finished, we can visualize the learning curves:
>>> ax = fig.add_subplot(1, 2, 1)
>>> ax.legend(fontsize=15)
>>> ax = fig.add_subplot(1, 2, 2)
>>> ax.legend(fontsize=15)
>>> plt.show()
CopyExplain
Figure 14.13: Loss and accuracy graphs for the training and validation data
>>> is_correct = (
... ).float()
... horizontalalignment='center',
... verticalalignment='center',
... transform=ax.transAxes)
>>> plt.show()
CopyExplain
Figure 14.14 shows the handwritten inputs and their predicted labels:
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.
... )
... )
... )
>>>
Validation: 19867
>>> ax = fig.add_subplot(2, 5, 1)
>>> ax.imshow(img)
>>> ax = fig.add_subplot(2, 5, 6)
>>> ax.imshow(img_cropped)
>>>
>>> ax = fig.add_subplot(2, 5, 2)
>>> ax.imshow(img)
>>> ax = fig.add_subplot(2, 5, 7)
>>> ax.imshow(img_flipped)
>>>
>>> ax = fig.add_subplot(2, 5, 3)
>>> ax.imshow(img)
>>> ax = fig.add_subplot(2, 5, 8)
... )
>>> ax.imshow(img_adj_contrast)
>>>
>>> ax = fig.add_subplot(2, 5, 4)
>>> ax.imshow(img)
>>> ax = fig.add_subplot(2, 5, 9)
... )
>>> ax.imshow(img_adj_brightness)
>>>
>>> ## Column 5: cropping from image center
>>> ax = fig.add_subplot(2, 5, 5)
>>> ax.imshow(img)
... )
... )
>>> ax.imshow(img_resized)
>>> plt.show()
CopyExplain
A figura 14.15 mostra os resultados:
>>> torch.manual_seed(1)
... ax.imshow(img)
... if i == 0:
...
... ])
... ax.imshow(img_cropped)
... if i == 0:
...
... transforms.RandomHorizontalFlip()
... ])
... ax.imshow(img_flip)
... if i == 0:
...
... )
... ax.imshow(img_resized)
... if i == 0:
... if i == 2:
... break
>>> plt.show()
CopyExplain
A figura 14.16 mostra transformações aleatórias em três imagens de exemplo:
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.RandomHorizontalFlip(),
... 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):
... 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
... )
>>> torch.manual_seed(1)
>>> data_loader = DataLoader(celeba_train_dataset, batch_size=2)
>>> num_epochs = 5
... ax = fig.add_subplot(2, 5, j + 1)
... ax.set_xticks([])
... ax.set_yticks([])
...
... ax = fig.add_subplot(2, 5, j + 6)
... ax.set_xticks([])
... ax.set_yticks([])
>>> 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
... )
... )
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:
... torch.arange(1000))
>>> batch_size = 32
>>> torch.manual_seed(1)
Now that the data loaders are ready, we will develop a CNN model, and train and
evaluate it in the next section.
>>> model.add_module(
... 'conv1',
... nn.Conv2d(
... )
... )
>>>
>>> model.add_module(
... 'conv2',
... nn.Conv2d(
... )
... )
>>>
>>> model.add_module(
... 'conv3',
... nn.Conv2d(
... )
... )
>>>
>>> model.add_module(
... 'conv4',
... nn.Conv2d(
... )
... )
Let’s see the shape of the output feature maps after applying these layers using a
toy batch input (four images arbitrarily):
>>> model(x).shape
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(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(x).shape
torch.Size([4, 1])
>>> model
Sequential(
(relu1): ReLU()
(relu2): ReLU()
(relu3): ReLU()
(relu4): ReLU()
(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()
... model.train()
... loss.backward()
... optimizer.step()
... optimizer.zero_grad()
...
... model.eval()
... loss_hist_valid[epoch] += \
... is_correct = \
...
... f'{accuracy_hist_valid[epoch]:.4f}')
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
...
Epoch 15 accuracy: 0.8544 val_accuracy: 0.8700
...
Let’s now visualize the learning curve and compare the training and validation loss
and accuracies after each epoch:
>>> ax = fig.add_subplot(1, 2, 1)
>>> ax.legend(fontsize=15)
>>> ax = fig.add_subplot(1, 2, 2)
>>> ax.legend(fontsize=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()
... if y_batch[j] == 1:
... label='Smile'
... else:
... ax.text(
... 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
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.
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
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.
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
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:
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:
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:
>>> torch.manual_seed(1)
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
>>> out_man = []
...
...
... if t > 0:
... else:
... + b_hh
... ot = torch.tanh(ot)
... out_man.append(ot)
... print()
Time step 0 =>
Gradient clipping
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.
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
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,
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).
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.
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.
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
>>> torch.manual_seed(1)
>>> import re
>>>
... )
>>>
... token_counts.update(tokens)
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
... )
>>> vocab.insert_token("<pad>", 0)
>>> vocab.insert_token("<unk>", 1)
>>> vocab.set_default_index(1)
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
>>> text_pipeline =\
... dtype=torch.int64)
... text_list.append(processed_text)
... lengths.append(processed_text.size(0))
>>>
>>> print(text_batch)
...
0, 0, 0, 0, 0, 0, 0, 0]],
>>> print(label_batch)
>>> print(length_batch)
>>> 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.
>>> batch_size = 32
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.
... num_embeddings=10,
... embedding_dim=3,
... padding_idx=0)
>>> print(embedding(text_encoded_input))
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
... super().__init__()
... batch_first=True)
... # batch_first=True)
... # batch_first=True)
...
... # layer
>>>
>>> print(model)
RNN(
(rnn): RNN(64, 32, num_layers=2, batch_first=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.
... fc_hidden_size):
... super().__init__()
... embed_dim,
... padding_idx=0)
... self.rnn = nn.LSTM(embed_dim, rnn_hidden_size,
... batch_first=True)
...
... )
>>>
>>> embed_dim = 20
>>> rnn_hidden_size = 64
>>> fc_hidden_size = 64
>>> torch.manual_seed(1)
>>> model
RNN(
(relu): ReLU()
(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
... model.train()
... optimizer.zero_grad()
... loss.backward()
... optimizer.step()
... total_acc += (
... ).float().sum().item()
... total_loss/len(dataloader.dataset)
CopyExplain
... model.eval()
... total_acc += (
... ).float().sum().item()
... 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
Now we will train the model for 10 epochs and display the training and validation
performances:
>>> num_epochs = 10
>>> torch.manual_seed(1)
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
... super().__init__()
... )
...
... )
>>>
>>> torch.manual_seed(1)
>>> model
RNN(
(relu): ReLU()
(sigmoid): Sigmoid()
)
CopyExplain
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
... text=fp.read()
Unique Characters: 80
CopyExplain
... dtype=np.int32
... )
... ''.join(char_array[text_encoded[15:21]]))
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.
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:
>>> seq_length = 40
...
...
>>>
... repr(''.join(char_array[seq])))
... repr(''.join(char_array[target])))
... print()
... if i == 1:
... break
Finally, the last step in preparing the dataset is to transform this dataset into mini-
batches:
>>> batch_size = 64
>>> torch.manual_seed(1)
Now that the dataset is ready, building the model will be relatively straightforward:
... super().__init__()
... batch_first=True)
...
...
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:
>>> torch.manual_seed(1)
>>> model
RNN(
)
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
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
>>> torch.manual_seed(1)
... optimizer.zero_grad()
... loss = 0
... loss.backward()
... optimizer.step()
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.
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
>>> torch.manual_seed(1)
>>> print('Probabilities:',
>>> m = Categorical(logits=logits)
>>> 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)
>>> m = Categorical(logits=logits)
>>> print(samples.numpy())
[[0]
[2]
[2]
[1]
[2]
[1]
[2]
[2]
[2]
[2]]
CopyExplain
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
... len_generated_text=500,
... scale_factor=1.0):
... )
... )
...
... model.eval()
... )
...
... )
... m = Categorical(logits=scaled_logits)
...
>>> torch.manual_seed(1)
It was found, they full to time to remove. About this neur prowers, perhaps ended? It is might be
rather rose?"
Pencroft calling, themselves in time to try them what proves that the sailor and Neb bounded this
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.
:
>>> torch.manual_seed(1)
... 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
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.
:
>>> torch.manual_seed(1)
... scale_factor=0.5))
The island
deep incomele.
fell oprely
ran trail.
Withinhe)tiny turns returned, after owner plan bushelsion lairs; they were
Herbert, omep
CopyExplain
7.
Production machine learning combines which two key disciplines?
Software testing
Orchestrators
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).
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
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.
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
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.
saída, . Para
>>> 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
>>> torch.manual_seed(123)
>>> embedded_sentence.shape
torch.Size([8, 16])
CopyExplain
Agora, podemos calcular como o produto de ponto entre as
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
True
CopyExplain
>>> attention_weights.shape
torch.Size([8, 8])
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:
entrada:
>>> context_vec_2
-2.1601e+00])
CopyExplain
True
CopyExplain
Sequência de consulta:
para
Sequência de teclas:
para
Sequência de valores:
para
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.
>>> torch.manual_seed(123)
>>> d = embedded_sentence.shape[1]
:
>>> key_2 = U_key.matmul(x_2)
>>> omega_23
tensor(14.3667)
CopyExplain
Como precisaremos deles mais tarde, podemos escalar essa computação para
todas as chaves:
>>> omega_2
>>> attention_weights_2
3.1936e-10])
CopyExplain
>>> context_vector_2
entrada sequencial e
mapeá-la em uma
...
>>> torch.manual_seed(123)
>>> d = embedded_sentence.shape[1]
>>> h = 8
Como podemos ver no código, várias cabeças de atenção podem ser adicionadas
simplesmente adicionando uma dimensão adicional.
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.
>>> 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
>>> multihead_key_2[2]
>>> stacked_inputs.shape
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.shape
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.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]
multihead_U_value, stacked_inputs)
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
>>> context_vector_2.shape
torch.Size([16])
CopyExplain
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.
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.
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
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:
https://huggingface.co/transformers/installation.html
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)
... 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
{'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.
>>> encoded_input
{'input_ids': tensor([[ 5756, 514, 37773, 428, 6827]]), 'attention_mask': tensor([[1, 1, 1, 1, 1]])}
CopyExplain
>>> output['last_hidden_state'].shape
torch.Size([1, 5, 768])
CopyExplain
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
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
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.
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.
O código a seguir importa todos os pacotes que usaremos neste capítulo para
preparar os dados e ajustar o modelo DistilBERT:
>>> torch.manual_seed(RANDOM_SEED)
>>> NUM_EPOCHS = 3
CopyExplain
... "machine-learning-book/raw/"
... "main/ch08/movie_data.csv.gz")
... r = requests.get(url)
... f.write(r.content)
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
>>> df.head(3)
CopyExplain
... 'distilbert-base-uncased'
... )
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
... 'distilbert-base-uncased')
>>> model.to(DEVICE)
>>> model.train()
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:
... attention_mask = \
... batch['attention_mask'].to(device)
... attention_mask=attention_mask)
... correct_pred += \
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:
... model.train()
... attention_mask=attention_mask,
... labels=labels)
... optim.zero_grad()
... loss.backward()
... optim.step()
... f'{batch_idx:04d}/'
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):
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.
... 'distilbert-base-uncased')
>>> model.to(DEVICE)
>>> model.train();
CopyExplain
... 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,
... )
... model=model,
... args=training_args,
... train_dataset=train_dataset,
... )
CopyExplain
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
>>> trainer=Trainer(
... model=model,
... args=training_args,
... train_dataset=train_dataset,
... eval_dataset=test_dataset,
... compute_metrics=compute_metrics,
... )
CopyExplain
>>> trainer.train()
Num Epochs = 3
10 0.705800
20 0.684100
30 0.681500
40 0.591600
50 0.328600
60 0.478300
...
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())
Batch size = 16
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
>>> model.eval()
>>> model.to(DEVICE)
>>> trainer=Trainer(
... model=model,
... args=training_args,
... train_dataset=train_dataset,
... eval_dataset=valid_dataset,
... compute_metrics=compute_metrics,
... )
CopyExplain
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-
write ):
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.
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
, 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:
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..
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
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.
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.
>>> print(torch.__version__)
1.9.0+cu111
>>> if torch.cuda.is_available():
... else:
>>> 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.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
... input_size=20,
... num_hidden_layers=1,
... num_hidden_units=100,
... num_output_units=784):
... model.add_module(f'fc_g{i}',
... model.add_module(f'fc_g{num_hidden_layers}',
>>>
... input_size,
... num_hidden_layers=1,
... num_hidden_units=100,
... num_output_units=1):
... model.add_module(
... f'fc_d{i}',
... )
... model.add_module(f'fc_d{num_hidden_layers}',
>>> z_size = 20
>>> gen_hidden_layers = 1
>>> disc_hidden_layers = 1
>>> disc_hidden_size = 100
>>> torch.manual_seed(1)
... 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(
(relu_g0): LeakyReLU(negative_slope=0.01)
(tanh_g): Tanh()
)
... input_size=np.prod(image_size),
... num_hidden_layers=disc_hidden_layers,
... num_hidden_units=disc_hidden_size
... )
>>> print(disc_model)
Sequential(
(relu_d0): LeakyReLU(negative_slope=0.01)
)
CopyExplain
... transforms.ToTensor(),
... ])
... )
>>> print(example.shape)
Min: -1.0 Max: 1.0
>>> batch_size = 32
>>> torch.manual_seed(1)
>>> torch.manual_seed(1)
>>> np.random.seed(1)
... 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)
... input_size=np.prod(image_size),
... num_hidden_layers=disc_hidden_layers,
... num_hidden_units=disc_hidden_size
... ).to(device)
... disc_model.zero_grad()
... d_loss.backward()
... d_optimizer.step()
... d_proba_fake.detach()
>>>
... gen_model.zero_grad()
...
... 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)
>>>
>>> epoch_samples = []
>>> all_d_losses = []
>>> all_g_losses = []
>>> all_d_real = []
>>> all_d_fake = []
>>>
... 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())
... epoch_samples.append(
... )
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
>>> ax = fig.add_subplot(1, 2, 1)
>>> plt.legend(fontsize=20)
>>>
>>> ax = fig.add_subplot(1, 2, 2)
>>> plt.legend(fontsize=20)
>>> plt.show()
CopyExplain
Figure 17.10 shows the results:
... ax.set_xticks([])
... ax.set_yticks([])
... if j == 0:
... ax.text(
... horizontalalignment='right',
... verticalalignment='center',
... transform=ax.transAxes
... )
...
...
>>> plt.show()
CopyExplain
Figure 17.11 shows the produced images:
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...