.. _sec_linear_concise:
Implementação Concisa de Regressão Linear
=========================================
Amplo e intenso interesse em *deep learning* nos últimos anos inspiraram
empresas, acadêmicos e amadores para desenvolver uma variedade de
estruturas de código aberto maduras para automatizar o trabalho
repetitivo de implementação algoritmos de aprendizagem baseados em
gradiente. Em :numref:`sec_linear_scratch`, contamos apenas com (i)
tensores para armazenamento de dados e álgebra linear; e (ii) auto
diferenciação para cálculo de gradientes. Na prática, porque iteradores
de dados, funções de perda, otimizadores, e camadas de rede neural são
tão comuns que as bibliotecas modernas também implementam esses
componentes para nós.
Nesta seção, mostraremos como implementar o modelo de regressão linear
de:numref:\ ``sec_linear_scratch`` de forma concisa, usando APIs de alto
nível de estruturas de *deep learning*.
Gerando the Dataset
-------------------
Para começar, vamos gerar o mesmo conjunto de dados como em
:numref:`sec_linear_scratch`.
.. raw:: html
.. raw:: html
.. code:: python
from mxnet import autograd, gluon, np, npx
from d2l import mxnet as d2l
npx.set_np()
true_w = np.array([2, -3.4])
true_b = 4.2
features, labels = d2l.synthetic_data(true_w, true_b, 1000)
.. raw:: html
.. raw:: html
.. code:: python
import numpy as np
import torch
from torch.utils import data
from d2l import torch as d2l
true_w = torch.tensor([2, -3.4])
true_b = 4.2
features, labels = d2l.synthetic_data(true_w, true_b, 1000)
.. raw:: html
.. raw:: html
.. code:: python
import numpy as np
import tensorflow as tf
from d2l import tensorflow as d2l
true_w = tf.constant([2, -3.4])
true_b = 4.2
features, labels = d2l.synthetic_data(true_w, true_b, 1000)
.. raw:: html
.. raw:: html
Lendo o Dataset
---------------
Em vez de usar nosso próprio iterador, podemos chamar a API existente em
uma estrutura para ler os dados. Passamos *``features``* e *``labels``*
como argumentos e especificamos *``batch_size``* ao instanciar um objeto
iterador de dados. Além disso, o valor booleano ``is_train`` indica se
ou não queremos que o objeto iterador de dados embaralhe os dados em
cada época (passe pelo conjunto de dados).
.. raw:: html
.. raw:: html
.. code:: python
def load_array(data_arrays, batch_size, is_train=True): #@save
"""Construct a Gluon data iterator."""
dataset = gluon.data.ArrayDataset(*data_arrays)
return gluon.data.DataLoader(dataset, batch_size, shuffle=is_train)
batch_size = 10
data_iter = load_array((features, labels), batch_size)
.. raw:: html
.. raw:: html
.. code:: python
def load_array(data_arrays, batch_size, is_train=True): #@save
"""Construct a PyTorch data iterator."""
dataset = data.TensorDataset(*data_arrays)
return data.DataLoader(dataset, batch_size, shuffle=is_train)
batch_size = 10
data_iter = load_array((features, labels), batch_size)
.. raw:: html
.. raw:: html
.. code:: python
def load_array(data_arrays, batch_size, is_train=True): #@save
"""Construct a TensorFlow data iterator."""
dataset = tf.data.Dataset.from_tensor_slices(data_arrays)
if is_train:
dataset = dataset.shuffle(buffer_size=1000)
dataset = dataset.batch(batch_size)
return dataset
batch_size = 10
data_iter = load_array((features, labels), batch_size)
.. raw:: html
.. raw:: html
Now we can use ``data_iter`` in much the same way as we called the
``data_iter`` function in :numref:`sec_linear_scratch`. To verify that
it is working, we can read and print the first minibatch of examples.
Comparing with :numref:`sec_linear_scratch`, here we use ``iter`` to
construct a Python iterator and use ``next`` to obtain the first item
from the iterator.
.. raw:: html
.. raw:: html
.. code:: python
next(iter(data_iter))
.. parsed-literal::
:class: output
[array([[-0.6015945 , 0.29670078],
[-1.9421831 , 0.39020136],
[ 1.3099662 , -0.49157172],
[ 0.17462298, -0.6705778 ],
[-0.05871473, 0.60052294],
[ 1.0442022 , 0.47742996],
[ 1.2208143 , 0.34541664],
[ 1.0702589 , 0.22332872],
[ 0.7740038 , 0.4838046 ],
[-0.63442576, 1.5001022 ]]),
array([[ 1.9725974],
[-1.0176986],
[ 8.485775 ],
[ 6.8331265],
[ 2.034631 ],
[ 4.670243 ],
[ 5.472156 ],
[ 5.5869737],
[ 4.0968843],
[-2.1558237]])]
.. raw:: html
.. raw:: html
.. code:: python
next(iter(data_iter))
.. parsed-literal::
:class: output
[tensor([[ 0.4291, 0.1270],
[ 1.7995, -1.2012],
[ 0.9239, -0.7505],
[ 1.3561, -0.4303],
[ 0.6144, -0.5138],
[-1.0876, -0.8626],
[ 1.1090, 3.4219],
[-1.6905, 0.1326],
[ 0.6009, 0.9365],
[ 0.1519, -1.1885]]),
tensor([[ 4.6351],
[11.8713],
[ 8.5979],
[ 8.3702],
[ 7.1819],
[ 4.9432],
[-5.2131],
[ 0.3592],
[ 2.2248],
[ 8.5418]])]
.. raw:: html
.. raw:: html
.. code:: python
next(iter(data_iter))
.. parsed-literal::
:class: output
(,
)
.. raw:: html
.. raw:: html
Definindo o Modelo
------------------
Quando implementamos a regressão linear do zero em
:numref:`sec_linear_scratch`, definimos nossos parâmetros de modelo
explicitamente e codificamos os cálculos para produzir saída usando
operações básicas de álgebra linear. Você *deveria* saber como fazer
isso. Mas quando seus modelos ficam mais complexos, e uma vez que você
tem que fazer isso quase todos os dias, você ficará feliz com a ajuda. A
situação é semelhante a codificar seu próprio blog do zero. Fazer uma ou
duas vezes é gratificante e instrutivo, mas você seria um péssimo
desenvolvedor da web se toda vez que você precisava de um blog você
passava um mês reinventando tudo.
Para operações padrão, podemos usar as camadas predefinidas de uma
estrutura, o que nos permite focar especialmente nas camadas usadas para
construir o modelo em vez de ter que se concentrar na implementação.
Vamos primeiro definir uma variável de modelo ``net``, que se refere a
uma instância da classe ``Sequential``. A classe ``Sequential`` define
um contêiner para várias camadas que serão encadeadas. Dados dados de
entrada, uma instância ``Sequential`` passa por a primeira camada, por
sua vez passando a saída como entrada da segunda camada e assim por
diante. No exemplo a seguir, nosso modelo consiste em apenas uma camada,
portanto, não precisamos realmente de ``Sequencial``. Mas como quase
todos os nossos modelos futuros envolverão várias camadas, vamos usá-lo
de qualquer maneira apenas para familiarizá-lo com o fluxo de trabalho
mais padrão.
Lembre-se da arquitetura de uma rede de camada única, conforme mostrado
em :numref:`fig_single_neuron`. Diz-se que a camada está *totalmente
conectada* porque cada uma de suas entradas está conectada a cada uma de
suas saídas por meio de uma multiplicação de matriz-vetor.
.. raw:: html
.. raw:: html
No Gluon, a camada totalmente conectada é definida na classe ``Densa``.
Uma vez que queremos apenas gerar uma única saída escalar, nós definimos
esse número para 1.
É importante notar que, por conveniência, Gluon não exige que
especifiquemos a forma de entrada para cada camada. Então, aqui, não
precisamos dizer ao Gluon quantas entradas vão para esta camada linear.
Quando tentamos primeiro passar dados por meio de nosso modelo, por
exemplo, quando executamos ``net (X)`` mais tarde, o Gluon irá inferir
automaticamente o número de entradas para cada camada. Descreveremos
como isso funciona com mais detalhes posteriormente.
.. raw:: html
.. raw:: html
: begin_tab: ``pytorch`` No PyTorch, a camada totalmente conectada é
definida na classe ``Linear``. Observe que passamos dois argumentos para
``nn.Linear``. O primeiro especifica a dimensão do recurso de entrada,
que é 2, e o segundo é a dimensão do recurso de saída, que é um escalar
único e, portanto, 1.
.. raw:: html
.. raw:: html
.. code:: python
# `nn` is an abbreviation for neural networks
from mxnet.gluon import nn
net = nn.Sequential()
net.add(nn.Dense(1))
.. raw:: html
.. raw:: html
.. code:: python
# `nn` is an abbreviation for neural networks
from torch import nn
net = nn.Sequential(nn.Linear(2, 1))
.. raw:: html
.. raw:: html
No Keras, a camada totalmente conectada é definida na classe ``Dense``.
Como queremos gerar apenas uma única saída escalar, definimos esse
número como 1.
É importante notar que, por conveniência, Keras não exige que
especifiquemos a forma de entrada para cada camada. Então, aqui, não
precisamos dizer a Keras quantas entradas vão para esta camada linear.
Quando tentamos primeiro passar dados por meio de nosso modelo, por
exemplo, quando executamos ``net (X)`` mais tarde, Keras inferirá
automaticamente o número de entradas para cada camada. Descreveremos
como isso funciona com mais detalhes posteriormente.
.. code:: python
# `keras` is the high-level API for TensorFlow
net = tf.keras.Sequential()
net.add(tf.keras.layers.Dense(1))
.. raw:: html
.. raw:: html
Inicializando os Parâmetros do Modelo
-------------------------------------
Antes de usar ``net``, precisamos inicializar os parâmetros do modelo,
como os pesos e *bias* no modelo de regressão linear. As estruturas de
*deep learning* geralmente têm uma maneira predefinida de inicializar os
parâmetros. Aqui especificamos que cada parâmetro de peso deve ser
amostrado aleatoriamente a partir de uma distribuição normal com média 0
e desvio padrão 0,01. O parâmetro bias será inicializado em zero.
.. raw:: html
.. raw:: html
Vamos importar o módulo *``initializer``* do MXNet. Este módulo fornece
vários métodos para inicialização de parâmetros do modelo. Gluon
disponibiliza ``init`` como um atalho (abreviatura) para acessar o
pacote ``initializer``. Nós apenas especificamos como inicializar o peso
chamando ``init.Normal (sigma = 0,01)``. Os parâmetros de polarização
são inicializados em zero por padrão.
.. code:: python
from mxnet import init
net.initialize(init.Normal(sigma=0.01))
O código acima pode parecer simples, mas você deve observar que algo
estranho está acontecendo aqui. Estamos inicializando parâmetros para
uma rede mesmo que Gluon ainda não saiba quantas dimensões a entrada
terá! Pode ser 2 como em nosso exemplo ou pode ser 2.000. Gluon nos
permite fugir com isso porque, nos bastidores, a inicialização é, na
verdade, *adiada*. A inicialização real ocorrerá apenas quando tentamos,
pela primeira vez, passar dados pela rede. Apenas tome cuidado para
lembrar que, uma vez que os parâmetros ainda não foram inicializados,
não podemos acessá-los ou manipulá-los.
.. raw:: html
.. raw:: html
As we have specified the input and output dimensions when constructing
``nn.Linear``. Now we access the parameters directly to specify their
initial values. We first locate the layer by ``net[0]``, which is the
first layer in the network, and then use the ``weight.data`` and
``bias.data`` methods to access the parameters. Next we use the replace
methods ``normal_`` and ``fill_`` to overwrite parameter values.
.. code:: python
net[0].weight.data.normal_(0, 0.01)
net[0].bias.data.fill_(0)
.. parsed-literal::
:class: output
tensor([0.])
.. raw:: html
.. raw:: html
O módulo *``initializers``* no TensorFlow fornece vários métodos para a
inicialização dos parâmetros do modelo. A maneira mais fácil de
especificar o método de inicialização no Keras é ao criar a camada
especificando *``kernel_initializer``*. Aqui, recriamos o ``net``
novamente.
.. code:: python
initializer = tf.initializers.RandomNormal(stddev=0.01)
net = tf.keras.Sequential()
net.add(tf.keras.layers.Dense(1, kernel_initializer=initializer))
O código acima pode parecer simples, mas você deve observar que algo
estranho está acontecendo aqui. Estamos inicializando parâmetros para
uma rede mesmo que o Keras ainda não saiba quantas dimensões a entrada
terá! Pode ser 2 como em nosso exemplo ou pode ser 2.000. O Keras nos
permite fugir do problema com isso porque, nos bastidores, a
inicialização é, na verdade, *adiada*. A inicialização real ocorrerá
apenas quando tentamos, pela primeira vez, passar dados pela rede.
Apenas tome cuidado para lembrar que, uma vez que os parâmetros ainda
não foram inicializados, não podemos acessá-los ou manipulá-los.
.. raw:: html
.. raw:: html
Definindo a Função de Perda
---------------------------
.. raw:: html
.. raw:: html
No Gluon, o módulo ``loss`` define várias funções de perda. Neste
exemplo, usaremos a implementação de perda quadrática do Gluon
(``L2Loss``).
.. code:: python
loss = gluon.loss.L2Loss()
.. raw:: html
.. raw:: html
A classe ``MSELoss`` calcula o erro quadrático médio, também conhecido
como norma $ L_2 $ quadrada. Por padrão, ela retorna a perda média sobre
os exemplos.
.. code:: python
loss = nn.MSELoss()
.. raw:: html
.. raw:: html
A classe ``MeanSquaredError`` calcula o erro quadrático médio, também
conhecido como norma :math:`L_2` quadrada. Por padrão, ela retorna a
perda média sobre os exemplos.
.. code:: python
loss = tf.keras.losses.MeanSquaredError()
.. raw:: html
.. raw:: html
Definindo o Algoritmo de Otimização
-----------------------------------
.. raw:: html
.. raw:: html
O gradiente descendente estocástico de *minibatch* é uma ferramenta
padrão para otimizar redes neurais e assim o Gluon o apoia ao lado de
uma série de variações desse algoritmo por meio de sua classe
*``Trainer``*. Quando instanciamos o *``Trainer``*, iremos especificar
os parâmetros para otimizar (que pode ser obtido em nosso modelo ``net``
via\ ``net.collect_params ()``), o algoritmo de otimização que desejamos
usar (``sgd``), e um dicionário de hiperparâmetros exigido por nosso
algoritmo de otimização. O gradiente descendente estocástico de
*minibatch* requer apenas que definamos o valor *``learning_rate``*, que
é definido como 0,03 aqui.
.. code:: python
from mxnet import gluon
trainer = gluon.Trainer(net.collect_params(), 'sgd', {'learning_rate': 0.03})
.. raw:: html
.. raw:: html
O gradiente descendente estocástico de *minibatch* é uma ferramenta
padrão para otimizar redes neurais e, portanto, PyTorch o suporta ao
lado de uma série de variações deste algoritmo no módulo ``optim``.
Quando nós instanciamos uma instância ``SGD``, iremos especificar os
parâmetros para otimizar (podem ser obtidos de nossa rede via
``net.parameters ()``), com um dicionário de hiperparâmetros exigido por
nosso algoritmo de otimização. O gradiente descendente estocástico de
*minibatch* requer apenas que definamos o valor ``lr``, que é definido
como 0,03 aqui.
.. code:: python
trainer = torch.optim.SGD(net.parameters(), lr=0.03)
.. raw:: html
.. raw:: html
O gradiente descendente estocástico de *minibatch* é uma ferramenta
padrão para otimizar redes neurais e, portanto, Keras oferece suporte ao
lado de uma série de variações deste algoritmo no módulo
``otimizadores``. O gradiente descendente estocástico de *minibatch*
requer apenas que definamos o valor ``learning_rate``, que é definido
como 0,03 aqui.
.. code:: python
trainer = tf.keras.optimizers.SGD(learning_rate=0.03)
.. raw:: html
.. raw:: html
Treinamento
-----------
Você deve ter notado que expressar nosso modelo por meio APIs de alto
nível de uma estrutura de *deep learning* requer comparativamente poucas
linhas de código. Não tivemos que alocar parâmetros individualmente,
definir nossa função de perda ou implementar o gradiente descendente
estocástico de *minibatch*. Assim que começarmos a trabalhar com modelos
muito mais complexos, as vantagens das APIs de alto nível aumentarão
consideravelmente. No entanto, uma vez que temos todas as peças básicas
no lugar, o loop de treinamento em si é surpreendentemente semelhante ao
que fizemos ao implementar tudo do zero.
Para refrescar sua memória: para anguns números de épocas, faremos uma
passagem completa sobre o conjunto de dados (*``train_data``*), pegando
iterativamente um *minibatch* de entradas e os *labels* de verdade
fundamental correspondentes. Para cada *minibatch*, passamos pelo
seguinte ritual:
- Gerar previsões chamando ``net (X)`` e calcular a perda ``l`` (a
propagação direta).
- Calcular gradientes executando a retropropagação.
- Atualizar os parâmetros do modelo invocando nosso otimizador.
Para uma boa medida, calculamos a perda após cada época e a imprimimos
para monitorar o progresso.
.. raw:: html
.. raw:: html
.. code:: python
num_epochs = 3
for epoch in range(num_epochs):
for X, y in data_iter:
with autograd.record():
l = loss(net(X), y)
l.backward()
trainer.step(batch_size)
l = loss(net(features), labels)
print(f'epoch {epoch + 1}, loss {l.mean().asnumpy():f}')
.. parsed-literal::
:class: output
[03:41:18] src/base.cc:49: GPU context requested, but no GPUs found.
epoch 1, loss 0.024782
epoch 2, loss 0.000091
epoch 3, loss 0.000051
.. raw:: html
.. raw:: html
.. code:: python
num_epochs = 3
for epoch in range(num_epochs):
for X, y in data_iter:
l = loss(net(X) ,y)
trainer.zero_grad()
l.backward()
trainer.step()
l = loss(net(features), labels)
print(f'epoch {epoch + 1}, loss {l:f}')
.. parsed-literal::
:class: output
epoch 1, loss 0.000239
epoch 2, loss 0.000098
epoch 3, loss 0.000099
.. raw:: html
.. raw:: html
.. code:: python
num_epochs = 3
for epoch in range(num_epochs):
for X, y in data_iter:
with tf.GradientTape() as tape:
l = loss(net(X, training=True), y)
grads = tape.gradient(l, net.trainable_variables)
trainer.apply_gradients(zip(grads, net.trainable_variables))
l = loss(net(features), labels)
print(f'epoch {epoch + 1}, loss {l:f}')
.. parsed-literal::
:class: output
epoch 1, loss 0.000263
epoch 2, loss 0.000089
epoch 3, loss 0.000089
.. raw:: html
.. raw:: html
Abaixo, nós comparamos os parâmetros do modelo aprendidos pelo
treinamento em dados finitos e os parâmetros reais que geraram nosso
*dataset*. Para acessar os parâmetros, primeiro acessamos a camada que
precisamos de ``net`` e, em seguida, acessamos os pesos e a polarização
dessa camada. Como em nossa implementação do zero, observe que nossos
parâmetros estimados são perto de suas contrapartes verdadeiras.
.. raw:: html
.. raw:: html
.. code:: python
w = net[0].weight.data()
print(f'error in estimating w: {true_w - w.reshape(true_w.shape)}')
b = net[0].bias.data()
print(f'error in estimating b: {true_b - b}')
.. parsed-literal::
:class: output
error in estimating w: [7.5769424e-04 1.3828278e-05]
error in estimating b: [0.00082684]
.. raw:: html
.. raw:: html
.. code:: python
w = net[0].weight.data
print('error in estimating w:', true_w - w.reshape(true_w.shape))
b = net[0].bias.data
print('error in estimating b:', true_b - b)
.. parsed-literal::
:class: output
error in estimating w: tensor([0.0004, 0.0001])
error in estimating b: tensor([-0.0005])
.. raw:: html
.. raw:: html
.. code:: python
w = net.get_weights()[0]
print('error in estimating w', true_w - tf.reshape(w, true_w.shape))
b = net.get_weights()[1]
print('error in estimating b', true_b - b)
.. parsed-literal::
:class: output
error in estimating w tf.Tensor([0.00035477 0.00064921], shape=(2,), dtype=float32)
error in estimating b [-0.00018072]
.. raw:: html
.. raw:: html
Resumo
------
.. raw:: html
.. raw:: html
- Usando o Gluon, podemos implementar modelos de forma muito mais
concisa.
- No Gluon, o módulo ``data`` fornece ferramentas para processamento de
dados, o módulo\ ``nn`` define um grande número de camadas de rede
neural e o módulo ``loss`` define muitas funções de perda comuns.
- O módulo ``inicializador`` do MXNet fornece vários métodos para
inicialização dos parâmetros do modelo.
- A dimensionalidade e o armazenamento são inferidos automaticamente,
mas tome cuidado para não tentar acessar os parâmetros antes de eles
serem inicializados.
.. raw:: html
.. raw:: html
- Usando as APIs de alto nível do PyTorch, podemos implementar modelos
de forma muito mais concisa.
- No PyTorch, o módulo ``data`` fornece ferramentas para processamento
de dados, o módulo\ ``nn`` define um grande número de camadas de rede
neural e funções de perda comuns.
- Podemos inicializar os parâmetros substituindo seus valores por
métodos que terminam com ``_``.
.. raw:: html
.. raw:: html
- Usando as APIs de alto nível do TensorFlow, podemos implementar
modelos de maneira muito mais concisa.
- No TensorFlow, o módulo ``data`` fornece ferramentas para
processamento de dados, o módulo\ ``keras`` define um grande número
de camadas de rede neural e funções de perda comuns.
- O módulo *``initializers``* do TensorFlow fornece vários métodos para
a inicialização dos parâmetros do modelo.
- A dimensionalidade e o armazenamento são inferidos automaticamente
(mas tome cuidado para não tentar acessar os parâmetros antes de
serem inicializados).
.. raw:: html
.. raw:: html
Exercícios
----------
.. raw:: html
.. raw:: html
1. Se substituirmos ``l = loss (output, y)`` por
``l = loss (output, y).mean()``, precisamos alterar
``trainer.step(batch_size)`` para ``trainer.step(1)``\ para que o
código se comporte de forma idêntica. Por quê?
2. Revise a documentação do MXNet para ver quais funções de perda e
métodos de inicialização são fornecidos nos módulos ``gluon.loss``
e\ ``init``. Substitua a perda pela perda de Huber.
3. Como você acessa o gradiente de ``dense.weight``?
`Discussions `__
.. raw:: html
.. raw:: html
1. Se substituirmos ``nn.MSELoss (*reduction* = 'sum')`` por
``nn.MSELoss ()``, como podemos alterar a taxa de aprendizagem para
que o código se comporte de forma idêntica. Por quê?
2. Revise a documentação do PyTorch para ver quais funções de perda e
métodos de inicialização são fornecidos. Substitua a perda pela perda
de Huber.
3. Como você acessa o gradiente de ``net[0].weight``?
`Discussions `__
.. raw:: html
.. raw:: html
1. Revise a documentação do TensorFlow para ver quais funções de perda e
métodos de inicialização são fornecidos. Substitua a perda pela perda
de Huber.
`Discussions `__
.. raw:: html
.. raw:: html
.. raw:: html