3.1 线性回归
线性回归输出是一个连续值,因此适用于回归问题。回归问题在实际中很常见,如预测房屋价格、气温、销售额等连续值的问题。与回归问题不同,分类问题中模型的最终输出是一个离散值。我们所说的图像分类、垃圾邮件识别、疾病检测等输出为离散值的问题都属于分类问题的范畴。softmax回归则适用于分类问题。
由于线性回归和softmax 回归都是单层神经网络,它们涉及的概念和技术同样适用于大多数的深度学习模型。我们首先以线性回归为例,介绍大多数深度学习模型的基本要素和表示方法。
3.1.1 线性回归的基本要素
我们以一个简单的房屋价格预测作为例子来解释线性回归的基本要素。这个应用的目标是预测一栋房子的售出价格(元)。我们知道这个价格取决于很多因素,如房屋状况、地段、市场行情等。为了简单起见,这里我们假设价格只取决于房屋状况的两个因素,即面积(平方米)和房龄(年)。接下来我们希望探索价格与这两个因素的具体关系。
1.模型
设房屋的面积为x1,房龄为x2,售出价格为y。我们需要建立基于输入x1 和x2来计算输出y的表达式,也就是模型(model)。顾名思义,线性回归假设输出与各个输入之间是线性关系:
其中
和
是权重(weight),b是偏差(bias),且均为标量。它们是线性回归模型的参数(parameter)。模型输出
是线性回归对真实价格 y的预测或估计。我们通常允许它们之间有一定误差。
2.模型训练
接下来我们需要通过数据来寻找特定的模型参数值,使模型在数据上的误差尽可能小。这个过程叫作模型训练(model training)。下面我们介绍模型训练所涉及的3个要素。
3.训练数据
我们通常收集一系列的真实数据,例如多栋房屋的真实售出价格和它们对应的面积和房龄。我们希望在这个数据上面寻找模型参数来使模型的预测价格与真实价格的误差最小。在机器学习术语里,该数据集被称为训练数据集(training data set)或训练集(training set),一栋房屋被称为一个样本(sample),其真实售出价格叫作标签(label),用来预测标签的两个因素叫作特征(feature)。特征用来表征样本的特点。
假设我们采集的样本数为n,索引为i的样本的特征为
和
,标签为
。对于索引为i的房屋,线性回归模型的房屋价格预测表达式为
4.损失函数
在模型训练中,我们需要衡量价格预测值与真实值之间的误差。通常我们会选取一个非负数作为误差,且数值越小表示误差越小。一个常用的选择是平方函数。它在评估索引为i 的样本误差的表达式为
其中常数1/2使对平方项求导后的常数系数为 1,这样在形式上稍微简单一些。显然,误差越小表示预测价格与真实价格越相近,且当二者相等时误差为 0。给定训练数据集,这个误差只与模型参数相关,因此我们将它记为以模型参数为参数的函数。在机器学习里,将衡量误差的函数称为损失函数(loss function)。这里使用的平方误差函数也称为平方损失(square loss)。
通常,我们用训练数据集中所有样本误差的平均来衡量模型预测的质量,即
在模型训练中,我们希望找出一组模型参数,记为
,来使训练样本平均损失最小:
5.优化算法
当模型和损失函数形式较为简单时,上面的误差最小化问题的解可以直接用公式表达出来。这类解叫作解析解(analytical solution)。本节使用的线性回归和平方误差刚好属于这个范畴。然而,大多数深度学习模型并没有解析解,只能通过优化算法有限次迭代模型参数来尽可能降低损失函数的值。这类解叫作数值解(numerical solution)。
在求数值解的优化算法中,小批量随机梯度下降(mini-batch stochastic gradient descent)在深度学习中被广泛使用。它的算法很简单:先选取一组模型参数的初始值,如随机选取;接下来对参数进行多次迭代,使每次迭代都可能降低损失函数的值。在每次迭代中,先随机均匀采样一个由固定数目训练数据样本所组成的小批量(mini-batch)
,然后求小批量中数据样本的平均损失有关模型参数的导数(梯度),最后用此结果与预先设定的一个正数的乘积作为模型参数在本次迭代的减小量。
在训练本节讨论的线性回归模型的过程中,模型的每个参数将作如下迭代:
在上式中,
代表每个小批量中的样本个数(批量大小,batch size),η称作学习率(learning rate)并取正数。需要强调的是,这里的批量大小和学习率的值是人为设定的,并不是通过模型训练学出的,因此叫作超参数(hyperparameter)。我们通常所说的“调参”指的正是调节超参数,例如通过反复试错来找到超参数合适的值。在少数情况下,超参数也可以通过模型训练学出。本书对此类情况不做讨论。
6.模型预测
模型训练完成后,我们将模型参数
在优化算法停止时的值分别记作
。注意,这里我们得到的并不一定是最小化损失函数的最优解
,而是对最优解的一个近似。然后,我们就可以使用学出的线性回归模型
来估算训练数据集以外任意一栋面积(平方米)为
、房龄(年)为
的房屋的价格了。这里的估算也叫作模型预测、模型推断或模型测试。
3.1.2 线性回归的表示方法
我们已经阐述了线性回归的模型表达式、训练和预测。下面我们解释线性回归与神经网络的联系,以及线性回归的矢量计算表达式。
1.神经网络图
在深度学习中,我们可以使用神经网络图直观地表现模型结构。为了更清晰地展示线性回归作为神经网络的结构,图3-1 使用神经网络图表示本节中介绍的线性回归模型。神经网络图隐去了模型参数权重和偏差。
图3-1 线性回归是一个单层神经网络
在图3-1所示的神经网络中,输入分别为x1 和x2,因此输入层的输入个数为 2。输入个数也叫特征数或特征向量维度。图3-1中网络的输出为o,输出层的输出个数为 1。需要注意的是,我们直接将图3-1 中神经网络的输出o作为线性回归的输出,即
。由于输入层并不涉及计算,按照惯例,图3-1 所示的神经网络的层数为 1。所以,线性回归是一个单层神经网络。输出层中负责计算o的单元又叫神经元。在线性回归中,o的计算依赖于x1和x2。也就是说,输出层中的神经元和输入层中各个输入完全连接。因此,这里的输出层又叫全连接层(fully-connected layer)或稠密层 ( dense layer)。
2.矢量计算表达式
在模型训练或预测时,我们常常会同时处理多个数据样本并用到矢量计算。在介绍线性回归的矢量计算表达式之前,让我们先考虑对两个向量相加的两种方法。
下面先定义两个 1 000 维的向量。
In [1]: from mxnet import nd from time import time a = nd.ones(shape=1000) b = nd.ones(shape=1000)
向量相加的一种方法是,将这两个向量按元素逐一做标量加法。
In [2]: start = time() c = nd.zeros(shape=1000) for i in range(1000): c[i] = a[i] + b[i] time() - startOut[2]: 0.16967248916625977
向量相加的另一种方法是,将这两个向量直接做矢量加法。
In [3]: start = time() d = a + b time() - startOut[3]: 0.00031185150146484375
结果很明显,后者比前者更省时。因此,我们应该尽可能采用矢量计算,以提升计算效率。
让我们再次回到本节的房价预测问题。如果我们对训练数据集里的3个房屋样本(索引分别为1、2和3)逐一预测价格,将得到
现在,我们将上面3个等式转化成矢量计算。设
对 3 个房屋样本预测价格的矢量计算表达式为
,其中的加法运算使用了广播机制(参见2.2节)。例如:
In [4]: a = nd.ones(shape=3) b = 10 a + bOut[4]: [11. 11. 11.]
广义上讲,当数据样本数为n,特征数为d时,线性回归的矢量计算表达式为
其中模型输出
,批量数据样本特征
,权重
, 偏差
。相应地,批量数据样本标签
。设模型参数
,我们可以重写损失函数为
小批量随机梯度下降的迭代步骤将相应地改写为
其中梯度是损失有关3个为标量的模型参数的偏导数组成的向量:
小结
练习
使用其他包(如 NumPy)或其他编程语言(如 MATLAB),比较相加两个向量的两种方法的运行时间。
3.2 线性回归的从零开始实现
在了解了线性回归的背景知识之后,现在我们可以动手实现它了。尽管强大的深度学习框架可以减少大量重复性工作,但若过于依赖它提供的便利,会导致我们很难深入理解深度学习是如何工作的。因此,本节将介绍如何只利用NDArray和autograd来实现一个线性回归的训练。
首先,导入本节中实验所需的包或模块,其中的matplotlib包可用于作图,且设置成嵌入显示。
In [1]: %matplotlib inline from IPython import display from matplotlib import pyplot as plt from mxnet import autograd, nd import random
3.2.1 生成数据集
我们构造一个简单的人工训练数据集,它可以使我们能够直观比较学到的参数和真实的模型参数的区别。设训练数据集样本数为 1000,输入个数(特征数)为2。给定随机生成的批量样本特征
,我们使用线性回归模型真实权重
和偏差 ![b=4.2](http://latex.codecogs.com/gif.latex?b=4.2,以及一个随机噪声项ϵ来生成标签
其中噪声项ϵ服从均值为 0、标准差为 0.01 的正态分布。噪声代表了数据集中无意义的干扰。下面,让我们生成数据集。
In [2]: num_inputs = 2 num_examples = 1000 true_w = [2, -3.4] true_b = 4.2 features = nd.random.normal(scale=1, shape=(num_examples, num_inputs)) labels = true_w[0] * features[:, 0] + true_w[1] * features[:, 1] + true_b labels += nd.random.normal(scale=0.01, shape=labels.shape)
注意,features的每一行是一个长度为 2 的向量,而labels的每一行是一个长度为1的向量(标量)。
In [3]: features[0], labels[0]Out[3]: ( [2.2122064 0.7740038] , [6.000587] )
通过生成第二个特征features[:, 1]和标签labels的散点图,可以更直观地观察两者间的线性关系。
In [4]: def use_svg_display(): # 用矢量图显示 display.set_matplotlib_formats('svg') def set_f igsize(f igsize=(3.5, 2.5)): use_svg_display() # 设置图的尺寸 plt.rcParams['f igure.f igsize'] = f igsize set_f igsize() plt.scatter(features[:, 1].asnumpy(), labels.asnumpy(), 1); # 加分号只显示图
我们将上面的plt作图函数以及use_svg_display函数和set_f igsize函数定义在d2lzh包里。以后在作图时,我们将直接调用d2lzh.plt。由于plt在d2lzh包中是一个全局变量,我们在作图前只需要调用d2lzh.set_figsize()即可打印矢量图并设置图的尺寸。
3.2.2 读取数据集
在训练模型的时候,我们需要遍历数据集并不断读取小批量数据样本。这里我们定义一个函数:它每次返回batch_size(批量大小)个随机样本的特征和标签。
In [5]: # 本函数已保存在d2lzh包中方便以后使用 def data_iter(batch_size, features, labels): num_examples = len(features) indices = list(range(num_examples)) random.shuff le(indices) # 样本的读取顺序是随机的 for i in range(0, num_examples, batch_size): j = nd.array(indices[i: min(i + batch_size, num_examples)]) yield features.take(j), labels.take(j) # take函数根据索引返回对应元素
让我们读取第一个小批量数据样本并打印。每个批量的特征形状为(10, 2),分别对应批量大小和输入个数;标签形状为批量大小。
In [6]: batch_size = 10 for X, y in data_iter(batch_size, features, labels): print(X, y) break[[ 1.0876857 -1.7063738 ] [-0.51129895 0.46543437] [ 0.1533563 -0.735794 ] [ 0.3717077 0.9300072 ] [ 1.0115732 -0.83923554] [ 1.9738784 0.81172043] [-1.771029 -0.45138445] [ 0.7465509 -0.5054337 ] [-0.52480155 0.3005414 ] [ 0.5583534 -0.6039059 ]][12.174357 1.6139998 6.9870367 1.7626053 9.06552 5.3893285 2.1933131 7.4012175 2.1383817 7.379732 ]
3.2.3 初始化模型参数
我们将权重初始化成均值为 0、标准差为 0.01 的正态随机数,偏差则初始化成 0。
In [7]: w = nd.random.normal(scale=0.01, shape=(num_inputs, 1)) b = nd.zeros(shape=(1,))
之后的模型训练中,需要对这些参数求梯度来迭代参数的值,因此我们需要创建它们的梯度。
In [8]: w.attach_grad() b.attach_grad()
3.2.4 定义模型
下面是线性回归的矢量计算表达式的实现。我们使用dot函数做矩阵乘法。
In [9]: def linreg(X, w, b): # 本函数已保存在d2lzh包中方便以后使用 return nd.dot(X, w) + b
3.2.5 定义损失函数
我们使用3.1节描述的平方损失来定义线性回归的损失函数。在实现中,我们需要把真实值y变形成预测值y_hat的形状。以下函数返回的结果也将和y_hat的形状相同。
In [10]: def squared_loss(y_hat, y): # 本函数已保存在d2lzh包中方便以后使用 return (y_hat - y.reshape(y_hat.shape)) ** 2 / 2
3.2.6 定义优化算法
以下的sgd函数实现了3.1节中介绍的小批量随机梯度下降算法。它通过不断迭代模型参数来优化损失函数。这里自动求梯度模块计算得来的梯度是一个批量样本的梯度和。我们将它除以批量大小来得到平均值。
In [11]: def sgd(params, lr, batch_size): # 本函数已保存在d2lzh包中方便以后使用 for param in params: param[:] = param - lr * param.grad / batch_size
3.2.7 训练模型
在训练中,我们将多次迭代模型参数。在每次迭代中,我们根据当前读取的小批量数据样本(特征X和标签y),通过调用反向函数backward计算小批量随机梯度,并调用优化算法sgd迭代模型参数。由于我们之前设批量大小batch_size为 10,每个小批量的损失l的形状为(10, 1)。回忆一下2.3节。由于变量l并不是一个标量,运行l.backward()将对l中元素求和得到新的变量,再求该变量有关模型参数的梯度。
在一个迭代周期(epoch)中,我们将完整遍历一遍data_iter函数,并对训练数据集中所有样本都使用一次(假设样本数能够被批量大小整除)。这里的迭代周期个数num_epochs和学习率lr都是超参数,分别设3和 0.03。在实践中,大多超参数都需要通过反复试错来不断调节。虽然迭代周期数设得越大模型可能越有效,但是训练时间可能过长。我们会在后面第7章中详细介绍学习率对模型的影响。
In [12]: lr = 0.03 num_epochs = 3 net = linreg loss = squared_loss for epoch in range(num_epochs): # 训练模型一共需要num_epochs个迭代周期 # 在每一个迭代周期中, 会使用训练数据集中所有样本一次(假设样本数能够被批量大小整除)。X # 和y分别是小批量样本的特征和标签 for X, y in data_iter(batch_size, features, labels): with autograd.record(): l = loss(net(X, w, b), y) # l是有关小批量X和y的损失 l.backward() # 小批量的损失对模型参数求梯度 sgd([w, b], lr, batch_size) # 使用小批量随机梯度下降迭代模型参数 train_l = loss(net(features, w, b), labels) print('epoch %d, loss %f' % (epoch + 1, train_l.mean().asnumpy()))epoch 1, loss 0.040436epoch 2, loss 0.000155epoch 3, loss 0.000050
训练完成后,我们可以比较学到的参数和用来生成训练集的真实参数。它们应该很接近。
In [13]: true_w, wOut[13]: ([2, -3.4], [[ 1.9996936] [-3.3997262]] )In [14]: true_b, bOut[14]: (4.2, [4.199704] )
小结
练习
(1)为什么squared_loss函数中需要使用reshape函数?
(2)尝试使用不同的学习率,观察损失函数值的下降快慢。
(3)如果样本个数不能被批量大小整除,data_iter函数的行为会有什么变化?
3.3 线性回归的简洁实现
随着深度学习框架的发展,开发深度学习应用变得越来越便利。实践中,我们通常可以用比3.2节更简洁的代码来实现同样的模型。在本节中,我们将介绍如何使用 MXNet 提供的Gluon接口更方便地实现线性回归的训练。
3.3.1 生成数据集
我们生成与3.2节中相同的数据集。其中features是训练数据特征,labels是标签。
In [1]: from mxnet import autograd, nd num_inputs = 2 num_examples = 1000 true_w = [2, -3.4] true_b = 4.2 features = nd.random.normal(scale=1, shape=(num_examples, num_inputs)) labels = true_w[0] * features[:, 0] + true_w[1] * features[:, 1] + true_b labels += nd.random.normal(scale=0.01, shape=labels.shape)
3.3.2 读取数据集
Gluon 提供了data包来读取数据。由于data常用作变量名,我们将导入的data模块用添加了 Gluon 首字母的假名gdata代替。在每一次迭代中,我们将随机读取包含 10 个数据样本的小批量。
In [2]: from mxnet.gluon import data as gdata batch_size = 10 # 将训练数据的特征和标签组合 dataset = gdata.ArrayDataset(features, labels) # 随机读取小批量 data_iter = gdata.DataLoader(dataset, batch_size, shuff le=True)
这里data_iter的使用与3.2节中的一样。让我们读取并打印第一个小批量数据样本。
In [3]: for X, y in data_iter: print(X, y) break[[-1.4011667 -1.108803 ] [-0.4813231 0.5334126 ] [ 0.57794803 0.72061497] [ 1.1208912 1.2570045 ] [-0.2504259 -0.45037505] [ 0.08554042 0.5336134 ] [ 0.6347856 1.5795654 ] [-2.118665 3.3493772 ] [ 1.1353118 0.99125063] [-0.4814555 -0.91107726]][ 5.16208 1.4169512 2.9065104 2.164263 5.215756 2.558468 0.09139667 -11.421704 3.1042643 6.332793 ]
3.3.3 定义模型
在3.2节从零开始的实现中,我们需要定义模型参数,并使用它们一步步描述模型是怎样计算的。当模型结构变得更复杂时,这些步骤将变得更烦琐。其实,Gluon 提供了大量预定义的层,这使我们只需关注使用哪些层来构造模型。下面将介绍如何使用 Gluon 更简洁地定义线性回归。
首先,导入nn模块。实际上,“nn”是 neural networks(神经网络)的缩写。顾名思义,该模块定义了大量神经网络的层。我们先定义一个模型变量net,它是一个 Sequential实例。在 Gluon 中,Sequential实例可以看作是一个串联各个层的容器。在构造模型时,我们在该容器中依次添加层。当给定输入数据时,容器中的每一层将依次计算并将输出作为下一层的输入。
In [4]: from mxnet.gluon import nn net = nn.Sequential()
回顾图3-1 中线性回归在神经网络图中的表示。作为一个单层神经网络,线性回归输出层中的神经元和输入层中各个输入完全连接。因此,线性回归的输出层又叫全连接层。在 Gluon 中,全连接层是一个Dense实例。我们定义该层输出个数为 1。
In [5]: net.add(nn.Dense(1))
值得一提的是,在 Gluon 中我们无须指定每一层输入的形状,例如线性回归的输入个数。当模型得到数据时,例如后面执行net(X)时,模型将自动推断出每一层的输入个数。我们将在第4章详细介绍这种机制。Gluon 的这一设计为模型开发带来便利。
3.3.4 初始化模型参数
在使用net前,我们需要初始化模型参数,如线性回归模型中的权重和偏差。我们从 MXNet 导入init模块。该模块提供了模型参数初始化的各种方法。这里的init是initializer的缩写形式。我们通过init.Normal(sigma=0.01)指定权重参数每个元素将在初始化时随机采样于均值为0、标准差为 0.01 的正态分布。偏差参数默认会初始化为零。
In [6]: from mxnet import init net.initialize(init.Normal(sigma=0.01))
3.3.5 定义损失函数
在 Gluon 中,loss模块定义了各种损失函数。我们用假名gloss代替导入的loss模块,并直接使用它提供的平方损失作为模型的损失函数。
In [7]: from mxnet.gluon import loss as gloss loss = gloss.L2Loss() # 平方损失又称L2范数损失
3.3.6 定义优化算法
同样,我们也无须实现小批量随机梯度下降。在导入 Gluon 后,我们创建一个Trainer实例,并指定学习率为 0.03 的小批量随机梯度下降(sgd)为优化算法。该优化算法将用来迭代net实例所有通过add函数嵌套的层所包含的全部参数。这些参数可以通过collect_params函数获取。
In [8]: from mxnet import gluon trainer = gluon.Trainer(net.collect_params(), 'sgd', {'learning_rate': 0.03})
3.3.7 训练模型
在使用 Gluon 训练模型时,我们通过调用Trainer实例的step函数来迭代模型参数。3.2节中我们提到,由于变量l是长度为batch_size的一维 NDArray,执行l.backward()等价于执行l.sum().backward()。按照小批量随机梯度下降的定义,我们在step函数中指明批量大小,从而对批量中样本梯度求平均。
In [9]: num_epochs = 3 for epoch in range(1, num_epochs + 1): 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('epoch %d, loss: %f' % (epoch, l.mean().asnumpy()))epoch 1, loss: 0.040309epoch 2, loss: 0.000153epoch 3, loss: 0.000050
下面我们分别比较学到的模型参数和真实的模型参数。我们从net获得需要的层,并访问其权重(weight)和偏差(bias)。学到的模型参数和真实的参数很接近。
In [10]: dense = net[0] true_w, dense.weight.data()Out[10]: ([2, -3.4], [[ 1.9996833 -3.3997345]] )In [11]: true_b, dense.bias.data()Out[11]: (4.2, [4.1996784] )
小结
练习
(1)如果将l = loss(net(X), y)替换成l = loss(net(X), y).mean(),我们需要将trainer.step(batch_size)相应地改成trainer.step(1)。这是为什么呢?
(2)查阅 MXNet 文档,看看gluon.loss和init模块里提供了哪些损失函数和初始化方法。
(3)如何访问dense.weight的梯度?
本文摘自最新上架的新书《动手学深度学习》
目前市面上有关深度学习介绍的书籍大多可分两类,一类侧重方法介绍,另一类侧重实践和深度学习工具的介绍。本书同时覆盖方法和实践。本书不仅从数学的角度阐述深度学习的技术与应用,还包含可运行的代码,为读者展示如何在实际中解决问题。为了给读者提供一种交互式的学习体验,本书不但提供免费的教学视频和讨论区,而且提供可运行的Jupyter记事本文件,充分利用Jupyter记事本能将文字、代码、公式和图像统一起来的优势。这样不仅直接将数学公式对应成实际代码,而且可以修改代码、观察结果并及时获取经验,从而带给读者全新的、交互式的深度学习的学习体验。