3.3. Implementação Concisa de Regressão Linear¶ Open the notebook in SageMaker Studio Lab
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 Section 3.2, 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.
3.3.1. Gerando the Dataset¶
Para começar, vamos gerar o mesmo conjunto de dados como em Section 3.2.
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)
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)
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)
3.3.2. 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).
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)
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)
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)
Now we can use data_iter
in much the same way as we called the
data_iter
function in Section 3.2. To verify that
it is working, we can read and print the first minibatch of examples.
Comparing with Section 3.2, here we use iter
to
construct a Python iterator and use next
to obtain the first item
from the iterator.
next(iter(data_iter))
[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]])]
next(iter(data_iter))
[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]])]
next(iter(data_iter))
(<tf.Tensor: shape=(10, 2), dtype=float32, numpy=
array([[-0.4333908 , 0.668291 ],
[-0.12056218, 1.6938822 ],
[-0.61093557, 0.66606677],
[-0.42496178, 0.64752823],
[-0.9900723 , -0.08506326],
[ 0.5402116 , 0.11740936],
[-0.09974687, -1.0370846 ],
[-0.8218537 , 0.3895554 ],
[-0.40174934, 0.69312775],
[ 0.20610306, 1.057085 ]], dtype=float32)>,
<tf.Tensor: shape=(10, 1), dtype=float32, numpy=
array([[ 1.0540864],
[-1.8192354],
[ 0.7101214],
[ 1.1270143],
[ 2.5089824],
[ 4.8952 ],
[ 7.524812 ],
[ 1.2295262],
[ 1.0272019],
[ 1.023846 ]], dtype=float32)>)
3.3.3. Definindo o Modelo¶
Quando implementamos a regressão linear do zero em Section 3.2, 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 Fig. 3.1.2. 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.
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.
: 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.
# `nn` is an abbreviation for neural networks
from mxnet.gluon import nn
net = nn.Sequential()
net.add(nn.Dense(1))
# `nn` is an abbreviation for neural networks
from torch import nn
net = nn.Sequential(nn.Linear(2, 1))
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.
# `keras` is the high-level API for TensorFlow
net = tf.keras.Sequential()
net.add(tf.keras.layers.Dense(1))
3.3.4. 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.
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.
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.
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.
net[0].weight.data.normal_(0, 0.01)
net[0].bias.data.fill_(0)
tensor([0.])
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.
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.
3.3.5. Definindo a Função de Perda¶
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
).
loss = gluon.loss.L2Loss()
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.
loss = nn.MSELoss()
A classe MeanSquaredError
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.
loss = tf.keras.losses.MeanSquaredError()
3.3.6. Definindo o Algoritmo de Otimização¶
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
vianet.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.
from mxnet import gluon
trainer = gluon.Trainer(net.collect_params(), 'sgd', {'learning_rate': 0.03})
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.
trainer = torch.optim.SGD(net.parameters(), lr=0.03)
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.
trainer = tf.keras.optimizers.SGD(learning_rate=0.03)
3.3.7. 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 perdal
(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.
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}')
[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
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}')
epoch 1, loss 0.000239
epoch 2, loss 0.000098
epoch 3, loss 0.000099
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}')
epoch 1, loss 0.000263
epoch 2, loss 0.000089
epoch 3, loss 0.000089
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.
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}')
error in estimating w: [7.5769424e-04 1.3828278e-05]
error in estimating b: [0.00082684]
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)
error in estimating w: tensor([0.0004, 0.0001])
error in estimating b: tensor([-0.0005])
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)
error in estimating w tf.Tensor([0.00035477 0.00064921], shape=(2,), dtype=float32)
error in estimating b [-0.00018072]
3.3.8. Resumo¶
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ódulonn
define um grande número de camadas de rede neural e o móduloloss
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.
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ódulonn
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
_
.
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ódulokeras
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).
3.3.9. Exercícios¶
Se substituirmos
l = loss (output, y)
porl = loss (output, y).mean()
, precisamos alterartrainer.step(batch_size)
paratrainer.step(1)
para que o código se comporte de forma idêntica. Por quê?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
einit
. Substitua a perda pela perda de Huber.Como você acessa o gradiente de
dense.weight
?
Se substituirmos
nn.MSELoss (*reduction* = 'sum')
pornn.MSELoss ()
, como podemos alterar a taxa de aprendizagem para que o código se comporte de forma idêntica. Por quê?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.
Como você acessa o gradiente de
net[0].weight
?
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.