Concise Implementation of Linear Regression

:label:sec_linear_concise

Broad and intense interest in deep learning for the past several years has inspired companies, academics, and hobbyists to develop a variety of mature open source frameworks for automating the repetitive work of implementing gradient-based learning algorithms. In :numref:sec_linear_scratch, we relied only on (i) tensors for data storage and linear algebra; and (ii) auto differentiation for calculating gradients. In practice, because data iterators, loss functions, optimizers, and neural network layers are so common, modern libraries implement these components for us as well.

In this section, we will show you how to implement the linear regression model from :numref:sec_linear_scratch concisely by using high-level APIs of deep learning frameworks.

Generating the Dataset

To start, we will generate the same dataset as in :numref:sec_linear_scratch.

```{.python .input} from d2l import mxnet as d2l from mxnet import autograd, gluon, np, npx npx.set_np()

  1. ```{.python .input}
  2. #@tab pytorch
  3. from d2l import torch as d2l
  4. import numpy as np
  5. import torch
  6. from torch.utils import data

```{.python .input}

@tab tensorflow

from d2l import tensorflow as d2l import numpy as np import tensorflow as tf

  1. ```{.python .input}
  2. #@tab all
  3. true_w = d2l.tensor([2, -3.4])
  4. true_b = 4.2
  5. features, labels = d2l.synthetic_data(true_w, true_b, 1000)

Reading the Dataset

Rather than rolling our own iterator, we can call upon the existing API in a framework to read data. We pass in features and labels as arguments and specify batch_size when instantiating a data iterator object. Besides, the boolean value is_train indicates whether or not we want the data iterator object to shuffle the data on each epoch (pass through the dataset).

```{.python .input} 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)

  1. ```{.python .input}
  2. #@tab pytorch
  3. def load_array(data_arrays, batch_size, is_train=True): #@save
  4. """Construct a PyTorch data iterator."""
  5. dataset = data.TensorDataset(*data_arrays)
  6. return data.DataLoader(dataset, batch_size, shuffle=is_train)

```{.python .input}

@tab tensorflow

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

  1. ```{.python .input}
  2. #@tab all
  3. batch_size = 10
  4. 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 :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.

```{.python .input}

@tab all

next(iter(data_iter))

  1. ## Defining the Model
  2. When we implemented linear regression from scratch
  3. in :numref:`sec_linear_scratch`,
  4. we defined our model parameters explicitly
  5. and coded up the calculations to produce output
  6. using basic linear algebra operations.
  7. You *should* know how to do this.
  8. But once your models get more complex,
  9. and once you have to do this nearly every day,
  10. you will be glad for the assistance.
  11. The situation is similar to coding up your own blog from scratch.
  12. Doing it once or twice is rewarding and instructive,
  13. but you would be a lousy web developer
  14. if every time you needed a blog you spent a month
  15. reinventing the wheel.
  16. For standard operations, we can use a framework's predefined layers,
  17. which allow us to focus especially
  18. on the layers used to construct the model
  19. rather than having to focus on the implementation.
  20. We will first define a model variable `net`,
  21. which will refer to an instance of the `Sequential` class.
  22. The `Sequential` class defines a container
  23. for several layers that will be chained together.
  24. Given input data, a `Sequential` instance passes it through
  25. the first layer, in turn passing the output
  26. as the second layer's input and so forth.
  27. In the following example, our model consists of only one layer,
  28. so we do not really need `Sequential`.
  29. But since nearly all of our future models
  30. will involve multiple layers,
  31. we will use it anyway just to familiarize you
  32. with the most standard workflow.
  33. Recall the architecture of a single-layer network as shown in :numref:`fig_single_neuron`.
  34. The layer is said to be *fully-connected*
  35. because each of its inputs is connected to each of its outputs
  36. by means of a matrix-vector multiplication.
  37. :begin_tab:`mxnet`
  38. In Gluon, the fully-connected layer is defined in the `Dense` class.
  39. Since we only want to generate a single scalar output,
  40. we set that number to 1.
  41. It is worth noting that, for convenience,
  42. Gluon does not require us to specify
  43. the input shape for each layer.
  44. So here, we do not need to tell Gluon
  45. how many inputs go into this linear layer.
  46. When we first try to pass data through our model,
  47. e.g., when we execute `net(X)` later,
  48. Gluon will automatically infer the number of inputs to each layer.
  49. We will describe how this works in more detail later.
  50. :end_tab:
  51. :begin_tab:`pytorch`
  52. In PyTorch, the fully-connected layer is defined in the `Linear` class. Note that we passed two arguments into `nn.Linear`. The first one specifies the input feature dimension, which is 2, and the second one is the output feature dimension, which is a single scalar and therefore 1.
  53. :end_tab:
  54. :begin_tab:`tensorflow`
  55. In Keras, the fully-connected layer is defined in the `Dense` class. Since we only want to generate a single scalar output, we set that number to 1.
  56. It is worth noting that, for convenience,
  57. Keras does not require us to specify
  58. the input shape for each layer.
  59. So here, we do not need to tell Keras
  60. how many inputs go into this linear layer.
  61. When we first try to pass data through our model,
  62. e.g., when we execute `net(X)` later,
  63. Keras will automatically infer the number of inputs to each layer.
  64. We will describe how this works in more detail later.
  65. :end_tab:
  66. ```{.python .input}
  67. # `nn` is an abbreviation for neural networks
  68. from mxnet.gluon import nn
  69. net = nn.Sequential()
  70. net.add(nn.Dense(1))

```{.python .input}

@tab pytorch

nn is an abbreviation for neural networks

from torch import nn net = nn.Sequential(nn.Linear(2, 1))

  1. ```{.python .input}
  2. #@tab tensorflow
  3. # `keras` is the high-level API for TensorFlow
  4. net = tf.keras.Sequential()
  5. net.add(tf.keras.layers.Dense(1))

Initializing Model Parameters

Before using net, we need to initialize the model parameters, such as the weights and bias in the linear regression model. Deep learning frameworks often have a predefined way to initialize the parameters. Here we specify that each weight parameter should be randomly sampled from a normal distribution with mean 0 and standard deviation 0.01. The bias parameter will be initialized to zero.

:begin_tab:mxnet We will import the initializer module from MXNet. This module provides various methods for model parameter initialization. Gluon makes init available as a shortcut (abbreviation) to access the initializer package. We only specify how to initialize the weight by calling init.Normal(sigma=0.01). Bias parameters are initialized to zero by default. :end_tab:

:begintab:pytorch As we have specified the input and output dimensions when constructing nn.Linear. Now we access the parameters directly to specify there 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 `normalandfill_` to overwrite parameter values. :end_tab:

:begin_tab:tensorflow The initializers module in TensorFlow provides various methods for model parameter initialization. The easiest way to specify the initialization method in Keras is when creating the layer by specifying kernel_initializer. Here we recreate net again. :end_tab:

```{.python .input} from mxnet import init net.initialize(init.Normal(sigma=0.01))

  1. ```{.python .input}
  2. #@tab pytorch
  3. net[0].weight.data.normal_(0, 0.01)
  4. net[0].bias.data.fill_(0)

```{.python .input}

@tab tensorflow

initializer = tf.initializers.RandomNormal(stddev=0.01) net = tf.keras.Sequential() net.add(tf.keras.layers.Dense(1, kernel_initializer=initializer))

  1. :begin_tab:`mxnet`
  2. The code above may look straightforward but you should note
  3. that something strange is happening here.
  4. We are initializing parameters for a network
  5. even though Gluon does not yet know
  6. how many dimensions the input will have!
  7. It might be 2 as in our example or it might be 2000.
  8. Gluon lets us get away with this because behind the scene,
  9. the initialization is actually *deferred*.
  10. The real initialization will take place only
  11. when we for the first time attempt to pass data through the network.
  12. Just be careful to remember that since the parameters
  13. have not been initialized yet,
  14. we cannot access or manipulate them.
  15. :end_tab:
  16. :begin_tab:`pytorch`
  17. :end_tab:
  18. :begin_tab:`tensorflow`
  19. The code above may look straightforward but you should note
  20. that something strange is happening here.
  21. We are initializing parameters for a network
  22. even though Keras does not yet know
  23. how many dimensions the input will have!
  24. It might be 2 as in our example or it might be 2000.
  25. Keras lets us get away with this because behind the scenes,
  26. the initialization is actually *deferred*.
  27. The real initialization will take place only
  28. when we for the first time attempt to pass data through the network.
  29. Just be careful to remember that since the parameters
  30. have not been initialized yet,
  31. we cannot access or manipulate them.
  32. :end_tab:
  33. ## Defining the Loss Function
  34. :begin_tab:`mxnet`
  35. In Gluon, the `loss` module defines various loss functions.
  36. In this example, we will use the Gluon
  37. implementation of squared loss (`L2Loss`).
  38. :end_tab:
  39. :begin_tab:`pytorch`
  40. The `MSELoss` class computes the mean squared error, also known as squared $L_2$ norm.
  41. By default it returns the average loss over examples.
  42. :end_tab:
  43. :begin_tab:`tensorflow`
  44. The `MeanSquaredError` class computes the mean squared error, also known as squared $L_2$ norm.
  45. By default it returns the average loss over examples.
  46. :end_tab:
  47. ```{.python .input}
  48. loss = gluon.loss.L2Loss()

```{.python .input}

@tab pytorch

loss = nn.MSELoss()

  1. ```{.python .input}
  2. #@tab tensorflow
  3. loss = tf.keras.losses.MeanSquaredError()

Defining the Optimization Algorithm

:begin_tab:mxnet Minibatch stochastic gradient descent is a standard tool for optimizing neural networks and thus Gluon supports it alongside a number of variations on this algorithm through its Trainer class. When we instantiate Trainer, we will specify the parameters to optimize over (obtainable from our model net via net.collect_params()), the optimization algorithm we wish to use (sgd), and a dictionary of hyperparameters required by our optimization algorithm. Minibatch stochastic gradient descent just requires that we set the value learning_rate, which is set to 0.03 here. :end_tab:

:begin_tab:pytorch Minibatch stochastic gradient descent is a standard tool for optimizing neural networks and thus PyTorch supports it alongside a number of variations on this algorithm in the optim module. When we instantiate an SGD instance, we will specify the parameters to optimize over (obtainable from our net via net.parameters()), with a dictionary of hyperparameters required by our optimization algorithm. Minibatch stochastic gradient descent just requires that we set the value lr, which is set to 0.03 here. :end_tab:

:begin_tab:tensorflow Minibatch stochastic gradient descent is a standard tool for optimizing neural networks and thus Keras supports it alongside a number of variations on this algorithm in the optimizers module. Minibatch stochastic gradient descent just requires that we set the value learning_rate, which is set to 0.03 here. :end_tab:

```{.python .input} from mxnet import gluon trainer = gluon.Trainer(net.collect_params(), ‘sgd’, {‘learning_rate’: 0.03})

  1. ```{.python .input}
  2. #@tab pytorch
  3. trainer = torch.optim.SGD(net.parameters(), lr=0.03)

```{.python .input}

@tab tensorflow

trainer = tf.keras.optimizers.SGD(learning_rate=0.03)

  1. ## Training
  2. You might have noticed that expressing our model through
  3. high-level APIs of a deep learning framework
  4. requires comparatively few lines of code.
  5. We did not have to individually allocate parameters,
  6. define our loss function, or implement minibatch stochastic gradient descent.
  7. Once we start working with much more complex models,
  8. advantages of high-level APIs will grow considerably.
  9. However, once we have all the basic pieces in place,
  10. the training loop itself is strikingly similar
  11. to what we did when implementing everything from scratch.
  12. To refresh your memory: for some number of epochs,
  13. we will make a complete pass over the dataset (`train_data`),
  14. iteratively grabbing one minibatch of inputs
  15. and the corresponding ground-truth labels.
  16. For each minibatch, we go through the following ritual:
  17. * Generate predictions by calling `net(X)` and calculate the loss `l` (the forward propagation).
  18. * Calculate gradients by running the backpropagation.
  19. * Update the model parameters by invoking our optimizer.
  20. For good measure, we compute the loss after each epoch and print it to monitor progress.
  21. ```{.python .input}
  22. num_epochs = 3
  23. for epoch in range(num_epochs):
  24. for X, y in data_iter:
  25. with autograd.record():
  26. l = loss(net(X), y)
  27. l.backward()
  28. trainer.step(batch_size)
  29. l = loss(net(features), labels)
  30. print(f'epoch {epoch + 1}, loss {l.mean().asnumpy():f}')

```{.python .input}

@tab pytorch

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}’)

  1. ```{.python .input}
  2. #@tab tensorflow
  3. num_epochs = 3
  4. for epoch in range(num_epochs):
  5. for X, y in data_iter:
  6. with tf.GradientTape() as tape:
  7. l = loss(net(X, training=True), y)
  8. grads = tape.gradient(l, net.trainable_variables)
  9. trainer.apply_gradients(zip(grads, net.trainable_variables))
  10. l = loss(net(features), labels)
  11. print(f'epoch {epoch + 1}, loss {l:f}')

Below, we compare the model parameters learned by training on finite data and the actual parameters that generated our dataset. To access parameters, we first access the layer that we need from net and then access that layer’s weights and bias. As in our from-scratch implementation, note that our estimated parameters are close to their ground-truth counterparts.

```{.python .input} w = net[0].weight.data() print(f’error in estimating w: {true_w - d2l.reshape(w, true_w.shape)}’) b = net[0].bias.data() print(f’error in estimating b: {true_b - b}’)

  1. ```{.python .input}
  2. #@tab pytorch
  3. w = net[0].weight.data
  4. print('error in estimating w:', true_w - d2l.reshape(w, true_w.shape))
  5. b = net[0].bias.data
  6. print('error in estimating b:', true_b - b)

```{.python .input}

@tab tensorflow

w = net.get_weights()[0] print(‘error in estimating w’, true_w - d2l.reshape(w, true_w.shape)) b = net.get_weights()[1] print(‘error in estimating b’, true_b - b) ```

Summary

:begin_tab:mxnet

  • Using Gluon, we can implement models much more concisely.
  • In Gluon, the data module provides tools for data processing, the nn module defines a large number of neural network layers, and the loss module defines many common loss functions.
  • MXNet’s module initializer provides various methods for model parameter initialization.
  • Dimensionality and storage are automatically inferred, but be careful not to attempt to access parameters before they have been initialized. :end_tab:

:begin_tab:pytorch

  • Using PyTorch’s high-level APIs, we can implement models much more concisely.
  • In PyTorch, the data module provides tools for data processing, the nn module defines a large number of neural network layers and common loss functions.
  • We can initialize the parameters by replacing their values with methods ending with _. :end_tab:

:begin_tab:tensorflow

  • Using TensorFlow’s high-level APIs, we can implement models much more concisely.
  • In TensorFlow, the data module provides tools for data processing, the keras module defines a large number of neural network layers and common loss functions.
  • TensorFlow’s module initializers provides various methods for model parameter initialization.
  • Dimensionality and storage are automatically inferred (but be careful not to attempt to access parameters before they have been initialized). :end_tab:

Exercises

:begin_tab:mxnet

  1. If we replace l = loss(output, y) with l = loss(output, y).mean(), we need to change trainer.step(batch_size) to trainer.step(1) for the code to behave identically. Why?
  2. Review the MXNet documentation to see what loss functions and initialization methods are provided in the modules gluon.loss and init. Replace the loss by Huber’s loss.
  3. How do you access the gradient of dense.weight?

Discussions :end_tab:

:begin_tab:pytorch

  1. If we replace nn.MSELoss() with nn.MSELoss(reduction='sum'), how can we change the learning rate for the code to behave identically. Why?
  2. Review the PyTorch documentation to see what loss functions and initialization methods are provided. Replace the loss by Huber’s loss.
  3. How do you access the gradient of net[0].weight?

Discussions :end_tab:

:begin_tab:tensorflow

  1. Review the TensorFlow documentation to see what loss functions and initialization methods are provided. Replace the loss by Huber’s loss.

Discussions :end_tab: