写在前面:
公式符号详见 论文数学公式编辑
参考书: 动手学深度学习
1 线性回归
回归是多(或单)个自变量与因变量之间关系建模的一类方法。 在机器学习中通常与预测有关。线性回归基于两个基本假设:
- 1.自变量 x \mathbf{x} x和因变量 y y y之间的关系是线性的, 即 y y y可以表示为 x \mathbf{x} x中元素的加权和。
- 2.噪声遵循正态分布。原因见文末第二节。
1.1 术语
因变量
y
i
y^i
yi称为:标签。
因变量
y
i
y^i
yi对应的自变量
x
i
=
[
x
1
i
,
x
2
i
,
⋯
,
x
d
i
]
⊤
\mathbf{x}^{i}=[x_1^{i},x_2^{i}, \cdots, x_d^i]^\top
xi=[x1i,x2i,⋯,xdi]⊤称为:样本。
自变量
x
i
\mathbf{x}^{i}
xi对应的每个元素
[
x
1
i
,
x
2
i
,
⋯
,
x
d
i
]
⊤
[x_1^{i},x_2^{i}, \cdots, x_d^i]^\top
[x1i,x2i,⋯,xdi]⊤称为:特征
PS: 通常,我们使用 n n n来表示数据集中的样本数。 也就是说数据集表示为 X n × d \mathbf{X}_{n \times d} Xn×d,n为样本数,d为特征数。
1.2 模型表示
输入包含
d
d
d个特征时,我们将预测结果
y
^
\hat{y}
y^表示为:
y
^
=
w
1
x
1
+
…
+
w
d
x
d
+
b
.
(1)
\hat{y}=w_1x_1+\ldots+w_dx_d+b. \tag{1}
y^=w1x1+…+wdxd+b.(1)
PS: 通常使用“尖角”符号表示估计值
将所有特征放到向量
x
∈
R
d
\mathbf{x}\in\mathbb{R}^{d}
x∈Rd中, 并将所有权重放到向量
w
∈
R
d
\mathbf{w}\in\mathbb{R}^{d}
w∈Rd中得:
y
^
=
w
⊤
x
+
b
.
(2)
\hat{y}=\mathbf{w}^\top\mathbf{x}+b. \tag{2}
y^=w⊤x+b.(2)
在公式(2)中,向量 x \mathbf{x} x为单个数据样本的特征。矩阵-向量乘法可以很方便地表示整个数据集n个样本模型计算为:
y
^
=
X
w
+
b
(3)
\hat{\mathbf{y}}=\mathbf{X}\mathbf{w}+b\tag{3}
y^=Xw+b(3)
1.3 损失函数
损失函数量化目标的真实值与预测值之间的差距。我们希望差距越小越好。即损失函数值越小越好。回归问题中最常用的损失函数是平方误差函数。
当样本的预测值为 y ^ ( i ) \hat{y}^{(i)} y^(i),其相应的真实标签为 y ( i ) y^{(i)} y(i)时, 平方误差可以定义为以下公式:
l ( i ) ( w , b ) = 1 2 ( y ^ ( i ) − y ( i ) ) 2 . (4) l^{(i)}(\mathbf{w},b)=\frac{1}{2}\Big(\hat{y}^{(i)}-y^{(i)}\Big)^2. \tag{4} l(i)(w,b)=21(y^(i)−y(i))2.(4)
度量模型在整个数据集上的质量,我们需计算在训练集n个样本上的损失均值。
L
(
w
,
b
)
=
1
n
∑
i
=
1
n
l
(
i
)
(
w
,
b
)
=
1
n
∑
i
=
1
n
1
2
(
w
⊤
x
(
i
)
+
b
−
y
(
i
)
)
2
.
(5)
L(\mathbf{w},b)=\frac1n\sum_{i=1}^nl^{(i)}(\mathbf{w},b)=\frac1n\sum_{i=1}^n\frac12\left(\mathbf{w}^\top\mathbf{x}^{(i)}+b-y^{(i)}\right)^2.\tag{5}
L(w,b)=n1i=1∑nl(i)(w,b)=n1i=1∑n21(w⊤x(i)+b−y(i))2.(5)
在训练模型时,我们希望寻找一组参数
(
w
∗
,
b
∗
)
(\mathbf{w}^*,b^*)
(w∗,b∗) 最小化所有样本总损失 如下式:
w
∗
,
b
∗
=
argmin
w
,
b
L
(
w
,
b
)
.
(6)
\mathbf{w}^*,b^*=\underset{\mathbf{w},b}{\operatorname*{argmin}} L(\mathbf{w},b). \tag{6}
w∗,b∗=w,bargminL(w,b).(6)
1.4 随机梯度下降
梯度下降几乎可以优化所有深度学习模型。
本质:使参数在损失函数递减的方向上更新来降低误差。
我们目标是模型输出结果贴近真实值,即误差小,即用于衡量误差的损失函数值小。那就每次算一下当前的损失函数在哪个方向上会减小,按照这个方向更新参数就会使损失函数值减小。随机是为了防止停在极小值点而非最小值点。
我们用下面的数学公式来表示这一更新过程:
( w , b ) ← ( w , b ) − η ∣ B ∣ ∑ i ∈ B ∂ ( w , b ) l ( i ) ( w , b ) . (7) (\mathbf{w},b)\leftarrow(\mathbf{w},b)-\frac\eta{|\mathcal{B}|}\sum_{i\in\mathcal{B}}\partial_{(\mathbf{w},b)}l^{(i)}(\mathbf{w},b). \tag{7} (w,b)←(w,b)−∣B∣ηi∈B∑∂(w,b)l(i)(w,b).(7)
η ∣ B ∣ \frac\eta{|\mathcal{B}|} ∣B∣η可以视为自定义系数,用于控制更新幅度。 B \mathcal{B} B是数据集的一个小批量。
对于平方损失,我们可以明确地写成如下形式:
w
←
w
−
η
∣
B
∣
∑
i
∈
B
∂
w
l
(
i
)
(
w
,
b
)
=
w
−
η
∣
B
∣
∑
i
∈
B
x
(
i
)
(
w
⊤
x
(
i
)
+
b
−
y
(
i
)
)
,
b
←
b
−
η
∣
B
∣
∑
i
∈
B
∂
b
l
(
i
)
(
w
,
b
)
=
b
−
η
∣
B
∣
∑
i
∈
B
(
w
⊤
x
(
i
)
+
b
−
y
(
i
)
)
.
(8)
\begin{gathered} \mathbf{w}\leftarrow\mathbf{w}-{\frac{\eta}{|\mathcal{B}|}}\sum_{i\in\mathcal{B}}\partial_{\mathbf{w}}l^{(i)}(\mathbf{w},b)=\mathbf{w}-{\frac{\eta}{|\mathcal{B}|}}\sum_{i\in\mathcal{B}}\mathbf{x}^{(i)}\left(\mathbf{w}^{\top}\mathbf{x}^{(i)}+b-y^{(i)}\right), \\ b\leftarrow b-\frac\eta{|\mathcal{B}|}\sum_{i\in\mathcal{B}}\partial_bl^{(i)}(\mathbf{w},b)=b-\frac\eta{|\mathcal{B}|}\sum_{i\in\mathcal{B}}\Big(\mathbf{w}^\top\mathbf{x}^{(i)}+b-y^{(i)}\Big). \end{gathered}\tag{8}
w←w−∣B∣ηi∈B∑∂wl(i)(w,b)=w−∣B∣ηi∈B∑x(i)(w⊤x(i)+b−y(i)),b←b−∣B∣ηi∈B∑∂bl(i)(w,b)=b−∣B∣ηi∈B∑(w⊤x(i)+b−y(i)).(8)
2 pytorch实现
2.1 不借助框架组件
只运用:(1)通过张量来进行数据存储和线性代数; (2)通过自动微分来计算梯度。
import random
import torch
from d2l import torch as d2l
# 定义一个函数来生成合成数据集
def synthetic_data(w, b, num_examples):
"""生成合成数据集,其中y = Xw + b + 噪声"""
X = torch.normal(0, 1, (num_examples, len(w))) # 生成特征矩阵,均值为0,标准差为1
y = torch.matmul(X, w) + b # 计算标签,为特征矩阵与权重向量的矩阵乘加偏置
y += torch.normal(0, 0.01, y.shape) # 在标签上添加均值为0,标准差为0.01的噪声
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)
# 定义一个数据迭代器函数,用于批量生成数据
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
# 初始化权重参数,并设置需要计算梯度
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 # 计算均方损失
# 定义小批量随机梯度下降算法
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 = 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) # 计算小批量数据的损失
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}') # 打印当前轮次的损失
2.2 简洁实现
import numpy as np
import torch
# 导入PyTorch的数据工具,用于数据加载和批处理
from torch.utils import data
from d2l import torch as d2l
# 定义真实的权重和偏置
true_w = torch.tensor([2, -3.4])
true_b = 4.2
# 使用d2l库中的函数生成合成数据集
features, labels = d2l.synthetic_data(true_w, true_b, 1000)
# 定义一个函数来构造PyTorch数据迭代器
def load_array(data_arrays, batch_size, is_train=True): #@save
"""构造一个PyTorch数据迭代器"""
dataset = data.TensorDataset(*data_arrays)
# 返回一个DataLoader对象,用于批量加载数据
return data.DataLoader(dataset, batch_size, shuffle=is_train)
# 设置批次大小
batch_size = 10
# 使用load_array函数创建数据迭代器
data_iter = load_array((features, labels), batch_size)
# 导入torch.nn模块,用于构建神经网络
from torch import nn
# 构建一个顺序模型,包含一个线性层
net = nn.Sequential(nn.Linear(2, 1))
# 初始化线性层的权重和偏置
net[0].weight.data.normal_(0, 0.01)
net[0].bias.data.fill_(0)
# 设置损失函数为均方误差损失
loss = nn.MSELoss()
# 设置优化器为随机梯度下降
trainer = torch.optim.SGD(net.parameters(), lr=0.03)
# 设置训练轮数
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() # 更新模型参数
# 在每个epoch结束时计算并打印整个数据集上的损失
l = loss(net(features), labels)
print(f'epoch {epoch + 1}, loss {l:f}')
# 获取训练后的权重和偏置
w = net[0].weight.data
# 计算并打印权重的估计误差
print('w的估计误差:', true_w - w.reshape(true_w.shape))
b = net[0].bias.data
# 计算并打印偏置的估计误差
print('b的估计误差:', true_b - b)
3 正态分布与平方损失
我们拿到的数据是有噪声的。文首所言线性模型的基本假设第二条为:噪声遵循正态分布。其原因为在高斯噪声的假设下,最小化均方误差等价于对线性模型的极大似然估计。
你可能会问不用平方损失就不是线性模型了?我用其他损失函数不也可以训练出来很好甚至更佳的效果吗?
个人见解:
- 在线性模型初问世时,需要模型可解释。噪音正态分布假设+最大似然可以解释线性模型。
- 当今AI领域许多模型其实没有很好的可解释性,因为我们更看重实际性能,而不在乎他是不是可解释的。
- 我们当然可以用其他的损失函数。一个不恰当的例子:就像我们用牛顿力学解释世界肯定不如加上相对论更准确,但是能用就行了,我不一定要准确的解释世界,我只要有个不错的结果就行了。
3.1 证明
有兴趣可以看一下,不是很必要。
假设模型考虑噪声:
y
=
w
⊤
x
+
b
+
ϵ
,
(10)
y=\mathbf{w}^\top\mathbf{x}+b+\epsilon, \tag{10}
y=w⊤x+b+ϵ,(10)
其中,
ϵ
∼
N
(
0
,
σ
2
)
\epsilon\sim\mathcal{N}(0,\sigma^2)
ϵ∼N(0,σ2)。正态分布概率密度函数如下:
p
(
x
)
=
1
2
π
σ
2
exp
(
−
1
2
σ
2
(
x
−
μ
)
2
)
.
(11)
p(x)=\frac{1}{\sqrt{2\pi\sigma^2}}\exp\left(-\frac{1}{2\sigma^2}(x-\mu)^2\right). \tag{11}
p(x)=2πσ21exp(−2σ21(x−μ)2).(11)
因此,我们现在可以写出通过给定的 x \mathbf{x} x观测到特定 y y y的似然(likelihood):
根据极大似然估计法,参数 w \mathbf{w} w和 b b b的最优值是使整个数据集的似然最大的值:
P ( y ∣ X ) = ∏ i = 1 n p ( y ( i ) ∣ x ( i ) ) . (12) P(\mathbf{y}\mid\mathbf{X})=\prod_{i=1}^np(y^{(i)}|\mathbf{x}^{(i)}). \tag{12} P(y∣X)=i=1∏np(y(i)∣x(i)).(12)
我们可以改为最小化负对数似然。 由此可以得到的数学公式是:
P
(
y
∣
x
)
=
1
2
π
σ
2
exp
(
−
1
2
σ
2
(
y
−
w
⊤
x
−
b
)
2
)
.
(13)
P(y\mid\mathbf{x})=\frac{1}{\sqrt{2\pi\sigma^2}}\exp\left(-\frac{1}{2\sigma^2}(y-\mathbf{w}^\top\mathbf{x}-b)^2\right).\tag{13}
P(y∣x)=2πσ21exp(−2σ21(y−w⊤x−b)2).(13)
σ \sigma σ是某个固定常数。 现在第二项除了常数外,其余部分和前面介绍的均方误差是一样的。上面式子的解并不依赖于 σ \sigma σ。 因此,在高斯噪声的假设下,最小化均方误差等价于对线性模型的极大似然估计。