在机器学习领域中的大多数任务通常都与预测有关。 当我们想预测一个数值时,就会涉及到回归问题。 常见的例子包括:预测价格(房屋、股票等)、预测住院时间(针对住院病人等)、 预测需求(零售销量等)。 本篇文章将深入讨论线性回归的原理,并动手实现一个线性回归模型。
1、线性回归的定义
回归(regression)是能为一个或多个自变量与因变量之间关系建模的一类方法。 在自然科学和社会科学领域,回归经常用来表示输入和输出之间的关系。线性则代表自变量与因变量之间存在着线性关系。
在回归分析中,只包括一个自变量和一个因变量,且二者的关系可用一条直线近似表示,这种回归分析称为一元线性回归分析。如果回归分析中包括两个或两个以上的自变量,且因变量和自变量之间是线性关系,则称为多元线性回归分析。
2、线性回归的基本元素
线性回归是机器学习领域中最基础的算法模型,为了更好地解释线性回归,我们举一个实际的例子: 我们希望根据房屋的面积(平方英尺)和房龄(年)来估算房屋价格(美元)。 为了开发一个能预测房价的模型,我们需要收集一个真实的数据集。 这个数据集包括了房屋的销售价格、面积和房龄。在机器学习的术语中,该数据集称为训练数据集(training data set) 或训练集(training set)。 每行数据(比如一次房屋交易相对应的数据)称为样本(sample), 也可以称为数据点(data point)或数据样本(data instance)。 我们把试图预测的目标(比如预测房屋价格)称为标签(label)或目标(target)。 预测所依据的自变量(面积和房龄)称为特征(feature)或协变量(covariate)。
3、线性回归模型
线性回归基于几个简单的假设: 首先,假设自变量x和因变量y之间的关系是线性的, 即y可以表示为x中元素的加权和,这里通常允许包含观测值的一些噪声; 其次,我们假设任何噪声都比较正常,如噪声遵循正态分布。
线性假设是指目标(房屋价格)可以表示为特征(面积和房龄)的加权和,如下面的式子:
p
r
i
c
e
=
w
a
r
e
a
⋅
a
r
e
a
+
w
a
g
e
⋅
a
g
e
+
b
.
(1)
price=w_{area}·area+w_{age}·age+b.\tag{1}
price=warea⋅area+wage⋅age+b.(1)
公式(1)中的
w
a
r
e
a
w_{area}
warea和
w
a
g
e
w_{age}
wage被称为权重(weight),决定每个特征对最终预测值的影响。
b
b
b称为偏置(bias)、偏移量(offset)。偏移量可以增强我们模型的表达能力,使其能够拟合更多的模型。
在机器学习领域,我们通常使用的是高维的数据集,因此采用线性代数的方式来建模会很方便。我们将所有的特征放在向量
x
∈
R
d
\pmb{x}\in ℝ^d
xx∈Rd中,并将所有权重放在向量
w
∈
R
d
\pmb{w}\in ℝ^d
ww∈Rd中,其中
d
d
d是特征维度,并用
y
^
\hat y
y^来表示模型的预测结果,从而我们可以用点积形式来简洁地表达模型:
y
^
=
w
T
x
+
b
.
(2)
\hat y=\pmb{w}^T\pmb{x}+b.\tag{2}
y^=wwTxx+b.(2)
在公式(2)中,向量
x
\pmb{x}
xx对应单个数据样本的特征,此时我们用矩阵
X
∈
R
n
×
d
\pmb{X}\inℝ^{n×d}
XX∈Rn×d来引用整个数据集的
n
n
n个样本的特征,
X
\pmb{X}
XX的每一行是一个样本,每一列是一种特征。
那么对于特征集合
X
\pmb{X}
XX,预测值
y
^
∈
R
n
\hat y\inℝ^n
y^∈Rn可以通过矩阵-向量乘法来表示:
y
^
=
X
w
+
b
.
(3)
\hat y=\pmb{X}\pmb{w}+b.\tag{3}
y^=XXww+b.(3)
预测房价模型的公式就可以用(3)来进行表示,我们通过该模型就可以根据面积和房龄等信息来预测房价,但问题在于我们无法确定
w
w
w和
b
b
b的值,线性回归就是寻找出一组权重
w
w
w和偏置
b
b
b,当给定来自
X
\pmb{X}
XX中取样的独立同分布的新样本特征时,这组权重和偏置可以使得最终的预测值
y
^
\hat y
y^与真实值
y
y
y之间的误差尽可能小。
4、损失函数
在开始寻找最好的模型参数(model parameters)w和b之前, 我们还需要两个东西: (1)衡量模型的质量:衡量预测值与真实值之间的误差(2)一种能够更新模型以提高模型预测质量的方法。
如何去衡量预测值与真实值之间的误差?在回归问题中,常使用最小二乘法来衡量模型的质量,其定义为:
l
(
i
)
(
w
,
b
)
=
1
2
(
y
^
(
i
)
−
y
(
i
)
)
2
.
(4)
l^{(i)}(\pmb{w},b)=\frac{1}{2}(\hat y^{(i)}-y^{(i)})^2.\tag{4}
l(i)(ww,b)=21(y^(i)−y(i))2.(4)
(4)也就是该模型的损失函数,其中
i
i
i代表第
i
i
i个样本,
l
(
i
)
(
w
,
b
)
l^{(i)}(\pmb{w},b)
l(i)(ww,b)就是第
i
i
i个样本的预测误差。
上述只是对于每一条样本的预测误差,而我们最终的目的是为了去拟合整个数据集上的样本,因此为了度量模型在整个数据集上的质量,我们需要计算在训练集
n
n
n个样本上的损失均值:
L
(
w
,
b
)
=
1
n
∑
i
=
1
n
l
(
i
)
(
w
T
,
b
)
=
1
n
∑
i
=
1
n
1
2
[
(
w
T
x
(
i
)
+
b
)
−
y
(
i
)
]
2
.
(5)
L(w,b)=\frac{1}{n}\sum_{i=1}^{n}l^{(i)}(\pmb{w}^T,b)\\ \quad\quad\quad\quad\quad\quad\quad\quad\quad =\frac{1}{n}\sum_{i=1}^{n}\frac{1}{2} [(\pmb{w}^Tx^{(i)}+b)-y^{(i)}]^2.\tag{5}
L(w,b)=n1i=1∑nl(i)(wwT,b)=n1i=1∑n21[(wwTx(i)+b)−y(i)]2.(5)
训练模型也就是去寻找一组参数(
w
∗
,
b
∗
\pmb{w}^*,b^*
ww∗,b∗)来最小化在所有训练样本上总损失的过程:
w
∗
,
b
∗
=
a
r
g
m
i
n
w
,
b
L
(
w
,
b
)
.
(6)
\pmb{w}^*,b^*=\mathop{argmin}_{\pmb{w},b}\,L(\pmb{w},b).\tag{6}
ww∗,b∗=argminww,bL(ww,b).(6)
5、梯度下降
在第4节中我们提出用最小二乘法的损失函数来衡量模型的质量,那怎么去更新模型的参数来提高模型的质量?
我们将(5)继续化简:
L
=
1
2
n
∑
i
=
1
n
b
2
+
2
(
w
T
x
(
i
)
−
y
(
i
)
)
b
+
(
y
(
i
)
−
w
T
x
(
i
)
)
2
.
(7)
L=\frac{1}{2n}\sum_{i=1}^{n}b^2+2(\pmb{w}^Tx^{(i)}-y^{(i)})b+(y^{(i)}-\pmb{w}^Tx^{(i)})^2.\tag{7}
L=2n1i=1∑nb2+2(wwTx(i)−y(i))b+(y(i)−wwTx(i))2.(7)
可以从(7)得出:损失函数是关于
b
b
b的二次函数。
我们的目标是找到损失函数的极小值(最优值),但直接求最优解是一件很难的事情,并且只有线性回归模型才能最有值,在其他复杂的模型中,我们都只能无限逼近最优解,因此我们换一种思路来求解:
我们求当前损失函数
L
L
L对
b
b
b的偏导,就可以得出函数值的变化趋势。导数为正,则代表函数值呈上升趋势;导数为负,则代表函数值呈下降趋势。我们需要求函数的最小值,因此沿着导数的反方向就能逐渐靠近最优值:
b
←
b
−
ϵ
∂
L
∂
b
.
(8)
b\leftarrow\,b\,-\,\epsilon\frac{\partial\,L}{\partial\,b}.\tag{8}
b←b−ϵ∂b∂L.(8)
(8)中的
ϵ
\epsilon
ϵ是学习率,可以理解为沿着当前方向走多少“步”。
由上可推,
w
\pmb{w}
ww的更新与
b
b
b类似,因此:
(
w
,
b
)
←
(
w
,
b
)
−
ϵ
∂
(
w
,
b
)
l
(
i
)
(
w
,
b
)
.
(9)
(\pmb w,b)\leftarrow(\pmb{w},b)-\epsilon\,\partial_{(\pmb{w},b)}l^{(i)}(\pmb{w},b).\tag{9}
(ww,b)←(ww,b)−ϵ∂(ww,b)l(i)(ww,b).(9)
但上述,我们想要更新参数就必须计算所有的样本的梯度,然后根据梯度更新参数,计算量太多,模型的拟合速度慢,因此我们引入批量随机梯度下降来更新参数。所谓的批量随机梯度下降计算梯度下降和随机梯度下降的结合,每一次随机从样本集中抽取
β
\beta
β个样本来进行训练,然后计算这
β
\beta
β个样本的平均梯度来更新参数:
(
w
,
b
)
←
(
w
,
b
)
−
ϵ
∣
β
∣
∑
i
∈
β
∂
(
w
,
b
)
l
(
i
)
(
w
,
b
)
.
(9)
(\pmb w,b)\leftarrow(\pmb{w},b)-\frac{\epsilon}{|\beta|} \sum_{i\in\beta}\partial_{(\pmb{w},b)}l^{(i)}(\pmb{w},b).\tag{9}
(ww,b)←(ww,b)−∣β∣ϵi∈β∑∂(ww,b)l(i)(ww,b).(9)
(9)中的
∣
β
∣
|\beta|
∣β∣是小批量样本的个数,被称为批量大小(batch size);
ϵ
\epsilon
ϵ是学习率,是在模型训练前预先手动指定的参数,而不是训练得到的,因此也被称为超参数(hyperparameter)。
总结一下算法的步骤如下:(1)初始化模型参数的值,如随机初始化;(2)从数据集中随机抽取小批量样本且在梯度的反方向上更新参数,不断迭代这一步骤。
6、从线性回归到神经网络
神经网络中包括了各种的模型,本节我们用神经网络的方式来描述线性回归模型:
(图3)中只显示了输入层与输出层之间的连接,隐去了权重和偏置,我们可以看出线性回归模型其实就是一个单层的神经网络模型,且只有一个输出。
7、从零开始实现线性回归模型
import random
import torch
from d2l import torch as d2l
为了简单起见,我们将根据带有噪声的线性模型构造⼀个数据集。在下面的代码中,我们使用线性模型参数
w
=
[
2
,
−
3.4
]
T
\pmb{w}=[2,-3.4]^T
ww=[2,−3.4]T、
b
=
4.2
b=4.2
b=4.2和噪声项
ϵ
\epsilon
ϵ生成一个包含1000个样本的数据集及其标签,每个样本包含从标准正态分布中采样的2个特征,该数据集是一个矩阵
X
∈
R
1000
×
2
\pmb{X}\inℝ^{1000×2}
XX∈R1000×2,标签
y
∈
R
1000
\pmb{y}\inℝ^{1000}
yy∈R1000:
y
=
X
w
+
b
+
ϵ
.
(10)
\pmb{y}=\pmb{Xw}+b+\epsilon.\tag{10}
yy=XwXw+b+ϵ.(10)
将ϵ视为模型预测和标签时的潜在观测误差。我们认为标准假设成立,即ϵ服从均值为0的正态分布。为了简化问题,我们将标准差设为0.01。生成数据集和标签的函数:
def synthetic_data(w, b, num_examples): #@save
"""⽣成y=Xw+b+噪声"""
X = torch.normal(0, 1, (num_examples, len(w)))
y = torch.matmul(X, w) + b
y += torch.normal(0, 0.01, y.shape)
return X, y.reshape((-1, 1))
设定正确的参数值 w w w和 b b b,并调用函数生成数据集和标签:
true_w = torch.tensor([2, -3.4])
true_b = 4.2
features, labels = synthetic_data(true_w, true_b, 1000)
features里面的每一个样本都是包含二维特征的向量,labels的每一行都是一维标签值:
for i in range(0, len(features)):
print('features:', features[i],'\nlabel:', labels[i])
我们采用小批量随机梯度下降来训练模型,每次抽取一小批量的数据集,并使用它们来更新我们的模型。在下面的代码中,我们定义⼀个data_iter函数,该函数接收批量大小、特征矩阵和标签向量作为输入,生成大小为batch_size的小批量。每个小批量包含⼀组特征和标签:
def data_iter(batch_size, features, labels):
num_examples = len(features)
indices = list(range(num_examples)) # 根据样本集长度生成索引列表
# 这些样本是随机读取的,没有特定的顺序
random.shuffle(indices)
for i in range(0, num_examples, batch_size):
batch_indices = torch.tensor(indices[i: min(i + batch_size, num_examples)])
yield features[batch_indices], labels[batch_indices]
为了更加直观地去观察小批量抽取数据集的过程,我们打印小批量:
batch_size = 10
for X, y in data_iter(batch_size, features, labels):
print(X, '\n', y)
break
在我们开始训练模型之前,我们需要初始化模型的参数。在下面的代码中,我们通过从均值为0、标准差为0.01的正态分布中采样随机数来初始化权重,并将偏置初始化为0:
w = torch.normal(0, 0.01, size=(2,1), requires_grad=True)
b = torch.zeros(1, requires_grad=True)
我们在初始化参数时,为参数开启了梯度,有了这个梯度,我们就可以向减小损失的方向更新每个参数。因为手动计算梯度很枯燥而且容易出错,所以没有人会手动计算梯度。
接下来就是定义我们的线性回归模型:
def linreg(X, w, b): #@save
"""线性回归模型"""
return torch.matmul(X, w) + b
用平方损失函数来评估模型质量:
def squared_loss(y_hat, y): #@save
"""均⽅损失"""
return (y_hat - y.reshape(y_hat.shape)) ** 2 / 2
定义模型优化算法。在每一次更新模型参数之前,我们会使用从数据集中随机抽取的一个小批量来计算损失的梯度,然后向梯度的反方向来更新参数。下面的函数实现了小批量随机梯度下降更新,该函数接收模型参数集合、学习速率和批量大小作为输入,每一步的更新大小由学习率 l r lr lr决定。由于我们计算的是一个批量样本的总和损失,因此需要用批量大小(batch_size)来规范化步长,这样步长大小就不会依赖于我们对批量大小的选择:
def sgd(params, lr, batch_size): #@save
"""⼩批量随机梯度下降"""
with torch.no_grad():
for param in params:
param -= lr * param.grad / batch_size
param.grad.zero_()
在开始训练模型之前,我们先设置超参数:
lr = 0.03
num_epochs = 5
net = linreg
loss = squared_loss
开始训练模型:
for epoch in range(num_epochs):
for X, y in data_iter(batch_size, features, labels):
l = loss(net(X, w, b), y) # X和y的⼩批量损失
# 因为l形状是(batch_size,1),⽽不是⼀个标量。l中的所有元素被加到⼀起,
# 并以此计算关于[w,b]的梯度
l.sum().backward()
sgd([w, b], lr, batch_size) # 使⽤参数的梯度更新参数
with torch.no_grad():
train_l = loss(net(features, w, b), labels)
print(f'epoch {epoch + 1}, loss {float(train_l.mean()):f}')
因为数据集是根据我们自己设定的参数来生成的,真实参数已知,所以我们可以用真实参数与训练更新的参数来进行对比,可以看出偏差是非常小的:
print(f'w的估计误差: {true_w - w.reshape(true_w.shape)}')
print(f'b的估计误差: {true_b - b}')
8、总结
本篇文章通过房价预测问题来引入线性回归模型,然后利用平方损失函数来衡量模型的质量并用小批量随机梯度下降来优化模型质量。在最后,我们从零开始用pytorch实现了一个简单的线性回归模型。
参考资料
1、动手学深度学习 Release2.0.0-beta0