假设现有一些数据点,我们希望用一条直线对这些点进行拟合(该线成为最佳拟合直线),这个拟合过程就称作回归

模型

对于某一个样本:

给定nn维输入:x=[x1,x2,...,xn]T\mathbf{x}=[x_1, x_2, ..., x_n]^T

以及nn维权重:w=[w1,w2,...,wn]T\mathbf{w}=[w_1, w_2, ..., w_n]^T、一个标量偏差bb

输出是输入的加权和:

y=w1x1+w2x2+...+wnxn+by=w_1x_1+w_2x_2+...+w_nx_n+b

向量版本:

y=<w,x>+by=<\mathbf{w},\mathbf{x}>+b

线性模型可看作单层神经网络

衡量预估质量

假设某一样本ii,预测值为y^\hat{y},而yy 是真实标签,则平方误差:

l(i)(w,b)=12(y(i)y^(i))2l^{(i)}(\mathbf{w}, b)=\frac{1}{2}(y^{(i)}-\hat{y}^{(i)})^2

对于mm个样本,记为:
X=[x1,x2,...,xm]T,Y=[y1,y2,...,ym]T\mathbf{X}=[\mathbf{x_1}, \mathbf{x_2},...,\mathbf{x_m}]^T, \mathbf{Y}=[\mathbf{y_1}, \mathbf{y_2},...,\mathbf{y_m}]^T

为度量模型在整个数据集上的质量,需计算mm 个样本上的均方误差(Mean Squared Error, MSE):

L(w,b)=1mi=1ml(i)=12mi=1m(wTx(i)+by(i))2=12mXw+by2L(\mathbf{w}, b)= \frac{1}{m}\sum_{i=1}^{m}l^{(i)} =\frac{1}{2 m} \sum_{i=1}^{m}(\mathbf{w}^T\mathbf{x}^{(i)}+b-y^{(i)})^{2} =\frac{1}{2 m}\|\mathbf{X} \mathbf{w}+b-\mathbf{y}\|^{2}

在训练过程中应关心如何将损失最小化,而不是过多地关注损失值具体多少

参数学习与梯度下降

最小化损失来学习参数:

w,b=argminw,b L(w,b)\mathbf{w}^{*}, \mathbf{b}^{*}=\underset{\mathbf{w}, b}{\arg \min }\ L(\mathbf{w}, b)

也就说,找出一个w,b\mathbf{w}^{*},b^{*},使得损失函数最小化。

由于线性模型中损失函数是凸函数,故最优解满足:

w=(XTX)1XTy.\mathbf{w}^* = (\mathbf X^T \mathbf X)^{-1}\mathbf X^T \mathbf{y}.

并非所有问题均存在最优解,限制严格,无法真正应用在深度学习中

为优化绝大部分深度学习模型,梯度下降法(gradient descent),即不断沿着反梯度的方向更新参数求解。

挑选一个初始值w0\mathbf{w_0},重复迭代参数:wt=wt1αLwt1\mathbf{w}_t=\mathbf{w}_{t-1}-\alpha\frac{\partial L}{\partial \mathbf{w}_{t-1}},其中,α\alpha 为学习率(步长的超参数)

小批量随机梯度下降:每次需要计算更新的时候随机采样B\mathcal{B}个样本(B\mathcal{B},批量大小,也是重要的超参数):i1,i2,...,ibi_1,i_2,...,i_b(无需遍历整个数据集)来近似损失:

故更新过程:

wwαBiBwl(i)(w,b)=wαBiBx(i)(wTx(i)+by(i)),bbαBiBbl(i)(w,b)=bαBiB(wTx(i)+by(i)).\begin{aligned} \mathbf{w} &\leftarrow \mathbf{w} - \frac{\alpha}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} \partial_{\mathbf{w}} l^{(i)}(\mathbf{w}, b) = \mathbf{w} - \frac{\alpha}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} \mathbf{x}^{(i)} \left(\mathbf{w}^T \mathbf{x}^{(i)} + b - y^{(i)}\right),\\ b &\leftarrow b - \frac{\alpha}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} \partial_b l^{(i)}(\mathbf{w}, b) = b - \frac{\alpha}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} \left(\mathbf{w}^T \mathbf{x}^{(i)} + b - y^{(i)}\right). \end{aligned}

一般来说,求解梯度是最耗费时间的。因而小批量随机梯度下降,是深度学习默认的求解算法。

因此,梯度下降的超参数:批量大小学习率

基于PyTorch框架的实现(顺序块)

使用深度学习框架PyTorch来读入数据、训练模型

准备数据集

1
2
3
4
5
6
7
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)

读取数据集

调用框架中现有的API来读入数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 构造PyTorch数据迭代器
def load_array(data_arrays, batch_size, is_train=True):
# 首先对样本数据及其标注进行打包,从而两者一一对应
dataset = data.TensorDataset(*data_arrays)
# 返回一个按批量且满足随机的封装好的数据迭代器
# 每一次随机挑取batch_size的样本
return data.DataLoader(dataset, batch_size, shuffle = is_train)

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

# 读取并打印第一批量的样本数据及其标注
next(iter(data_iter))
[tensor([[ 0.0388,  0.7633],
         [ 0.0443, -1.0373],
         [ 0.4865, -1.2277],
         [-0.0102,  1.1730],
         [-0.8657, -0.1770],
         [-0.0660, -0.5046],
         [ 0.9981,  0.0407],
         [ 0.5265,  0.2863],
         [-0.5265,  1.4245],
         [ 0.0316,  0.6915]]),
 tensor([[ 1.6896],
         [ 7.8047],
         [ 9.3269],
         [ 0.1825],
         [ 3.0589],
         [ 5.7834],
         [ 6.0574],
         [ 4.2643],
         [-1.6963],
         [ 1.9182]])]

在变量前加*,则多余的函数参数会作为一个元组存在args中

定义模型

使用框架预定义好的层(只需关注使用哪些层来构造模型,而不必关注层的实现细节)。

Sequential 类为串联在一起的多个层定义了一个容器。当给定输入数据, Sequential 实例将数据传入到第一层,然后将第一层的输出作为第二层的输入,依此类推,形成一个标准的流水线。

1
2
3
4
5
from torch import nn # nn是神经网络的缩写
# nn.Linear() 能够计算y_hat,自动完成w^Tx+b操作
net = nn.Sequential(nn.Linear(2, 1)) # 模型变量net 是 Sequential 类的实例
# 需指定输入特征形状、输出特征形状,Sequential()返回list of layers,网络每一层按顺序排列
net
1
2
3
Sequential(
(0): Linear(in_features=2, out_features=1, bias=True)
)

初始化模型参数

指定每个权重参数从均值为0、标准差为0.01的正态分布中随机采样,偏置参数将初始化为零。

1
2
3
# 该网络只有一层
net[0].weight.data.normal_(0, 0.01) # weight访问参数w,normal_()即使用正态分布替换w的data值
net[0].bias.data.fill_(0) # 将偏差值b设为0

定义损失函数及优化器

计算均方误差,使用 MSELoss 类,也成为平方L2L_2范数。

实例化 SGD 实例,得到优化器

1
2
3
4
# 损失函数会自动构建计算图,可以传入reduction='sum',从而决定是否求均值
loss = nn.MSELoss()
# 实例化优化器,他不会构建计算图,需要指定优化参数(如模型参数、学习率),它会知道对哪些权重进行优化
trainer = torch.optim.SGD(net.parameters(), lr = 0.03)

PyTorch 在 optim 模块中实现了小批量随机梯度下降算法的许多变种。

训练

注意区分几个名词:

  • Epoch:迭代周期,一次 Epoch 中所有训练集数据都要参与训练

  • Batch-Size:每次训练的样本数量,批量大小

  • Iteration:迭代次数,指训练整个数据集需要迭代多少个批量

在每个迭代周期里,我们将完整遍历一次数据集,不停地从中获取一个小批量的输入和相应的标签。

对于每一个小批量,有以下步骤:

  • 通过调用 net(X) 生成预测值,并计算损失 total_loss(正向传播)。
  • 通过进行反向传播来计算损失函数对应的梯度。
  • 通过调用优化器来更新模型参数。
1
2
3
4
5
6
7
8
9
10
11
num_epochs = 3
for epoch in range(num_epochs):
for X, y in data_iter:
total_loss = loss(net(X), y) # 前馈过程,传入预测值以及真实值y,返回总的损失值(非向量)
trainer.zero_grad() # 清空梯度
total_loss.backward() # 反向传播
trainer.step() # 模型的更新

# 计算每个迭代周期后的损失,并打印它来监控训练过程
total_loss = loss(net(features), labels)
print(f'epoch {epoch + 1}, loss {total_loss:f}')
epoch 1, loss 0.000260
epoch 2, loss 0.000089
epoch 3, loss 0.000089

[附]:线性回归的从零开始实现

1
2
3
4
5
import random
import torch
from d2l import torch as d2l
import os
os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE"

生成数据集

带有噪声的线性模型构造一个人造数据集:线性模型参数w=[2,3.4]T\mathbf{w}=[2, -3.4]^Tb=4.2b=4.2以及噪声项生成ϵ\epsilon生成数据集及其标签

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def sysnthetic_data(w, b, m): # m为样本数量
# 均值为0、方差为1的随机数组成的m*len(w)的矩阵
X = torch.normal(0, 1, (m, len(w)))
# y = Xw + b
y = torch.matmul(X, w) + b
# 同时加入随机噪音
y += torch.normal(0, 0.01, y.shape)
return X, y.reshape((-1, 1))
# 等价于reshape((n,1))

# 真实值:
true_w = torch.tensor([2, -3.4])
true_b = 4.2
# 生成数据集以及标注
features, labels = sysnthetic_data(true_w, true_b, 1000)
print("第一个数据:" + str(features[0]) + " " + str(labels[0]))

# 绘制第一个特征的数据及其对应的标注的图
d2l.set_figsize()
d2l.plt.scatter(features[:, 1].detach().numpy(),
# detach分离出数值(不再含梯度)
labels.detach().numpy(), 1)
第一个数据:tensor([1.1518, 0.0801]) tensor([6.2316])

注意到,features 中每一行均包含一个二维数据样本,而labels中每一行都包含一维标签值(一个标量)

读取数据集

定义一个函数,用于接受批量大小、特征矩阵、标签向量作为输入,以生成大小为 batch_size 的小批量,每个小批量包含一组特征以及标签。(简而言之,定义的函数能够打乱数据集中的样本并以小批量的方式获取数据

手动实现的缺点:需要将所有数据加载到内存中,并执行大量的随机内存访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def data_iter(batch_size, features, labels):
m = len(features)
# 获取0到m-1的下标数组
indices = list(range(m))
# 打乱下标,用以随机读取样本
random.shuffle(indices)
for i in range(0, m, batch_size):
batch_indices = torch.tensor(
indices[i : min(i+batch_size, m)]
) # 取批量的下标,转化为tensor
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
tensor([[ 1.1161, -0.3515],
        [-0.7912,  0.1008],
        [-1.2919, -2.4004],
        [ 2.1880,  0.6330],
        [ 1.5566,  0.6568],
        [-0.8294, -0.1094],
        [ 0.4108, -0.5271],
        [ 0.5568,  0.0646],
        [ 0.4603, -1.7273],
        [-1.1144,  2.1087]]) 
 tensor([[ 7.6340],
        [ 2.2774],
        [ 9.7785],
        [ 6.4373],
        [ 5.0671],
        [ 2.9212],
        [ 6.8226],
        [ 5.0832],
        [10.9887],
        [-5.2094]])

yield 即返回值的同时记住这个返回的位置,下次迭代就从这个位置开始。

初始化模型参数

从均值为0、标准差为0.01的正态分布中采样随机数来初始化权重,并将偏置初始化为0

1
2
w = torch.normal(0, 0.01, size=(2, 1), requires_grad=True)
b = torch.zeros(1, requires_grad=True)

定义线性回归模型

将模型的输入和参数同模型的输出关联起来

1
2
def linreg(X, w, b):
return torch.matmul(X, w) + b # 采用广播机制,向量+标量,标量会被加到向量的每个分量上

定义(单个样本)损失函数:

1
2
def squared_loss(y_hat, y): # 传入预测值、真实值
return (y_hat - y.reshape(y_hat.shape)) ** 2 / 2 # 确保形状相同

定义优化算法

小批量随机梯度下降:
pt=pt1αLpt1\mathbf{p}_t=\mathbf{p}_{t-1}-\alpha\frac{\partial L}{\partial \mathbf{p}_{t-1}}

用批量大小(batch_size)来归一化步长,这样步长大小就不会取决于我们对批量大小的选择。

1
2
3
4
5
6
def sgd(params, lr, batch_size): 
# params的值包含w,b、lr(学习率)、batch_size
with torch.no_grad():
for param in params:
param -= lr * param.grad / batch_size
param.grad.zero_() # 手动设置梯度为0

with 关键字能够自动处理上下文环境产生的异常。并非所有操作都需要进行计算图的生成(计算过程的构建,以便梯度反向传播等操作),通过torch.no_grad()来强制包裹的内容不进行计算图的构建。

训练

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 指定超参数
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): # 遍历每一批
# 当前批中每一数据的损失向量
loss_list = loss(net(X, w, b), y)
# 先得到该批量的损失之和,再求其关于[w, b]的梯度
loss_list.sum().backward()
sgd([w, b], lr, batch_size)
with torch.no_grad():
train_loss = loss(net(features, w, b), labels)
print(f'epoch {epoch + 1}, loss {float(train_loss.mean()):f}')

epoch 1, loss 0.042724
epoch 2, loss 0.000161
epoch 3, loss 0.000049

比较真实参数与通过训练学习的参数,评估训练的成功程度:

1
2
print(f'w的估计误差:{true_w - w.reshape(true_w.shape)}')
print(f'b的估计误差:{true_b - b}')
w的估计误差:tensor([ 0.0001, -0.0004], grad_fn=<SubBackward0>)
b的估计误差:tensor([0.0013], grad_fn=<RsubBackward1>)