引言

监督学习

常见的机器学习是x->y或输入到输出映射的算法,监督学习的关键特征是你给予学习算法示例,包括正确答案。也就是说包括给定的x和正确的标签y,通过学习算法进行学习,最后对于没有标签的x可以做出合理准确的预测或猜测。

无监督学习

数据仅有输入x,但没有标签y,算法必须找到数据中的某种结构或某种模式或某些有趣的东西。

相关符号

  • 训练集:训练模型的数据集。
  • 测试集:检验最终选择最优的模型的性能如何。
  • f:function的缩写,指模型。
  • x:作为输入的特征值。
  • y:作为结果的标签,指实际真实值。
  • m:训练样本总数。
  • (x, y):一个训练样本。
  • ($x^i$, $y^i$):第i个训练样本。
  • $\hat{y}$:由x预测的结果,指预测值。

单变量线性回归

模型函数

现假设该模型只有一个特征值输入,则对于该线性回归,函数可以定义为:
$$
f_{w, b}(x) = wx + b
$$
该函数基于输入特征x的预测$\hat{y}$,并且取决于wb的值,上述式子也可以进行简写。

$$
f(x) = wx + b
$$
显而易见,该函数为单变量线性回归,即只有一个特征值输入的线性回归。

wb为模型的参数,需要找到一个合适的值,获取一条直线,从而更好的拟合数据。因此我们需要想办法去找到合适的权重。

得到的模型用于测试,则预测值应该满足该函数:
$$
\hat{y}^{(i)} = f_{w,b}(x^{(i)}) = wx^{(i)}+b
$$
需要找到wb,使其对每组样本$(x^{(i)},y^{(i)})$,$\hat{y}^{(i)}$的值尽可能接近${y}^{(i)}$。

成本函数

需要一个公式来计算每次的误差和是多少,该公式被称为成本函数公式,用于评判一个模型的好坏

首先应该计算每一次预测结果的差值:
$$
\hat{y}^{(i)}-{y}^{(i)}
$$
差值有正有负,因此对其进行平方:
$$
(\hat{y}^{(i)}-{y}^{(i)})^2
$$
共有m个样本,对这些样本误差求和并取平均值(在代码中下标从0开始):
$$
\frac{1}{m}\sum_{i=1}^{m}(\hat{y}^{(i)}-{y}^{(i)})^2
$$
为了方便后续操作,一般会将该函数除以2,得到成本函数$J(w,b)$。
$$
J(w,b)=\frac{1}{2m}\sum_{i=1}^{m}(\hat{y}^{(i)}-{y}^{(i)})^2
$$

条目 函数
模型 $f_{w, b}(x) = wx + b$
参数 $w,b$
成本函数 $J(w,b)=\frac{1}{2m}\sum_{i=1}^{m}(\hat{y}^{(i)}-{y}^{(i)})^2$
目标 $minimizi_{w,b}J(w,b)$

梯度下降

公式

梯度下降算法通俗来讲就是你现在在一座山上,然后每次环顾四周,找到你这一圈中下降最快(也就是坡最陡峭)的一条路,然后朝着这个方向稍微走一点。做完这些之后一直重复这个步骤,直到环顾四周后发现都是上升的,而不存在下降的坡。梯度下降的结果会取决于选定的初始值,如果初始值不一样,则会导致最后到达的山谷不一样。

先来看一下对于w的梯度下降的公式:
$$
w=w-\alpha\frac{\partial}{\partial{w}}J(w,b)
$$
在这个式子中,$\alpha$称之为学习率,学习率通常是介于0~1之间的一个小的正数,一般取0.01。可以理解为,学习率是从当前位置环顾四周后,选定好了下降最快的方向,朝这个方向迈的步子的大小。$\alpha$越大,则迈的步子越大,反之,步子越小。

可以发现,对于该线性回归模型,一共有两个参数,分别是w和b,因此同时对二者使用梯度下降:
$$
b=b-\alpha\frac{\partial}{\partial{b}}J(w,b)
$$
值得注意的是,如果先更新w的值,再更新b的值,会导致更新b的时候所用到的w不是原始的w,而是更新后的w。很显然,这是一种错误的做法,我们更希望他们同时更新,因此可以执行下面的操作,从而达到同时更新的效果。
$$
tmpw=w-\alpha\frac{\partial}{\partial{w}}J(w,b)
$$

$$
tmpb=b-\alpha\frac{\partial}{\partial{b}}J(w,b)
$$

$$
w=tmpw
$$

$$
b=tmpb
$$

偏导

对于每一个参数的更新,都需要求得在该参数方向上的导数,即该参数的偏导数,之后用上一次迭代的该参数的值减去学习率乘以偏导数的值。

现在我们定义一个函数如下所示:
$$
f(x)=x^{2}-4x+6
$$
很容易知道,该函数的对称轴为直线$x=2$,开口向上,在$(-\infty,2)$上单调递减,在$(-\infty,2)$上单调递增。

那么上述函数的梯度下降公式为:
$$
x=x-\alpha\frac{\partial}{\partial{x}}f(x)
$$
根据梯度下降的步骤,首先应该随机选取一个初始点。

如果初始点的位置在对称轴左侧,由于该点所在位置为单调递减处,因此该点处的导数一定小于0,根据公式可知,减去一个负数相当于加上一个数字,那么$x$会向右移动,以此类推,最后会移动到对称轴的位置上。

如果初始点的位置在对称轴右侧,由于该点所在位置为单调递增处,因此该点处的导数一定大于0,根据公式可知,$x$会向左移动,以此类推,最后会移动到对称轴的位置上。

通过这种方式,可以使得该参数一点一点移动到可以使最终结果尽可能小的位置上,从而该参数的值可以让成本函数更低。

总而言之,偏导数决定了梯度下降的方向。

学习率

对于学习率我们需要选择一个恰到好处的值,不能太大也不能太小,学习率决定了梯度下降的步长

如果学习率太小,那么每一次参数的变化量都会非常非常小,这就导致如果要找到成本函数最小点,需要迭代很多次,使得运行时间非常长,收敛过慢。

如果学习率太大,那么每一次参数的变化量都会非常非常大,这就导致很有可能变化速度太快从而冲过了成本函数的最小点,最坏的情况可能发散,离目标位置越来越远。

计算

通过上述公式可以知道我们首先需要求得$\frac{\partial}{\partial{w}}J(w,b)$:
$$
\frac{\partial}{\partial{w}}J(w,b)=\frac{\partial}{\partial{w}}\frac{1}{2m}\sum_{i=1}^{m}(f_{w,b}(x^{(i)})-{y}^{(i)})^2
$$

$$
\frac{\partial}{\partial{w}}\frac{1}{2m}\sum_{i=1}^{m}(f_{w,b}(x^{(i)})-{y}^{(i)})^2=\frac{\partial}{\partial{w}}\frac{1}{2m}\sum_{i=1}^{m}(wx^{(i)}+b-{y}^{(i)})^2
$$

$$
\frac{\partial}{\partial{w}}\frac{1}{2m}\sum_{i=1}^{m}(wx^{(i)}+b-{y}^{(i)})^2=\frac{1}{2m}\sum_{i=1}^{m}(wx^{(i)}+b-{y}^{(i)})2x^{(i)}
$$

$$
\frac{1}{2m}\sum_{i=1}^{m}(wx^{(i)}+b-{y}^{(i)})2x^{(i)}=\frac{1}{m}\sum_{i=1}^{m}(f_{w,b}(x^{(i)})-{y}^{(i)})x^{(i)}
$$

最终我们得到了关于w的偏导数:
$$
\frac{\partial}{\partial{w}}J(w,b)=\frac{1}{m}\sum_{i=1}^{m}(f_{w,b}(x^{(i)})-{y}^{(i)})x^{(i)}
$$
同理,我们也可以得到关于b的偏导数:
$$
\frac{\partial}{\partial{b}}J(w,b)=\frac{1}{m}\sum_{i=1}^{m}(f_{w,b}(x^{(i)})-{y}^{(i)})
$$

完整代码

Python

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
import numpy as np
import matplotlib.pyplot as plt

#导入数据集
x_train = np.array([1.0, 2.0]) #特征值
y_train = np.array([300.0, 500.0]) #与之对应的标签

#训练模型的参数
m = len(x_train) #样本数
alpha = 0.01 #学习率
iterations = 10000 #迭代次数
w = 0 #w初始值
b = 0 #b初始值

#计算成本
def compute_cost(x, y, w, b):
cost = 0 #当前w和b的值所得到的成本函数

for i in range(m):
cost += (w * x[i] + b - y[i]) ** 2 #每一个样本的成本
cost /= 2 * m #求和后除以2m

return cost #返回成本

#计算偏导
def compute_gradient(x, y, w, b):
dj_dw = 0 #w的偏导和
dj_db = 0 #b的偏导和

for i in range(m):
dj_dw += (w * x[i] + b - y[i]) * x[i] #w的偏导数
dj_db += (w * x[i] + b - y[i]) #b的偏导数

#求和后除以m
dj_dw /= m
dj_db /= m

return dj_dw, dj_db

x = [] #画图横坐标
y = [] #画图纵坐标

#迭代过程
for i in range(iterations):
dj_dw, dj_db = compute_gradient(x_train, y_train, w, b) #获取偏导数
#更新w和b
w -= alpha * dj_dw
b -= alpha * dj_db
if (i + 1) % 200 == 0:
cost = compute_cost(x_train, y_train, w, b) #计算当前成本
x.append(i + 1)
y.append(cost)
print(f"第{i + 1}次迭代\nw:{w}\nb:{b}\n成本:{cost}\n")

plt.rcParams['font.sans-serif'] = 'FangSong' #设置中文字体
plt.plot(x, y, label = '成本') #配置参数
plt.title("成本函数")
plt.legend()

C/C++

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
#include <cstdio>
#include <cmath>

using namespace std;

const int N = 1e5 + 7; //最大容量

//导入数据集
float x_train[N] = { 1.0, 2.0 }; //特征值
float y_train[N] = { 300.0, 500.0 }; //与之对应的标签

//训练模型的参数
int m = 2; //样本数
float alpha = 0.01; //学习率
int iterations = 10000; //迭代次数
float w = 0; //w的初始值
float b = 0; //b的初始值

//计算成本
float compute_cost(float w, float b)
{
float cost = 0; //当前w和b的值所得到的成本函数
for (int i = 0; i < m; i++)
cost += pow((w * x_train[i] + b - y_train[i]), 2); //每一个样本的成本
cost /= 2.0 * m; //求和后除以2m
return cost; //返回成本
}

//计算偏导
void compute_gradient(float& dj_dw, float& dj_db)
{
for (int i = 0; i < m; i++)
{
dj_dw += (w * x_train[i] + b - y_train[i]) * x_train[i]; //w的偏导数
dj_db += (w * x_train[i] + b - y_train[i]); //b的偏导数
}
dj_dw /= m;
dj_db /= m;
return;
}

int main()
{
for (int i = 0; i < iterations; i++)
{
float dj_dw = 0; //w的偏导和
float dj_db = 0; //b的偏导和
compute_gradient(dj_dw, dj_db); //获取偏导数
//更新w和b
w -= alpha * dj_dw;
b -= alpha * dj_db;
if ((i + 1) % 200 == 0)
{
float cost = compute_cost(w, b); //计算当前成本
printf("第%d次迭代\nw:%f\n%b:%f\n成本:%f\n\n", i + 1, w, b, cost);
}
}
return 0;
}

多元线性回归

多维特征

在上述模型中,只有房间大小这一个特征值。但在实际情况中,会有许多因素影响房子的价格,因此需要引入多维特征这一概念。

这里将介绍几个相关符号:

  • $x_j$:第$j$个特征。
  • $n$:特征总数。
  • $\vec{x}^{i}$:第$i$个特征向量。
  • $x_{j}^{(i)}$:第$i$个特征向量中的第$j$个特征。

模型函数

对于多变量线性回归模型,有多个参数对最终的价格产生影响,因此我们需要将各个参数使用向量来进行存储,故:
$$
\vec{x}=[x_1,x_2,x_3\dots x_n]
$$
对于每一个特征,都应该有一个参数$w$与之对应,这些参数也可以组成一个维度为$n$的向量:
$$
\vec{w}=[w_1,w_2,w_3\dots w_n]
$$
因此该模型函数为:
$$
f_{\vec{w},b}(\vec{x})=\vec{w}\cdot\vec{x}+b=w_1x_1+w_2x_2+w_3x_3+\dots+w_nx_n+b
$$

由此可知,多元线性回归模型对应的成本函数为:
$$
J(\vec{w},b)=\frac{1}{2m}\sum_{i=1}^{m}(\hat{y}^{(i)}-{y}^{(i)})^2
$$

向量化

很显然,在多元线性回归中,需要进行向量之间的点乘,因此,需要利用NumPy库来创建两个数组,用于存放参数$w$和样本$x$的所有特征。

1
2
3
4
5
import numpy as np

w = np.array([1.0, 2.5, -3.3])
x = np.array([10, 20, 30])
b = 4

接着获取特征数量。

1
n = len(x)

我们有三种方法来计算点乘

直接计算

这种方法直接列式子进行计算,将所有的参数和对应特征依次相乘再相加,最后加上$b$。
$$
f_{\vec{w},b}(\vec{x})=w_1x_1+w_2x_2+w_3x_3+b
$$
相应的代码:

1
f = w[0] * x[0] + w[1] * x[1] + w[2] * x[2] + b

很显然,这种方式在面对特征数量很多的情况使用起来非常困难,不推荐使用这种方式。

循环累加

这种方法直接利用循环,求得所有的累加值,最后加上$b$。
$$
f_{\vec{w},b}(\vec{x})=\sum_{i=1}^{n}w_jx_j+b
$$
相应的代码:

1
2
3
4
f = 0
for i in range(n):
f += w[i] * x[i]
f += b

这种方式可以降低代码的冗余,利用循环高效率达成目标。

使用点乘

Numpy库中自带了关于向量的点乘操作,可以直接进行调用计算点乘,最后加上$b$。
$$
f_{\vec{w},b}(\vec{x})=\vec{w}\cdot\vec{x}+b
$$
相应的代码:

1
2
f = 0
f = np.dot(w, x) + b

这种方式效率高,代码简洁。因为NumPy能调用并行硬件,所以它的效率比for循环或顺序计算要高得多。

梯度下降

对于多元线性回归模型来说,进行梯度下降相当于对每个参数进行一次单变量线性回归的运算。
$$
w_1=w_1-\alpha\frac{1}{m}\sum_{i=1}^{m}(f_{\vec{w},b}(\vec{x}^{(i)})-{y}^{(i)})x_1^{(i)}
$$

$$
\vdots
$$

$$
w_n=w_n-\alpha\frac{1}{m}\sum_{i=1}^{m}(f_{\vec{w},b}(\vec{x}^{(i)})-{y}^{(i)})x_n^{(i)}
$$

$$
b=b-\alpha\frac{1}{m}\sum_{i=1}^{m}(f_{\vec{w},b}(\vec{x}^{(i)})-{y}^{(i)})
$$

特征缩放

如果特征不止一个且取值范围差异较大,会面临一个问题:某一个特征参数改变很少一点,会导致成本函数的值变化得很大,最终导致没有办法快速收敛,使得梯度下降运行非常缓慢。举个实际的例子,如果房子价格由房子大小和厨房的数量所决定,那么很明显厨房的数量并不怎么影响房子的价格,这就导致厨房数量所对应的的参数变化量非常小,而房子大小所对应的参数的变化量非常大。通俗来说,每一次都会改变这两个参数的值,用于寻找最优值,但是对于房子大小,需要乘以一个很大的数字,那么只要其参数变化很少一点,就会对成本函数的值造成很大影响。反之厨房数量所对应的参数并不会造成很大影响,这就使得成本函数的等高线呈现出一种椭圆形的样子,会导致梯度下降的时候使其值反复横跳,没有办法快速收敛。

最大值特征缩放

该方法可以保证每一个值都在0~1之间。对于每一个特征,都会有一个取值范围,因此,我们可以令取值范围和该特征的所有特征值都除以取值范围的最大值来完成特征缩放。

接着用房子的例子,房子的大小取值范围为30$\leq$$x_1$$\leq$200,卧室数量的取值范围为0$\leq$$x_2$$\leq$5,那么使用最大值特征缩放,即:
$$
x_{1,scaled}=\frac{x_1}{200}
$$

$$
x_{2,scaled}=\frac{x_2}{5}
$$

最终房子大小的缩放结果为0.15$\leq$$x_{1,scaled}$$\leq$1,卧室数量的的缩放结果为0$\leq$$x_{2,scaled}$$\leq$1。

均值归一化

该方法可以保证每一个值都在-1~1之间。对于每一个特征值和取值范围,均有如下的计算公式:
$$
x=\frac{x-\mu}{max-min}
$$
其中x为特征值或者取值范围,$\mu$为该特征的所有特征值的平均值,maxmin分别为取值范围的最大值和最小值。

假设房子的大小的平均值为60,卧室数量的平均值为2.3,使用均值归一化:
$$
x_1=\frac{x_1-60}{200-30}
$$

$$
x_2=\frac{x_2-2.3}{5-0}
$$

最终房子大小的缩放结果为-0.18$\leq$$x_1$$\leq$0.82,卧室数量的缩放结果为-0.46$\leq$$x_2$$\leq$0.54。

Z-score标准化

该方法利用标准差来进行计算。对于每一个特征值和取值范围,均有如下的计算公式:
$$
x=\frac{x-\mu}{\sigma}
$$
其中x为特征值或者取值范围,$\mu$和$\sigma$分别为该特征的所有特征值的平均值和标准差。

假设房子的大小的平均值为60,标准差为45,卧室数量的平均值为2.3,标准差为1.4,使用Z-score标准化:
$$
x_1=\frac{x_1-60}{45}
$$

$$
x_2=\frac{x_2-2.3}{1.4}
$$

最终房子大小的缩放结果为-0.67$\leq$$x_1$$\leq$3.1,卧室数量的缩放结果为-1.6$\leq$$x_2$$\leq$1.9。

特征工程

所谓特征工程就是利用初始给的特征创造新的更有用的特征。我们回到房子价格的问题上,如果给定了房子的占地长和占地宽,那么很明显,我们会想到两个特征的值相乘,得到面积,面积更能反映出一个房子的价格。根据上述推论,我们可以创造一个新的特征:
$$
x_3=x_1x_2
$$
创造新特征的过程被称为特征工程,则最后的模型函数为:
$$
f_{\vec{w},b}(\vec{x})=w_1x_1+w_2x_2+w_3x_3+b
$$

逻辑回归

逻辑回归是一种分类,并非一种线性回归模型,它的作用主要是给定输入,然后对输入进行分类,一般分为两类,给定Yes或者No。举个栗子:给定一封邮件,判断是否为垃圾邮件,给定一个肿瘤的信息,判断是否为良性肿瘤……

Sigmoid函数

Sigmoid函数是一个在生物学中常见的S型函数,也称为S型生长曲线。在信息科学中,由于其单增以及反函数单增等性质,Sigmoid函数常被用作神经网络的激活函数,将变量映射到0,1之间。

该函数为:
$$
g(z)=\frac{1}{1+e^{-z}}\quad 0<g(z)<1
$$

Sigmoid函数

其中z是图像的横轴,e为自然常数。

逻辑回归模型

逻辑回归模型可以理解为将特征输入到模型中,然后给出0或者1的结果。

将前面讲到的线性回归模型和逻辑回归模型组合使用(注:线性回归和逻辑回归本质上是两回事,一个是线性回归,一个是分类),可以先列出线性回归的模型:
$$
z=\vec{w}\cdot\vec{x}+b
$$
将这里的z传递给线性回归模型:
$$
f_{\vec{w},b}(\vec{x})=g(\vec{w}\cdot\vec{x}+b)=\frac{1}{1+e^{-(\vec{w}\cdot\vec{x}+b)}}
$$
如果现在有一个患者想要预测他的肿瘤是恶性还是良性,使用该模型得到的结果为0.7,即$f_{\vec{w},b}(\vec{x})=0.7$,那么说明这个人有70%的概率是恶性,30%的概率是良性。

用数学符号表示为:
$$
f_{\vec{w},b}(\vec{x})=P(y=1|\vec{x};\vec{w},b)=0.7
$$
该式表明参数wb是影响计算的参数,在给定输入特征$\vec{x}$的前提下,y=1的概率是0.7。

由上述可见,一共分为两种情况,这两种情况概率相加应该是1,即:
$$
P(y=0)+P(y=1)=1
$$

决策边界

通过对逻辑回归模型的分析,观察图像,我们可以发现,0.5是一个分界点,如果预测结果大于0.5,那么预测值将会为1,反之将会为0。继续推导,我们发现,当预测值为0.5时,z的值应该为0。换言之,当回归模型的值为0时,将会得到一条分界线,用于分隔两种预测情况。

因此,在计算逻辑回归模型的参数时,就相当于求决策边界对应的曲线的参数。

成本函数和损失函数

损失函数指的是对于单个样本而言相差的值,成本函数指的是所有的样本总的相差值。

在前面线性回归模型中,我们使用的成本函数为:
$$
J(\vec{w},b)=\frac{1}{2m}\sum_{i=1}^{m}(\hat{y}^{(i)}-{y}^{(i)})^2
$$
显而易见,这个成本函数很明显不适用于逻辑回归中,因为逻辑回归的值只有0和1,如果使用该成本函数,很难得到一个凸函数,会导致有很多的局部极小值,不能使用梯度下降来找成本函数的最小值,因此不能采用该成本函数。

对于逻辑回归模型,我们需要采用一种新的损失函数,将其定义为:
$$
L(f_{\vec{w},b}(\vec{x}^{(i)}),y^{(i)})
$$
该损失函数是关于$f(x)$和真实标签y的函数。

让我们先来看一下这个损失函数的定义:
$$
L(f_{\vec{w},b}(\vec{x}^{(i)}),y^{(i)})=-log(f_{\vec{w},b}(\vec{x}^{(i)}))\quad y^{(i)}=1
$$

$$
L(f_{\vec{w},b}(\vec{x}^{(i)}),y^{(i)})=-log(1-f_{\vec{w},b}(\vec{x}^{(i)}))\quad y^{(i)}=0
$$

该代价函数是用最大似然估计的统计原理推导出来的,这种代价函数具有凸函数的优点。

逻辑回归损失函数

如图所示,红色代表真实标签为1,蓝色代表真实标签为0。

可以发现在真实标签为1时,预测值预测的概率越靠近1,则损失函数的值越小,反之越大;在真实标签为0时,预测值预测的概率越靠近0,则损失函数的值越小,反之越大。通过这种方式,可以使得成本函数变为一个凸函数,用于梯度下降。

因此,对于逻辑回归,其成本函数为:
$$
J(\vec{w},b)=\frac{1}{m}\sum_{i=1}^{m}L(f_{\vec{w},b}(\vec{x}^{(i)}),y^{(i)})
$$
对于上述的损失函数,可以进行简化。很容易发现,标签只能为0或者1,那么可以根据这个性质,将式子写作:
$$
L(f_{\vec{w},b}(\vec{x}^{(i)}),y^{(i)})=-y^{(i)}log(f_{\vec{w},b}(\vec{x}^{(i)}))-(1-y^{(i)})log(1-f_{\vec{w},b}(\vec{x}^{(i)}))
$$
因此,成本函数可以变为:
$$
J(\vec{w},b)=-\frac{1}{m}\sum_{i=1}^{m}[y^{(i)}log(f_{\vec{w},b}(\vec{x}^{(i)}))+(1-y^{(i)})log(1-f_{\vec{w},b}(\vec{x}^{(i)}))]
$$

梯度下降

和线性回归一样,逻辑回归进行梯度下降也是相同的思路:
$$
w_j=w_j-\alpha\frac{\partial}{\partial{w_j}}J(\vec{w},b)
$$

$$
b=b-\alpha\frac{\partial}{\partial{b}}J(\vec{w},b)
$$

在上述中我们已经知道:
$$
f_{\vec{w},b}(\vec{x})=g(\vec{w}\cdot\vec{x}+b)=\frac{1}{1+e^{-(\vec{w}\cdot\vec{x}+b)}}
$$
因此,将该式子代入,求偏导可得:
$$
w_j=w_j-\alpha\frac{1}{m}\sum_{i=1}^{m}(f_{\vec{w},b}(\vec{x}^{(i)})-{y}^{(i)})x_j^{(i)}
$$

$$
b=b-\alpha\frac{1}{m}\sum_{i=1}^{m}(f_{\vec{w},b}(\vec{x}^{(i)})-{y}^{(i)})
$$

过拟合问题

三种拟合情况

让我们先来看一个数据集:

房价预测数据集

欠拟合

在该数据集中,我们可以发现,这个是房子大小和价格的关系,我们可以使用线性回归来对数据集进行拟合,如图所示:

一次函数

很明显,该算法不能很好地拟合训练数据,用专业术语描述,就是模型对训练数据的拟合不足(欠拟合),另一种术语是算法有高偏差。

拟合

为了防止欠拟合,我们可以通过观察发现,这个函数可能是一个二次函数,那么我们可以尝试利用二次函数对这些训练集进行拟合:

二次函数

这里介绍一个名词:泛化(Generalization),指的是算法也能适用于没出现在训练集中的样本的能力。

过拟合

那如果我们用一个多项式进行数据集的拟合,可以使得代价为0,让所有的点都在我们的模型上:

多项式函数

很明显可以发现,虽然所有的数据集都在这条线上,与数据集吻合非常好,但是这个模型不具有泛化到新样本的能力。

综上所述,机器学习需要找到一个既不欠拟合,又不过拟合的模型、

解决过拟合

增加数据集

如果使用一个多项式来进行数据的拟合,可能会导致过拟合,我们可以增加训练样本来解决该问题,使用这种方法仍然可以使用多项式来对数据进行拟合,也可以得到一个很好的结果。

使用特征子集

如果没有那么多数据集,那么可以降低特征的数量,可以使用最小的特征子集来进行操作,即挑选几个影响度最高的特征来进行模型的创建。

正则化

正则化是一种比较温和的方式,可以利用正则化来减小参数的大小,正则化是一种非常常用也非常好用的训练算法模型的方法。

正则化

原理

正则化简单来说就是使用一个小的参数去乘以参数w,一般来说我们管这个叫惩罚,越不重要的特征,我们对其惩罚的力度就越大,即乘以的参数就越小,可以有效降低该特征在曲线中的权重,但又不是完全没有影响。

如果我们有非常多的特征,并且不知道哪个特征影响大,哪个特征影响小,因此我们可以对所有的参数进行惩罚,即对所有的w进行正则化操作。

用之前的线性回归来举例子,先看一下他的成本函数:
$$
J(\vec{w},b)=\frac{1}{2m}\sum_{i=1}^{m}(f_{\vec{w},b}(\vec{x}^{(i)})-{y}^{(i)})^2
$$
在进行正则化之前,我们先引入一个参数$\lambda$,该参数代表惩罚力度,一般是一个很小的值。同时这个值应该除以2m,用于保证与前面的系数一致,在改变样本数量的时候,可以等比的放大缩小。
$$
J(\vec{w},b)=\frac{1}{2m}\sum_{i=1}^{m}(f_{\vec{w},b}(\vec{x}^{(i)})-{y}^{(i)})^2+\frac{\lambda}{2m}\sum_{j=1}^{n}w_j^2
$$
其中的n代表特征的数量,对所有的参数w进行正则化。

不仅可以对w进行正则化,也可以对b进行正则化,但事实上这么做的人很少,因为在实践过程中,正则化b产生的影响非常小,所以我们会更多地去正则化参数w而不是参数b

当然,也可以对b进行正则化操作,即:
$$
J(\vec{w},b)=\frac{1}{2m}\sum_{i=1}^{m}(f_{\vec{w},b}(\vec{x}^{(i)})-{y}^{(i)})^2+\frac{\lambda}{2m}\sum_{j=1}^{n}w_j^2+\frac{\lambda}{2m}b^2
$$

线性回归正则化

在梯度下降过程中,需要对式子进行求偏导运算,我们现在只对w进行正则化,不对b进行正则化,则梯度下降中公式为:
$$
w_j=w_j-\alpha\frac{\partial}{\partial{w_j}}J(\vec{w},b)
$$
其中成本函数应该变更为:
$$
J(\vec{w},b)=\frac{1}{2m}\sum_{i=1}^{m}(f_{\vec{w},b}(\vec{x}^{(i)})-{y}^{(i)})^2+\frac{\lambda}{2m}\sum_{j=1}^{n}w_j^2
$$

那么现在求偏导的结果应该为:
$$
\frac{1}{m}\sum_{i=1}^{m}(f_{\vec{w},b}(\vec{x}^{(i)})-{y}^{(i)})x_j^{(i)}+\frac{\lambda}{m}w_j
$$
因此,正则化后,更新结果应该为:
$$
w_j=w_j-\alpha[\frac{1}{m}\sum_{i=1}^{m}(f_{\vec{w},b}(\vec{x}^{(i)})-{y}^{(i)})x_j^{(i)}+\frac{\lambda}{m}w_j]
$$

$$
b=b-\alpha\frac{1}{m}\sum_{i=1}^{m}(f_{\vec{w},b}(\vec{x}^{(i)})-{y}^{(i)})
$$

逻辑回归正则化

与线性回归相似,同样只正则化w,那么我们在成本函数中加上相应的正则化值:
$$
J(\vec{w},b)=-\frac{1}{m}\sum_{i=1}^{m}[y^{(i)}log(f_{\vec{w},b}(\vec{x}^{(i)}))+(1-y^{(i)})log(1-f_{\vec{w},b}(\vec{x}^{(i)}))]+\frac{\lambda}{2m}\sum_{j=1}^{n}w_j^2
$$
​ 那么求偏导后的结果应该为:
$$
\frac{1}{m}\sum_{i=1}^{m}(f_{\vec{w},b}(\vec{x}^{(i)})-{y}^{(i)})x_j^{(i)}+\frac{\lambda}{m}w_j
$$

$$
b=b-\alpha\frac{1}{m}\sum_{i=1}^{m}(f_{\vec{w},b}(\vec{x}^{(i)})-{y}^{(i)})
$$

神经网络

结构

神经网络实际上就是来模拟人类大脑的运算过程,将若干个基础信息作为输入,然后中间有多个神经元。神经元与输入进行连接,每个神经元都可以获取到所有的输入,并根据自己的简易数学模型来挑选需要的输入进行运算,得到若干个新的参数,最后通过这几个新的参数来获得我们需要的最终结果。

我们拿一个实例来说明这件事,如果一件衣服的特征有:成本,运输费,营销,材料质量。根据这些特征,求出这件衣服被认为是十分划算的概率。

神经网络模型

根据上图,最左边是输入的四个特征,被称为输入层,中间一共有三个神经元,第一个神经元代表可负担程度,通过成本和运费得到,第二个神经元代表品牌效应,由营销所决定,第三个是质量,由成本和材料质量所决定。这一层被称为隐藏层,每个参数都是由逻辑回归得到的,得到的新的参数被称之为激活值。最后这三个参数再进行一次逻辑回归,得到最终的结果,即这件衣服被认为是十分划算的概率,得到结果的这一层被称为输出层

在实际中,我们只需要决定一共需要多少个层,和每一层有多少个神经元。神经网络作为一种很强的学习算法,因此不用具体考虑每一个神经元要干什么。这里有一个名词,有多层的神经网络被称为多层感知器。

总的来说,神经网络的工作原理就是每一层输入一个数字向量,应用一堆逻辑回归单元,然后计算另一个向量,一层接着一层,直到得到最终的输入层计算。

第几层的变量应该是在右上角标明,比如,第一层输出的值应该标为$\vec{a}^{[1]}$,第二层逻辑回归模型中的第三个神经元的参数w应该为$\vec{w}_3^{[2]}$,其中,输入层有时候也会被称之为第0层,神经网络的层数的计数方式是所有的隐藏层加上输出层,不包含输入层。

对于每一层的激活值,有一个通项公式:
$$
a_j^{[l]}=g(\vec{w}_j^{[l]}\cdot\vec{a}^{[l-1]}+b_j^{[l]})
$$
该公式中,llayer的缩写,表示第l层神经网络,每一层的每一个神经元的的激活值的结果都等于该神经元的逻辑回归模型接收上一层的激活值所运算出的结果。j表示的是第j个神经元,g表示的是激活函数Sigmoid,该激活函数在上文中提到过,是一个s型函数。激活函数指的就是可以输出激活值的函数。还有一个注意点,输入的特征值$\vec{x}$(即输入层)在神经网络中也可以被称作$\vec{a}^{[0]}$。

前向传播

神经网络的前向传播指的是从左向右依次传播神经元的激活值。

用手写数字识别来举例,现在我们有一个需求,用来判定一个手写数字是0还是1,那么我们需要构建一个神经网络,如图所示:

前向传播

显然,该神经网络是一层一层向后进行传播的,每一层都会获取前一层的激活值,并根据这一层每一个神经元的逻辑回归,得到该层的激活值。这个操作就是神经网络的前向传播,按照从左到右前进的方向进行计算。

推理实现

定义

神经网络的一个显著特点是可以将相同的算法应用于许多不同的应用程序之中,对于不同的应用场景而言,很多时候只是神经网络的参数不同。当我们拥有针对某一场景的参数时,我们便可以在获得输入时准确预测输出结果,这一过程便被称之为神经网络推理。

烘焙咖啡案例

案例讲解

我们现在用一个煮咖啡的例子,在烘焙咖啡豆的时候,一般来讲会有两个参数,一个是烘焙的温度(横轴),一个是烘焙的时间(纵轴)。我们现在需要做的是,训练一个神经网络,让其能够预测在某一特定温度和时间下,这个咖啡豆是否能煮出好咖啡。在下图中,红色的叉代表好咖啡,蓝色的圆圈代表坏咖啡。

咖啡烘焙

代码实现

烘焙咖啡神经网络模型

根据前面所学,该模型有一个输入层,一个隐藏层和一个输出层。对于烘焙咖啡这一场景而言,输入层一共有两个参数,分别是温度和时间。

首先设置一下输入层的参数,我们假设要推理的200度,17分钟情况下烘焙出的咖啡豆是否能煮出好咖啡。

1
x = np.array([[200.0, 17.0]])

接下来创建第一层作为第一个隐藏层,使其含有三个神经元,这意味着该层中的三个隐藏单元用作激活函数,即sigmoid函数。下面这段代码中的Dense代表全连接层,每个神经元都与上一层的所有神经元相连接,这意味着每个神经元都接收来自上一层的所有输入,并产生一个输出。随着对神经网络的学习,会了解其他类型的层。

1
layer_1 = Dense(units = 3, activation = "sigmoid")

现在我们的神经网络的第一个隐藏层已经构建完成了,用一个变量去接收初始值传入后得到的结果。

1
a1 = layer_1(x)

很明显,这个a1将包含三个元素,因为第一层有三个神经元,每个神经元都会产生一个输出结果。

以上就是第一层的构建,下面将进行第二层也就是输出层的构建。

第二层只有一个神经元建立方法如下:

1
layer_2 = Dense(unites = 1, activation = "sigmoid")

最后,我们可以得到输出层的结果,将用a2来进行存储。

1
a2 = layer_2(a1)

这个结果将会是一个概率值,如果想要实现二分类,只需要再写一个判断语句,如果大于等于0.5,那么就是1,反之就是0。

手写数字识别案例

案例讲解

该案例与上述极其相似,只需要更改隐藏层的层数和每一层神经元的个数即可,在此就不过多赘述了。

手写数字识别模型

上述是手写数字识别的神经网络模型图。

代码实现

相对于上述代码,只需要更改每一层神经元的个数即可:

1
2
3
4
5
6
7
x = np.array([[0.0, ..., 245, ..., 240, ..., 0]])
layer_1 = Dense(units = 25, activation = "sigmoid")
a1 = layer_1(x)
layer_2 = Dense(units = 15, activation = "sigmoid")
a2 = layer_2(a1)
layer_3 = Dense(units = 1, activation = "sigmoid")
a3 = layer_3(a2)

TensorFlow中的数据形式

TensorFlow

TensorFlow是一个基于数据流编程的符号数学系统,被广泛应用于各类机器学习算法的编程实现,其前身是谷歌的神经网络算法库DistBelief

现如今,NumPy已经成为了线性代数和Python的标准库,但是NumPyTensorFlow中的数据表示方式存在一些不一致,因此,需要去学习这些约定,这样可以实现正确的代码,并有望在神经网络中运行。

NumPy中的存储

在上述咖啡的案例中可以发现,创建矩阵的时候,里面有两个中括号。

1
x = np.array([[200.0, 17.0]])

在我们解决这个问题之前,首先来看两个例子。

NumPy矩阵格式

上图所表示的分别是$2\times3$和$4\times2$的矩阵,如果学过C/C++的话,你会发现这种表示方法和其中的二维数组很像,只不过一个用了花括号,一个用了中括号。在咖啡的例子中,我们可以把它理解成是一个$1\times2$的矩阵,也就是一个只有一行的二维数组。

同理可知:

1
x = np.array([[200, 17]])

表示的是一个$1\times2$的矩阵。

1
2
x = np.array([[200], 
[17]])

表示的是一个$2\times1$的矩阵。

上述的这两个所表示的都是二维的矩阵。

在之前学习线性回归存储数据的时候,使用的矩阵只有一个中括号而非两个,这种情况下我们称之为一维向量,可以通过数中括号套了几层来判断是几维矩阵。

1
x = np.array([200, 17])

上述表示的是一个一维向量。

TensorFlow中的存储

在之前搭建神经网络的时候,我们写过如下这段代码:

首先设置一下输入层的参数,我们假设要推理的200度,17分钟情况下烘焙出的咖啡豆是否能煮出好咖啡。

1
2
3
x = np.array([[200.0, 17.0]])
layer_1 = Dense(units = 3, activation = "sigmoid")
a1 = layer_1(x)

这里面的$a1$其实是一个$1\times3$的张量,如果输出这个$a1$的话,会显示:

1
tf.Tensor([[0.2 0.7 0.3]], shape=(1, 3), dtype=float32)

在这里,中括号的三个数字代表这一层全连接层的计算结果,数量与这一层的神经元的数量相匹配,得到的是一个$1\times3$的矩阵,float32意味着它是一个32为的小数,Tensor表示的是张量,这种数据类型可以有效地存储和执行矩阵计算。

从技术上来讲,张量比矩阵更通用一些,在这里可以把张量看作是矩阵的一种方式。

可以使用一个函数将张量转换为NumPy数组:

1
a1.numpy()

搭建神经网络

烘焙咖啡

回顾一下之前烘焙咖啡的例子,我们要搭建的模型如下:

烘焙咖啡神经网络模型

现在我们要搭建一个这样的神经网络,并用其进行训练,训练数据如下:

烘焙咖啡数据

和之前一样,需要先建立两个层,一个是拥有三个神经元的全连接层,另一个是只有一个神经元的输出层:

1
2
layer_1 = Dense(units = 3, activation = "sigmoid")
layer_2 = Dense(units = 1, activation = "sigmoid")

我们希望使用这两个层,将它们串在一起形成一个神经网络,可以使用下述代码来实现这一功能:

1
model = Sequential([layer_1, layer_2])

这个函数是一种组合函数,可以将多个函数顺序连接起来,实现函数的组合。可以将多个函数视为一个整体,从而获得更高的效率。

接下来需要进行数据的导入,我们需要创建一个数据集:

1
2
3
4
x = np.array([[200.0, 17.0],
[120.0, 5.0],
[425.0, 20.0],
[212.0, 18.0]])

对于这个数据集,我们有相应的标签,因此需要创建一个标签集与数据集一一对应,表示当前参数下是否可以制作出好的咖啡:

1
y = np.array([1, 0, 0, 1])

然后我们可以来训练神经网络了,使用之前组合好的函数model,首先对其进行编译,需要使用一些参数调用模型,具体的参数调用在接下来会写:

1
model.compile(...)

编译过后,就可以来进行训练了:

1
model.fit(x, y)

该函数告诉张量流采用model所表示的神经网络,使用数据xy进行训练。

训练完成后,如果我们要在上面推理新的数据集,只需要调用下述函数即可:

1
model.predict(x_new)

其中,x_new为新的数据集,即要进行推理预测的数据集。

手写数字识别

在手写数字识别的案例中,我们搭建了一个如下的神经网络:

手写数字识别模型

该部分的代码只需要更改神经元的个数和隐藏层的层数即可:

1
2
3
4
5
6
7
8
9
10
layer_1 = Dense(units = 25, activation = "sigmoid")
layer_2 = Dense(units = 15, activation = "sigmoid")
layer_2 = Dense(units = 1, activation = "sigmoid")
model = Sequential([layer_1, layer_2, layer_3])
model.compile(...) #这部分为编译,需要使用一些参数,在后面会具体讲到
x = np.array([[0, ..., 245, ..., 17],
[0, ..., 200, ..., 184]])
y = np.array([1, 0])
model.fit(x, y)
model.predict(x_new)

第四行其实是相当于将前三行给整合到一起,因此我们也可以把前四行写成一整句话,前四行代码等价于:

1
2
3
4
model = Sequential([
Dense(units = 25, activation = "sigmoid"),
Dense(units = 15, activation = "sigmoid"),
Dense(units = 1, activation = "sigmoid")])

单个网络层上的前向传播

我们接着使用之前的烘焙咖啡的例子,来看一看单个网络层上的神经网络都做了哪些事情。

单个网络层模型

Python中,将使用一维数组来表示所有这些向量和参数,因此只需要使用一个方括号,首先将输入层的数据存储起来:

1
x = np.array([200, 17])

接下来我们要计算第一个神经元的参数,计算公式:
$$
a_1^{[1]}=g(\vec{w}_1^{[1]}\cdot\vec{x}+b_1^{[1]})
$$
现在是在进行模拟推理的过程,因此参数都是已经确定好的,只需要直接使用即可,所以现在不需要纠结这些参数是如何确定下来的。而实际上进行神经网络训练的时候,是没有事先确定好的参数的,因此我们需要用有监督学习来计算参数,方便以后的推理过程,在这种情况下,当前步骤的参数是随机初始化的,经过前向传播之后,对比预测结果,再通过反向传播来调整参数。

$\vec w_1^{[1]}$的两个参数分别为$1$和$2$,$b_1^{[1]}$的参数为$-1$,使用Sigmoid函数作为激活函数:

1
2
3
4
w1_1 = np.array([1, 2])
b1_1 = np.array([-1])
z1_1 = np.dot(w1_1, x) + b1_1 #该函数在上文中提到过,可以直接进行点乘操作,效率比循环要高
a1_1 = sigmoid(z1_1)

接下来我们要计算第二个神经元的参数,计算公式:
$$
a_2^{[1]}=g(\vec{w}_2^{[1]}\cdot\vec{x}+b_2^{[1]})
$$
$\vec w_2^{[1]}$的两个参数分别为$-3$和$4$,$b_2^{[1]}$的参数为$1$,使用Sigmoid函数作为激活函数:

1
2
3
4
w1_2 = np.array([-3, 4])
b1_2 = np.array([1])
z1_2 = np.dot(w1_2, x) + b1_2
a1_2 = sigmoid(z1_2)

接下来我们要计算第三个神经元的参数,计算公式:
$$
a_3^{[1]}=g(\vec{w}_3^{[1]}\cdot\vec{x}+b_3^{[1]})
$$
$\vec w_3^{[1]}$的两个参数分别为$5$和$-6$,$b_3^{[1]}$的参数为$2$,使用Sigmoid函数作为激活函数:

1
2
3
4
w1_3 = np.array([5, -6])
b1_3 = np.array([2])
z1_3 = np.dot(w1_3, x) + b1_3
a1_3 = sigmoid(z1_3)

至此,三个神经元的参数均已计算完成,接下来需要将它们组合为一个新的向量,作为输入输入至下一层:

1
a1 = np.array([a1_1, a1_2, a1_3])

在最后,需要计算输出层的结果,计算公式:
$$
a_1^{[2]}=g(\vec{w}_1^{[2]}\cdot\vec{a}^{[1]}+b_1^{[2]})
$$
$\vec w_1^{[2]}$的三个参数分别为$-7$,$8$和$2$(在这里需要有三个值才可以和该层输入进行点积操作),$b_1^{[2]}$的参数为$3$,使用Sigmoid函数作为激活函数:

1
2
3
4
w2_1 = np.array([-7, 8, 2])
b2_1 = np.array([3])
z2_1 = np.dot(w2_1, a1) + b2_1
a2_1 = sigmoid(z2_1)

前向传播的一般实现

参数设置

在上文中讲到了单个网络层上的前向传播,这部分将详细地讲一下如何实现dense函数,即全连接层的内部构造。

回顾一下单个网络层模型:

单个网络层模型

首先我们需要准备一下初始参数。在推理的过程中,这些参数是已经计算好的;在训练神经网络的过程中,这些参数是随机初始化的。

先设置一下$w$的参数:

w参数

1
2
W = np.array([[1, -3, 5],
[2, 4, -6]])

接着设置$b$的参数:

b参数

1
b = np.array([-1, 1, 2])

最后设置一下该层的输入:
$$
\vec{a}^{[0]}=\vec{x}
$$

1
a_in = np.array([-2, 4])

全连接层函数

在设置好参数后,需要使用全连接层,也就是dense函数来搭建神经网络,我们先来看一下前文中是如何搭建的:

1
2
layer_1 = Dense(units = 3, activation = "sigmoid")
layer_2 = Dense(units = 1, activation = "sigmoid")

下面将具体讲一下Dense内部到底做了什么:

1
2
3
4
5
6
7
8
def dense(a_in, W, b, g):
units = W.shape[1] #获取i + 1维度的长度,在这里表示列数,获取到的值为3
a_out = np.zeros(units) #将a设置为与神经元个数相同维数的零矩阵,获取到的是1×3的零矩阵
for j in range(units): #j的取值是0到神经元的数量-1,这里为0, 1, 2
w = W[:, j] #二维数组切片,获取第j + 1列的元素
z = np.dot(w, a_in) + b[j] #计算第j + 1个神经元的z值,np.dot会直接让两个矩阵点乘求和
a_out[j] = g(z) #用相应的激活函数处理z,得到第j + 1个神经元的输出结果
return a_out #返回计算结果

组合函数

当搭建好每一层神经网络后,需要将它们用组合函数合并在一起,前文中是这样做的:

1
2
3
4
model = Sequential([
Dense(units = 25, activation = "sigmoid"),
Dense(units = 15, activation = "sigmoid"),
Dense(units = 1, activation = "sigmoid")])

下面将具体讲一下Sequential内部到底做了什么:

1
2
3
4
5
6
7
def sequential(x):
a1 = dense(x. W1, b1)
a2 = dense(a1, W2, b2)
a3 = dense(a2, W3, b3)
a4 = dense(a3, W4, b4)
f_x = a4
return f_x

注:根据线性代数的符号约定,大写字母一般指代矩阵,小写字母一般指代向量和标量。

矩阵乘法

矩阵乘法在神经网络中的应用

为什么神经网络会如此高效?主要是因为计算机可以快速计算矩阵乘法,矩阵乘法在前向传播中有着广泛的应用。首先回顾一下上面的一段代码:

1
2
3
4
5
6
7
8
def dense(a_in, W, b, g):
units = W.shape[1] #获取i + 1维度的长度,在这里表示列数,获取到的值为3
a_out = np.zeros(units) #将a设置为与神经元个数相同维数的零矩阵,获取到的是1×3的零矩阵
for j in range(units): #j的取值是0到神经元的数量-1,这里为0, 1, 2
w = W[:, j] #二维数组切片,获取第j + 1列的元素
z = np.dot(w, a_in) + b[j] #计算第j + 1个神经元的z值,np.dot会直接让两个矩阵点乘求和
a_out[j] = g(z) #用相应的激活函数处理z,得到第j + 1个神经元的输出结果
return a_out #返回计算结果

这段代码具体展示了dense函数主要都做了哪些工作,事实上,这部分代码完全符合矩阵乘法的运算方式,因此可以改写成如下这段代码:

1
2
3
4
def dense(A_in, W, B):  #注意A_in,W,B均为二维矩阵
Z = np.matmul(A_in, W) + B #矩阵乘法,可以直接对A_in矩阵和W矩阵进行矩阵乘法操作
A_out = g(Z)
return A_out

矩阵乘法会用第一个矩阵的每一行分别乘第二个矩阵的每一列,如果一个$a\times b$的矩阵和一个$b\times c$的矩阵进行矩阵乘法运算,将会得到一个$a\times c$的矩阵。不难发现,第一段代码的4~6行正好可以表示为如第二段代码中的第2行的矩阵乘法运算。

矩阵乘法代码

我们现在来看一下,如何用代码来实现矩阵乘法,我们要计算的矩阵$Z$如下所示:

矩阵乘法

首先创建一个$A$矩阵:

1
2
A = np.array([[1, -1, 0.1],
[2, -2, 0.2]])

接下来获取该矩阵的转置$AT$,这里有一个函数可以直接获取:

1
AT = A.T

r然后创建$W$矩阵:

1
2
W = np.array([[3, 5, 7, 9],
[4, 6, 8, 0]])

最后就是计算矩阵乘法了,可以直接使用相应函数来计算:

1
2
Z = np.matmul(AT, W)
Z = AT @ W

这两种方式都可以计算矩阵乘法,一般来讲,使用第一种的情况会更多一点,这样更加直观,便于理解。

让我们看一下输出结果:

1
2
3
[[ 11.   17.   23.    9. ]
[-11. -17. -23. -9. ]
[ 1.1 1.7 2.3 0.9]]

模型优化

经过上述学习,我们可以对最开始的烘焙咖啡案例进行一些优化,代码如下(参数均为提前设置好的,现阶段不需要考虑参数):

1
2
3
4
5
6
7
8
9
10
11
import numpy as np

AT = np.array([[200, 17]])
W = np.array([[1, -3, 5],
[2, 4, -6]])
b = np.array([[-1, 1, 2]])

def dense(AT, W, b, g):
z = np.matmul(AT, W) + b
a_out = g(z)
return a_out

TensorFlow实现

在这一部分,将讲一下如何使用TensorFlow实现神经网络的搭建。

我们这次使用手写数字识别的例子,下图是手写数字识别的模型:

手写数字识别模型

第一步是按照顺序将神经网络的这三个层串联起来:

1
2
3
4
5
6
7
8
9
import tensorflow as tf
from tensorflow.keras import Sequential
from tensorflow.keras.layers import Dense

model = Sequential([
Dense(units = 25, activation = "sigmoid"),
Dense(units = 15, activation = "sigmoid"),
Dense(units = 1, activation = "sigmoid")
])

一共有三层:25个神经元的隐藏层,15个神经元的隐藏层,1个神经元的输出层,都是把sigmoid函数作为激活函数。

第二步是对模型进行编译:

1
2
3
from tensorflow.keras.losses import BinaryCrossentropy

model.compile(loss = BinaryCrossentropy())

编译的时候需要指定使用哪一种损失函数,上述的的代码使用的是二元交叉熵损失函数,在后面会具体讲到它到底是什么。

第三步是将前两步拟合在一起:

1
model.fit(X, Y, epochs = 100)

这个函数的作用是开始训练你的模型,其中XY表示的是训练集及其标签,训练的方式和前文讲的到的梯度下降是一个原理,梯度下降需要指定一个终止条件,这里面的$100$表示梯度下降一共执行$100$代。

逻辑回归的实现

在之前学习逻辑回归的时候,讲了如何使用梯度下降来实现逻辑回归,我们现在来实现一下这部分的代码。
$$
f_{\vec{w},b}(\vec{x})=g(\vec{w}\cdot\vec{x}+b)=\frac{1}{1+e^{-(\vec{w}\cdot\vec{x}+b)}}
$$
上面是求解$f_{\vec{w},b}$的公式,代码如下:

1
2
z = np.dot(w, x) + b
f_x = 1 / (1 + np.exp(-z))

然后要实现其损失函数,也就是上述提到的交叉熵损失函数
$$
L(f_{\vec{w},b}(\vec{x}^{(i)}),y^{(i)})=-y^{(i)}log(f_{\vec{w},b}(\vec{x}^{(i)}))-(1-y^{(i)})log(1-f_{\vec{w},b}(\vec{x}^{(i)}))
$$
上述公式对应代码如下:

1
loss = -y * np.log(f_x) - (1 - y) * np.log(1 - f_x)

1最后要更新wb的值,代码如下:

1
2
w = w - alpha * dj_dw
b = b - alpha * dj_db

损失函数

这部分是对目前学过的两个损失函数做一个小小的总结。

交叉熵损失函数

$$
L(f_{\vec{w},b}(\vec{x}^{(i)}),y^{(i)})=-y^{(i)}log(f_{\vec{w},b}(\vec{x}^{(i)}))-(1-y^{(i)})log(1-f_{\vec{w},b}(\vec{x}^{(i)}))
$$

该损失函数非常适合用来处理二分类问题,调用方法:

1
2
from tensorflow.keras.losses import BinaryCrossentropy
model.compile(loss = BinaryCrossentropy())

均方误差损失函数

$$
J(w,b)=\frac{1}{m}\sum_{i=1}^{m}(\hat{y}^{(i)}-{y}^{(i)})^2
$$

该函数非常时候用来处理回归问题,调用方法:

1
2
from tensorflow.keras.losses import MeanSquaredError
model.compile(loss = MeanSquaredError())

激活函数

Sigmoid

Sigmoid函数是是一个S型函数,常常作为二分类的激活函数,其图像如下所示:

Sigmoid函数

该激活函数的解析式:
$$
g(z)=\frac{1}{1+e^{-z}}\quad 0<g(z)<1
$$

Identity

该激活函数是一个线性激活函数,也可以说是没有使用激活函数,其图像如下所示:

Identity函数

该激活函数的解析式:
$$
g(z)=z
$$

ReLU

该函数叫做线性整流函数,在y轴左侧一直为$0$,在y轴右侧是一条$45^\circ$的直线,其图像如下所示:

ReLU函数

该激活函数的解析式:
$$
g(z)=max(0,z)
$$

激活函数的选择

激活函数有很多种,选择哪一种主要看我们需要解决的问题。

如果我们要解决的是二分类问题,那么很显然,Sigmoid函数非常适合作为输出层的激活函数;如果我们要预测股票的价格,对于股票而言,价格变动有正有负,那么使用线性激活函数作为输出层的激活函数是非常合适的;如果要预测的东西只能取非负值,比如房价,那么输出层的激活函数应该选择ReLU,该函数只有非负值,很适合完成这件事情。

事实证明ReLU激活函数是迄今为止许多从业者训练神经网络的最常见选择,而Sigmoid函数使用的比较少,主要是因为前者的计算速度会更快一些,因为它只需要计算$0$和$z$中的最大值就可以了,而后者需要先取幂,再计算分数等等,效率相对来说比较低。还有一个原因是ReLU函数只有左半部分很平坦,而Sigmoid函数在左下和右上都非常平坦,这会导致在进行梯度下降的时候,会导致在这些平坦的地方下降的非常慢。虽然梯度下降是在处理Wb,并不会直接处理激活函数,但是激活函数是计算的一部分,这就导致成本函数也有很多地方会受到其影响,也会编程平坦的,这就会导致梯度很小,学习速度很慢。

Sigmoid和ReLU

如果是二分类问题,那么输出层使用Sigmoid

1
activation = "sigmoid"

如果预测值可以取正值或负值,那么输出层使用liner,也就是线性函数:

1
activation = "liner"

如果预测值只能取非负值,那么输出层使用ReLU

1
activation = "relu"

对于隐藏层而言,建议只使用ReLU作为默认的激活函数。

如果不使用激活函数,或者全部使用线性激活函数,那整个神经网络其实就相当于一个普通的线性回归或者逻辑回归,因为若干个线性多项式组合过之后依旧是一个线性多项式,因此不能单纯使用线性激活函数,而ReLU激活函数虽然简单,但是已经可以做到单纯的线性回归和逻辑回归做不到的事情了。

多分类问题

二分类与多分类问题

之前解决的分类问题都是二分类问题,对于一个初始输入,其结果只有两种可能值。而现实情况中,往往会存在多种情况,比如在进行手写数字识别的时候,一共有十个数字需要进行识别,在这种情况下,只依靠二分类是远远不够的,我们把这类需要进行更加详细分类的问题称之为多分类问题。

二分类问题

多分类问题

Softmax

对于二分类而言,使用Sigmoid函数进行逻辑回归,其两种情况的概率如下所示:
$$
a_1=g(z)=\frac{1}{1+e^{-z}}=P(y=1|\vec{x})
$$

$$
a_2=1-a_1=P(y=0|\vec{x})
$$

对于多分类来说(下面用的是一个四分类的例子),一般会使用Softmax函数来进行预测,首先求出每一个分类相应的$z$值:
$$
z_1=\vec{w_1}\vec{x}+b_1
$$

$$
z_2=\vec{w_2}\vec{x}+b_2
$$

$$
z_3=\vec{w_3}\vec{x}+b_3
$$

$$
z_4=\vec{w_4}\vec{x}+b_4
$$

每一种分类对应的公式如下所示:
$$
a_1=\frac{e^{z_1}}{e^{z_1}+e^{z_2}+e^{z_3}+e^{z_4}}=P(y=1|\vec{x})
$$

$$
a_2=\frac{e^{z_2}}{e^{z_1}+e^{z_2}+e^{z_3}+e^{z_4}}=P(y=2|\vec{x})
$$

$$
a_3=\frac{e^{z_3}}{e^{z_1}+e^{z_2}+e^{z_3}+e^{z_4}}=P(y=3|\vec{x})
$$

$$
a_4=\frac{e^{z_4}}{e^{z_1}+e^{z_2}+e^{z_3}+e^{z_4}}=P(y=4|\vec{x})
$$

我们将上述两组式子综合一下,可以求得:
$$
z_j=\vec{w_j}\cdot\vec{x}+b_j(j=1,\dots,N)
$$

$$
a_j=\frac{e^{z_j}}{\sum^N_{k=1}e^{z_k}}=P(y=j|\vec{x})
$$

事实证明,如果对于二分类问题使用了Softmax函数来进行回归,那么其计算结果与Sigmoid函数来进行逻辑回归基本相同。

损失函数

让我们回忆一下对于二分类问题的损失函数是怎么定义的:
$$
loss=-yloga_1-(1-y)log(1-a_1)
$$
对于多分类问题,采用相似的策略:

Softmax损失函数

手写数字识别

在前文中讲到的手写数字识别问题是一个简单的二分类问题,但实际上,数字并不只有简单的01,一共包含有十个数字,因此,这其实是一个多分类问题。对于前面讲到的解决方案,我们只需要把最后的输出层换成十个神经元组成的输出层就可以了,输出层的激活函数也应该使用softmax函数,隐藏层使用的激活函数为ReLU函数。

十数字手写识别神经网络结构

下面的公式对应的是输出层每一个神经元的计算公式:

每个神经元计算公式

TensorFlow实现

首先需要调用相应的函数和库:

1
2
3
import tensorflow as tf
from tensorflow.keras import Sequential
from tensorflow.keras,layers import Dense

接下来需要搭建神经网络的每一层,并将他们组合在一起,前两层使用Relu作为激活函数,输出层采用Softmax作为激活函数。

1
2
3
4
model = Sequential([
Dense(units = 25, activation = "relu"),
Dense(units = 15, activation = "relu"),
Dense(units = 10, activation = "softmax")])

然后需要引用相应的损失函数,并利用该损失函数编译模型:

1
2
from tensorflow.keras.losses import SparseCategoricalCrossentropy
model.compile(loss = SparseCategoricalCrossentropy())

编译过后需要开始训练模型:

1
model.fit(X, Y, epochs = 100)

实际上,这段代码及时有效也并不推荐使用,在后面会有一个更推荐代码来完成这件事情。

Softmax函数改进

事实证明,在程序设计中,很多数据都是有精度上限的,举个简单的例子:

1
2
x1 = 2.0 / 10000
x2 = 1 + (1 / 10000) - (1 - 1 / 10000)

在我们计算的时候,会认为x1x2的值应该是相等的,其结果应该都是0.0002,但是如果实际输出这两个结果值,会发现并不相同,这是因为,浮点数的精度是有限的,导致没有办法很精确地表示出来最终结果。

下面来看一下之前学过的逻辑回归函数:
$$
a=g(z)=\frac{1}{1+e^{-z}}
$$

$$
loss=-ylog(a)-(1-y)log(1-a)
$$

在上述公式中,引入了一个中间变量。如果我们正常进行计算的话,是不会产生任何问题的,但是实际上,由于使用了中间变量,会导致误差变大(由于变量的精度是有限的),因此我们可以在这个基础上进行一个简单的优化,即不使用中间变量进行损失值的计算。
$$
loss=-ylog(\frac{1}{1+e^{-z}})-(1-y)log(1-\frac{1}{1+e^{-z}})
$$
如果使用sigmoid函数来实现十个手写数字识别的问题话,代码应该如下所示:

1
2
3
4
5
model = Sequential([
Dense(units = 25, activation = "relu"),
Dense(units = 15, activation = "relu"),
Dense(units = 10, activation = "sigmoid")])
model.compile(loss = BinaryCrossEntropy())

回到上面的问题,如果在最后一层使用sigmoid函数的话,在用得到的结果用损失值函数进行编译,会导致精度降低。因此,可以将最后一层的逻辑回归sigmoid激活函数与损失函数组合在一起,代码如下:

1
2
3
4
5
model = Sequential([
Dense(units = 25, activation = "relu"),
Dense(units = 15, activation = "relu"),
Dense(units = 10, activation = "linear")])
model.compile(loss = BinaryCrossEntropy(from_logits = True))

通过改变最后一行代码可以解决上述问题,会让误差变得更小一些。当涉及到softmax函数时,数值的舍入误差会变得更加糟糕。需要注意的是,如果我们使用这种方式,那最后一层的激活函数相当于和损失函数组合在一起了,因此在神经网络的最后一层中,需要使用线性激活函数。

同理,如果要优化softmax函数的话,需要做类似的事情,让我们先来看一下原版代码:

1
2
3
4
5
model = Sequential([
Dense(units = 25, activation = "relu"),
Dense(units = 15, activation = "relu"),
Dense(units = 10, activation = "softmax")])
model.compile(loss = SparseCategoricalCrossEntropy())

优化之后的代码如下所示:

1
2
3
4
5
model = Sequential([
Dense(units = 25, activation = "relu"),
Dense(units = 15, activation = "relu"),
Dense(units = 10, activation = "linear")])
model.compile(loss = SparseCategoricalCrossEntropy(from_logits = True))

这部分改进只是除了增加了精度以外并没有什么变化,但是如果要实现其底层代码的话,也建议采用此种方法。

最后,再让我们将优化好的这部分代码搭载进去,看一下整体的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 创建模型
import tensorflow as tf
from tensorflow.keras import Sequential
from tensorflow.keras.layers import Dense
model = Sequential([
Dense(units = 25, activation = "relu"),
Dense(units = 15, activation = "relu"),
Dense(units = 10, activation = "linear")])

# 设置损失函数
from tensorflow.keras.losses import SparseCategoricalCrossEntropy
model.compile(..., loss = SparseCategoricalCrossEntropy(from_logits = True))

# 开始训练
model.fit(X, Y, epochs = 100)

# 进行预测
logits = model(X)
f_x = tf.nn.softmax(logits)

需要注意的是,使用这种方法进行预测,最后产生的结果是线性激活函数处理后的结果,也就是相应神经元的$z_i$的值,因此需要再放到softmax函数中处理一下。对于逻辑回归而言,这个操作也是非常必要的。

多标签分类问题

多分类问题指的是对于一个东西,可能有多种分类,要求模型识别出相应的东西属于哪一分类,最典型的例子是手写数字识别。

多标签分类问题与其十分相似,其目的是对于一个东西而言,要在上面识别出不同标签的东西。例如给定一张图片,要求识别上面的汽车、公交车、行人,这种问题是对于单一输入要查找三个不同的标签。

对于这种问题,可以训练三个神经网络,第一个检测汽车,第二个检测公交车,第三个检测行人:

多标签分类问题的三个神经网络

上述的这种方法并不是很推荐,还有另一种方法也可以做到这一点,那就是训练一个神经网络同时检测汽车、公共汽车和行人这三者,神经网络结构如下所示:

三标签分类神经网络

对于这个问题而言,使用Sigmoid函数作为输出层的激活函数是非常合理的,因为输出的三个结果是相互独立的关系,Softmax函数并不适用这个问题,因为这个函数一般解决的是非独立事件,所有输出的结果的概率应该相加为$1$。

Adam算法

之前学习过的梯度下降算法,是线性回归和逻辑回归等许多算法以及神经网络早期实现的基础,但事实证明,现在有一些其他优化算法可以最小化成本函数,甚至比梯度下降更好。

回想一下梯度下降的表达式:
$$
w_j=w_j-\alpha\frac{\partial}{\partial{w_j}}J(\vec{w},b)
$$
在传统梯度下降公式中,收敛速度很大程度上取决于学习率。如果下降的时候,每次都朝着同一个相似的方向收敛一小步,那么我们希望学习率稍微变大一点,使其更快收敛;反之,如果下降的时候,每次变化都是一个震荡的形式,那么我们会希望学习率稍微小一些。

Adam算法

左图是学习率较小的情况,右图是学习率较大的情况。

Adam代表Adaptive Moment Estimation,该算法不会设置一个全局的学习率,而是对模型的每个参数使用不同的学习率,有几个参数就会有几个与之对应的学习率。

Adam算法的实现过程有一些复杂,如果以后学习更加高级的深度学习课程,会学习到相关的细节,现阶段,可以用这种方法来实现该算法,需要在编译的时候添加一个新的参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 创建模型
import tensorflow as tf
from tensorflow.keras import Sequential
from tensorflow.keras.layers import Dense
model = Sequential([
Dense(units = 25, activation = "relu"),
Dense(units = 15, activation = "relu"),
Dense(units = 10, activation = "linear")])

# 设置损失函数
from tensorflow.keras.losses import SparseCategoricalCrossEntropy
model.compile(optimizer = tf.keras.optimizers.Adam(learning_rate = 1e - 3), loss = SparseCategoricalCrossEntropy(from_logits = True))

# 开始训练
model.fit(X, Y, epochs = 100)

# 进行预测
logits = model(X)
f_x = tf.nn.softmax(logits)

网络类型

密集层

密集层是我们一直使用的隐藏层,他的输入是上一层的每个神经元的输出值:
$$
\vec{a}_1^{[2]}=g(\vec{w}_1^{[2]}\cdot \vec{a}^{[1]}+b_1^{[2]})
$$

卷积层

用手写数字识别来举例子,对于输入的图像,每一个神经元都获取一部分像素,也就是每个神经元获取到的数据是不一样的,他们只关注自己所负责的那片区域。

使用这种方法可以加快计算速度,同时卷积层需要更少的训练数据,也不太容易过拟合。

如果神经网络中有多个卷积层,我们也可以将其称之为卷积神经网络,卷积神经网络会在深度学习中详细介绍。

导数

在这一部分,将介绍一下求导数的相关代码。

sympy

求导数的时候,可以使用这个库,里面有丰富的处理导数的方法。

1
import sympy

symbols

该方法的作用是确定要以什么符号作为变量。

代码:

1
2
3
J, w = sympy.symbols('J,w')
J = w**2
print(J)

输出结果:

1
w**2

diff

该方法的作用是对函数进行求导。

代码:

1
2
dJ_dw = sympy.diff(J, w)
print(dJ_dw)

输出结果:

1
2*w

subs

该方法的作用是将值代入函数并求最终结果。

代码:

1
print(dJ_dw.subs([(w, 2)]))

输出结果:

1
4

计算图

计算图不是物理意义上的图像,而是计算机科学中的图,是一组由边连接或由箭头连接的节点。

计算图

上述计算图是用来计算在$w=2,b=8,x=-2,y=2$时的成本函数,其成本函数为:
$$
J(w,b)=\frac{1}{2}(a-y)^2
$$
上面的这种计算方式是从左到右计算的,被叫做前向传播,但是计算导数的时候是从右向左计算的(链式求导法则),因此其被称为反向传播。

模型评估

训练集与测试集

在我们进行模型训练的时候,例如进行线性回归,我们如果设置足够多的参数,一般来讲会更好的拟合训练集,但这往往也会造成过拟合,例如下面这张图:

过拟合

对于该模型,有着太多的曲线,这虽然可以很好的拟合我们的训练数据,但很显然,如果给一个其他数据的话,它并不一定能产生很好的结果。

为了降低产生这种情况的可能性,我们一般会将数据集拆分成两份:训练集和测试集。一般来讲训练集的数量会较多一些,测试集会较少。例如,如果我有十个数据,我可以将$70%$的数据用作训练集,$30%$的数据用于测试集。

当我们具体在训练模型的时候,需要用训练集进行训练,在上图中举的例子中,很明显,对于训练集来说,其成本函数非常非常小,甚至趋近于$0$,但是这并不意味着它在测试集上的表现也是同样的。因此,我们不仅需要算一下训练集的成本函数,还需要去计算一下测试集的成本函数,通过测试集的成本函数来评判一下我们的模型是否具有良好的泛化能力。

使用这种方法可以去评估模型的好坏,我们可以进一步完善这个想法,用这个技术让算法自动选择出一个好的模型。

模型选择

在训练线性回归的模型的时候,我们无法确定该使用几次幂的多项式来作为最终模型,因此我们把这些多项式全都列出来,从这里面选择一个最好的模型。

线性回归多项式

训练集

上图中列举了十个可能的多项式,我们可以都对他们进行训练,对每一个训练好的模型都求解一下它的成本函数,看看哪个是最低的,最低的那个往往可以更好地拟合我们的数据集。

但是这种方法有一个弊端,非常容易造成过拟合,因为我们的模型是基于已知的数据集生成的,这样得到的最优模型往往只是在当前训练集的前提下成本函数是最低的,如果用该模型去泛化别的数据,可能效果不是很好。

训练集+测试集

为了解决这个问题,我们可以将数据集划分成训练集和测试集,训练的时候使用训练集的数据进行模型训练,等到挑选哪个模型是最优的时候,可以采用测试集计算成本函数,找成本值最低的模型即可。

对于上述这种方式,可以大幅度降低过拟合的概率,但是这样出来的结果也是会比较乐观的。因为就算是训练和测试采用的不是同一批数据进行操作,最后挑选出来的模型往往会更偏向于测试集的结果,这会使我们评判模型好坏的时候更加乐观。

训练集+测试集+交叉验证集

我们这次使用一个新的方式,将原本的数据集分为训练集,测试集和交叉验证集。例如如果数据集一共有十条数据,那么可以将六条分为训练集,两条分为测试集,两条分为交叉验证集。

如下分别是训练集、交叉验证集、测试集的成本函数计算公式:

三种子集的成本函数

我们依旧使用训练集来训练多个模型,之后使用交叉验证集来挑选一个成本函数最低的模型,作为我们的最优模型,如果我们要评判模型的好坏,需要用测试集来计算模型的成本函数,将该结果作为模型泛化能力的评判标准,使用这种方式,可以最大程度上保证评判结果的客观性。

使用这种方法,其实就是由交叉验证集的结果选出来它认为的最好的模型,测试集在这里只起到一个客观评价这个模型泛化能力的作用。

偏差与方差

偏差指的是训练出来的模型与训练集的差距程度,方差指的是训练出来的模型与未出现在训练集中的数据的差距程度。

对于欠拟合而言,偏差和方差都会很高。

对于过拟合而言,偏差很低,但是方差很高。

对于一个拟合的非常好的模型而言,其偏差和方差都会很低。

在我们选择不同最高次数项的函数时,得到的结果也会不一样,他们的大概关系图如下:

偏差和方差关系图

该图像的横坐标是多项式的最高次数,纵坐标代表与成本函数值。

可以发现对于训练数据,随着次数的升高,其偏差值会越来越小,但这最终会导致过拟合。对于未知的测试数据,随着次数的升高,呈现先下降再升高的趋势。

正则化的影响

正则化在前面的部分学习过,在这就不过多赘述了,这里写一下它的公式:
$$
J(\vec{w},b)=\frac{1}{2m}\sum^m_{i=1}(f_{\vec{w},b}(\vec{x}^{(i)})-y^{(i)})^2+\frac{\lambda}{2m}\sum^n_{j=1}w^2_j
$$
如果$\lambda$的的值非常大,那么算法就会让这些w参数非常小,最后会使得他们的值都非常接近于0,会导致最后的模型约等于b的值。

如果$\lambda$的的值非常小,那么就相当于没有进行正则化操作,最终会导致模型过拟合。

因此,$\lambda$的选择对于模型而言也会起到很大作用,所以我们可以利用之前的思想,对于$\lambda$也进行交叉验证,计算多个$\lambda$值的不同情况,然后选择一个最好的情况。

评估基准

对于我们设计出来的模型,并不是必须百分百识别数据才是好的模型,我们一般需要制定一个评估基准,来判定我们这个模型的效果怎么样,如下有三种常用的方法:

  1. 在使用非结构化数据时,例如音频、图像或文本等,人类水平的表现通常是一个很好的基准,我们可以以人类的识别率作为基准。
  2. 如果有一些竞争算法,可能是其他人已经实现的或者是以前实现的,又或者是竞争对手的算法,都可以以他们为基础来建立基准性能水平。
  3. 还可以那句经验来指定基准性能水平,就是希望达到的错误水平是多少,或者希望算法达到的期望性能水平是多少。

指定好基准后,我们在训练模型后,一共会有三个数据,一个是基准性能水平,一个是训练误差,一个是交叉验证误差。

如果训练误差与基准性能水平有着较大的差距,说明有一个高偏差问题;如果训练误差和交叉验证误差之间有很大的差距,说明有一个高方差问题。

建立评估基准是因为并不是所有的情况都可以以0作为基准的,例如在进行语音识别的时候,可能会出现声音嘈杂的情况,这样会导致完全识别几乎是不可能的,因此我们需要指定一个基准来评判我们这个算法是否达标。

学习曲线

拟合

拟合学习曲线

对于恰好能够拟合的模型来说,即使用的函数的最高数正好合适,其学习曲线如上图所示。其中横坐标表示的是训练集的样本数量,纵坐标表示的是误差值。对于训练误差而言,随着数据集样本数的增大,会导致其训练误差越来越大;对于交叉验证误差而言,随着数据集样本数的增大,会导致其验证误差越来越小。这是因为前期样本数量非常少,假设只有一个样本,那么训练集可以很好的拟合数据,但是随着样本的不断增多,会越来越难以拟合数据。交叉验证集在前期的时候很难去根据很少的样本预测数据集,所以最开始的时候交叉验证误差会很大,最后逐步降低。随着样本数的不断增加,最后二者的误差会越来越接近。

总结一下:训练集越大,模型越难拟合训练集,但泛化能力会增强。

欠拟合

欠拟合学习曲线

上图是使用了欠拟合的模型,即使用的函数的最高次数较低。其中红色的线是基准性能水平线,是根据人类水平来定义的。对于训练误差而言,在数据很少的时候还是可以较好拟合的,训练误差会比较低。但是随着训练误差的增多,会急剧变高,会使得学习曲线相对于拟合比较好(即第一种)的曲线来讲看起来比较胖,最后会趋于平缓。这种情况下会导致训练误差和交叉验证误差在最后都远远高于基准线。

过拟合

过拟合学习曲线

上图是使用了过拟合的模型,即使用的函数的最高次数较高。对于训练误差而言,其增长会十分缓慢;同理,对于交叉验证误差而言,其下降也会非常缓慢,并且二者的差距会很大。但是如果训练样本足够多的话,训练误差也会平稳增大,但是增加得非常缓慢,交叉验证误差也会缓慢下降,最后会与基准线越来越贴合,但是这需要非常多的数据集。

总而言之,如果学习算法存在高方差,那么获得更多训练数据确实可能有所帮助。

优化模型的方法

优化模型的方法主要包括以下几种,他们都在降低训练误差和交叉验证误差上有很好的效果,破折号后面会写出适合解决哪类问题:

  • 获取更多训练集——高方差问题
  • 尝试小的特征集——高方差问题
  • 加入更多特征——高偏差问题
  • 增加多项式特征(即进行特征工程)——高偏差问题
  • 尝试降低$\lambda$的值——高偏差问题
  • 尝试增大$\lambda$的值——高方差问题

如果你发现你的算法具有高方差,那么解决这个问题的方法主要是增加训练数据或者简化模型,简化模型可以使用更小的特征集,或者增加正则化参数$\lambda$的值。

如果你的算法具有高偏差,则意味着即使在训练集上也表现不佳,这种情况可以去赋予你的模型更大的灵活性以适应更复杂或更多功能,主要方法是提供额外的特征或添加一些特征工程产生的特征,或者减少正则化参数$\lambda$的值。减少训练集也可以去解决高偏差问题,会让你的模型更好地适应训练集,但这样往往会恶化你的交叉验证误差和算法的性能,所以不要使用这种方法来解决高偏差问题。

方差与偏差

在机器学习中,偏差与方差的平衡是一个很重要的课题,因为如果模型太简单,就会有高偏差,太复杂就会有高方差。但是在神经网络中,如果神经网络足够大,那么几乎能很好地适应你的训练集。

对于神经网络,可以现在训练集上训练算法,然后询问它在训练集上是否表现良好,如果相对于基准水平算法表现不佳,那么就可能遇到了高偏差问题。减少偏差可以使用更大的神经网络,即使用更多的隐藏层,每个隐藏层上设置更多的神经元,之后再次训练神经网络,询问其在训练集上的比较。重复上述过程,直到它在训练集上表现良好。通过这种方式,可以让模型在训练集中达到期望的水平,即基准性能水平。

在进行完上述步骤后,可以询问算法在交叉验证集上的表现情况,如果表现得不好,这说明其具有高方差。可以增加训练集来重新训练模型,降低方差。

方差与偏差流程图

当时这个方法也存在着一定的局限性,训练更大的神经网络会减少偏差,但同时也会增加计算成本。

事实证明,正则化比较好的大型神经网络和较小的神经网络在方差上的效果差不多好或者更好。

在代码实现中,可以在搭建神经网络的时候选择相应的$\lambda$值:

1
layer = Dense(units = 25, activation = "relu", kernel_regularizer = L2(0.01))

后面这半句代码就是在设定$\lambda$的值,其中L2指的是L2范数。

  • L0范数是指向量中非零的元素的个数
  • L1范数是指向量中各个元素绝对值之和
  • L2范数是指向量各元素平方和然后求平方根

机器学习开发流程

首先需要决定系统的总体架构是什么,这一步需要选择机器学习的模型,并决定要使用什么样的数据,同时还要决定各种超参数。接着,根据这些决定,开始训练模型,训练后的模型往往不会达到期望的结果。在下一步,需要去优化我们的算法,例如去查看算法的偏差和方差,还有各种各样的错误分析,根据分析结果来做出决定去优化你的算法模型。一直执行上述过程,进行多次迭代,直到获得想要的性能。

机器学习开发流程

误差分析

如果你训练了一个分类模型,一共有五百个数据,分类错了一百个,你可以分析这一百个预测错误的样本。首先将这些样本根据主题进行分类,例如用垃圾邮件分类的例子,可能对与拼写错误的垃圾邮件分类效果不好,或者是使用图片嵌入文字的垃圾邮件分类效果不好,又或者是对药物主题的垃圾邮件分类效果不好,等等。根据这些不同类别的情况,分别统计他们的出现次数,然后看哪种情况比较多,之后可以着重解决出现频率比较高的分类错误的情况。

值得注意的是,可能一个样本会被分类到很多类别之中,每个类别之间并不是互斥的关系,他们完全可以重叠。如果分类错误的样本非常多,我们根本没有那么多时间和精力挨个去查看,面对这种情况,我们可以随机抽取一百个样本,对其进行分类。使用这种方法可以让我们知道问题出在哪里,对接下来怎么做有着重要的指导意义。

在查明你的算法对于哪种类型的数据效果不好后,可以尝试去添加相应类型的数据去进行训练,从而提升其分类效果。如果你有很多为打标签的数据,那么你可以找效果不好的数据,打上标签后投喂给你的算法模型。

或者还可以使用数据增强的方法来根据现有的数据去创造新的数据,比如要进行手写数字识别,那么可以把要识别的图像通过旋转、缩放、翻转、增加对比度等方法来创造新的数据集。还可以通过把相应的图片置入一个网格中,然后进行随机扭曲,从而得到更多的数据集,具体如下所示:

网格随机扭曲

对于语音识别的项目来说,可以使用原始声音与各种各样场景的噪音混合在一起,合成出一个新的数据。

迁移学习

如果你当前想要训练的模型没有足够多的数据,那么可以先训练一个别的模型,训练若干代之后,将其参数调用至你想要训练的模型中。

比如说你想要训练一个手写数字识别的模型,但是你没有足够多的数据,不过你有一个很大的数据集,其中包括了几万张猫和狗的等一千个种类的图片,那你完全可以先搭建一个识别这些东西的神经网络,如下所示:

迁移学习神经网络1

训练一段时间后,只需要消除输出层,并使用一个适用于手写数字识别的输出层替代它就可以了,前面的参数可以保留,然后再用手写数字识别的训练集在这个基础上进行训练,就可以得到一个效果不错的手写数字识别的算法模型了。

迁移学习神经网络2

迁移学习的训练可以分为两种方式:第一种是只训练输出层上的参数,别的参数保持不变;第二种是训练所有的参数。

迁移学习的原理是让解决类似问题的模型先在有着较大数据集的地方进行训练,之后在我们想要训练的数据集上进行训练,得到我们想要的模型。这是因为对于类似的问题,前面的隐藏层在学习的时候已经做好了相应的工作,后期在进行训练的时候,可以更快获得我们想要的模型。这种方法也叫作监督预训练,可以在我们的训练集数量很少的时候也可以得到较好的模型。

网上也会有许多人上传的预训练好的用于迁移学习的神经网络,可以下载他们上传的训练网络,之后根据自己的数据进一步训练或者微调网络。

机器学习项目的完整周期

机器学习项目的第一步是确定项目范围,简单来说就是决定项目是什么以及你想做什么。在这步结束后,必须收集数据,确定训练机器学习系统所需的数据,然后着手获取相应的数据和数据集所对应的标签。完成数据收集后,就可以开始训练模型了,在这个过程中,需要不断去改进模型,对错误进行分析,然后优化数据集以获得更好的算法。在做完上述所有事情之后,如果模型足够好,那么就可以在生产环境中进行部署。在部署一个系统之后,还必须确保继续监控系统的性能并维护系统以防性能变差。如果你发现部署好的模型没有像希望的那样工作,就需要回去训练模型并再次改进它,甚至回去获取更多的数据。

机器学习项目的完整周期

倾斜数据集的误差指标

对于一个分类算法,如果标签的现实比例差距很大的话,即使正确率高达$99.5%$也不能说这是一个好的模型。

举个例子,如果有一种病十分罕见,它在同一症状患者中的发病率只占$0.5%$,极端情况下,如果我只输出No的话,虽然正确率为$99.5%$,但很显然,这并不是一个好的算法,甚至还没有一个误判率稍高一些的算法意义大。

因此面对这种情况,需要一个更好的误差评价指标,可以创建一个$2\times2$的矩阵,如图所示:

倾斜数据集

该矩阵用于处理的二分类问题,其中横坐标是真实标签,纵坐标是预测标签,格子里的数字表示属于这个情况的数据数量,例如左上角的数字表示真实标签是$1$且预测标签是$1$的情况出现过$15$次,以此类推。左上角的格子是真阳性,右上角的格子是假阳性,左下角的格子是假阴性,右下角的格子是真阳性。

注:真和假表示预测是否正确,阳性和阴性表示预测的值。

准确率

准确率指的是在所有正确的样本中,实际正确预测的比例是多少,即预测为真的样本中有多少预测正确,其公式如下所示:
$$
准确率=\frac{真阳性}{预测阳性}=\frac{真阳性}{真阳性+假阳性}=\frac{15}{15+5}=0.75
$$

召回率

召回率意味着在真实标签中,有多少是真的被检测出来的,即真实标签中有多少被正确预测,其公式如下所示:
$$
召回率=\frac{真阳性}{真实阳性}=\frac{真阳性}{真阳性+假阴性}=\frac{15}{15+10}=0.6
$$
使用这种方法可以有效判断预测是否准确,可以用于评估倾斜数据集的模型性能。

在实际应用中,需要这两个指标都比较高。前者可以理解为预测的准确度比较高,后者可以理解为遗漏率比较低。

准确率与召回率的权衡

一般来说,我们希望准确率和召回率都有着一个非常高的值,但事实上在大多数情况下这并不能兼得,因此,我们需要对其进行一个权衡。

例如如果我们训练了一个能够预测某些疾病的模型,在之前的学习中,我们一般会把阈值设置为$0.5$,即高于阈值的情况预测为$1$,反之预测为$0$。

上述的这种方式并不能完全满足现实情况,如果疾病的后果不是那么糟糕,即使没有积极治疗,也不会造成很大影响,但是治疗的话会有着非常高的成本,并且会对人体造成损伤。面对这种情况,我们可以适当把阈值调高一些,例如将阈值设置为$0.7$,这样可以很好地提高模型的准确率,不过也会相应地降低其召回率。

与上述同理,如果该疾病不处理会造成很大的问题的话,并且成本也没有很高的时候,我们可以适当降低其阈值。例如给阈值降低至$0.3$,这样虽然会降低其准确率,但是也会增加它的召回率,可以尽可能多的排查患有该疾病的人。

算法选择

我们在实际应用中,会训练出许多算法来供我们选择,这个时候我们可以看他们的准确率和召回率来进行选择。

最简单的,我们可以直接求准确率和召回率的平均值,然后挑选最大的那一个,但是这种方法并不合适,很难挑选出好的算法,因为就算是只会打印$1$的算法,平均下来也有$0.5$,因此这种方法很难作为评判标准。

我们这里引入了一种新的计算方法,在数学中被称为调和平均数,使用这种方法也是类似于求平均值,但是会更加强调较小的值。

其计算公式如下:
$$
F_1\ score=\frac{1}{\frac{1}{2}(\frac{1}{P}+\frac{1}{R})}=2\frac{PR}{P+R}
$$
其中$P$表示的是准确率(Precision),$R$表示的是召回率(Recall)。通过这种方式可以较好地评判出每种算法的优劣,从而选择一个比较好的算法。

假设现在有三个算法,其分数如下所示:

算法 准确率 召回率 $Average$ $F_1\ score$
算法1 0.5 0.4 0.45 0.444
算法2 0.7 0.1 0.4 0.175
算法3 0.02 1.0 0.51 0.0392

决策树

模型

决策树(Decision Tree)是在已知各种情况发生概率的基础上,通过构成决策树来求取净现值的期望值大于等于零的概率,评价项目风险,判断其可行性的决策分析方法,是直观运用概率分析的一种图解法。由于这种决策分支画成图形很像一棵树的枝干,故称决策树。在机器学习中,决策树是一个预测模型,他代表的是对象属性与对象值之间的一种映射关系。

学习过程

训练决策树模型其实本质上就是在构建一个决策树,将训练集按照某种方式进行分类,从而将初始的数据集成功分类。

纯度

用分类猫的任务作为例子,即使用决策树来进行猫的分类。那么需要将一系列的数据作为输入,之后构建整棵决策树。如果在其中一个节点都是单一类别的猫,那么可以说这个节点是非常纯的;如果都不是猫,也可以说是非常纯的;但是如果介于二者之间,那就需要去量化一下这组例子有多纯。

因此我们引入了熵的概念,它是衡量一组数据不纯度的指标。

现给定一组样本,其中有三只猫三只狗,具体如下所示:

纯度数据集

其中$p_1$表示猫的样本所占的比例,很明显,在上述数据集中:
$$
p_1=\frac{3}{6}
$$
同时,我们将使用一个称为熵的函数来测量一组样本的不纯度,它的图像如下所示:

熵图像

熵函数常用一个大写的H表示,该函数看起来如上图所示。其中横坐标是要计算熵的目标的所占比例,纵坐标是对应的熵的值。

对于该数据集,由于$p_1=0.5$,因此其对应的熵值将等于$1$。

简单来说,信息的混乱度越大,其对应的熵值也就会越大,熵可以理解为是数据集的不纯度

下面的图中表示的是不同数据集所对应的熵值:

熵计算

对于熵值,计算公式如下:
$$
H(p_1)=-p_1log_2(p_1)-p_0log_2(p_0)
$$
要注意,这个函数的底数是$2$而不是$e$,这是因为如果取$e$的话,该函数的峰值将不会是一个整数,因此很难去进行解释,取$2$的话,该函数的峰值将会是$1$。

其中$p_1$是我们要进行分类的目标,$p_0$表示其余目标,因此有如下公式:
$$
p_0=1-p_1
$$
值得注意的是,$log_2(0)$的值正常情况下是$-\infty$,但是为了正确的计算熵,我们将$0log_2(0)$的值设置为$0$。

选择拆分信息增益

在构建决策树时,我们决定在节点上拆分哪个特征的方式将基于哪种特征选择最能减少熵。在决策树学习中,熵的减少称为信息增益。

假设我们现在需要构建决策树的其中一个节点,可以根据某种属性进行分类,分别求出如果按照该属性进行分类后的熵值,需要保证熵尽可能减小,选择熵最小的那一种就可以了。

现在一共有十个动物,其中猫和狗的数量各占一半,现在有如下的三种分类方式:

分类方式 第一类动物数量 第一类猫的数量 第二类动物数量 第二类猫的数量
是否是尖耳朵 5 4 5 1
是否是圆脸 7 4 3 1
是否有胡须 4 3 6 2

根据上一小节纯度的学习,我们可以求出他们各自的熵值分别是多少。

分类方式 第一类$p_1$ 第一类$H$ 第二类$p_2$ 第二类$H$
是否是尖耳朵 $p_1=\frac{4}{5}=0.8$ $H(0.8)=0.72$ $p_1=\frac{1}{5}=0.2$ $H(0.2)=0.72$
是否是圆脸 $p_1=\frac{4}{7}=0.57$ $H(0.57)=0.99$ $p_1=\frac{1}{3}=0.33$ $H(0.33)=0.92$
是否有胡须 $p_1=\frac{3}{4}=0.75$ $H(0.75)=0.81$ $p_1=\frac{2}{6}=0.33$ $H(0.33)=0.92$

我们可以通过加权平均值的计算方式来评估每一种分类方式,权重为这一个分类的样本数占总体样本数的比例。

分类方式 加权熵
是否是尖耳朵 $\frac{5}{10}H(0.8)+\frac{5}{10}H(0.2)$
是否是圆脸 $\frac{7}{10}H(0.57)+\frac{3}{10}H(0.33)$
是否有胡须 $\frac{4}{10}H(0.75)+\frac{6}{10}H(0.33)$

我们也可以在这个基础上计算一下根节点的熵值,然后用根节点的熵值减去计算出来的每个分支的熵值,从而判断一下他们每一个的熵值减少了多少。

该节点一共有十个样本,其中猫和狗的样本各占一半,也就是说$p_1=\frac{5}{10}=0.5$,其熵值$H(0.5)=1$。

分类方式 熵减
是否是尖耳朵 $H(0.5)-(\frac{5}{10}H(0.8)+\frac{5}{10}H(0.2))=0.28$
是否是圆脸 $H(0.5)-(\frac{7}{10}H(0.57)+\frac{3}{10}H(0.33))=0.03$
是否有胡须 $H(0.5)-(\frac{4}{10}H(0.75)+\frac{6}{10}H(0.33))=0.12$

通过这种方式去拆分,可以很好地训练模型。决定何时不再进一步分裂的停止标准之一可以是判断熵的减少是否太小,如果一直细化,一味地增加树的大小,这样会增加过拟合的风险。因此如果熵的减少太小或者低于阈值,就不需要去理会了。

让我们来规范一下上述计算相关的符号,其中数据集如下所示:

信息增益数据集

按照是否是尖耳朵进行分类,示意图如下:

耳朵形状分类

在上图中,$p_1^{root}$表示根节点进行分类的目标的占比,$p_1^{left}$和$p_1^{right}$分别表示左右两个分支中进行分类的目标的占比,$w^{left}$和$w^{right}$分别表示左右两个分支中样本占总样本数的比例。

因此最后的熵减计算公式如下所示:
$$
熵减=H(p_1^{root})-(w^{left}H(p_1^{left})+w^{right}H(p_1^{right})
$$
我们在进行分类方式选择的时候,需要去选择熵减尽可能高的那种方式。

构建决策树全过程

对于决策树的构建,一般为如下过程:

  1. 从树的根节点处的所有训练示例开始。
  2. 计算所有可能特征的信息增益,并选择要拆分的特征,从而提供最高的信息增益。
  3. 选择要拆分的特征后,将会把数据集拆分为两个子集,并创建树的左右分支,将训练集分别放在左侧或右侧分支,具体取决于该特征的值。
  4. 在树的左分支、右分支等重复拆分过程,直到满足停止条件位置。

对于停止条件,可以采用下面几种策略(可以只使用一个或者同时使用多个):

  • 该节点的熵已经达到零。
  • 当进一步拆分节点将导致树超过设置的最大深度。
  • 拆分后的信息增益值小于阈值。
  • 节点中的样本数量低于阈值。

在训练完成之后,通过交叉验证的方式来选择模型,比如用这种方式选择最大深度值,在交叉验证集上找出效果最好的参数。

独热编码

在上述的例子中,我们的每个特征都采用了两个可能值中的一种。假设有两个以上的离散值特征时,可以使用独热编码来解决此类问题。

让我们先来看一下下面这个数据集:

独热编码数据集

该数据集中,对于耳朵形状这一特征,不再局限于是否是尖耳朵这一种情况了,而是会将其细化为尖耳朵、松软耳朵和椭圆形耳朵这三种。如果按照之前构建决策树的流程,对于这一特征会拆分成三个子集,从而构建出三个子分支。

为了解决上述问题,我们可以使用one-hot独热编码,将上述的这三种情况依次拆分,变为是否是尖耳朵,是否是松软耳朵,是否是椭圆形耳朵这三个分类。通过这种方式,将一个特征值拆分成了三个独立的特征值,可以保证构建决策树的时候依旧是每个节点只有两个分支。之后使用$1$表示是,$0$表示否,通过二进制的方式就可以表示出是否具有相应的特征了。

如果一个分类特征可以取$k$个可能的值,那么就可以创建$k$个只能取$0$或$1$的二进制特征来替换它。具体编码如下所示:

独热编码

不难发现,对于拆分的特征,可以保证每一个都只有一个新的特征取值为$1$,即热特征,因此这种编码得名one-hot encoding

独热编码不仅可以用于决策树,同时还可以用于训练神经网络。

不仅经过拆分的特征可以使用$0$和$1$,其他不需要进行拆分的特征也可以用这种方法进行表示,这样就可以使用$0$和$1$对分类特征进行编码,使其可以作为输入提供给神经网络。

连续值处理

在上述案例中,处理的都是一些离散值,但是很明显,在实际应用中不仅仅只有离散值,还会有许多连续值的情况。

举个例子,现在增加一个新的特征“体重”,一般来讲,猫的平均体重会低于狗的平均体重,因此这个特征还是很有帮助的。现在的数据集如下所示:

连续值数据集

对于这种情况,很明显我们没有办法使用独热编码的策略来进行划分,这里引入一种新的方法去解决该问题。

现在一共有$k$个样本,也就是对应了$k$个值,那么我们有$k-1$中方式对其进行划分,使他们最终变为两类。

连续值处理

如上图所示,横坐标代表了各个样本的体重,纵坐标表示该体重所对应的是不是猫。使用$k-1$条线对他们进行划分(图中只画出了$3$条用于举例,实际应该是$9$条),然后分别求出每一种划分情况的熵减,选取熵减最大的那个划分方法。对于本例而言,熵减最大的划分方法是选取绿色那条线的划分方式,其熵减为$0.61$,因此我们可以将其根据这种划分方式处理连续值,最后结果依旧是离散值。

回归树

回归树和决策树类似,但是回归树是用于预测一个值的,而不是用于像决策树一样的分类任务。同样的,我们先来看一个数据集:

回归树数据集

与之前的数据集类似,不过我们这次并不预测当前样本是不是猫了,而是去预测当前样本的体重是多少。

我们可以用类似建立决策树的方式,建立之后,对每一个节点求出这些样本的加权平均值体重,将其作为我们的最终预测。具体的决策树如下所示:

回归树

和决策树类似,这个的关键也在于节点的划分。对于决策树而言,我们划分的方式取决于每种方式的熵减;对于回归树而言,我们划分的方式取决于每种方式的方差。可以通过计算每一种类别的方差,之后求其加权平均值,再用根节点的方差减去这个值,选取值最大的作为该节点的划分方式。

回归树节点划分

由于这部分内容和决策树的节点划分非常相似,因此在这里就不在此赘述了。

决策树集成

单个决策树对于数据中的微小变化高度敏感,为了让它不那么敏感或者说让它更健壮,我们可以构建很多决策树,这称之为树集成。

事实上,在我们只构建一棵决策树的时候,仅仅改变一个训练样本,就可能会导致算法在根部产生不同的分裂,从而产生一棵完全不同的树,这使得该算法不那么健壮。

如下图,我们现在有三棵决策树组成的集合,那么每棵树都可能是对猫与非猫分类的一种合理方法:

树集成

现在我们要预测下面这个数据是否是一只猫:

树集成预测数据

很显然,对于第一棵和第三棵树,都将其预测为了一只猫,第二棵树将其预测为了非猫,因此根据投票情况,最后认为该数据是一只猫。

随机森林

有放回抽样

为了构建出上述那么多的决策树,我们就需要不同的数据集,因此我们可以采用有放回抽样的方式来构造数据集。通过这种方式可以让我们构造出来的新的数据集和初始的数据集有些许的不一样,并且我们可以构造出很多这样的数据集,从而让我们有足够多的数据集来进行决策树集成。

构造随机森林

首先我们需要准备第一个大小为$m$的数据集,其中的数据应该互不相同。之后我们将构造$B$个新的数据集。构造方法和上述类似,通过有放回抽样的方式,把数据一个个抽取出来。尽管这会导致我们很有可能抽出相同的数据,但是这并没有关系。我们抽取出来的新的数据集,也应该有$m$个样本,换句话还说,我们进行了$m$次有放回抽样。

$B$为我们构建出的决策树的数量,也就是随机森林中树的数量,这个$B$我们可以取$100$左右,建议是$64\sim 228$这个范围。事实证明,将$B$设置的很大并不会损害性能,但是超过某个点的时候,最后会得到的收益很低,当把$B$的值设置为远大于$100$的时候,其效果也没有变得非常好。但是如果构造过多的树,会显著降低计算速度,而不会很好的提高整体算法的性能。

这种构建决策树的方式有时候也被称为袋装决策树,指的就是将训练示例放入那个虚拟包中,这也就是为什么用$B$来表示新的数据集的个数的原因,因为它代表的是“包”的意思。

我们在构造决策树的时候,可以随机选择$K$个特征作为允许被拆分的特征,然后从这$K$个特征中选择具有最高信息增益的特征作为使用分割的特征选择。使用这种技术往往更多的用于具有大量特征的问题,其中可以使得$K=\sqrt n$,$n$代表特征总数,这样会有许多个不同的随机树产生。

XGBoost

在我们准备考试的时候,如果某个类型的题掌握的不是很好的话,可以好好练习一下这部分的题目,这样有助于我们最后取得更好的分数。对于随机森林也是一样的,我们如果发现在当前决策树中对于某种示例的分类表现没有那么好,可以提高下次构造数据集时这部分错误分类的数据出现的概率,这样可以使我们的模型更加有针对性的进行训练。

对于提升多少概率等等这些数学细节非常复杂的问题,XGBoost可以很好地解决,这是一个优化的分布式梯度增强库,旨在实现高效,灵活和便携。同时,它还可以很好地选择默认的分裂标准和何时停止分裂的标准,甚至还内置了正则化以防止过拟合。这个库中的细节实现起来相当复杂,这也就是为什么很多从业者会使用实现XGBoost的开源库。

1
2
3
4
from xgboost import XGBClassifier
model = XGBClassifier()
model.fit(X_train, y_train)
y_pred = model.predict(X_test)

上述代码是使用XGBoost所需要做的全部工作,可以按照这种方式导入该库,并将模型初始化为XGBoost分类器。

如果想用XGBoost执行回归任务而不是分类任务,那么可以将代码改成如下所示:

1
2
3
4
from xgboost import XGBRegressor
model = XGBRegressor()
model.fit(X_train, y_train)
y_pred = model.predict(X_test)

何时使用决策树

决策树和树集成通常适用于表格数据,也称为结构化数据,这意味着如果你的数据集看起来像一个巨大的电子表格,那么决策树就值得考虑。例如房价预测那种所有的数据都是以表格的形式呈现的,最后预测一个离散值或者是一个连续值,那么决策树就是一个很好的选择。

但是如果是那种非结构化的数据,类似于图像、视频、音频和文本,那就更加适合使用神经网络来进行训练了。

决策树相对于神经网络而言,有着更快的训练速度,因此可以更有效地提高学习算法的性能。

相对来说,决策树的可解释性要远远好于神经网络,但是并不能夸大决策树的可解释性,因为如果建立了一个很大的决策树森林,并且每一棵树都有许许多多的节点,那么要查看整个整体来试图弄清楚它在做什么将会变得非常困难,并且可能需要一些单独的可视化技术来作为支撑。

K-means算法

工作原理

K-means数据集

如图所示是K-means的数据集,我们需要做的是通过算法将这些点自动地分为两类。

第一步是随机初始化K个簇质心。在这个示例中,我们随机选择两个聚类中心就可以了,分别用红色十字和蓝色十字来表示簇质心。

K-means初始质心

下一步,对于所有$m$个训练示例,依次计算它们与所有质心的距离,然后分配给离得更近的那个质心。

在数学上中,我们计算两点之间的距离通常这么写:
$$
\lVert x^{i}- \mu_k\rVert
$$
同时这也被称为L2范数。

我们需要找到的是最小化它的k值,因此有如下公式:
$$
min_k\lVert x^{i}- \mu_k\rVert
$$
当实现这个算法的时候,会发现最小化平方距离实际上会更方便一些,因为最小平方距离的簇质心应该与最小距离的簇质心相同,并且计算量会更小一些,因此我们要找的最小距离的公式如下:
$$
min_k\lVert x^{i}- \mu_k\rVert ^2
$$
这步之后,我们将循环所有的质心,更新质心位置为分配给该集群的所有点的平均值。这意味着,我们将查看每一类的所有点,求得他们所在的坐标平均值,之后赋值给质心即可。

一直重复上述步骤,我们就可以获得K个集群,也就实现了我们需要的聚类操作。

在实际运行中,可能会出现某个集群一个点都没有被分配到,一般来讲我们会直接删除该集群,但是如果真的非常需要这个集群的话,我们可以重新初始化,希望他在新的一次聚类操作后被分配到几个数据点。

优化目标

我们先来介绍一下聚类的几个常用的符号所代表的意义:

  • $c^{(i)}$:当前分配给训练示例$x^{(i)}$的集群索引。
  • $\mu_k$:指的是集群的质心$k$的位置。
  • $\mu_{c^{(i)}}$:指的是示例$x^{(i)}$分配到的集群的质心。

对于K-means算法,我们有如下的成本函数:
$$
J(c^{(1)},\dots,c^{(m)},\mu_1,\dots,\mu_k)=\frac{1}{m}\sum^m_{i=1}\lVert x^{(i)}- \mu_{c^{(i)}}\rVert ^2
$$
简单来说,我们希望周到最小化平方距离的聚类质心的位置,上述公式其实就是在求每一个样本与其分配的集群的质心的距离。

这个成本函数在有些地方被称之为失真函数。

对于质心的选择,一般会选择训练示例中的平均值,这样可以使得平方距离最小。

失真函数一般来讲都会下降或者保持不变,如果它出现了上升的情况,那么说明代码中存在错误,因为失真函数永远都不可能上升。

初始化

对于K-means算法而言,随机初始化非常重要。

首先是对于聚类数量的选择,如果$K>m$,那么这个聚类显然是没有意义的,这样甚至没有办法保证有足够的训练样例来让每个聚类质心至少有一个训练样例。因此我们最基本的要求是满足$K<m$,这样可以保证我们的聚类算法是有意义的。

在我们选择好了聚类的数量之后,我们就可以使用随机初始化的方式,来给定这几个质心的初始值。例如对于下面这个数据集,我们就可以将其划分为$3$类,也就是随机初始化$3$个质心来进行聚类操作。

K-means初始化数据集

我们随机三种情况,经过K-means算法聚类之后,可能产生如下结果:

K-means初始化三种情况

这三种情况明显可以看出最上面的一种比较好,剩下两种可以认为陷入了局部最优解中。为了避免这件情况的发生,我们可以多次选取初始值,之后计算它们各自的成本函数,选取最小的那一种作为最后的聚类结果。

一般来讲,我们会执行大概$50\sim1000$次左右,用于选择最好情况。如果运算的次数过多,那么计算成本会变得很高很高,那样的话回报往往是递减的。

聚类数量

对于聚类问题,$K$的正确值一般都是模棱两可的,没有一个标准的答案。

我们来看一种叫做“手肘法“的方法,我们可以画一个图,其横坐标是聚类的簇树,纵坐标是相应的簇数对应的成本函数值,如下图所示:

手肘法

一般来讲,前期的时候成本函数的值下降很快,后面下降的速度就非常平缓了。我们可以找到相应的转折点,然后以该点的簇数作为我们的聚类数量。

但这种方法有时候并不适用,很多时候你的曲线会非常平滑,导致找不到相应的“肘”。比较建议的做法是根据K-means为接下来的项目执行的性能来评估。简单来说,需要看后续要执行的操作,以此来决定要分为多少个簇。

到底要分成多少类,是一种主观的判断,和具体应用是相关的,没有一种客观的评估方式。

异常检测算法

工作原理

异常检测算法会查看未标记的正常事件数据集,从而学会检测异常,从而在出现异常事件的时候发出危险信号。

我们可以先准备许多数据集,进行无监督学习训练。因为异常情况相对来说很少,因此我们可以让模型知道大多数的情况是什么样的,之后我们再用新的数据进行检测的时候,就可以让模型分辨该情况是不是异常情况了。

发动机数据集

上图中是一个关于发动机的数据集,其中横坐标是发动机运行时的产热量,纵坐标是发动机运行时的振动频率。

我们如果拿到一个新的数据集,需要对其进行判断是不是异常情况。

执行异常检测的最常见方法是使用一种被称为密度估计的技术。

当得到一个大小为$m$的数据集时,我们要做的第一件事就是为$x$的概率建立一个模型。换句话说,学习算法将尝试找出具有高概率的特征$x_1$和$x_2$的值是什么,以及在数据中出现的可能性较小或概率较低的值是什么。

密度估计

如上图所示,中间的圈内的密度是最大的,如果一个新的示例出现在中间的圈里,我们可以认为这个是正常情况,外面的几个圈以此类推。

在实际预测中,如果我们已经训练出来了这个模型,我们可以将新的测试数据$x_{test}$放入这个模型中,它会给我们返回一个当前情况对应的概率$p(x_{test})$。接着我们需要用这个值去和我们的阈值进行比较,也就是如果$p(x_{test})<\epsilon$,那么我们会认为当前情况属于异常情况。

高斯正态分布

高斯曲线,是正态分布中的一条标准曲线,其函数图像如下所示:

高斯正态分布曲线

该图像的横坐标是随机变量$x$,纵坐标是$x$所对应的概率密度。

该函数的解析式为:
$$
p(x)=\frac{1}{\sqrt{2\pi}\sigma}e^{\frac{-(x-\mu)^2}{2\sigma^2}}
$$
$\mu$代表中心点的横坐标,$\sigma$代表的是标准差。对于任何给定的$\mu$和$\sigma$值,如果将此函数绘制为$x$的函数,则会得到以$\mu$为中心的钟形曲线,并且该钟形曲线的宽度由分母中的$\sigma$决定。

让我们来看一下不同取值所造成的图像变化。

$\mu=0,\sigma=1$时的高斯曲线:

高斯曲线1

$\mu=0,\sigma=0.5$时的高斯曲线:

高斯曲线2

$\mu=0,\sigma=2$时的高斯曲线:

高斯曲线3

$\mu=3,\sigma=0.5$时的高斯曲线:

高斯曲线4

当我们将该函数应用于异常检测时,需要做的是对于数据集$m$,尝试估计出平均参数$\mu$以及方差参数$\sigma$的最好选择。

对于$\mu$值的计算, 一般是取所有训练示例的平均值。
$$
\mu=\frac{1}{m}\sum^m_{i=1}x^{(i)}
$$
对于$\sigma$的计算,一般是取这些数据之间的标准差。
$$
\sigma^2=\frac{1}{m}\sum^m_{i=1}(x^{(i)}-\mu)^2
$$
在计算$\sigma$的很多时候,需要去除以$m-1$而不是$m$,即:
$$
\sigma^2=\frac{1}{m-1}\sum^m_{i=1}(x^{(i)}-\mu)^2
$$
通过这种方式可以计算出无偏估计。

多特征异常检测

对于上述的高斯正态分布,我们如果只有一个特征需要检测的话,那么直接预测相应的概率密度即可。

但是面对实际应用场景,预测的东西一般拥有多个特征,因此我们对于每个特征都可以建立一个高斯正态分布,之后将它们的概率结果乘在一起。尽管我们直接将概率相乘在一起的前提是每一个事件都是独立的,但是事实证明,对于异常检测算法而言,即使存在特征并非独立,也可以很好地将其应用于实际工程中。

综上所述,我们对于一个多特征的预测,其概率密度可以表示为:
$$
p(\vec{x})=p(x_1;\mu_1,\sigma^2_1)\times p(x_2;\mu_2,\sigma^2_2)\times p(x_3;\mu_3,\sigma^2_3)\times\dots p(x_n;\mu_n,\sigma^2_n)=\prod^n_{j=1}p(x_j;\mu_j,\sigma^2_j)
$$
当我们拿到一个新的示例的时候,可以对其进行如下预测:
$$
p(\vec x)=\prod^n_{j=1}p(x_j;\mu_j,\sigma^2_j)=\prod^n_{j=1}\frac{1}{\sqrt{2\pi}\sigma_j}exp(-\frac{(x_j-\mu_j)^2}{2\sigma_j^2})
$$
进行计算之后,将结果与阈值进行比较,如果该值小于阈值,那么我们就可以认为这个示例是异常的。

使用相乘而不是相加的方式,主要是因为如果有一个特征出现异常,相乘的方式可以保证最后的结果也变得非常小,对于单个或者少量的特征值异常也非常敏感。

评估系统

挡在开发学习算法的时候,选择不同的特征或尝试不同的参数值(例如$\epsilon$),往往会得到不同的结果,我们会选择效果更好的那一种,因此,如果有一个评估学习算法的方法,那就会容易很多。这种方法被称为实数评估,如果能够以某种方式快速改变算法,例如改变一个特征或者改变一个参数,同时有一种计算数字的方法告诉你算法是好是坏,那么用这种方式来更改算法就会变得容易得多。

尽管异常检测算法是一个无监督学习,也就是使用的数据全都是没有经过标注的。但假设我们有一些有标记的数据,通常包括少量以前观察到的异常,一般使用$y=0$表示正常,$y=1$表示异常。异常检测算法学习的训练集仍然是从$x^{(1)},x^{(2)},\dots,x^{(m)}$的未标记训练集,我们会把这些示例全都视为正常的,因此他们均为$y=0$。

在实际情况中,会有一些异常的示例进入这个训练集中,但是这个算法通常仍然可以正常工作。为了评估这个算法,如果数据集中存在少量的异常示例,那么可以创建一个交叉验证集:
$$
(x ^{(1)} _{cv},y^{(1)} _{cv}), \dots ,(x ^{(m _{cv})} _{cv},y ^{(m _{cv})} _{cv})
$$
同样,有一些示例的测试集:
$$
(x ^{(1)} _{test},y ^{(1)} _{test}),\dots,(x ^{(m _{test})} _{test},y ^{(m _{test})} _{test})
$$
其中交叉验证集和测试集都包含一些异常示例。如果有一些示例实际上是异常的,但是意外地标记为了正常,那么这些异常算法仍将正常工作。因为在进行测试的时候,重点是能否将我们已知的异常标记为异常,而不是从大量的正确示例中选择太多标记为异常。

如果我们只有很少的异常示例,那么可以把他们都放在交叉验证集中,这样可以保证我们训练出来的模型能够很好的完成我们的需求。这种方案的缺点是,在调整算法后,没有一种公平的方法来判断它在未来的实际情况中的效果如何,因为我们没有异常示例放在测试集中。

异常检测与监督学习

在上一小节中,我们使用了一些带有标签的样例来评估我们的模型,从而选择一个最好的模型,如果我们使用大量的带有标签的数据集来进行有监督学习,是否也可以训练出来一个模型用于检测异常情况。

当我们正例数量非常少时,异常检测算法通常是更合适的选择。相反,如果有更多的正面和负面例子,那么使用监督学习的效果会更好一些。

还有就是在有多个特征的情况下,只要有一个特征出现问题,就被认为可能是异常,解决这类问题的时候异常检测可能更加合适。数据集中的异常可能无法涵盖所有的异常情况,没有出现过的异常与迄今为止看到的任何一种异常示例都不一样,所以这个时候会更加倾向于使用异常检测算法,对于监督学习算法而言这是很难发现的,因为监督学习会尝试找出所有正例情况的一个平均值。

在面对垃圾邮件识别的问题时,可能会发送大量相似的垃圾邮件,这就导致异常检测算法可能认为这些垃圾邮件不是异常情况,而监督学习就可以很好地将其识别出来。

简单来说,异常检测是在找与之前见过的常见情况不同的情况,并认为这是异常的,监督学习是在题库中总结做题技巧,之后尽可能使用已经学会的做题方法解决问题。前者是通过反向排除,后者是在进行正向学习。

特征选择

在监督学习中,如果没有完全正确的特征,或者如果有一些与问题无关的额外特征,通常结果是没有问题的。但是对于运行的异常检测,或者只从未标记的数据中学习,异常检测算法很难找出需要忽略掉的特征。因此,仔细选择特征对于异常检测算法而言,比监督学习算法更为重要。

如果想让异常检测算法有着更好的效果,第一件事是保证提供的特征或多或少是满足高斯分布的。

但实际情况是,我们的数据很难保证是高斯分布的,因此可以绘制数据的图像,然后进行某些变换,从而使其更像是高斯分布。例如我们可以取对数,或者开根号等等方式进行处理,这样能够更好的应用异常检测。
$$
x=log(x+C)
$$
举个例子,通过使用上述公式,通过改变$C$的值,可以更好地绘制出相应的高斯曲线,最后每次通过该变换处理输入,我们就可以得到一个拟合得不错的异常检测算法。

如果出现了一个示例,你认为是异常的,但是程序认为这并非异常,你可以单独把这个示例拿出来,然后分析你为什么觉得这是异常的,看看这个示例是否与别的示例是相似的。做完这些后,你可以尝试引入一个新的区分度较大的特征,然后构建一个相应的高斯分布模型,进行预测,期望能够达到想要的预测效果。

不仅如此,在面对那种虽然每一项数值都很正常的示例,我们可以将某些特征进行组合。例如检测计算机是否正常运作的异常检测算法,有着CPU负载和网络流量两个参数,尽管两个指标可能都在正常范围内,但是可能有着较高的CPU负载和较低的网络流量,虽然单项数值都很正常,不过这很明显是一个异常的示例。面对这种情况,我们可以求这两个参数的比值,得到一个新的参数,用这个参数用于异常检测,这样就可以检测出某些单项数值均正常,但实际是异常的示例了。

推荐系统

数据处理

推荐系统在商业中的价值是非常巨大的,它可以预测用户可能会喜欢什么东西。

假设我们现在有一个电影推荐网站,用户可以对电影进行评级,使用$1$星$\sim$$5$星表示对这部电影的喜爱程度,那我们可以得到如下的这个表格,也就是我们的使用的数据集:

推荐系统数据集

左侧的一列表示电影名称,右侧表示每个用户对应相应电影的评级,使用?表示的意思是该用户没有看过这部电影,我们可以使用数据来表示出这个表格中的信息。

我们使用$n_u$表示一共有多少个用户,在这里$n_u=4$;使用$n_m$表示一共有多少部电影,在这里$n_m=5$。

使用$r(i,j)$来表示第$j$个用户是否对第$i$个电影进行过评级,其中$r(1,1)=1,r(3,1)=0$,表明Alice对第$1$部电影进行过评级,而没有对第$3$部电影进行评级。

使用$y^{(i,j)}$表示第$j$个用户对第$i$个电影的评分具体是多少,例如$y^{(3,2)}=4$表示的是Bob对第$3$部电影的评级是$4$。

使用每个特征

上述数据中只有用户对于电影的评级,很显然,每部电影也有他们本身具有的特征,下面这个数据集相对于之前的数据集多了两个属性,分别是浪漫程度(romance)和动作程度(action):

使用每个特征数据集

最后两列的数值分别表示该电影的这两个特征的符合等级,$1$表示完全符合,$0$表示完全不符合。

因此我们可以得到每部电影的特征向量,例如第一部电影和第三部电影,其特征向量如下所示:

电影特征向量

对于每个用户而言,我们可以使用一个线性回归来进行训练和分类,其公式如下:
$$
\vec{w}\cdot x^{(i)}+b
$$
现在我们要预测Alice对于第三部电影的评分,假设她所对应的参数如下所示:

Alice参数

那么我们就可以直接进行运算,也就是:
$$
\vec{w}^{(1)}\cdot x^{(3)}+b^{(1)}=4.95
$$
这就表示,Alice会对该部电影的评级在$4.95$左右,每个用户都有一个专属的参数设置,用于对其进行预测。

对于任何一个用户$j$,预测他对电影$i$的评级的公式如下所示:
$$
\vec{w}^{(j)}\cdot x^{(i)}+b^{(j)}
$$
现在的问题在于,我们如何去得到这些参数,一般来讲,都会去使用相应的成本函数,然后训练模型。

我们再引入两个,第一个参数是$m^{(j)}$,表示用户$j$评价的电影数量;第二个参数是$n$,表示的是特征的总数。

我们可以根据这些参数和信息得到如下的成本函数:
$$
min_{\vec {w} ^{(j)}b ^{(j)}}J(\vec w ^{(j)},b ^{(j)})=\frac{1}{2m ^{(j)}} \sum_{i:r(i,j)=1}( \vec {w} ^{(j)}\cdot \vec x ^{(i)}+b ^{(j)}-y ^{(i,j)}) ^2
$$
很显然,用户没法对所有的电影进行评级,所以我们只去使用那些经过用户评级的电影作为数据集。

我们在训练模型的时候,希望这个成本函数的值是最小的,这样就可以更好地选择参数$w^{(i)}$和$b^{(j)}$。

为了防止过拟合,我们可以为这个成本函数添加正则化:
$$
min_{\vec {w} ^{(j)}b ^{(j)}}J(\vec w ^{(j)},b ^{(j)})=\frac{1}{2m ^{(j)}} \sum_{i:r(i,j)=1}( \vec {w} ^{(j)}\cdot \vec x ^{(i)}+b ^{(j)}-y ^{(i,j)}) ^2 + \frac{ \lambda }{2m ^{(j)}}\sum_{k=1}^{n} ( \vec{w} _ k^{(j)}) ^2
$$
事实证明,对于推荐系统而言,消除掉$m^{(j)}$其实会更方便,该项只是该表达式中的一个常数。所以,即使把这一项删掉,最终也会得到相同的结果。
$$
min_{\vec {w} ^{(j)}b ^{(j)}}J(\vec w ^{(j)},b ^{(j)})=\frac{1}{2} \sum_{i:r(i,j)=1}( \vec {w} ^{(j)}\cdot \vec x ^{(i)}+b ^{(j)}-y ^{(i,j)}) ^2 + \frac{ \lambda }{2}\sum_{k=1}^{n} ( \vec{w} _ k^{(j)}) ^2
$$
得到这个式子后,我们需要将每个人的成本函数加在一起,得到如下的式子:
$$
J=\frac{1}{2}\sum^{n_u} _ {j=1}\sum_{i:r(i,j)=1}(\vec w^{(j)}\cdot \vec x^{(i)}+b^{(j)}-y^{(i,j)})^2+\frac{\lambda}{2}\sum^{n_u} _ {j=1}\sum^n_{k=1}(\vec w^{(j)}_k)^2
$$
通过这种方式,我们可以得到一组非常好的参数,用于预测所有用户的电影评级。

协同过滤算法

对于我们事先不知道特征值是什么的情况,可以使用协同过滤算法来计算出相应的特征值。

协同过滤算法数据集

假设我们已经以某种方式为四个用户学习了参数,其中$\vec w$参数如下所示,$b$参数均为$0$:

协同过滤算法参数

使用的模型是:
$$
\vec{w}^{(j)}\cdot x^{(i)}+b^{(j)}
$$
根据第一部电影的得分情况,我们可以得出:
$$
\vec{w}^{(1)}\cdot x^{(1)}\approx 5
$$

$$
\vec{w}^{(2)}\cdot x^{(1)}\approx 5
$$

$$
\vec{w}^{(3)}\cdot x^{(1)}\approx 0
$$

$$
\vec{w}^{(4)}\cdot x^{(1)}\approx 0
$$

如果拥有了所有四个用户的参数,那么就可以合理预测出电影的特征向量的值。

在这个算法中,有多个用户对同一部电影的同一项目进行评分,这是能够猜测这些特征的可能值的原因。

为了能够更好地训练模型,我们有一个相应的成本函数,该成本函数与上述成本函数非常相似:
$$
J(x^{(i)})=\frac{1}{2}\sum_{i:r(i,j)=1}(\vec w^{(j)}\cdot \vec x^{(i)}+b^{(j)}-y^{(i,j)})^2+\frac{\lambda}{2}\sum^n_{k=1}(\vec x^{(i)}_k)^2
$$
我们需要让该成本函数最小,从而预测出相应的特征向量。

可以求一个总体的成本,让该总成本最小即可:
$$
\frac{1}{2}\sum^{n_m} _ {i=1}\sum _ {i:r(i,j)=1}(\vec w^{(j)}\cdot \vec x^{(i)}+b^{(j)}-y^{(i,j)})^2+\frac{\lambda}{2}\sum^{n_m} _ {i=1}\sum^n _ {k=1}(\vec x^{(i)} _ k)^2
$$
通过梯度下降算法,最小化这个成本函数,可以更好地猜测学习好的特征电影。

所谓协同过滤算法,就是从已有电影特征分类和打分中学到相应的参数,再用学到的参数去学习未知电影的特征分类。

因此,我们可以将这两个算法合并在一起,得到如下的成本函数:
$$
J(\vec w,b,\vec x)=\frac{1}{2}\sum _ {i:r(i,j)=1}(\vec w^{(j)}\cdot \vec x^{(i)}+b^{(j)}-y^{(i,j)})^2+\frac{\lambda}{2}\sum^{n_u} _ {j=1}\sum^n _ {k=1}(\vec w^{(j)} _ k)^2+\frac{\lambda}{2}\sum^n _ {k=1}(\vec x^{(i)} _ k)^2
$$
可以通过梯度下降算法,计算这三个参数,这样可以得到一个较好的值。

我们得出的平均值成为协同过滤,协同过滤这个名称指的是因为多个用户协同评价同一部电影,让你了解这部电影可能是什么样子,还可以反过来预测用户对某一电影的评价。

二进制标签

推荐系统和协同过滤算法的许多重要应用都涉及二进制标签,而不是用户对他们的喜好进行评级,因此,我们只能通过某种方式猜测他们是否喜欢某个东西。

二进制标签

如图所示,其中使用$0$表示的是用户对该项目不感兴趣,$1$表示的是对其感兴趣,$?$表示用户还没有浏览过该项目。判断用户是否喜欢该项目的方式,可以是看他有没有进行点赞,或者计算他在该项目上停留的时间,亦或者是对该项目的搜索频率等等方式,这些都可以转化为相应的二进制标签。对于用户没有浏览过的项目,也可以推荐给用户,看一看用户是否对该项目产生兴趣。

在之前的协同过滤算法中,我们使用的损失函数与线性回归模型非常相似,而对于二进制标签而言,更加适合使用逻辑回归的损失函数。
$$
f_{(\vec w,b,\vec x)}=g(\vec w^{(j)}\cdot \vec x^{(i)}+b^{(j)})
$$

$$
L(f_{(\vec w,b,\vec x)}(\vec x),y^{(i,j)})=-y^{(i,j)}log(f_{(\vec w,b,\vec x)}(\vec x))-(1-y^{(i,j)})log(1-f_{(\vec w,b,\vec x)}(\vec x))
$$
上述公式使我们之前讲到的二元交叉熵成本函数,为了能够更好地适应协同过滤算法,我们可以将该公式写成如下这样,用来计算所有样本的成本函数:
$$
J(w,b,x)=\sum_{(i,j):r(i,j)=1}L(f_{(\vec w,b,\vec x)}(\vec x),y^{(i,j)})
$$

均值归一化

我们接着沿用之前的电影推荐系统的例子,如果此时引入了一个新的用户,那么他对所有电影的评分均是未知的,也就是需要全都使用$?$来表示。

均值归一化数据集

很明显,如果我们使用$0$来对新用户进行初始化的话是不合适的,因为这样会导致该用户训练出来的参数全部为$0$,因此我们将会使用均值归一化的方式进行初始化赋值。

首先我们需要把我们的数据集转换为相应的矩阵:

初始矩阵

接着我们需要求出每一部电影所对应的评级的平均值,可以得到每部电影平均星级的向量,我们使用$\mu$来表示该向量:

平均评级向量

然后我们用初始矩阵的每一位都减去$\mu$矩阵:

均值矩阵

通过这种方式,每一个用户$j$对于电影$i$的评级预测有如下公式:
$$
\vec w^{(j)}\cdot \vec x^{(i)}+b^{(j)}+\mu_i
$$
对于新用户而言,我们也采用该公式,那么他对于每部电影的初始评级应该和$\mu$向量是相同的,这样的效果会比全都设置为$0$要好很多。

我们在上面所使用的计算均值归一化的方式是行规则规范化,也就是对电影求其均值归一化,那么如果出现了一个电影,没有人对其进行评价,那我们就可以对列进行归一化,使用这种方式预测电影的参数。但事实上,这并不是一种合适的方式,遇到一部新电影的话,应该把收集这部电影的参数作为首要任务。

寻找相关特征

在电影推荐网站上,如果用户喜欢看一类电影,那系统就会给他推荐更多这一类的电影。也就是说,需要让推荐系统自动找到用户喜欢的类型,即特征最相近的电影。

我们可以通过如下公式评估两个电影是否是相似的:
$$
\sum^n_{l=1}(x^{(k)}_l-x^{(i)}_l)^2
$$
上述公式中,$k$代表目标电影(也就是用户喜欢的电影)的编号,也就是计算两部电影的所有特征的距离之和,可以简写成下面这个公式:
$$
\lVert\vec x^{(k)}-\vec x^{(i)}\rVert^2
$$
通过这种方式,可以找出与用户喜欢的电影相类似的电影,然后推荐给用户。甚至可以对搜索出来的所有电影进行排名,这样就知道用户对每一部电影的大致好感度了。

协同过滤算法的局限性

对于协同过滤算法,可以很好的实现推荐系统,但是也存在一定局限性,比如它很难处理冷启动问题。

例如,现在有一部新电影,很少人对其进行评价,这就导致推荐系统很难将其推荐给别人;或者现在有一位新用户,也很难决定最开始给他推荐什么样的电影。均值归一化可以很好的解决这个问题,但有一种更好的方法是将新电影推荐给经常对很冷门的电影进行评分的用户,这些项目可能会让他们感兴趣。

协同过滤算法没有办法使用一个自然的方式来使用一些附加信息,例如你的算法可能知道用户的基本信息,还有电影的一些基本信息,但是还有许多的附加信息没有办法加以利用。比如说用户鼠标悬停的时间,使用什么方式看电影,移动端还是电脑端等等,这些信息都属于附加信息,它们很有可能与用户的偏好密切相关。

基于内容的过滤算法

基本原理

协同过滤算法会根据给出相似评级的用户向用户推荐项目,我们有一定数量的用户对某些项目给出了一些评级,算法会计算出如何使用它来向用户推荐新项目。

相比之下,基于内容的过滤采用不同的方法来决定向用户推荐,该算法会根据用户的特征和物品的特征向用户进行推荐,以找到合适的匹配项。换句话说,它需要每个用户的一些特征,以及每个项目的一些特征,它使用这些特征来尝试决定哪些项目和用户可能彼此匹配。

我们使用$\vec x^{(j)}_u$表示第$j$个用户的特征,例如年龄,性别,国家等等,这些都可以使用独热编码来存储。

使用$\vec x^{(i)}_m$表示第$i$部电影的特征,例如年份,电影类型,影评人对电影的评价等等。

我们根据这两组特征,用于给用户推荐相应的项目。值得注意的是,二者的特征数量可能并不相同,因此可以使用下面这种算法来学习如何匹配用户和相应的电影。

我们先来看一下协同过滤算法的公式:
$$
\vec w^{(j)}\cdot \vec x^{(i)}+b^{(j)}
$$
在基于内容的过滤算法中,参数$b$对结果没有什么影响,因此我们可以把上述公式转换为下面这个公式,能够很好地应用于该算法:
$$
\vec v^{(j)}_u \cdot \vec v^{(i)}_m
$$
其中,$\vec v^{(j)}_u$是由$\vec x^{(j)}_u$计算而来的,$\vec v^{(i)}_m$是由$\vec x^{(i)}_m$计算而来的。由于二者要进行点乘运算,因此他们的维度需要保持一致,尽管用户和项目的初始特征的维度并不一致,但我们也需要把他们转化以下,否则将无法进行运算。

训练

我们一般使用深度学习的方式来进行训练,首先需要搭建一个神经网络。

用户网络模型

搭建一个神经网络用于处理用户的特征信息,将其转化为我们使用的用户向量。在上述模型中,最后的输出是一个有着$32$个数字的向量(因为之后要进行点乘运算)。

项目网络模型

接着搭建一个神经网络用于处理项目的特征信息,将其转化为我们使用的项目向量。要注意,我们最后的输出都是有着$32$个数字的向量,这样方便后续进行运算。

在神经网络的隐藏层中,神经元的数量可能不一致,但是最后输出的向量的维度一定要相同。

预测神经网络模型

上图中是整个架构模型,需要将计算出来的两个向量进行点乘,可以把结果放入sigmoid函数中进行最终预测。

最后是这个神经网络训练时所使用的成本函数:
$$
J=\sum_{(i,j):r(i,j)=1}(\vec v^{(j)}_u \cdot \vec v^{(i)}_m-y^{(i.j)})^2
$$
还可以在后面加上相应的正则化项,可以更好地进行训练。

我们也可以使用这个模型查找相似的项目,与之前的小节类似,可以使用如下公式:
$$
\lVert\vec v^{(k)}_m-\vec v^{(i)}_m\rVert^2
$$
如果我们要查找相似电影,就可以通过这种方式,求出其对应的向量,然后与其他电影的向量进行距离上的计算,距离小的电影就是与要查找的电影相类似的电影。

大型目录推荐系统

现如今,一个大型的推荐系统往往有着成千上万,甚至几百万几千万的项目需要进行推荐。

由于基于内容的过滤算法拼接了两个神经网络,所以当出现一个新的用户的时候,不仅需要训练用户的神经网络参数,同时也需要重新训练项目的神经网络。这就导致,每当有新用户出现的时候,就会产生数亿次的计算,这样会使得算法不可行。

对于这种发规模的推荐系统,我们将其分为两个步骤,分别是检索排名

首先在检索步骤生成大量可能的项目作为候选列表,这其中涵盖了许多可能向用户推荐的东西,如果这其中涵盖了许多用户不太可能喜欢的项目,那么在排名步骤中将微调并选择最好的项目推荐给用户。

例如,对于用户最近观看的$10$部电影中的每一部电影,我们可以找出$10$部最相似的电影,通过使用之前的方法可以快速计算出相关的电影。这将提供一组初始的具有可信度的电影,可以将这些电影推荐给用户。也可以将用户看的最多的类型的电影前几名放入候选列表中,也可以选择用户所在地区的排名高的电影,通过类似的方式,我们就可以得到一个拥有很多电影的候选列表,最后删除掉用户浏览过的不感兴趣的项目。

接下来是排名步骤,可以对这几百部可能得电影,使用学习模型对它们进行排名。可以把用户的特征向量和电影的特特征向量输入进这个神经网络中,最后预测每部候选电影的评分,并根据评分进行排名,然后将排名高的电影推荐给用户。

在检索步骤中,检索更多的项目往往会带来更好的性能,但是运算时间也会相应地增加,所以我们需要进行一个权衡。因此可以进行离线实验,来看看检索额外的项目会产生多少更相关的推荐,最后选择一个合适的额外检索数量。

通过检索步骤和排名步骤,使得今天许多推荐系统能够提供快速和准确的结果。

PCA算法

降低特征数量

当数据集中包含很多特征,很显然,我们没有办法绘制出这么多维度的数据。PCA又称主成分分析是一种能够很好地解决该问题的方法,可以获取具有大量特征的数据,并将特征的数量减少至两三个特征,以便于绘图和可视化。

对于汽车长度和宽度这两个特征而言,我们将其分别定义为$x_1$和$x_2$,前者表示长度,后者表示宽度。事实上,汽车的宽度一般为了符合道路的行驶要求,所以差距不是很大,而长度的变化就会比较大了,因此我们可以根据数据绘制出如下这张图:

汽车长宽图像

上图中,横轴表示汽车的长度,纵轴表示汽车的宽度。很容易发现,汽车的长度变化幅度较大,而宽度基本上没有发生变化。如果想要减少特征的数量,那么对于该数据集,我们就可以只取$x_1$,忽略掉$x_2$。

PCA算法做的不仅仅是舍去用处不大的特征,例如现在我们有汽车的长度和高度两个特征,这两个特征的变化幅度都比较大,我们舍弃哪一个都是不合适的。因此对于这种情况,我们可以创建一个新的轴,它是由汽车的长度和高度两个特征综合出来的新的特征,该轴大致上可以反映出汽车的尺寸,那么我们就可以将这个新轴作为最终选择的特征。

因此,PCA算法的想法是找到一个或多个新轴,当在新轴上测量数据坐标时,最终仍然会获得有关目标的非常有用的信息。

工作原理

PCA算法是一个无监督学习,因此只有数据,但是没有相应的标签。如图所示,是一个拥有$5$个数据的数据集,一共有两个特征,分别为$x_1$和$x_2$。

PCA数据集

为了保证数据的尺度范围是一致的,因此我们需要对数据集先进行归一化操作,即先进行缩放再减去均值使其变为零均值。

接下来,我们需要选择或创造一个轴,将所有点投影在这个轴上,希望所有的点都可以相距尽可能地远。在PCA算法中,这个轴成为主成分,在这个轴上,当数据投影在上面时,最终会得到最大可能得方差,可以捕获原始数据集中的更多信息。

如果我们已经找到了主成分轴,那么我们需要求出它的单位向量,例如现在的主成分轴的函数是$y=x$,那么对于点$(2,3)$,我们可以通过以下方式求出它在该主成分轴上的数据是多少:

主成分计算

简单来说,就是用它的坐标与主成分轴的$x$和$y$值组成的向量进行点乘,就可以转化为在这个轴上的坐标。

通过这种方式找到的第一个轴被称作第一主成分。如果要选择第二个轴,那么第二个轴始终与第一个轴的夹角为$90\degree$,也就是与第一个轴相垂直。同理,第三个轴也会与第二个轴有着$90\degree$的夹角。

如果有$50$个特征,并且相找到$3$个主成分,那么这三个轴将会形成一个三维直角坐标系。

强化学习

基本概念

现在有这样一个任务,有一架遥控直升机,给出直升机的位置来让你决定如何操作直升机。

在强化学习中,我们将直升机的位置和方向以及速度等成为状态$s$,其任务是找到一个函数,将直升机的状态映射到动作$a$,即通过操作直升机来保持直升机在空中飞行且保持平衡不会坠毁。

面对这个问题,通常会想到使用监督学习来训练模型,但是每一步动作都很难说明是对还是错,需要整个决策过程才能对其进行评价。

为了解决这个问题,我们可以采用强化学习的方式。强化学习的关键是一个叫做奖励或者奖励函数的东西,它会告诉直升机什么时候做得好,什么时候做的不好。

当直升机飞得好的时候,你可以奖励它,每飞好一秒奖励值就加一;当飞得不好的时候,可以给它一个负奖励,或者说当它坠毁的时候,可以给它一个非常大的负奖励,比如$-1000$。通过这种方式,可以激励直升机花更多的时间飞行,并希望永远不会坠毁。

回报

强化学习有许多状态,智能体通过采取不同的行动,从而享受到不同的奖励。对于强化学习而言,设置奖励是非常重要的,那么如何知道一组特定的奖励比另一组不同的奖励更好还是更差这一问题就非常值得探讨。

火星探测器示例

上图中是一个火星探测器的示例,当探测器到达$1$号位置这个状态后可以得到$100$的奖励,到达$6$号位置这个状态后可以得到$40$的奖励,其中探测器的初始位置是$4$号位置。

如果探测器要到$1$号位置位置,那么它的回报计算为:
$$
Return=0+(0.9)\times0+(0.9)^2\times0+(0.9)^3\times100=72.9
$$
上述算式中,$0.9$表示折扣因子,一般取一个较小于$1$的数字,这样可以保证智能体会尽可能采取更快的速度达到目标状态。简单来说,智能体越早获得奖励的话会导致总回报值越高。

我们可以总结出一个更加通用的公式:
$$
Return=R_1 +\gamma R_2 +\gamma^2 R_3+\cdots
$$
在学多强化学习算法中,折扣因子一般选取非常接近$1$的数字,例如:$0.9$、$0.99$、$0.999$。

为了降低学习难度,更加便于计算,本示例中奖折扣因子$\gamma$的值设置为$0.5$,这将会严重降低权重,也可以说是降低了未来的奖励,因为每增加一次状态转移,就会导致奖励变为早一步获得的一半,所以前文中的回报值将会变为:
$$
Return=0+(0.5)\times0+(0.5)^2\times0+(0.5)^3\times100=12.5
$$
我们现在假设只能向左走,对于不同的初始位置,可以得到如下回报:

向左走回报

如果从$5$号位置开始向左走的话,就只能得到$6.25$的回报;在$6$号位置开始向左走的话,由于该位置就是终止状态,所以其回报值时$40$,其余情况以此类推。

我们再假设只能向右走,对于不同的初始位置,可以得到如下回报:

向右走回报

除了上述这两种方式,我们还可以针对不同的初始位置决定走向,从而让最后的综合回报最大化:

综合回报

总而言之,强化学习中的回报是系统获得的奖励总和,由折扣因子加权计算得到的结果。

策略

策略指的是强化学习中的策略,对于火星探测器的例子,可以选择离哪一个终端奖励更近就往哪边走;也可以选择一直向左走;或者也可以选择一直向右走;亦或者选择朝向获得终端奖励更大的那个方向去走。上面的这四种方法都属于策略,对于这些策略有好有坏,强化学习中提出了一个被称为策略$\pi$的函数,其工作是将任何状态$s$作为输入并将其映射到它希望我们采取的某个动作$a$。而我们的目标是,找到一个策略$\pi$函数,让它告诉你在每个状态下采取什么行动以获得最大化回报。

关键概念

  1. 状态(states):当前智能体处于的状态。
  2. 动作集(action):当前智能体可以选择的行动。
  3. 奖励(rewards):到每一个状态所能获得的奖励。
  4. 折扣因子(discount factor$\gamma$):每行动一步所要乘以的折扣系数。
  5. 回报(return):智能体从某个状态开始出发,所能得到的最终回报值。
  6. 策略(policy$\pi$):智能体在每一个状态应该选择的动作所依赖的策略函数。

对于拥有以上形式的强化学习,我们称之为马尔可夫决策过程。MDP或马尔可夫决策过程指的是未来仅取决于当前状态,而不取决于在达到当前状态之前可能发生的任何事情。换句话说,在马尔可夫决策过程中,未来只取决于你现在所处的位置,而不取决于你是如何到达这里的。

状态-动作价值函数

状态-动作值函数是一个通常用大写字母Q表示的函数,$Q(s,a)$表示从状态$s$开始,在执行一次操作$a$之后,可以达到最佳状态,在那之后,你采取任何行动都会带来尽可能高的回报。

火星探测器最佳策略

上图是火星探测器最佳策略,我们假设初始状态位于第二个格子中,那么可以得出:
$$
Q(2,\rightarrow)=0+(0.5)\times 0+(0.5)^2\times 0+(0.5)^3\times 100=12.5
$$

$$
Q(2,\leftarrow)=0+(0.5)\times 100=50
$$

需要注意的是,状态-动作价值函数是在执行一次$a$操作后, 再根据相应的策略进行执行,计算相应的回报值。

同理,我们还可以得出下面的回报值:
$$
Q(4,\leftarrow)=0+(0.5)\times 0+(0.5)^2\times 0+(0.5)^3\times 100=12.5
$$

$$
Q(4,\rightarrow)=0+(0.5)\times 0+(0.5)^2\times 40=10
$$

对于$2\sim 5$号格子,我们都可以求出相应的$Q(i,\leftarrow)$和$Q(i,\rightarrow)$,通过这种方式,最终会得到所有的$Q(s,a)$,对于不同的状态和不同的动作,最终到达终端状态。对于两侧的终端状态,无论采取什么行动,都会得到相应的奖励值,所以它们的$Q$值为本身的奖励值。综上所述,我们可以得到下面这张图:

状态-动作价值函数

因为状态-动作价值函数基本都是使用字母$Q$表示,所以这也通常称为$Q$函数。

首先用状态-动作价值函数求出所有的情况,然后对于每一种状态,根据$Q$值选择较大的那一种策略。也就是说,如果有办法去计算$Q(s,a)$,那么对于每个状态和每个动作,只需要查看不同的结果就可以选择执行哪一种动作了,即$\pi(s)=a$。

贝尔曼方程

在强化学习中,有一个关键方程叫做贝尔曼方程,它可以帮助我们计算状态-动作价值函数。

我们先来解释几个符号:

  • $s$:表示当前状态。
  • $R(s)$:表示当前状态的奖励。
  • $a$:表示当前动作,即在状态$s$中采取动作$a$后,将进入某个新状态。
  • $s\prime$:表示从当前状态$s$采取动作$a$后到达的状态。
  • $a\prime$:表示在状态$s\prime$中可能采取的操作。

让我们来看一下贝尔曼方程:
$$
Q(s,a)=R(s)+\gamma max_{a\prime}Q(s\prime,a\prime)
$$
状态-动作价值函数

对于火星探测器的例子,我们可以求出$Q(2,\rightarrow)$的值:
$$
Q(2,\rightarrow)=R(2)+0.5max_{a\prime}Q(3,a\prime)=0+(0.5)\times25=12.5
$$
若以状态$4$为例,那么可以求得:
$$
Q(4,\leftarrow)=R(4)+0.5max_{a\prime}Q(3,a\prime)=0+(0.5)\times25=12.5
$$
简单来说,贝尔曼方程是一种动态规划方程,将最终的状态作为贝尔曼方程的初始状态,从该状态开始转移,计算出其他状态对应的回报值。

随机马尔可夫决策过程

在某些应用程序中,当采取行动时,结果并不总是完全可靠的。例如:对于火星探测器而言,如果向左行驶,可能会出现一点岩石滑坡,然后滑向不同的方向。在实践中,由于各种各样客观性因素影响,智能体可能没有办法总是按照指令去做。

还是用之前火星探测器来举例子:

火星探测器示例

当探测器向左走时,大多数时间是能够成功的,但是如果有$10%$的概率意外滑倒并朝相反的方向前进,那么就会与我们之前的计算结果产生不同。

在随机强化学习问题中,我们感兴趣的不是最大回报,因为那是一个随机数,我们更感兴趣的是最大化折扣奖励综总和的平均值。
$$
Expected\ Return=Average(R_1+\gamma R_2 + \gamma^2 R_3 + \gamma^3 R_4+\cdots)
$$
就平均值而言,如果采用当前策略并尝试很多次,那么就会得到许多不同的奖励序列,如果对这些值取平均值,那么就可以得到预期收益。

这个预期收益也就是我们常说的期望值,因此上述公式也可以写成如下形式:
$$
Expected\ Return=E[R_1+\gamma R_2 + \gamma^2 R_3 + \gamma^3 R_4+\cdots]
$$
对于上述的贝尔曼方程,如果要解决随机马尔可夫决策过程,需要对公式进行一些修改:
$$
Q(s,a)=R(s)+\gamma E[max_{a\prime}Q(s\prime,a\prime)]
$$

连续状态空间

在火星探测器的例子中,所有的状态都是离散的,但是现实情况中基本都是连续的状态空间。例如对于一辆车,它的状态不止包括一个数字,其状态中可以有多个参数,$x$表示横坐标位置,$y$表示纵坐标位置,$\theta$表示朝向的角度,$\dot{x}$表示横坐标方向上的速度,$\dot{y}$表示纵坐标方向的速度,$\dot{\theta}$表示角度的变化速度。

我们现在有一个登月器的例子,希望登月器可以平稳的落在我们希望的地方。其参数中,$x$表示横坐标位置,$y$表示高度,$\dot{x}$表示横坐标方向上的速度,$\dot{y}$表示下落速度,$\theta$表示倾斜的角度,$\dot{\theta}$表示角度的变化速度,$l$和$r$分别表示左腿和右腿是否接触到地面,这两个参数只能取$0$或$1$。

如果登月器成功抵达目标位置,将会得到$100\sim140$的奖励,这取决于与目标位置的相差距离;如果登月器坠毁将得到$-100$的奖励;成功软着陆可以得到$+100$的奖励;左腿和右腿着陆接触到地面可以各得到$+10$的奖励;使用一次主引擎(让登月器升高)得到$-0.3$的奖励;使用一次左或者右引擎将会得到$-0.03$的奖励。

因此我们需要求得一个策略$a=\pi(s)$,以此找到一个最高的回报,对于该问题,可以将$\gamma$值设置为$0.985$。

学习状态值函数

对于登月器的例子,关键思想是训练一个神经网络来计算或近似计算相应的$Q(s,a)$,以此来选择好的动作。

对于任意一个状态$s$,我们可以计算如下所示的四个$Q$值:
$$
Q(s,nothing),Q(s,left),Q(s,main),Q(r,right)
$$
每一次选择动作的时候,只需要选择最大的$Q(s,a)$即可。因此,我们可以搭建一个神经网络模型:

登月器神经网络模型

其中输入层一共有$12$个值,前$8$个分别是登月器现在的信息参数,后$4$个值时通过独热编码来编码的动作选择,这$4$个值只有$1$个$1$和$3$个$0$。例如登月器选择什么都不做的话,那么就会编码为$1,0,0,0$;如果选择启动主引擎,那么所对应的编码就是$0,0,1,0$。

我们将状态和选择的动作作为输入层的输入,神经网络会返回一个对应的价值。当登月器处于任意一个状态时,将该状态的信息和四个动作分别输入,选择返回值最大的那种动作执行即可。

让我们来看一下完整的算法是什么样子的:

  1. 首先,我们将搭建神经网络模型,然后随机初始化神经网络的所有参数。最初我们不知道$Q(s,a)$的值应该是多少,所以我们可以完全随机生成一个,我们假设这个神经网络是我们对$Q$函数的初始随机猜测。
  2. 重复下述步骤:
    1. 对登月器采取行动,得到元组$(s,a,R(s),s\prime)$。
    2. 存储最新的$10000$个元组。(这种只存储最近示例的技术在强化学习算法中称为重放缓冲区)
    3. 查看我们保存的这$10000$个最近的元组,并创建一个包含$10000$个示例的训练集。其中$x=(s,a)$,$y=R(s)+\gamma max_{a\prime}Q(s\prime,a\prime)$。
    4. 训练$Q_{new}$,其中$Q_{new}(s,a)\approx y$,现在这个神经网络估计$Q$函数的能力会稍微提升一些。
    5. 将$Q$设置为我们刚刚学习的神经网络。

算法优化

改进神经网络架构

登月器神经网络模型

上图是我们在登月器中的网络架构,当我们处于某个状态时,都必须在神经网络中分别进行四次推理来计算对应的四个值,以便选择有着最大$Q$值的动作$a$。

很明显,这是一个非常低效率的做法,每次要计算一个状态和对应动作都需要推理四次。那么我们可以对上述的模型进行一下修改,输入层变为$8$个值,舍去了四种状态对应的动作,同时将输出层调整为$4$个,分别对应每种动作的$Q$值。

登月器改进神经网络架构

神经网络的工作是同时计算出我们处于状态$s$时所有四种可能动作的$Q$值,事实证明这样会更加有效,可以加快运算速度,更快地选择出最优动作。

$\epsilon$-贪婪策略

在登月器的例子中,即使还在学习如何近似计算出$Q(s,a)$,也需要让登月器采取一些行动,在学习过程中采取动作的最常见方法是$\epsilon-greedy$策略。

在我们处于某个状态时,往往不想随机地采取行动,因为那通常是一个糟糕的行动。对于状态$s$,我们第一种策略是选择$Q(s,a)$值最大的那种动作$a$;还有一种方法是大概率选择$Q(s,a)$值最大的那种动作$a$,小概率选择一个随机的动作$a$。

对于第二种策略,在我们随机初始化的时候,可能会让某些动作对应的$Q$值非常非常低,导致模型永远不会选择这种动作,它也不知道采用这种方式是否会产生积极影响,默认只会产生消极影响。对于这种情况,我们可以有很小的概率去尝试不同的操作,这样神经网络就可以克服先入为主的概念,即便随机选择可能会带来不好的结果,但是也可能会带来积极影响,让神经网络调整这方面的参数。

这种随机选择动作的想法有时被称为探索步骤,因为我们要尝试的步骤不一定是最好的想法,但是我们只是在某些情况下尝试一些新的动作,在以前没有那么多经验的情况下探索和了解关于更多行动的信息。

选择当前已经学习到的最优情况的做法被称之为贪婪,随机进行探索的做法称之为探索,使用$\epsilon$表示。如果我们有$95%$的概率进行贪婪,$5%$的概率进行探索,那么就可以表示为:
$$
\epsilon-greedy\ polocy\ (\epsilon=0.05)
$$
为了能够更好地训练模型,我们可以在训练初期选择一个较大的$\epsilon$值,随着训练的进行,逐渐减少该值。通过这种方式,随着时间的推移,选择随机动作的概率也就越来越小,选择已经学习到的动作的概率会越来越大,有助于模型训练。

小批量梯度下降

在进行梯度下降的时候,如果我们的数据集很大,那么求相应的导数和平均值时会消耗大量的时间。为了解决这一问题,我们可以将数据集分成若干个小批量的数据集,每次使用其中一个小批量的数据集进行梯度下降,这样可以大大加快计算速度。

小批量梯度下降会趋向于全局最小值,相对于使用所有数据来进行梯度下降,这种方式虽然也会趋向于全局最小值,但是下降的方向没有前者稳定,不过这种方法的计算成本要低很多,所以对于非常大的数据集而言还是非常实用的。

在强化学习算法中,模型训练的时候,会在重放缓冲区中存储$10000$个最近的元组,如果使用小批量梯度下降的话,我们不会每次训练都使用所有的元组,而是会使用这些示例的子集进行模型训练。使用该方法会让模型训练的每次迭代变得更加嘈杂,但是运算速度更快,总体上会加速这种强化学习算法。

软更新

训练强化学习的最后一个步骤是用新的模型替换掉旧的模型,这会使得模型的变化非常突然,可能新的模型是一个不太好的模型,也可能比之前的模型差一点点,然后就会用一个可能更糟糕的神经网络覆盖掉原本的$Q$函数,而软更新方法有助于防止新的$Q$函数变得更糟。

神经网络$Q$中有一些参数,例如$W$和$B$,当在训练新的神经网络时,会得到一些参数$W_{new}$和$B_{new}$,对于没有使用软更新的做法,实际上就是在做以下步骤:
$$
W=W_{new}
$$

$$
B=B_{new}
$$

对于软更新,我们所使用的方法是如下公式:
$$
W=0.01\cdot W_{new}+0.99\cdot W
$$

$$
B=0.01\cdot B_{new}+0.99\cdot B
$$

每当我们训练一个新的神经网络参数时,都只会接受一点点新值。对于上述式子中的$0.01$和$0.99$,这些是可以设置的超参数,用于控制变化程度,并且这两个数字相加的结果一定要为$1$。