3.3. 线性回归的简洁实现
Open the notebook in Colab
Open the notebook in Colab
Open the notebook in Colab
Open the notebook in Colab
Open the notebook in SageMaker Studio Lab

在过去的几年里,出于对深度学习强烈的兴趣, 许多公司、学者和业余爱好者开发了各种成熟的开源框架。 这些框架可以自动化基于梯度的学习算法中重复性的工作。 在 3.2节中,我们只运用了: (1)通过张量来进行数据存储和线性代数; (2)通过自动微分来计算梯度。 实际上,由于数据迭代器、损失函数、优化器和神经网络层很常用, 现代深度学习库也为我们实现了这些组件。

本节将介绍如何通过使用深度学习框架来简洁地实现 3.2节中的线性回归模型。

3.3.1. 生成数据集

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)
[07:03:00] ../src/storage/storage.cc:196: Using Pooled (Naive) StorageManager for CPU
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)
import warnings
from d2l import paddle as d2l

warnings.filterwarnings("ignore")
import numpy as np
import paddle

true_w = paddle.to_tensor([2, -3.4])
true_b = 4.2
features, labels = d2l.synthetic_data(true_w, true_b, 1000)

3.3.2. 读取数据集

我们可以调用框架中现有的API来读取数据。 我们将featureslabels作为API的参数传递,并通过数据迭代器指定batch_size。 此外,布尔值is_train表示是否希望数据迭代器对象在每个迭代周期内打乱数据。

def load_array(data_arrays, batch_size, is_train=True):  #@save
    """构造一个Gluon数据迭代器"""
    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
    """构造一个PyTorch数据迭代器"""
    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
    """构造一个TensorFlow数据迭代器"""
    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)
#@save
def load_array(data_arrays, batch_size, is_train=True):
    """构造一个Paddle数据迭代器"""
    dataset = paddle.io.TensorDataset(data_arrays)
    return paddle.io.DataLoader(dataset, batch_size=batch_size,
                                shuffle=is_train,
                                return_list=True)

batch_size = 10
data_iter = load_array((features, labels), batch_size)

使用data_iter的方式与我们在 3.2节中使用data_iter函数的方式相同。为了验证是否正常工作,让我们读取并打印第一个小批量样本。 与 3.2节不同,这里我们使用iter构造Python迭代器,并使用next从迭代器中获取第一项。

next(iter(data_iter))
[array([[ 0.6631022 , -0.16627775],
        [ 1.4011933 , -0.19310263],
        [-0.9567186 , -1.6827176 ],
        [-0.7114856 , -2.0320427 ],
        [ 0.8230793 , -0.26465464],
        [-1.0424827 ,  0.35005474],
        [-0.594405  ,  0.7975923 ],
        [ 1.1555725 , -0.13389243],
        [-0.1386715 ,  0.7079162 ],
        [ 1.0653706 , -0.02712403]]),
 array([[6.0939207 ],
        [7.659188  ],
        [8.01163   ],
        [9.69425   ],
        [6.749446  ],
        [0.91951895],
        [0.30207413],
        [6.9719377 ],
        [1.5070254 ],
        [6.423725  ]])]
next(iter(data_iter))
[tensor([[-1.3116, -0.3062],
         [-1.5653,  0.4830],
         [-0.8893, -0.9466],
         [-1.2417,  1.6891],
         [-0.7148,  0.1376],
         [-0.2162, -0.6122],
         [ 2.4048, -0.3211],
         [-0.1516,  0.4997],
         [ 1.5298, -0.2291],
         [ 1.3895,  1.2602]]),
 tensor([[ 2.6073],
         [-0.5787],
         [ 5.6339],
         [-4.0211],
         [ 2.3117],
         [ 5.8492],
         [10.0926],
         [ 2.1932],
         [ 8.0441],
         [ 2.6943]])]
next(iter(data_iter))
(<tf.Tensor: shape=(10, 2), dtype=float32, numpy=
 array([[ 1.3562248 ,  1.1343712 ],
        [ 0.6813848 , -0.90772367],
        [ 0.69755095, -0.73168105],
        [-0.13335657,  0.25166443],
        [ 0.05556395,  0.42053273],
        [-1.6209612 , -0.604224  ],
        [-0.33418524, -0.85788333],
        [ 0.9328292 ,  0.4032445 ],
        [-0.37482202,  1.1207972 ],
        [ 0.5011033 ,  1.7449698 ]], dtype=float32)>,
 <tf.Tensor: shape=(10, 1), dtype=float32, numpy=
 array([[ 3.0570889 ],
        [ 8.641059  ],
        [ 8.069751  ],
        [ 3.0649056 ],
        [ 2.87996   ],
        [ 3.0126424 ],
        [ 6.469513  ],
        [ 4.7013783 ],
        [-0.35395175],
        [-0.71745443]], dtype=float32)>)
next(iter(data_iter))
[Tensor(shape=[10, 2], dtype=float32, place=Place(cpu), stop_gradient=True,
        [[-2.94040632,  0.82155126],
         [-0.83796650,  0.14774266],
         [-0.97951430,  0.21689114],
         [ 1.12803590,  0.40856126],
         [-0.86111712,  0.13562034],
         [ 1.18152142,  0.49508804],
         [ 1.64668143, -1.02052391],
         [ 0.58453119,  2.85113072],
         [-1.34762752, -0.43485016],
         [ 0.16040383,  0.10818470]]),
 Tensor(shape=[10, 1], dtype=float32, place=Place(cpu), stop_gradient=True,
        [[-4.48145008],
         [ 2.03535795],
         [ 1.50818276],
         [ 5.06655407],
         [ 2.03503680],
         [ 4.89476252],
         [10.96025372],
         [-4.31975508],
         [ 2.99133062],
         [ 4.15751839]])]

3.3.3. 定义模型

当我们在 3.2节中实现线性回归时, 我们明确定义了模型参数变量,并编写了计算的代码,这样通过基本的线性代数运算得到输出。 但是,如果模型变得更加复杂,且当我们几乎每天都需要实现模型时,自然会想简化这个过程。 这种情况类似于为自己的博客从零开始编写网页。 做一两次是有益的,但如果每个新博客就需要工程师花一个月的时间重新开始编写网页,那并不高效。

对于标准深度学习模型,我们可以使用框架的预定义好的层。这使我们只需关注使用哪些层来构造模型,而不必关注层的实现细节。 我们首先定义一个模型变量net,它是一个Sequential类的实例。 Sequential类将多个层串联在一起。 当给定输入数据时,Sequential实例将数据传入到第一层, 然后将第一层的输出作为第二层的输入,以此类推。 在下面的例子中,我们的模型只包含一个层,因此实际上不需要Sequential。 但是由于以后几乎所有的模型都是多层的,在这里使用Sequential会让你熟悉“标准的流水线”。

回顾 图3.1.2中的单层网络架构, 这一单层被称为全连接层(fully-connected layer), 因为它的每一个输入都通过矩阵-向量乘法得到它的每个输出。

在Gluon中,全连接层在Dense类中定义。 由于我们只想得到一个标量输出,所以我们将该数字设置为1。

值得注意的是,为了方便使用,Gluon并不要求我们为每个层指定输入的形状。 所以在这里,我们不需要告诉Gluon有多少输入进入这一层。 当我们第一次尝试通过我们的模型传递数据时,例如,当后面执行net(X)时, Gluon会自动推断每个层输入的形状。 本节稍后将详细介绍这种工作机制。

# nn是神经网络的缩写
from mxnet.gluon import nn

net = nn.Sequential()
net.add(nn.Dense(1))

在PyTorch中,全连接层在Linear类中定义。 值得注意的是,我们将两个参数传递到nn.Linear中。 第一个指定输入特征形状,即2,第二个指定输出特征形状,输出特征形状为单个标量,因此为1。

# nn是神经网络的缩写
from torch import nn

net = nn.Sequential(nn.Linear(2, 1))

在Keras中,全连接层在Dense类中定义。 由于我们只想得到一个标量输出,所以我们将该数字设置为1。

值得注意的是,为了方便使用,Keras不要求我们为每个层指定输入形状。 所以在这里,我们不需要告诉Keras有多少输入进入这一层。 当我们第一次尝试通过我们的模型传递数据时,例如,当后面执行net(X)时, Keras会自动推断每个层输入的形状。 本节稍后将详细介绍这种工作机制。

# keras是TensorFlow的高级API
net = tf.keras.Sequential()
net.add(tf.keras.layers.Dense(1))

在PaddlePaddle中,全连接层在Linear类中定义。 值得注意的是,我们将两个参数传递到nn.Linear中。 第一个指定输入特征形状,即2,第二个指定输出特征形状,输出特征形状为单个标量,因此为1。

# nn是神经网络的缩写
from paddle import nn

net = nn.Sequential(nn.Linear(2, 1))

3.3.4. 初始化模型参数

在使用net之前,我们需要初始化模型参数。 如在线性回归模型中的权重和偏置。 深度学习框架通常有预定义的方法来初始化参数。 在这里,我们指定每个权重参数应该从均值为0、标准差为0.01的正态分布中随机采样, 偏置参数将初始化为零。

我们从MXNet导入initializer模块,这个模块提供了各种模型参数初始化方法。 Gluon将init作为访问initializer包的快捷方式。 我们可以通过调用init.Normal(sigma=0.01)来指定初始化权重的方法。 默认情况下,偏置参数初始化为零。

from mxnet import init

net.initialize(init.Normal(sigma=0.01))

上面的代码可能看起来很简单,但是这里有一个应该注意到的细节: 我们正在为网络初始化参数,而Gluon还不知道输入将有多少维! 网络的输入可能有2维,也可能有2000维。 Gluon让我们避免了这个问题,在后端执行时,初始化实际上是推迟(deferred)执行的, 只有在我们第一次尝试通过网络传递数据时才会进行真正的初始化。 请注意,因为参数还没有初始化,所以我们不能访问或操作它们。

正如我们在构造nn.Linear时指定输入和输出尺寸一样, 现在我们能直接访问参数以设定它们的初始值。 我们通过net[0]选择网络中的第一个图层, 然后使用weight.databias.data方法访问参数。 我们还可以使用替换方法normal_fill_来重写参数值。

net[0].weight.data.normal_(0, 0.01)
net[0].bias.data.fill_(0)
tensor([0.])

TensorFlow中的initializers模块提供了多种模型参数初始化方法。 在Keras中最简单的指定初始化方法是在创建层时指定kernel_initializer。 在这里,我们重新创建了net

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

上面的代码可能看起来很简单,但是这里有一个应该注意到的细节: 我们正在为网络初始化参数,而Keras还不知道输入将有多少维! 网络的输入可能有2维,也可能有2000维。 Keras让我们避免了这个问题,在后端执行时,初始化实际上是推迟(deferred)执行的。 只有在我们第一次尝试通过网络传递数据时才会进行真正的初始化。 请注意,因为参数还没有初始化,所以我们不能访问或操作它们。

正如我们在构造nn.Linear时指定输入和输出尺寸一样, 现在我们能直接访问参数以设定它们的初始值。 我们通过net[0]选择网络中的第一个图层, 然后使用weightbias方法访问参数。 我们可以通过调用nn.initializer.Normal(0, 0.01)来指定初始化权重的方法。 默认情况下,偏置参数初始化为零。

weight_attr = paddle.ParamAttr(initializer=
                               paddle.nn.initializer.Normal(0, 0.01))
bias_attr = paddle.ParamAttr(initializer=None)
net = nn.Sequential(nn.Linear(2, 1, weight_attr=weight_attr,
                              bias_attr=bias_attr))

3.3.5. 定义损失函数

在Gluon中,loss模块定义了各种损失函数。 在这个例子中,我们将使用Gluon中的均方误差(L2Loss)。

loss = gluon.loss.L2Loss()

计算均方误差使用的是MSELoss类,也称为平方\(L_2\)范数。 默认情况下,它返回所有样本损失的平均值。

loss = nn.MSELoss()

计算均方误差使用的是MeanSquaredError类,也称为平方\(L_2\)范数。 默认情况下,它返回所有样本损失的平均值。

loss = tf.keras.losses.MeanSquaredError()

计算均方误差使用的是MSELoss类,也称为平方\(L_2\)范数。 默认情况下,它返回所有样本损失的平均值。

loss = nn.MSELoss()

3.3.6. 定义优化算法

小批量随机梯度下降算法是一种优化神经网络的标准工具, Gluon通过Trainer类支持该算法的许多变种。 当我们实例化Trainer时,我们要指定优化的参数 (可通过net.collect_params()从我们的模型net中获得)、 我们希望使用的优化算法(sgd)以及优化算法所需的超参数字典。 小批量随机梯度下降只需要设置learning_rate值,这里设置为0.03。

from mxnet import gluon

trainer = gluon.Trainer(net.collect_params(), 'sgd', {'learning_rate': 0.03})

小批量随机梯度下降算法是一种优化神经网络的标准工具, PyTorch在optim模块中实现了该算法的许多变种。 当我们实例化一个SGD实例时,我们要指定优化的参数 (可通过net.parameters()从我们的模型中获得)以及优化算法所需的超参数字典。 小批量随机梯度下降只需要设置lr值,这里设置为0.03。

trainer = torch.optim.SGD(net.parameters(), lr=0.03)

小批量随机梯度下降算法是一种优化神经网络的标准工具, Keras在optimizers模块中实现了该算法的许多变种。 小批量随机梯度下降只需要设置learning_rate值,这里设置为0.03。

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

小批量随机梯度下降算法是一种优化神经网络的标准工具, PaddlePaddle在optimizer模块中实现了该算法的许多变种。 小批量随机梯度下降只需要设置learning_rate值,这里设置为0.03。

trainer =  paddle.optimizer.SGD(learning_rate=0.03,
                                parameters=net.parameters())

3.3.7. 训练

通过深度学习框架的高级API来实现我们的模型只需要相对较少的代码。 我们不必单独分配参数、不必定义我们的损失函数,也不必手动实现小批量随机梯度下降。 当我们需要更复杂的模型时,高级API的优势将大大增加。 当我们有了所有的基本组件,训练过程代码与我们从零开始实现时所做的非常相似。

回顾一下:在每个迭代周期里,我们将完整遍历一次数据集(train_data), 不停地从中获取一个小批量的输入和相应的标签。 对于每一个小批量,我们会进行以下步骤:

  • 通过调用net(X)生成预测并计算损失l(前向传播)。

  • 通过进行反向传播来计算梯度。

  • 通过调用优化器来更新模型参数。

为了更好的衡量训练效果,我们计算每个迭代周期后的损失,并打印它来监控训练过程。

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}')
[07:03:00] ../src/base.cc:48: GPU context requested, but no GPUs found.
epoch 1, loss 0.024902
epoch 2, loss 0.000086
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.000248
epoch 2, loss 0.000103
epoch 3, loss 0.000103
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.000198
epoch 2, loss 0.000098
epoch 3, loss 0.000098
num_epochs = 3
for epoch in range(num_epochs):
    for i,(X, y) in enumerate (data_iter()):
        l = loss(net(X) ,y)
        trainer.clear_grad()
        l.backward()
        trainer.step()
    l = loss(net(features), labels)
    print(f'epoch {epoch + 1},'f'loss {l}')
epoch 1,loss Tensor(shape=[1], dtype=float32, place=Place(cpu), stop_gradient=False,
       [0.00033613])
epoch 2,loss Tensor(shape=[1], dtype=float32, place=Place(cpu), stop_gradient=False,
       [0.00011017])
epoch 3,loss Tensor(shape=[1], dtype=float32, place=Place(cpu), stop_gradient=False,
       [0.00010988])

下面我们比较生成数据集的真实参数和通过有限数据训练获得的模型参数。 要访问参数,我们首先从net访问所需的层,然后读取该层的权重和偏置。 正如在从零开始实现中一样,我们估计得到的参数与生成数据的真实参数非常接近。

w = net[0].weight.data()
print(f'w的估计误差: {true_w - w.reshape(true_w.shape)}')
b = net[0].bias.data()
print(f'b的估计误差: {true_b - b}')
w的估计误差: [0.00050712 0.0001173 ]
b的估计误差: [0.00094557]
w = net[0].weight.data
print('w的估计误差:', true_w - w.reshape(true_w.shape))
b = net[0].bias.data
print('b的估计误差:', true_b - b)
w的估计误差: tensor([-0.0010, -0.0003])
b的估计误差: tensor([-0.0003])
w = net.get_weights()[0]
print('w的估计误差:', true_w - tf.reshape(w, true_w.shape))
b = net.get_weights()[1]
print('b的估计误差:', true_b - b)
w的估计误差: tf.Tensor([ 0.00078022 -0.00024295], shape=(2,), dtype=float32)
b的估计误差: [-0.00017881]
w = net[0].weight
print('w的估计误差:', true_w - w.reshape(true_w.shape))
b = net[0].bias
print('b的估计误差:', true_b - b)
w的估计误差: Tensor(shape=[2], dtype=float32, place=Place(cpu), stop_gradient=False,
       [-0.00036430, -0.00014281])
b的估计误差: Tensor(shape=[1], dtype=float32, place=Place(cpu), stop_gradient=False,
       [-0.00020885])

3.3.8. 小结

  • 我们可以使用Gluon更简洁地实现模型。

  • 在Gluon中,data模块提供了数据处理工具,nn模块定义了大量的神经网络层,loss模块定义了许多常见的损失函数。

  • MXNet的initializer模块提供了各种模型参数初始化方法。

  • 维度和存储可以自动推断,但注意不要在初始化参数之前尝试访问参数。

  • 我们可以使用PyTorch的高级API更简洁地实现模型。

  • 在PyTorch中,data模块提供了数据处理工具,nn模块定义了大量的神经网络层和常见损失函数。

  • 我们可以通过_结尾的方法将参数替换,从而初始化参数。

  • 我们可以使用TensorFlow的高级API更简洁地实现模型。

  • 在TensorFlow中,data模块提供了数据处理工具,keras模块定义了大量神经网络层和常见损耗函数。

  • TensorFlow的initializers模块提供了多种模型参数初始化方法。

  • 维度和存储可以自动推断,但注意不要在初始化参数之前尝试访问参数。

3.3.9. 练习

  1. 如果将小批量的总损失替换为小批量损失的平均值,需要如何更改学习率?

  2. 查看深度学习框架文档,它们提供了哪些损失函数和初始化方法?用Huber损失代替原损失,即

    (3.3.1)\[\begin{split}l(y,y') = \begin{cases}|y-y'| -\frac{\sigma}{2} & \text{ if } |y-y'| > \sigma \\ \frac{1}{2 \sigma} (y-y')^2 & \text{ 其它情况}\end{cases}\end{split}\]
  3. 如何访问线性回归的梯度?