深度学习基础
线性回归
类型 | 描述 | 适用 |
---|---|---|
线性回归 | 线性回归输出是一个连续值:预测放假、气温、销售额等连续值问题 | 回归问题 |
Softmax回归 | Softmax回归输出是一个离散值:图像分类、垃圾邮件识别、疾病监测等离散值问题 | 分类问题 |
基本要素
以一个简单房屋价格预测为例,仅考虑两个因素(特征),即面积和房龄
1.模型(Model)
$$\hat{y} = x_1w_1+x_2w_2+b$$
- 其中$w_1$和$w_2$是权重(weight),b是偏差(bias),且均为标量。
- 它们是线性回归模型的参数(parameter)。
- 模型输出$\hat{y}$是线性回归对真是价格$y$的预测或估计,通常允许它们之间有一定误差。
2.模型训练(Model training)
通过数据来寻找特定的模型参数值,使模型在数据上的误差尽可能小。
(1)训练数据
- 通常收集一系列真实数据,希望在这个数据上面寻找模型参数来使模型的预测价格与真实价格的误差最小。
- 在机器学习术语里,该数据集被称为训练数据集(training data set)或训练集(training set),移动房屋被称为一个样本(sample),其真实出售价格叫作标签(label),用来预测标签的两个因素叫作特征(feature)。特征表示样本的特点。
假设采集的样本数为$n$,索引为$i$的样本的特征为$x_1^{(i)}$和$x_2^{(i)}$。对于索引为$i$的房屋,线性回归模型的房屋价格预测表达式为:
$$\hat{y}^{(i)}=x_1^{(i)}w_1+x_2^{(i)}w_2+b.$$
(2)损失函数
- 衡量价格预测值与真实值之间的误差。
- 通常选取一个非负数作为误差,且数值越小表示误差越小。
一个常用的选择是平方函数,其在评估索引为$i$的样本误差的表达式为:
$$\ell^{(i)}(w_1,w_2,b)=\frac{1}{2}(\hat{y}^{(i)}-y^{(i)})^2.$$
其中常数$\frac{1}{2}$使对平方项求导后的常数系数为1。
- 给定训练数据及,这个误差只与模型参数相关,因此我们将它记为以模型参数为参数的函数。
- 在机器学习里,将衡量误差的函数称为损失函数(loss function)。
- 这里使用的平方误差函数也称为平方损失(square loss)
通常,用训练数据集中所有样本误差的平均来衡量模型预测的质量:
$$\ell(w_1,w_2,b)=\frac{1}{n}\sum_{i=1}^n l^{(i)}(w_1,w_2,b)=\frac{1}{n}\sum_{i=1}^n \frac{1}{2} (x_1^{(i)}w_1+x_2^{(i)}w_2+b-y^{(i)})^2.$$
在模型训练中,我们希望找出一组模型参数,记为$w_1^{*},w_2^{*},b^{*}$,来使训练样本平均损失最小:
$$
w_1^{*},w_2^{*},b=
\begin{gathered}
\operatorname*{arg,min}_{w_1,w_2,b} \ell(w_1,w_2,b)
\end{gathered}.
$$
(3)优化算法
- 解析解(analytical solution):当模型和损失函数较为简单时,上面的误差最小化问题的解可以直接用公式表达出来。
- 数值解:大多数深度学习模型并没有解析解,只能通过优化算法有限次迭代模型参数来尽可能降低损失函数的值。
- 小批量随机梯度下降(mini-batch stochastic gradient descent):
①先选取一组模型参数的初始值,如随机选取;
②接下来对参数进行多次迭代,使每次迭代都可能降低损失函数的值。
③在每次迭代中,先随机均匀采样一个由固定数目训练样本所组成的小批量(mini-batch)$\mathcal{B}$,然后求小批量中数据样本的平均损失有关模型参数的导数(梯度),最后用此结果与预先设定的一个正数的乘积作为模型参数在本次迭代的减小量。
模型的每个参数将作如下迭代:
$$
w_1\leftarrow w_1 - \frac{\eta}{|\mathcal{B}|} \sum_{i\in\mathcal{B}}\frac{\partial\ell^{(i)}(w_1,w_2,b)}{\partial w_1} = w_1 - \frac{\eta}{|\mathcal{B}|}\sum_{i\in\mathcal{B}} x_1^{(i)}(x_1^{(i)}w_1+x_2^{(i)}w_2+b-y^{(i)}),
$$
$$
w_2\leftarrow w_2 - \frac{\eta}{|\mathcal{B}|} \sum_{i\in\mathcal{B}}\frac{\partial\ell^{(i)}(w_1,w_2,b)}{\partial w_2} = w_2 - \frac{\eta}{|\mathcal{B}|}\sum_{i\in\mathcal{B}} x_2^{(i)}(x_1^{(i)}w_1+x_2^{(i)}w_2+b-y^{(i)}),
$$
$$
b\leftarrow b - \frac{\eta}{|\mathcal{B}|} \sum_{i\in\mathcal{B}}\frac{\partial\ell^{(i)}(w_1,w_2,b)}{\partial b} = b - \frac{\eta}{|\mathcal{B}|}\sum_{i\in\mathcal{B}}(x_1^{(i)}w_1+x_2^{(i)}w_2+b-y^{(i)}).
$$
- $|\mathcal{B}|$代表每个小批量中的样本个数(批量大小,batch_size),$\eta$称作学习率(learning rate)并取正数
- 批量大小和学习率是人为设定的,并不是通过模型训练学出的,因此称作超参数(hyperparameter)。
- 通常所说的“调参”指的正是调节超参数。
- 极少数情况下,超参数也可以通过模型训练学出。
3.模型预测
- 模型训练完成后,将模型参数$w_1,w_2,b$在优化算法停止时的值分别记作$\hat{w}_1,\hat{w}_2,\hat{b}$。
- 这里得到的并不一定是最小化损失函数的最优解$w_1^{*},w_2^{*},b^{*}$,而是对最优解的一个近似。
- 训练后的线性回归模型$x_1\hat{w}_1+x_2\hat{w}_2+\hat{b}$来估算训练数据集以外任意一栋房屋面积(平方米)为$x_1$、房龄(年)为$x_2$的房屋的价格了。
表示方法
1.神经网络图
2.矢量计算表达式
当数据样本数为$n$,特征数为$d$时,线性回归的矢量计算表达式为:
$$\hat{\pmb{y}}=\pmb{Xw}+b.$$
$$\hat{\pmb{y}}=
\begin{bmatrix}
\hat{y}^{(1)} \
\hat{y}^{(2)} \
\cdots \
\hat{y}^{(n)} \
\end{bmatrix},
\pmb{X}=
\begin{bmatrix}
x_1^{(1)} & x_2^{(1)} \
x_1^{(2)} & x_2^{(2)} \
\cdots & \cdots \\
x_1^{(n)} & x_2^{(n)} \
\end{bmatrix},
\pmb{w}=
\begin{bmatrix}
w_1 \
w_2 \
\end{bmatrix}.
$$
- 模型输出$\hat{y}\in\mathbb{R}^{n\times 1}$,批量数据样本特征$\pmb{X}\in\mathbb{R}^{n\times d}$,权重$\pmb{w}\in\mathbb{R}^{d\times 1}$,偏差$b\in\mathbb{R}$,批量样本数据标签$\pmb{y}\in\mathbb{R}^{n\times 1}$。
设模型参数$\theta={w_1,w_2,b}^{\top}$,重写损失函数:
$$\ell(\theta)=\frac{1}{2n}(\hat{y}-y)^{\top}(\hat{y}-y).$$
小批量随机梯度下降的迭代步骤将相应改写为:
$$\theta\leftarrow\theta-\frac{\eta}{|\mathcal{B}|}\sum_{i\in\mathcal{B}}\nabla_{\theta}(\theta)$$
其中梯度是损失有关3个标量的模型参数的偏导数组成的向量:
$$
\require{enclose}
\nabla_{\theta}\ell^{(i)}(\theta)=
\begin{bmatrix}
\frac{\partial\ell^{(i)}(w_1,w_2,b)}{\partial w_1} \
\frac{\partial\ell^{(i)}(w_1,w_2,b)}{\partial w_2} \
\frac{\partial\ell^{(i)}(w_1,w_2,b)}{\partial b} \
\end{bmatrix}=
\enclose{horizontalstrike,updiagonalstrike}{
\begin{bmatrix}
x_1^{(i)}(x_1^{(i)}w_1+x_2^{(i)}w_2+b-y^{(i)}) \
x_2^{(i)}(x_1^{(i)}w_1+x_2^{(i)}w_2+b-y^{(i)}) \
x_1^{(i)}w_1+x_2^{(i)}w_2+b-y^{(i)}
\end{bmatrix}}=
\enclose{horizontalstrike,updiagonalstrike}{
\begin{bmatrix}
x_1^{(i)} \
x_2^{(i)} \
1 \
\end{bmatrix}
(\hat{y}^{(i)}-y^{(i)})
}.
$$
上述等号后两个公式并非不正确,而是代码中自动求梯度实现非常简洁
自动求梯度
如下代码所示的模型$\pmb{y}=2\pmb{x}^{\top}\pmb{x}$,梯度结果为$\pmb{y}=4\pmb{x}$,在给关于$\pmb{x}$各分量求偏导(梯度)时,自动求梯度需要保存每个分量的偏导数(梯度),那么需要给各分量的偏导数分配空间,x.attach_grad()
,而后自动求梯度y.backward()
,$\pmb{y}$这里是模型函数计算结果,而各分量梯度的向量为x.grad
,非常方便!!!
1 | from mxnet import autograd, nd |
1 | Output: |
注意,多维情况下,自动求和
1 | from mxnet import autograd, nd |
1 | [[3.] |
原生实现
1 | from IPython import display |
1 | Output: |
MXNet简洁实现
1 | from mxnet import autograd, nd |
图像分类数据集(Fashion-MNIST)
获取数据集并显示
1 | import d2lzh as d2l |
读取小批量数据
1 | from mxnet.gluon import data as gdata |
Softmax回归
原生实现
1 | import d2lzh as d2l |
1 | Output: |
简洁实现
注意:简洁实现中,并没有对y_hat进行softmax处理,仅在loss里进行了softmax处理
1 | import d2lzh as d2l |
多层感知机
激活函数
1 | from IPython import display |
原生实现
1 | from mxnet import autograd, nd |
1 | Output: |
简洁实现
1 | from mxnet import autograd, nd, gluon, init |
模型选择、欠拟合和过拟合
类型 | 说明 |
---|---|
训练集(train set) | 用于模型模拟的数据样本 |
验证集(validation set) | 模型训练过程中单独留出的样本集,它可以用于调整模型的超参数和用于对模型的能力进行初步评估 |
测试集(test set) | 用来评估最终模型的泛化能力。但不能作为调参、选择特征等算法相关的选择依据 |
1 | from mxnet import autograd, gluon, nd |
权重衰减
原生实现
1 | from IPython import display |
简洁实现
1 | from IPython import display |
丢弃法
疑问:
关于练习那题的疑问,如果改变输入形状就报错的话,那么之前的丢弃法岂不是丢弃只发生一次?即首次前向计算时,各层参数形状就定了,之后的每次小批量训练时参数形状就不变了,此时丢弃的隐藏单元就固定了。。。对吗?
哦,知道了,丢弃只是特征置0,而非改变输入形状
原生实现
1 | from mxnet import autograd, gluon, init, nd |
1 | Outputs with dropout: |
简洁实现
1 | from mxnet import autograd, gluon, init, nd |
正向传播、反向传播和计算图
类型 | 说明 |
---|---|
正向传播 (forward propagation) |
对神经网络沿着从输入层到输出层的顺序, 依次计算并存储模型中的中间变量(包括输出) |
反向传播 (back propagation) |
计算神经网络参数梯度的方法。 依据微积分中的链式法则,沿着从输出层到输入层的顺序, 依次计算并存储目标函数有骨感神经网络各层的中间变量以及参数的梯度。 |
## 数值稳定性和模型初始化 |
- 当神经网络的层数较多时,模型的数值稳定性容易变差。典型问题是衰减(vanishing)和爆炸(explosion)
实战:房价预测
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145from mxnet import autograd, gluon, init, nd
from mxnet.gluon import data as gdata, loss as gloss, nn
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from IPython import display
train_data = pd.read_csv('C:/Users/Louris/Desktop/d2l-zh/data/train.csv')
test_data = pd.read_csv('C:/Users/Louris/Desktop/d2l-zh/data/test.csv')
all_features = pd.concat((train_data.iloc[:, 1:-1], test_data.iloc[:, 1:]))
numeric_features = all_features.dtypes[all_features.dtypes != 'object'].index
all_features[numeric_features] = all_features[numeric_features].apply(
lambda x: (x - x.mean()) / (x.std())
)
all_features[numeric_features] = all_features[numeric_features].fillna(0)
# dummpy_na=True将缺失值也当作合法的特征值并为其创建指示特征
all_features = pd.get_dummies(all_features, dummy_na=True)
n_train = train_data.shape[0]
train_features = nd.array(all_features[:n_train].values)
test_features = nd.array(all_features[n_train:].values)
train_labels = nd.array(train_data.SalePrice.values).reshape((-1, 1))
loss = gloss.L2Loss()
def get_net():
net = nn.Sequential()
net.add(nn.Dense(1))
net.initialize()
return net
def log_rmse(net, features, labels):
# 将小于1的值设立成1,使得取对数时数值更稳定
clipped_preds = nd.clip(net(features), 1, float('inf'))
rmse = nd.sqrt(2 * loss(clipped_preds.log(), labels.log()).mean())
return rmse.asscalar()
def train(net, train_features, train_labels, test_features, test_labels,
num_epochs, learning_rate, weight_decay, batch_size):
train_ls, test_ls = [], []
train_iter = gdata.DataLoader(gdata.ArrayDataset(train_features, train_labels),
batch_size,
shuffle=True)
# 使用Adam优化算法
trainer = gluon.Trainer(net.collect_params(),
'adam',
{'learning_rate': learning_rate,
'wd': weight_decay})
for epoch in range(num_epochs):
for X, y in train_iter:
with autograd.record():
l = loss(net(X), y)
l.backward()
trainer.step(batch_size)
train_ls.append(log_rmse(net, train_features, train_labels))
if test_labels is not None:
test_ls.append(log_rmse(net, test_features, test_labels))
return train_ls, test_ls
def get_k_fold_date(k, i, X, y):
assert k > 1
fold_size = X.shape[0] // k
X_train, y_train = None, None
for j in range(k):
idx = slice(j * fold_size, (j + 1) * fold_size)
X_part, y_part = X[idx, :], y[idx]
if j == i:
X_valid, y_valid = X_part, y_part
elif X_train is None:
X_train, y_train = X_part, y_part
else:
X_train = nd.concat(X_train, X_part, dim=0)
y_train = nd.concat(y_train, y_part, dim=0)
return X_train, y_train, X_valid, y_valid
# 矢量图显示
# SVG Display
def user_svg_display():
display.set_matplotlib_formats('svg')
# 设置图的尺寸
# Set size
def set_figsize(figsize=(3.5, 2.5)):
user_svg_display()
plt.rcParams['figure.figsize'] = figsize
def semilogy(x_vals, y_vals, x_label, y_label, x2_vals=None, y2_vals=None,
legend=None, figsize=(3.5, 2.5)):
set_figsize(figsize)
plt.xlabel(x_label)
plt.ylabel(y_label)
plt.semilogy(x_vals, y_vals)
if x2_vals and y2_vals:
plt.semilogy(x2_vals, y2_vals, linestyle=':')
plt.legend(legend)
plt.show()
def k_fold(k, X_train, y_train, num_epochs, learning_rate, weight_decay, batch_size):
train_l_sum, valid_l_sum = 0, 0
for i in range(k):
data = get_k_fold_date(k, i, X_train, y_train)
net = get_net()
train_ls, valid_ls = train(net, *data, num_epochs, learning_rate, weight_decay, batch_size)
train_l_sum += train_ls[-1]
valid_l_sum += valid_ls[-1]
if i == 0:
semilogy(range(1, num_epochs + 1), train_ls, 'epochs', 'rmse',
range(1, num_epochs + 1), valid_ls,
['train', 'valid'])
print('fold %d, train rmse %f, valid rmse %f' % (i, train_ls[-1], valid_ls[-1]))
return train_l_sum / k, valid_l_sum / k
k, num_epochs, lr, weight_decay, batch_size = 5, 100, 5, 0, 64
train_l, valid_l = k_fold(k, train_features, train_labels, num_epochs, lr, weight_decay, batch_size)
print('%d-fold validation: avg train rmse %f, avg valid rmse %f' % (k, train_l, valid_l))
def train_and_pred(train_features, test_features, train_labels, test_data,
num_epochs, lr, weight_decay, batch_size):
net = get_net()
train_ls, _ = train(net, train_features, train_labels, None, None,
num_epochs, lr, weight_decay, batch_size)
semilogy(range(1, num_epochs + 1), train_ls, 'epochs', 'rmse')
print('train rmse %f' % train_ls[-1])
preds = net(test_features).asnumpy()
test_data['SalePrice'] = pd.Series(preds.reshape(1, -1)[0])
submission = pd.concat([test_data['Id'], test_data['SalePrice']], axis=1)
submission.to_csv('submission.csv', index=False)
train_and_pred(train_features, test_features, train_labels, test_data,
num_epochs, lr, weight_decay, batch_size)1
2
3
4
5
6
7
8Outputs:
fold 0, train rmse 0.169721, valid rmse 0.156991
fold 1, train rmse 0.162148, valid rmse 0.189789
fold 2, train rmse 0.163688, valid rmse 0.168196
fold 3, train rmse 0.167601, valid rmse 0.154646
fold 4, train rmse 0.162926, valid rmse 0.183022
5-fold validation: avg train rmse 0.165217, avg valid rmse 0.170529
train rmse 0.162530