回归(regression)是能为一个或多个自变量与因变量之间关系建模的一类方法。 在自然科学和社会科学领域,回归经常用来表示输入和输出之间的关系。
在机器学习领域中的大多数任务通常都与预测(prediction)有关。 当我们想预测一个数值时,就会涉及到回归问题。 常见的例子包括:预测价格(房屋、股票等)、预测住院时间(针对住院病人等)、 预测需求(零售销量等)。
环境使用 Kaggle 里免费建立的 Notebook
教程使用李沐老师的 动手学深度学习 网站和 视频讲解
小技巧:当遇到函数看不懂的时候可以按 Shift+Tab
查看函数详解。
为了解释线性回归,我们举一个实际的例子: 我们希望根据房屋的面积(平方英尺)和房龄(年)来估算房屋价格(美元)。 为了开发一个能预测房价的模型,我们需要收集一个真实的数据集。 这个数据集包括了房屋的销售价格、面积和房龄。 在机器学习的术语中,该数据集称为训练数据集(training data set) 或训练集(training set)。 每行数据(比如一次房屋交易相对应的数据)称为样本(sample), 也可以称为数据点(data point)或数据样本(data instance)。 我们把试图预测的目标(比如预测房屋价格)称为标签(label)或目标(target)。 预测所依据的自变量(面积和房龄)称为特征(feature)或协变量(covariate)。
目标(房屋价格)可以表示为特征(面积和房龄)的加权和,如下式子:
p r i c e = w a r e a ⋅ a r e a + w a g e ⋅ a g e + b \mathrm{price} = w_{\mathrm{area}} \cdot \mathrm{area} + w_{\mathrm{age}} \cdot \mathrm{age} + b price=warea⋅area+wage⋅age+b
w a r e a w_{area} warea 和 w a g e w_{age} wage 称为权重(weight),权重决定了每个特征对我们预测值的影响。
b b b 称为偏置(bias)、偏移量(offset)或截距(intercept)。 偏置是指当所有特征都取值为0时,预测值应该为多少。 即使现实中不会有任何房子的面积是0或房龄正好是0年,我们仍然需要偏置项。 如果没有偏置项,我们模型的表达能力将受到限制。
上述式子是输入特征的一个 仿射变换(affine transformation)。 仿射变换的特点是通过加权和对特征进行线性变换(linear transformation), 并通过偏置项来进行平移(translation)。
在机器学习领域,我们通常使用的是高维数据集,建模时采用线性代数表示法会比较方便。 当我们的输入包多个特征时,我们将预测结果 y ^ \hat{y} y^(通常使用“尖角”符号表示 y y y 的估计值)用点积形式简洁表示:
y ^ = w ⊤ x + b . \hat{y} = \mathbf{w}^\top \mathbf{x} + b. y^=w⊤x+b.
在拟合数据之前我们需要评价模型质量的度量方式。损失函数(loss function)能够量化目标的实际值与预测值之间的差距。 通常我们会选择非负数作为损失,且数值越小表示损失越小,完美预测时的损失为0。 回归问题中最常用的损失函数是平方误差函数。
回归问题中最常用的损失函数是平方误差函数。 当样本 i i i 的预测值为 y ^ ( i ) \hat{y}^{(i)} y^(i),其相应的真实标签为 y ( i ) y^{(i)} y(i) 时, 平方误差可以定义为以下公式:
l ( i ) ( w , b ) = 1 2 ( y ^ ( i ) − y ( i ) ) 2 . l^{(i)}(\mathbf{w}, b) = \frac{1}{2} \left(\hat{y}^{(i)} - y^{(i)}\right)^2. l(i)(w,b)=21(y^(i)−y(i))2.
其中 1 2 \frac{1}{2} 21 对本质无影响,就是为了方便,求导后的式子系数为 1。
如果每次都遍历整个数据集之后才进行更新参数,计算量大,故常采用小批量随机梯度下降法。
梯度下降方法最关键的参数就是学习率和批量。
学习率设置不能太大也不能太小。
学习率太大会造成收敛缓慢,陷入局部最优解。
学习率太小可能会导致在最优解附近震荡。
批量大小不能太大也不能太小。
批量太小,不适合进行并行计算,不能最大利用计算资源。
批量太大,内存消耗增加,浪费计算。
!pip install -U d2l
%matplotlib inline
import random
import torch
from d2l import torch as d2l
d2l 模块是李沐老师的库,在 kaggle 中的 notebook 中用 !pip install -U d2l
安装即可。
使用线性模型参数 w = [ 2 , − 3.4 ] ⊤ \mathbf{w} = [2, -3.4]^\top w=[2,−3.4]⊤、 b = 4.2 b = 4.2 b=4.2 和噪声项 ϵ \epsilon ϵ 生成数据集及其标签:
y = X w + b + ϵ . \mathbf{y}= \mathbf{X} \mathbf{w} + b + \mathbf\epsilon. y=Xw+b+ϵ.
# 生成数据
# w, b, 样本数
def synthetic_data(w, b, num_examples):
# X 为矩阵,表示生成 样本数*len(w) 的(0,1)正态分布的矩阵。
# len(w) 表示特征数
X = torch.normal(0, 1, (num_examples, len(w)))
y = torch.matmul(X, w) + b
# y = Xw + b + 噪声,噪声呈现标准差为 0.01 的正态分布。
y += torch.normal(0, 0.01, y.shape)
# (-1, 1) 代表作为列向量返回
return X, y.reshape((-1, 1))
true_w = torch.tensor([2, -3.4])
true_b = 4.2
features, labels = synthetic_data(true_w, true_b, 1000)
# 查看数据分布
d2l.set_figsize()
d2l.plt.scatter(features[:, (1)].detach().numpy(), labels.detach().numpy(), 1);
# 最后一个参数为点的大小,也可以传入一个为向量设置每个点的大小
# 每次抽取一小批量样本,并使用它们来更新我们的模型
def data_iter(batch_size, features, labels):
num_examples = len(features)
indices = list(range(num_examples))
# 这些样本是随机读取的,没有特定的顺序,用 shuffle 打乱顺序
random.shuffle(indices)
for i in range(0, num_examples, batch_size):
# min 函数为了解决样本数不能整除批量的情况,最后一个 batch 截取剩余部分
batch_indices = torch.tensor(
indices[i: min(i + batch_size, num_examples)])
# yield 的作用就是把一个函数变成一个 generator
yield features[batch_indices], labels[batch_indices]
我们定义一个data_iter函数, 该函数接收批量大小、特征矩阵和标签向量作为输入,生成大小为batch_size的小批量。 每个小批量包含一组特征和标签。
随机初始化参数,并计算梯度。
划重点: 需要 w w w, b b b 进行更新,所以才将 requires_grad
设置为 True
w = torch.normal(0, 0.01, size=(2,1), requires_grad=True)
b = torch.zeros(1, requires_grad=True)
def linreg(X, w, b):
"""线性回归模型"""
return torch.matmul(X, w) + b
# y_hat 为预测值,y 为真实值
def squared_loss(y_hat, y):
"""均方损失"""
return (y_hat - y.reshape(y_hat.shape)) ** 2 / 2
reshape
是为了统一形状。
# 参数,学习率,批量
def sgd(params, lr, batch_size):
"""小批量随机梯度下降"""
# 更新时不参与梯度计算
with torch.no_grad():
for param in params:
param -= lr * param.grad / batch_size
param.grad.zero_()
Python参数传递采用的肯定是“传对象引用”的方式。这种方式相当于传值和传引用的一种综合。
如果函数收到的是一个可变对象(比如字典或者列表)的引用,就能修改对象的原始值,相当于通过“传引用”来传递对象。
如果函数收到的是一个不可变对象(比如数字、字符或者元组)的引用,就不能直接修改原始对象,相当于通过“传值’来传递对象。
在每次迭代中,我们读取一小批量训练样本,并通过我们的模型来获得一组预测。 计算完损失后,我们开始反向传播,存储每个参数的梯度。 最后,我们调用优化算法 sgd 来更新模型参数。
概括一下,我们将执行以下循环:
初始化参数
重复以下训练,直到完成
2.1 计算梯度: g ← ∂ ( w , b ) 1 ∣ B ∣ ∑ i ∈ B l ( x ( i ) , y ( i ) , w , b ) \mathbf{g} \leftarrow \partial_{(\mathbf{w},b)} \frac{1}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} l(\mathbf{x}^{(i)}, y^{(i)}, \mathbf{w}, b) g←∂(w,b)∣B∣1∑i∈Bl(x(i),y(i),w,b)
2.2 更新参数: ( w , b ) ← ( w , b ) − η g (\mathbf{w}, b) \leftarrow (\mathbf{w}, b) - \eta \mathbf{g} (w,b)←(w,b)−ηg
# 学习率
lr = 0.03
# 数据便利三遍
num_epochs = 3
# 为了方便换成别的模型和损失函数
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]的梯度,sum 求总梯度,sgd 里会根据平均梯度进行优化。
l.sum().backward()
sgd([w, b], lr, batch_size) # 使用参数的梯度更新参数
with torch.no_grad():
# 这里 labels 是真实值
train_l = loss(net(features, w, b), labels)
print(f'epoch {epoch + 1}, loss {float(train_l.mean()):f}')
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)
def load_array(data_arrays, batch_size, is_train=True):
"""构造一个 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)
next(iter(data_iter))
TensorDataset
提供一种方式去获取数据和 labels。
DataLoader
为后面的训练提供数据形式。
星号 ∗ * ∗ 的作用是为了解包,并且解包后的结果可以用于传递参数。
在 PyTorch 中,全连接层在
Linear
类中定义。 值得注意的是,我们将两个参数传递到nn.Linear
中。 第一个指定输入特征形状,即2
,第二个指定输出特征形状,输出特征形状为单个标量,因此为1
。
# nn是神经网络的缩写
from torch import nn
net = nn.Sequential(nn.Linear(2, 1))
我们通过
net[0]
选择网络中的第一个图层, 然后使用weight.data
和bias.data
方法访问参数。 我们还可以使用替换方法normal_
和fill_
来重写参数值。
net[0].weight.data.normal_(0, 0.01)
net[0].bias.data.fill_(0)
net[0].weight, net[0].bias
计算均方误差使用的是
MSELoss
类,也称为平方范数。 默认情况下,它返回所有样本损失的平均值。
loss = nn.MSELoss()
当我们实例化一个 SGD 实例时,我们要指定优化的参数 (可通过
net.parameters()
从我们的模型中获得)以及优化算法所需的超参数字典。 小批量随机梯度下降只需要设置lr
值,这里设置为0.03
。
trainer = torch.optim.SGD(net.parameters(), lr=0.03)
在每个迭代周期里,我们将完整遍历一次数据集(train_data), 不停地从中获取一个小批量的输入和相应的标签。 对于每一个小批量,我们会进行以下步骤:
net(X)
生成预测并计算损失 l
(前向传播)。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)
# l:f 代表输出 float
print(f'epoch {epoch + 1}, loss {l:f}')
如果用 nn.MSELoss(reduction=‘sum’)
替换 nn.MSELoss()
,应该把学习率除以 batch_size
,因为默认参数是 mean
,换成 sum
需要除以批量数,一般会采用默认,因为这样学习率可以跟 batch_size
解耦。
print(net[0].weight.grad, net[0].bias.grad)