一、线性模型

1.1 模型的数学表示

  • 输入:$\textbf x=[x_1,x_2,\dots,x_n]^T$
  • 权重:$\textbf w=[w_1, w_2, \dots, w_n]^T$,决定每个特征对预测值的英雄
  • 偏差:$b$,决定特征都为0时,预测值应为多少。
  • 输出为输入的加权和:$y=w_1x_1+w_2x_2+\dots+w_nx_n+b$
  • 向量形式的输出:$y=<\textbf w,\textbf x>+b$

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

1.2 衡量预估质量

衡量预估的质量就是比较真实值和预估值,假设$y$是真实值,$\hat y$是估计值,可以有多种形式衡量两者的差距,例如平方损失:$l(y,\hat y)=\frac{1}{2}(y-\hat y)^2$

1.3 训练数据的表示

通常我们会收集一些数据点包括训练样本和对应的真实值,记为:

$\textbf X=[\textbf x_1,\textbf x_2,\cdots,\textbf x_n]^T$

$\textbf y=[y_1,y_2,\cdots,y_n]^T$

其中$\textbf x_n$是列向量,代表每一个样本数据。这样$\textbf X$的每一行就是一个样本。

这样所有样本的预测值就可以表示为:

$\hat{\textbf y}=\textbf X\textbf w+b$

1.4 参数学习

训练的损失带入1.2中提到的平方损失公式。

$l(\textbf w,b)=\frac{1}{2}(y-\hat y)^2=\frac{1}{2}\left(\mathbf{w}^\top \mathbf{x} + b - \hat y\right)$

$L(\mathbf{w}, b) =\frac{1}{n}\sum_{i=1}^n l^{(i)}(\mathbf{w}, b) =\frac{1}{n} \sum_{i=1}^n \frac{1}{2}\left(\mathbf{w}^\top \mathbf{x}^{(i)} + b - y^{(i)}\right)^2.$

通过令$L(\textbf w,b)$最小,找到对应的参数$\textbf w$和$b$。

1.5 线性模型求解

为了便于计算,令$\textbf X=[\textbf X, 1]$,$\textbf w=[\textbf w, b]^T$,在$\textbf X$的最右侧添加全一的列,在$\textbf w$的下面添加标量$b$。这样就可以直接计算$\textbf y = \textbf X \textbf w$

带入损失函数:

$l(\textbf X,\textbf y,\textbf w)=\frac{1}{2n}||\textbf y-\textbf X \textbf w||^2$

$\frac{\partial}{\partial \textbf w}l(\textbf X,\textbf y,\textbf w)=\frac{1}{n}(\textbf y-\textbf X \textbf w)^T\textbf X$

由于损失函数是凸函数,所以最优解满足梯度为0,可以解得

$\textbf w^*=(\textbf X^T\textbf X)^{-1}\textbf X\textbf y$

1.6 线性回归的从零实现

(1)导入包

1
2
3
import random
import torch
from d2l import torch as d2l # pip install d2l

(2)构造数据集

创建一个带有噪声的人造数据集,使用真实的$w$和$b$生成数据集及标签。

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

features的每一行都包含一个二维数据样本,labels中的每一行都包含一个标签值。

(3)读取批量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def data_iter(batch_size, features, labels):
num_examples = len(features) # 样本数量
indices = list(range(num_examples)) # 生成每个样本的index
random.shuffle(indices) # 将index随机打乱
for i in range(0, num_examples, batch_size): # 步长为batch_size
# 获取i到i+batch_size的下标
batch_indices = torch.tensor(indices[i:min(i + batch_size, num_examples)])
# 每次yield返回一个值,下次调用从上次的yield开始
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

(4)初始化模型参数

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

(5)定义模型

1
2
3
def linreg(X, w, b):  
"""线性回归模型。"""
return torch.matmul(X, w) + b

(6)定义损失函数

1
2
3
def squared_loss(y_hat, y):  
"""均方损失。"""
return (y_hat - y.reshape(y_hat.shape))**2 / 2

(7)定义优化算法

1
2
3
4
5
6
def sgd(params, lr, batch_size):  
"""小批量随机梯度下降。"""
with torch.no_grad():
for param in params:
param -= lr * param.grad / batch_size
param.grad.zero_()

(8)训练过程

1
2
3
4
5
6
7
8
9
10
11
12
13
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}')

1.7 线性回归的简单实现

(1)导入包

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

(2)调用框架中现有的API读取数据

1
2
3
4
5
6
7
8
9
10
11
12
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)

# 下面代码用于在屏幕显示读取到的数据,打包成iter迭代器,然后依次读取
next(iter(data_iter))

(3)定义模型

1
2
3
4
5
6
from torch import nn

# 将线性层放到Sequential容器中
net = nn.Sequential(nn.Linear(2, 1)) # 指定输入维度2,输出维度1
net[0].weight.data.normal_(0, 0.01) # 第0层线性层 -> 权重w -> 数据 -> 使用正态分布替换data
net[0].bias.data.fill_(0) # 第0层线性层 -> 偏差b -> 数据 -> 填充0

(4)均方误差

1
loss = nn.MSELoss()

(5)随机梯度下降

1
trainer = torch.optim.SGD(net.parameters(), lr=0.03)

(6)训练过程

1
2
3
4
5
6
7
8
9
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)
print(f'epoch {epoch + 1}, loss {l:f}')

二、基础优化方法——梯度下降

当一个模型没有显式解时,一般通过这种方法求解。

  1. 挑选一个初始值$w_0$
  2. 在训练中重复迭代参数:$w_t=w_{t-1}-\eta\frac{\partial l}{\partial w_{t-1}}$($\eta$:学习率)

也就是说让$w$沿梯度方向(数值下降最快的方向)进行参数更新来找到最优解。类似于下山时一直找坡度最陡的方向走,最终到达山脚(最优解)。

(1)超参数1:学习率

其中的学习率代表每一次参数更新的大小,学习率太小参数更新很慢,学习率太大模型会震荡找不到最优解。

(2)超参数2:批量大小

由于在整个训练集上计算梯度时间消耗太大,可能需要数分钟乃至数个小时。

因此通常随机采样$b$个样本来近似训练集的损失。

同样批量大小不能太大,也不能太小。如果批量太小,不适合并行计算利用GPU资源,如果批量太大,内存消耗增加,浪费计算。

三、Softmax回归

上面提到的回归,目的是估计一个连续值。而分类问题是用来预测一个离散类别。Softmax回归虽然名字叫回归,但其实是一个分类问题。

常见的分类问题包括:手写数字识别、自然物体分类、蛋白质显微镜图片分类、恶意软件分类、恶意评论分类。

对比回归与分类

回归 分类
单连续值数值输出 多个输出
跟真实值的区别作为损失 输出i是预测i的置信度

1.1 如何将分类问题转化为回归问题

(1)类别编码

对于n个类别,可以用一维有效编码来编码。那么标号就是长为n的向量,每个类别的标号向量分别是$\textbf y_1,\textbf y_2\dots \textbf y_n$。

其中第i个类别的标号是$\textbf y_i=[0,0,1,0\dots 0]$,其中只有第i个元素是1,其余元素都是0。

(2)确定损失函数

在Softmax中可以选择使用均方损失函数训练。

(3)进行预测

$\hat y=argmax\ o_i$

其中$o_i$是每个类别的置信度,$argmax\ o_i$是置信度最大的类别的索引。这样就能找到置信度最大的那个类别。

进行检测的关键是使对于正确类的置信度远大于其它类别。通常需要保证$o_y-o_i\ge\Delta(y,i)$

(4)置信度转换为概率

我们获得了所有类别的置信度$\textbf o=[o_1,o_2,\dots,o_n]$。

但是置信度只是预测的物体与每个类别的匹配程度,而非识别为该物体的概率,具体转换方法如下:

$\hat{\textbf y}=softmax(\textbf o)$

$\hat y_i=\frac{exp(o_i)}{\sum_k exp(o_k)}$

这样就实现了置信度$\textbf o$的所有元素非负,且和为1。

然后将真实的概率$\textbf y$与$\hat {\textbf y}$的区别作为损失。