3.3. Implementação Concisa de Regressão Linear
Open the notebook in Colab
Open the notebook in Colab
Open the notebook in Colab
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 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.

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

  • 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

  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 einit. Substitua a perda pela perda de Huber.

  3. Como você acessa o gradiente de dense.weight?

Discussions

  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

  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