0%

白话机器学习的数学

机器学习的入门门槛确实降低了,现在有很多机器学习的专用程序库,方便又多样的数据集也能唾手可得。一个人即使不懂理论知识,只要准备好程序库和数据集,再写上几行代码就可以制作出有模有样的东西。但是一直使用一个不知道原理的 黑盒,对程序员而言,始终有所担心。《白话机器学习的数学》一书,则介绍了机器学习背后的理论知识。

机器学习简介

无论是过去还是现在,计算机都特别擅长处理重复的任务。所以计算机能够比人类更高效地读取大量的数据、学习数据的特征并从中找出数据的模式。这样的任务也被称为机器学习或者模式识别。机器学习就是从数据中找出特征和模式的技术。

机器学习快速除了收益于计算机理论的发展,还得益于于一下两点:

  • 具备了能够收集大量数据的环境:当我们打算用机器学习做什么事情的时候,首先需要的就是数据。由于互联网的发展,个人行为和生活的一部分已经被数字化,海量数据也随之而生
  • 具备了能够处理大量数据的环境:现在计算机的性能也越来越高,处理同样多的数据所需的时间变得越来越短,可以使用 GPU 进行数值计算,Hadoop、Spark 之类的分布式处理技术也逐渐成熟

以下列举了几个机器学习非常擅长的任务:

  • 回归(regression):回归就是在处理连续数据(如时间序列数据)时使用的技术,从数据中学习它的趋势
  • 分类(classification):分类就是在处理分类数据(如垃圾邮件识别)时使用的技术。只有两个类别的分类叫做二分类,有三个或三个以上类别的分类叫做多分类
  • 聚类(clustering):聚类与分类不同,它不是事先定义好类别(不带标签),而是根据数据本身的特征自动地把它们分成几个组

使用有标签的数据进行学习称为有监督学习,与之相反,使用没有标签的数据进行学习称为无监督学习。回归和分类是有监督学习,而聚类是无监督学习。

机器学习多多少少还是需要一些数学基础知识的。数学表达式是很方便的工具,它可以把那些说起来会很啰唆的东西,以谁都能够理解的方式严密、简洁地表达出来。

学习回归:基于广告费预测点击量

假设投入的广告费越多,广告的点击量就越高,进而带来访问数的增加。之前说过,机器学习就是从数据中找出特征和模式的技术。所以,我们首先需要收集广告费和点击量的数据,假设已经有如下数据:

把图中的数据想象成一个函数,只要知道通过图中各点所构成的函数的形式,就能根据广告费得知点击量了。这里我们使用了一次函数来近似这些数据:

y=θ0+θ1xy = \theta_0 + \theta_1 x

在统计学领域,人们常常使用 θ\theta 来表示未知数/参数。采用 θ\theta 加数字下标的形式,是为了防止当未知数增加时,表达式中大量出现 a、b、c、d 这样的符号。这样不但不易理解,还可能会出现符号本身不够用的情况。

我们需要使用机器学习来求出正确的 θ0\theta_0θ1\theta_1 的值。我们首先把之前的数据表达式换个写法:

fθ(x)=θ0+θ1xf_\theta(x) = \theta_0 + \theta_1 x

这样我们可以一眼看出这是一个含有参数 \theta、并且和变量 x 有关的函数。我们的目标就是求解 θ\theta,使得对于训练数据:把训练数据中的广告费作为输入,代入 fθ(x)f_\theta(x),计算得出的点击量与实际的点击量之间的误差最小。找到这样的参数 θ\theta,就是我们的目标

把这个目标用数学公式展示出来,就是:

Eθ=12i=1n(fθ(x(i))y(i))2E_{\theta} = \frac{1}{2}\sum_{i=1}^{n}(f_\theta(x^{(i)}) - y^{(i)})^2

  • n 表示 n 个训练数据
  • x(i)x^{(i)} 表示第 i 个训练数据的广告费
  • fθ(x(i))f_\theta(x^{(i)}) 表示第 i 个训练数据的广告费代入 fθ(x)f_\theta(x) 计算出的点击量
  • y(i)y^{(i)} 表示第 i 个训练数据的真实点击量
  • 对每个训练数据的误差取平方后,进行求和。之所以取平方,是因为如果只是简单地计算差值,我们就得考虑误差为负值的情况。而一般我们也不用绝对值,因为之后要对目标函数进行微分,比起绝对值,平方的微分更加简单。最后将求和的结果乘以 12\frac{1}{2},也和之后的微分有关系,这是为了让作为结果的表达式变得简单而添加的常数

我们的目标就是找到使 EθE_{\theta}` 最小的 θ\theta。这样的问题也被称为最优化。

最速下降法

如果我们一边随意修改 θ\theta 的值,一边计算 EθE_{\theta} 并与之前的值相比较的,这种做法实在是太麻烦了。要让 E(θ) 越来越小,我们可以使用 微分 来求解。

微分是计算变化的快慢程度时使用的方法,可以根据导数的符号来判断是变大还是变小,即移动 x 的方向:只要向与导数的符号相反的方向移动 x,g(x) 就会自然而然地沿着最小值的方向前进了。

这就是最速下降法,或者称为梯度下降法。它可以表示为:

x:=xηddxg(x)x := x - \eta \frac{d}{dx}{g(x)}

η\eta 是称为学习率的正常数,读作 伊塔​。根据学习率的大小,到达最小值的更新次数也会发生变化。换种说法就是收敛速度会不同。有时候甚至会出现完全无法收敛,一直发散的情况。举个例子,比如 η=1,从 x=3 开始,那么 x 会如何变化呢?

1
2
3
4
x = 3 - 1(2 * 3 - 2) = 3 - 4 = -1
x = -1 - 1(2 * (-1) - 2) = -1 + 4 = 3
x = 3 - 1(2 * 3 - 2) = 3 - 4 = -1
......

如果 η=0.1,从 x=3 开始,x 的变化情况:

1
2
3
4
5
x := 3 - 0.1(2 * 3 - 2) = 3 - 0.4 = 2.6
x = 2.6 - 0.1(2 * 2.6 - 2) = 2.6 - 0.3 = 2.3
x = 2.3 - 0.1(2 * 2.3 - 2) = 2.3 - 0.2 = 2.1
x = 2.1 - 0.1(2 * 2.1 - 2) = 2.1 - 0.2 = 1.9
......

偏微分基础

在继续学习之前,首先额外介绍下偏微分的基础知识。在机器学习的最优化问题中,有多少参数就有多少变量,所以目标函数正是这样的多变量函数。

g(x1,x2,...xn)=x1+x22+...xnng(x_1, x_2, ... x_n) = x_1 + x_2^2 + ... x_n^n

但是对于参数有多个的情况,每个参数的切线都不同,移动方向也不同。所以对多变量函数微分时,我们只需关注要微分的变量,把其他变量都当作常数来处理。这种微分的方法就称为偏微分。例如对于如下包含两个变量的函数:

h(x1,x2)=x12+x23h(x_1, x_2) = x_1^2 + x_2^3

由于有两个变量,所以需要在三维空间内画图:

接下来求这个函数 h 对 $x_1 的偏微分。刚才介绍偏微分时说过,除了关注的变量以外,其他变量都作为常数来处理,换言之就是把变量的值固定。比如把 x2x_2 固定为 x2=1x_2=1,这样 h 就会变成只有 x1x_1 一个变量的函数:

h(x1,x2)=x12+1h(x_1, x_2) = x_1^2 + 1

尽管图依然在三维空间内,但它看上去却是简单的二次函数了。由于常数的微分都是 0,所以 h 对进行偏微分的结果是下面这样的。

x1h(x1,x2)=2x1\frac{\partial}{\partial x_1} h(x_1, x_2) = 2x_1

基于同样的思路,考虑一下 h 对 x2x_2 的偏微分。比如将 x1x_1 固定为 x1=1x_1=1,那么 h 将成为只有 x2x_2 一个变量的函数:

h(x1,x2)=1+x23h(x_1, x_2) = 1 + x_2^3

x2h(x1,x2)=3x22\frac{\partial}{\partial x_2} h(x_1, x_2) = 3x_2^2

像这样只关注要微分的变量,将其他变量全部作为常数来处理,我们就可以知道在这个变量下函数的斜率是多少。不管变量增加到多少,这个方法都是适用的。

复合函数是指由多个函数组合而成的函数,而我们也经常需要对复合函数进行微分,例如对 f(g(x))f(g(x)) 进行微分,我们可以这样理解:

假设:

f(x)=10+x2f(x) = 10 + x^2

g(x)=3+xg(x) = 3 + x

因此:

y=f(u)y = f(u)

u=g(x)u = g(x)

dydu=dduf(u)=ddu(10+u2)=2u\frac{dy}{du} = \frac{d}{du}f(u) = \frac{d}{du}(10 + u^2) = 2u

dudx=ddxg(x)=ddx(3+x)=1\frac{du}{dx} = \frac{d}{dx}g(x) = \frac{d}{dx}(3 + x) = 1

dydx=dfdududx=2u1=2g(x)=2(3+x)\frac{dy}{dx} = \frac{df}{du} * \frac{du}{dx} = 2u * 1 = 2 *g(x) = 2 * (3 + x)

在机器学习领域,对复杂的函数进行微分的情况很多,这时把函数当作由多个简单函数组合而成的复合函数再进行微分,就可以相对简单地完成处理。

求解目标函数

接下来回到我们的目标函数 EθE_{\theta},这个目标函数中包含 fθ(x)f_θ(x),而 fθ(x)f_θ(x) 又包含 θ0\theta_0θ1\theta_1 两个参数(即真正需要变化的是 θ0\theta_0θ1\theta_1,这里的 x 是输入,不要把它和上面微分举例中的 x 搞混淆),也就是说这个目标函数是拥有 θ0\theta_0θ1\theta_1 的双变量函数,所以不能用普通的微分,而要用偏微分。因此表达式为:

θ0:=θ0ηEθ0\theta_0 := \theta_0 - \eta\frac{\partial E}{\partial \theta_0}

θ1:=θ1ηEθ1\theta_1 := \theta_1 - \eta\frac{\partial E}{\partial \theta_1}

之前的 $g_(x) 替换成了 E,然后需要使用偏微分。由于 EθE_\theta 是一个复合函数,所以需要用到复合函数的偏微分,如下是对 \theta_0 的偏微分计算过程:

类似地,完成对 θ1\theta_1 的偏微分计算:

所以最终 \theta_0 和 \theta_1 的更新公式为:

只要根据这个表达式来更新 θ0\theta_0θ1\theta_1,就可以找到正确地一次函数 f_θ(x) 了。

多项式回归

之前的预测模型我们使用的是一次函数,f_θ(x) = θ0 + θ1 * x,由于是一次函数,因此它的图像是直线。如果我们想要使用曲线来拟合数据,我们可以尝试将 f_θ(x) 改为二次函数,如下所示:

fθ(x)=θ0+θ1x+θ2x2f_θ(x) = θ_0 + θ_1 * x + θ_2 * x^2

用更大次数的表达式也可以,这样就能表示更复杂的曲线了(虽然次数越大拟合得越好,但也可能出现过拟合的问题):

fθ(x)=θ0+θ1x+θ2x2+...+θnxnf_θ(x) = θ_0 + θ_1 * x + θ_2 * x^2 + ... + θ_n * x^n

对于二次函数,我们用上面类似的方法,求解出更新表达式如下:

那么即使增加参数,比如有 θ3\theta_3θ4\theta_4 等,我们依然可以用同样的方法求出它们的更新表达式。像这样增加函数中多项式的次数,然后再使用函数的分析方法被称为多项式回归。

多重回归

在这个例子中,我们是根据广告费来预测点击量的。相当于变量只有一个广告费,实际中要解决的很多问题是变量超过 2 个的复杂问题。注意不要和上面的多项式回归搞混淆:多项式回归问题中确实会涉及不同次数的项,但是使用的变量依然只有广告费一项。例如,假设决定点击量的除了广告费之外,还有广告的展示位置和广告版面的大小等多个要素,这就是指多个变量。

例如假设有 3 个变量,为了求解 θ_0θ_1θ_2θ_3,我们依然可以使用之前的偏微分方法:

fθ(x)=θ0+θ1x1+θ2x2+θ3x3f_θ(x) = θ_0 + θ_1 * x1 + θ_2 * x2 + θ_3 * x3

对于有 n 个变量的情况,我们将参数 \theta 和变量 x 写成向量形式,它们都是列向量:

fθ(x)f_θ(x) 则可以写成:

fθ(x)=θTxf_θ(x) = \theta^T * x

同样使用偏微分的方法来求解 θ 的更新公式。为了一般化,我们可以考虑对第 j 个元素 θj\theta_j 偏微分的表达式。

uθj=uvvθj\frac {\partial u} {\partial \theta_j} = \frac {\partial u} {\partial v} * \frac {\partial v} {\partial \theta_j}

由于 u 对 v 微分的部分是一样的,所以只需要求 v 对 θj\theta_j 的微分就可以了。

vθj=θj(θTx)=θj(θ0x0+θ1x1+...+θnxn)=xj\frac {\partial v} {\partial \theta_j} = \frac {\partial} {\partial \theta_j} (\theta^T * x) = \frac {\partial} {\partial \theta_j} (θ_0x_0 + θ_1x_1 + ... + θ_nx_n) = x_j

最终第 j 个参数的更新表达式就是这样的:

θj:=θjηi=1n(fθ(x(i))yi)xj(i)\theta_j := \theta_j - η * \sum _{i=1}^{n} (f_θ(x^{(i)}) - y_i) * x_j^{(i)}

像这样包含了多个变量的回归称为多重回归。

随机梯度下降法

最速下降法就是对所有的训练数据都重复进行计算,而计算量大、计算时间长是最速下降法的一个缺点。而且它还有个缺点,容易陷入局部最优解。在讲解 平方误差目标函数 时,这个函数形式简单,所以用最速下降法没有问题。但是如果是如下形式的函数:

那随机选取的初始值就有可能陷入局部最优解,如下所示:

而随机梯度下降法是以最速下降法为基础的,在 最速下降法 中使用了所有训练数据的误差:

θj:=θjηi=1n(fθ(x(i))yi)xj(i)\theta_j := \theta_j - η * \sum _{i=1}^{n} (f_θ(x^{(i)}) - y_i) * x_j^{(i)}

而在随机梯度下降法中会随机选择一个训练数据,并使用它来更新参数。这个表达式中的 k 就是被随机选中的数据索引:

θj:=θjη(fθ(x(k))y(k))xj(k)\theta_j := \theta_j - η * (f_θ(x^{(k)}) - y^{(k)}) * x_j^{(k)}

最速下降法更新 1 次参数的时间,随机梯度下降法可以更新 n 次。此外,随机梯度下降法由于训练数据是随机选择的,更新参数时使用的又是随机选择数据时的梯度,所以不容易陷入目标函数的局部最优解。

除了随机选择 1 个训练数据的做法,此外还有随机选择 m 个训练数据来更新参数的做法。例如,假设随机选择 m 个训练数据的索引的集合为 K,那么我们这样来更新参数:

θj=θjηkK(fθ(x(k))y(k))xj(k)\theta_j = \theta_j - η * \sum _{k ∈ K} (f_θ(x^{(k)}) - y^{(k)}) * x_j^{(k)}

这种方法称为 小批量(mini-batch)梯度下降法,是一种介于 最速下降法随机梯度下降法 之间的方法。

学习分类:基于图像大小进行分类

接下来学习 分类 问题,具体来说,图像尺寸把它分类为纵向图像和横向图像,这是一个 二分类 问题。设 x 轴为图像的宽、y 轴为图像的高。然后把训练数据在图上进行展示(白色的点是纵向图像、黑色的点是横向图像):

而分类的目的就是找到一条线,将图中白色的点和黑色的点分开:

几何向量

向量拥有大小和方向。如下所示:

如果用几何语言表示向量的加法和减法,那么加法是让箭头相连,而减法是逆转向量的方向之后再让箭头相连:

这种计算在代数上只是做了向量中各元素的相加和相减而已:

a+b=[31]+[23]=[3+21+3]=[54]a + b = \begin{bmatrix} 3 \\ 1 \\ \end{bmatrix} + \begin{bmatrix} 2 \\ 3 \\ \end{bmatrix} = \begin{bmatrix} 3 + 2 \\ 1 + 3 \\ \end{bmatrix} = \begin{bmatrix} 5 \\ 4 \\ \end{bmatrix}

ab=[31][23]=[3213]=[12]a - b = \begin{bmatrix} 3 \\ 1 \\ \end{bmatrix} - \begin{bmatrix} 2 \\ 3 \\ \end{bmatrix} = \begin{bmatrix} 3 - 2 \\ 1 - 3 \\ \end{bmatrix} = \begin{bmatrix} 1 \\ -2 \\ \end{bmatrix}

向量之间的积,向量之间的积,存在称为内积的定义。内积是向量间定义的一种积运算,对于二维向量来说:

ab=a1b1+a2b2a \cdot b = a_1 * b_1 + a_2 * b_2

计算向量内积之后得到的已经不是向量,而是普通的数字,这就是 标量。由于内积的运算符号不是乘法符号 *​,而是点 ·​,所以有时它也被称为点积。假设向量 a 和 b 之间的夹角为 θ,那么内积也可以这样表示:

ab=abcosθa \cdot b = |a| * |b| * cosθ

a|a|b|b| 分别是向量 a 和 b 的长度,例如假设向量 a=(a1,a2)a = (a_1, a_2),那么它的长度就是:

a=a12+a22|a| = \sqrt {a_1^2 + a_2^2}

最后介绍下 法线,法线向量指的是与某条直线相垂直的向量。例如图中直线的表达式为 ax+by+c=0,那么这时的法线向量 p 为 p=(a, b)

内积

分类用图形来解释更容易理解,所以把它想象为有大小和方向的、带箭头的向量比较好。而上面我们所画的分割线,就是使 权重向量 成为 法线向量 的直线。假设权重向量为 w,那么那条直线的表达式就是这样的:

wx=0w \cdot x = 0

权重向量就是我们想要知道的未知参数(类似于上一节回归问题中我们想要求解的 θ\theta),w 是 weight 的缩写。实向量空间的内积是各相应元素乘积的和,所以刚才的表达式也可以写成这样:

wx=i=1nwixi=0w \cdot x = \sum _{i=1}^{n} w_i * x_i = 0

假设权重向量 w 为 (1, 1),那么求解出的直线就是 x1+x2=0x_1 + x_2 = 0,也就是 x1=x2x_1 = -x_2。可以看到,权重向量和求解出来的直线的确是垂直的,这就解释了 使权重向量成为法线向量的直线

从另一个角度计算:

w \ctdot x = |w| * |x| * cosθ = 0

所以要想使内积为 0,只能使 cosθ=0,也就是说 θ=90θ=270,这两种情况也是直角。

所以我们的目标就是:通过训练找到权重向量,然后才能得到与这个向量垂直的直线,最后根据这条直线就可以对数据进行分类了

感知机

那如何求出权重向量呢?将权重向量用作参数,创建更新表达式来更新参数。接下来介绍感知机(perceptron)模型。感知机是接受多个输入后将每个值与各自的权重相乘,最后输出总和的模型。感知机是非常简单的模型,基本不会应用在实际的问题中,但它是神经网络和深度学习的基础模型。

假设表示宽的轴为 x1x_1、表示高的轴为 x2x_2,用 y 来表示图像是横向还是纵向的,横向的值为 1、纵向的值为 -1。根据参数向量 x 来判断图像是横向还是纵向的函数,即返回 1 或者 -1 的函数 fw(x)f_w(x) 的定义如下,这个函数被称为判别函数:

fw(x)={1(wx>=0)1(wx<0)f_w(x) = \begin{cases} 1 & (w \cdot x >= 0) \\ -1 & (w \cdot x < 0) \end{cases}

由于

wx=wxcosθw \cdot x = |w| * |x| * cosθ

cosθ 就决定了内积的正负值。θ 表示直线与权重向量 w 之间的夹角:

  • 90<θ<270 范围内的所有向量,都符合 wx<0w \cdot x < 0
  • 0<θ≤90 范围内的所有向量,都符合 wx>=0w \cdot x >= 0

内积是衡量向量之间相似程度的指标。结果为正,说明二者相似;为 0 则二者垂直;为负则说明二者不相似。

现在,重新定义权重向量的更新表达式:

w:={w+y(i)x(i)(fw(x(i))y(i))w(fw(x(i))=y(i))w := \begin{cases} w + y^{(i)} * x^{(i)} & (f_w(x^{(i)}) \neq y^{(i)}) \\ w & (f_w(x^{(i)}) = y^{(i)}) \end{cases}

这里的 fw(x(i))y(i)f_w(x^{(i)}) \neq y{(i)} 即表示通过判别函数对宽和高的向量 x 进行分类的结果与实际的标签 y 不同,也就是说更新表达式只有在判别函数分类失败的时候才会更新参数值。在分类失败时更新权重向量,使得直线旋转相应的角度,像这样重复更新所有的参数,就是感知机的学习方法。

逻辑回归

感知机最大的缺点就是它只能解决线性可分的问题,线性可分指的就是能够使用直线分类的情况。接下来介绍另外的算法,它与感知机的不同之处在于,它是把分类作为概率来考虑的。首先介绍 sigmoid 函数:

fθ(x)=11+eθTxf_\theta(x) = \frac {1} {1 + e^{-\theta^T x}}

假设 θTx\theta^T x 为横轴,fθ(x)f_\theta(x) 为纵轴,那么这个函数如下所示:

我们把未知数据 x 是横向图像的概率作为 f_\theta(x),其表达式如下:

P(y=1x)=fθ(x)P(y=1|x) = f_\theta(x)

这里 P 中的竖线表示条件概率。P(A|B) 是指事件 B 发生的条件下事件 A 发生的概率,因此 P(y=1|x) 就是在 x 数据下,图像是横向的概率。我们以 0.5 为阈值,然后把 f_\theta(x) 的值与 0.5 进行比较,如果大于 0.5,则认为是横向图像,否则认为是纵向图像:

y={1(fθ(x)>=0.5)0(fθ(x)<0.5)y = \begin{cases} 1 & (f_\theta(x) >= 0.5) \\ 0 & (f_\theta(x) < 0.5) \end{cases}

对于 sigmoid 函数来说,在 θTx\theta^T x 为 0 时,fθ(x)f_\theta(x) 为 0.5。所以上面的表达式可以继续改为:

y={1(θTx>=0)0(θTx<0)y = \begin{cases} 1 & (\theta^T x >= 0) \\ 0 & (\theta^T x < 0) \end{cases}

θTx=0\theta^T x = 0 这条直接,就是决策边界,可以把这条线两侧的数据分类为横向和纵向了。而我们的目标就是求解正确的参数 θ\theta,为此需要定义目标函数、微分、求参数的更新表达式。这种算法就称为 逻辑回归

似然函数

但是逻辑回归的目标函数不同于之前的 最小二乘法(最小化误差的平方和)。我们的目标是使如下联合概率的值最大:

L(θ)=i=1mP(y(i)=1x(i))y(i)P(y(i)=0x(i))1y(i)L(\theta) = \prod _{i=1}^{m} P(y^{(i)} = 1|x^{(i)})^{y^{(i)}} P(y^{(i)} = 0|x^{(i)})^{1-y^{(i)}}

这个表达式虽然看上去复杂,但它其实是一个 汇总表达式,对于 y(i)=1y_{(i)} = 1,它就是 P(y=1x(i))P(y=1|x^{(i)}) 的概率,对于 y(i)=0y_{(i)} = 0,它就是 P(y=0x(i))P(y=0|x^{(i)}) 的概率。

我们的目标是求解使这个目标函数最大化的参数 \theta。回归的时候处理的是误差,所以要最小化,而现在考虑的是联合概率,我们希望概率尽可能大,所以要最大化。这里的目标函数 L(θ) 也被称为 似然,函数的名字 L 取自似然的英文单词 Likelihood 的首字母,表示近似的意思(近似地接近训练数据)。

对数似然函数

为了方便求解,我们取似然函数的对数就好了,像这样在等式两边加上 log 即可:

logL(θ)=logi=1mP(y(i)=1x(i))y(i)P(y(i)=0x(i))1y(i)logL(\theta) = log\prod _{i=1}^{m} P(y^{(i)} = 1|x^{(i)})^{y^{(i)}} P(y^{(i)} = 0|x^{(i)})^{1-y^{(i)}}

因为 log 是单调递增函数,因此使 L(θ) 最大化等价于使 log L(θ) 最大化。之后按照如下方式进行变形:

逻辑回归将这个变形后的 对数似然函数 用作目标函数。最终对这个目标函数进行微分运算:

接下来从这个表达式导出参数更新表达式。不过现在是以最大化为目标,所以必须按照与最小化时相反的方向移动参数哦(最小化时要按照与微分结果的符号相反的方向移动,而最大化时要与微分结果的符号同向移动)。最终结果为:

θj:=θ+ηi=1m(y(i)fθ(x(i)))xj(i)\theta_j := \theta + \eta \sum _{i=1}^{m} (y^{(i)} - f_\theta(x^{(i)})) x_j^{(i)}

为了与回归时的符号保持一致,也可以将表达式调整为下面这样:

θj:=θηi=1mfθ(x(i)y(i))xj(i)\theta_j := \theta - \eta \sum _{i=1}^{m} f_\theta(x^{(i)} - y^{(i)}) x_j^{(i)}

线性不可分

虽然用直线不能分类,但是用曲线就可以处理 线性不可分 问题了。例如

如同多项式回归时那样,我们需要增加变量的次数。例如:

θTx=θ0+θ1x1+θ2x2+θ3x12\theta^T x = \theta_0 + \theta_1 * x_1 + \theta_2 * x_2 + \theta_3 * x_1^2

这样,按照类似的原理,逻辑回归同样可以应用于线性不可分问题。当然除了逻辑回归来解决分类问题,还有 SVM(支持向量机)等其他分类方法。

评估已建立的模型

模型评估

在进行回归和分类时,为了进行预测,我们定义了函数 ftheta(x)f_theta(x)。然后根据训练数据求出了函数参数 θ\theta(通过对目标函数进行微分,然后求出参数更新表达式),但最终我们真正想要的是通过预测函数得到预测值,我们希望 ftheta(x)f_theta(x) 对未知数据 x 输出的预测值尽可能准确。为此,我们需要能够定量地表示机器学习模型的精度。

交叉验证

我们把获取的全部训练数据分成两份(例如按照 2:8 进行划分):一份用于测试,一份用于训练,然后用前者来评估模型。模型评估就是检查训练好的模型对测试数据的拟合情况。如下就展示了两个对训练数据 过拟合 的情况:

对于回归的情况,只要在训练好的模型上计算测试数据的误差的平方,再取其平均值就可以了。假设测试数据有 n 个,那么可以这样计算:

1ni=1n(y(i)fθ(x(i)))2\frac {1} {n} \sum _{i=1}^{n} (y^{(i)} - f_\theta(x^{(i)}))^2

这个值被称为 均方误差 或者 MSE,全称 Mean Square Error​。这个误差越小,精度就越高,模型也就越好。

由于回归是连续值,所以可以从误差入手,但是在分类中我们必须要考虑分类的类别是否正确。对于二分类,我们可以用如下表格来梳理分类结果:

  • 分类成功用 True/False 来表示
  • 分类结果用 Positive/Negative 来表示

所以可以用如下公式来计算精度:

Accuracy=TP+TNTP+FP+FN+TNAccuracy = \frac {TP + TN} {TP + FP + FN + TN}

一般来说,只要计算出这个 Accuracy 值,基本上就可以掌握分类结果整体的精度了。但是有时候只看这个结果会有问题,所以还有别的指标。例如如果数据本身分配不均衡,那么哪怕出现模型把数据全部分类为一种类别的极端情况,它的 Acuracy 值也可能很高。遇到这种情况,只看整体的精度看不出来问题。

  • 准确率 Precision 的计算公式如下,它的含义是在被分类为 Positive 的数据中,实际就是 Positive 的数据所占的比例

Precision=TPTP+FPPrecision = \frac {TP} {TP + FP}

  • 召回率 Recall 的计算公式如下,它的含义是在实际为 Positive 的数据中,被正确分类的数据所占的比例

Recall=TPTP+FNRecall = \frac {TP} {TP + FN}

一般来说,精确率和召回率会一个高一个低,需要我们取舍,有些麻烦。为了评定综合性能,引入 Fmeasure 这个指标,即 F 值。它的计算公式如下:

F=21Precision+1RecallF = \frac {2} {\frac {1} {Precision} + \frac {1} {Recall}}

也可以变形为:

F=2PrecisionRecallPrecision+RecallF = \frac {2 * Precision * Recall} {Precision + Recall}

还有一个带权重的 F 值指标:

WeightedFmeasure=(β2+1)PrecisionRecallβ2Precision+RecallWeightedFmeasure = \frac {(\beta^2 + 1) * Precision * Recall} {\beta^2 * Precision + Recall}

目前看到的 精确率召回率 都是以 TP 为主进行计算的,也可以使用 TN 来进行计算。当数据不平衡时,使用数量少的那个会更好。

把全部训练数据分为测试数据和训练数据的做法称为交叉验证。交叉验证的方法中,尤为有名的是 K 折交叉验证

  • 把全部训练数据分为 K 份
  • K-1 份数据用作训练数据,剩下的 1 份用作测试数据
  • 每次更换训练数据和测试数据,重复进行 K 次交叉验证
  • 最后计算K个精度的平均值,把它作为最终模型的精度

但是如果全部训练数据的量较大,这种方法必须训练多次,会比较耗费时间。所以需要确定一个合适的 K 值。

正则化

如果模型只能拟合训练数据,这被称为过拟合,英文是 overfitting。有几种方法可以避免过拟合:

  • 增加全部训练数据的数量
  • 使用简单的模型
  • 正则化

这里介绍一下正则化的概念。之前介绍回归时提到了如下目标函数:

Eθ=12i=1n(y(i)fθ(x(i)))2E_\theta = \frac {1} {2} \sum _{i=1}^{n} (y^{(i)} - f_\theta(x^{(i)}))^2

我们要向这个目标函数增加下面这样的正则化项:

Rθ=12λj=1mθj2R_\theta = \frac {1} {2} \lambda \sum _{j=1}^{m} \theta_j^2

最终目标函数变成了:

Eθ=12i=1n(y(i)fθ(x(i)))2+12λj=1mθj2E_\theta = \frac {1} {2} \sum _{i=1}^{n} (y^{(i)} - f_\theta(x^{(i)}))^2 + \frac {1} {2} \lambda \sum _{j=1}^{m} \theta_j^2

我们要对这个新的目标函数进行最小化,这种方法就称为正则化:

  • m 是参数的个数,但是一般来说不对 θ0\theta_0 应用正则化。所以仔细看会发现 j 的取值是从 1 开始的。
  • λ\lambda 是决定正则化项影响程度的正常数,这个值需要我们自己来定

正则化的效果是可以防止参数变得过大,有助于参数接近较小的值。参数的值变小,意味着该参数的影响也会相应地变小。这正是通过减小不需要的参数的影响,将复杂模型替换为简单模型来防止过拟合的方式

以上讨论的是回归的正则化,接下来再来看分类的正则化。之前已经介绍过逻辑回归的目标函数:

logL(θ)=i=1m[y(i)log(fθ(x(i)))+(1y(i))log(1fθ(x(i)))]logL(\theta) = \sum _{i=1}^{m} [y^{(i)} log(f_\theta(x^{(i)})) + (1 - y^{(i)}) log(1 - f_\theta(x^{(i)}))]

分类也是在这个目标函数中增加正则化项就行了,道理是相同的:

logL(θ)=i=1m[y(i)log(fθ(x(i)))+(1y(i))log(1fθ(x(i)))]+12λj=1mθj2logL(\theta) = -\sum _{i=1}^{m} [y^{(i)} log(f_\theta(x^{(i)})) + (1 - y^{(i)}) log(1 - f_\theta(x^{(i)}))] + \frac {1} {2} \lambda \sum _{j=1}^{m} \theta_j^2

由于对数似然函数本来以最大化为目标,为了让它变成和回归的目标函数一样的最小化问题,所以加了负号。这样就可以像处理回归一样处理它,所以只要加上正则化项就可以了。当然,目标函数的形式变了后,参数更新的表达式也会变。

这种正则化方法称为 L2 正则化,除此之外还有 L1 正则化,它的正则化项 R 是这样的:

Rθ=j=1mθjR_\theta = \sum _{j=1}^{m} |\theta_j|

L1 正则化的特征是被判定为不需要的参数会变为 0,从而减少变量个数。而 L2 正则化不会把参数变为 0。L2 正则化会抑制参数,使变量的影响不会过大,而 L1 会直接去除不要的变量。

学习曲线

过拟合 相反的一种状态是 迁拟合(underfitting),它是没有拟合训练数据的状态。出现这种情况的主要原因就是模型相对于要解决的问题来说太简单了。

只根据精度不能判断是哪种不好的拟合。如果模型过于简单,那么随着数据量的增加,误差也会一点点变大。换句话说就是精度会一点点下降。

训练数据较少时训练好的模型难以预测未知的数据,所以精度很低;反过来说,训练数据变多时,预测精度就会一点点地变高。用图来展示就是这样的:

将两份数据的精度用图来展示后,如果是这种形状,就说明出现了欠拟合的状态。也有一种说法叫作高偏差,指的是一回事:这是一种即使增加数据的数量,无论是使用训练数据还是测试数据,精度也都会很差的状态。

而在过拟合的情况下,图是这样的,也称为 高方差:随着数据量的增加,使用训练数据时的精度一直很高,而使用测试数据时的精度一直没有上升到它的水准。

像这样展示了数据数量和精度的图称为学习曲线。在知道模型精度低,却不知道是过拟合还是欠拟合的时候,可以通过学习曲线来判断出是过拟合还是欠拟合状态。

实现:使用 Python 编程

在前面学习回归和分类的时候,我们已经看过用训练数据更新参数的过程。虽然每一种算法的具体方法不同,但这个基本的思路对于其他机器学习算法来说也是相通的。只要掌握了根据数据来更新参数这一点,就容易理解算法了。

接下来使用 Python 来实现这些算法,以加深对机器学习的理解,先从回归开始。

回归

  • Python 中使用 matplotlib 库来绘制图形。对训练该数据,我们就可以使用 matplotlib 库来做数据可视化
1
2
3
4
5
6
7
8
9
10
import numpy as np
import matplotlib.pyplot as plt

train = np.loadtxt('data.csv', delimiter=',', skiprows=1)
train_x = train[:, 0]
train_y = train[:, 1]

plt.plot(train_x, train_y)
plt.show()
plt.savefig('data.png')
  • 接下来我们把训练数据变成 平均值为 0、方差为 1 的数据,这种做法也被称为标准化或者 z-score 规范化。这个预处理不是必须的,但是做了之后,参数的收敛会更快。其中 μ\mu 是训练数据的平均值,σ\sigma 是标准差。

zi=xiμσz^i = \frac {x^i - \mu} {\sigma}

1
2
3
4
5
6
7
8
9
10
11
12
mu = np.mean(train_x)
sigma = np.std(train_x)
def standardize(x):
return (x - mu) / sigma

train_z = standardize(train_x)

# 创建新的图表对象
plt.figure()
plt.plot(train_z, train_y)
plt.show()
plt.savefig('standardized_data.png')
  • 预测函数是一次函数,而对应的目标函数则如下:

fθ(x)=θ0+θ1xf_\theta(x) = \theta_0 + \theta_1 x

Eθ=12i=1n(y(i)fθ(x(i)))2E_\theta = \frac {1} {2} \sum _{i=1}^{n} (y^{(i)} - f_\theta(x^{(i)}))^2

1
2
3
4
5
6
7
8
theta_0 = np.random.rand()
theta_1 = np.random.rand()

def f(x):
return theta_0 + theta_1 * x

def E(x, y):
return 0.5 * np.sum(((f(x) - y) ** 2))
  • 根据之前推导出来的 θ\theta 更新表达式

θ0=θ0ηi=1n(fθ(x(i))y(i))\theta_0 = \theta_0 - \eta \sum _{i=1}^{n} (f_\theta(x^{(i)}) - y^{(i)})

θ1=θ1ηi=1n(fθ(x(i))y(i))x(i)\theta_1 = \theta_1 - \eta \sum _{i=1}^{n} (f_\theta(x^{(i)}) - y^{(i)}) x^{(i)}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ETA = 1e-3
diff = 1
count = 0
error = E(train_z, train_y)
while diff > 1e-2:
count += 1

tmp_theta_0 = theta_0 - ETA * np.sum(f(train_z) - train_y)
tmp_theta_1 = theta_1 - ETA * np.sum((f(train_z) - train_y) * train_z)

theta_0 = tmp_theta_0
theta_1 = tmp_theta_1
new_error = E(train_z, train_y)
diff = error - new_error
error = new_error

log = '{}: theta_0 = {}, theta_1 = {}, error = {}, diff = {}'.format(count, theta_0, theta_1, new_error, diff)
print(log)
  • 最后展示训练数据和我们计算出来的预测函数 fθ(x)f_\theta(x) 的图形

接下来再来实现多项式回归,假设要实现如下多项式:

fθ(x)=θ0+θ1x+θ2x2f_\theta(x) = \theta_0 + \theta_1 x + \theta_2 x^2

我们把参数和训练数据都作为向量来处理,可以使计算变得更简单:

之前介绍多项式回归时说过它的更新表达式:

θj=θjηi=1n(fθ(x(i))y(i))xj(i)\theta_j = \theta_j - \eta \sum _{i=1}^{n} (f_\theta(x^{(i)}) - y^{(i)}) x_j^{(i)}

当 j = 0 时,把更新表达式中的 \sum 展开,就会变成:

(fθ(x(1))y(1))x0(1)+(fθ(x(2))y(2))x0(2)+(f_\theta(x^{(1)}) - y^{(1)})x_0^{(1)} + (f_\theta(x^{(2)}) - y^{(2)})x_0^{(2)} + \cdots

等效于将如下 ff 向量转置后与 x0x_0 向量相乘:

这里考虑的还只是 j=0 的情况,而参数共有 3 个,再用同样的思路考虑 x1x_1x2x_2 的情况就好了:

最后直接计算 fTXf^T X 就可以了。

将上述思路用 Python 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def to_matrix(x):
return np.vstack([np.ones(len(x)), x, x**2]).T

def f(x):
return np.dot(x, theta)

def E(x, y):
return 0.5 * np.sum((y - f(x)) ** 2)

X = to_matrix(train_z)
theta = np.random.rand(3)

ETA = 1e-3
diff = 1
error = E(X, train_y)
while diff > 1e-2:
theta = theta - ETA * np.dot(f(X) - train_y, X)
new_error = E(X, train_y)
diff = error - new_error
error = new_error

也可以使用随机梯度下降法实现,之前分析过随机梯度下降法的参数更新如下,k 是随机选择的:

θj=θjη(fθ(x(k))y(k))xj(k)\theta_j = \theta_j - \eta (f_\theta(x^{(k)}) - y^{(k)}) x_j^{(k)}

由于已经有了训练数据 X,只要把行的顺序随机调整,然后重复应用新表达式即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def MSE(x, y):
return (1 / x.shape[0]) * np.sum((y - f(x)) ** 2)

theta = np.random.rand(3)
errors = []

diff = 1
errors.append(MSE(X, train_y))
while diff > 1e-2:
p = np.random.permutation(X.shape[0])
for x, y in zip(X[p], train_y[p]):
theta = theta - ETA * (f(x) - y) * x
errors.append(MSE(X, train_y))
diff = errors[-2] - errors[-1]

对于多重回归,也可以像多项式回归那样使用矩阵。不过要注意对多重回归的变量进行标准化时,必须对每个参数都进行标准化。如果有变量 x1x_1x2x_2x3x_3,就要分别使用每个变量的平均值和标准差进行标准化。

分类:感知机实现

接下来使用感知机算法来实现图片分类(区分横向图片和纵向图片)。首先同样是对数据集进行可视化:

1
2
3
4
5
6
7
8
9
10
11
12
import numpy as np
import matplotlib.pyplot as plt

train = np.loadtxt('images.csv', delimiter=',', skiprows=1)
train_x = train[:,0:2]
train_y = train[:,2]

plt.plot(train_x[train_y==1, 0], train_x[train_y==1, 1], 'o')
plt.plot(train_x[train_y==-1, 0], train_x[train_y==-1, 1], 'x')
plt.axis('scaled')
plt.show()
plt.savefig('images.png')

感知机的表达式如下:

fw(x)={1wx>=01wx<0f_w(x) = \begin{cases} 1 & w * x >=0 \\ -1 & w * x < 0 \end{cases}

而感知机的更新表达式如下:

w:={w+y(i)x(i)fw(x(i))y(i)wfw(x(i))=y(i)w := \begin{cases} w + y^{(i)} x^{(i)} & f_w(x^{(i)}) \neq y^{(i)} \\ w & f_w(x^{(i)}) = y^{(i)} \end{cases}

用 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
w = np.random.rand(2)
def f(x):
if np.dot(w, x) >= 0:
return 1
else:
return -1


epoch = 10
count = 0
for _ in range(epoch):
for x, y in zip(train_x, train_y):
if f(x) != y:
w = w + y * x
count += 1
print("第 {} 次,w = {}".format(count, w))

x1 = np.arange(0, 500)
plt.figure()
plt.plot(train_x[train_y == 1, 0], train_x[train_y == 1, 1], "o")
plt.plot(train_x[train_y == -1, 0], train_x[train_y == -1, 1], "x")
plt.plot(x1, -w[0] / w[1] * x1, linestyle="dashed")
plt.show()
plt.savefig("images_result.png")

实际运行结果如下:

分类:逻辑回归实现

继续使用原有的 images.csv 数据集,但是将 y 的值修改为 1 和 0(满足逻辑回归的要求)。之前已经介绍逻辑回归的似然函数、对 对数似然函数 进行微分等一系列操作,最终得到的更新表达式如下:

θj:=θjηi=1n(fθ(x(i))y(i))xj(i)\theta_j := \theta_j - \eta \sum_{i=1}^{n} (f_\theta(x^{(i)}) - y^{(i)}) x_j^{(i)}

与回归时一样,将 f_\theta(x^{(i)}) - y^{(i)} 当作向量来处理,将它与训练数据的矩阵相乘就行了。代码实现如下:

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
theta = np.random.rand(3)

mu = train_x.mean(axis=0)
sigma = train_x.std(axis=0)

def standardize(x):
return (x - mu) / sigma

train_z = standardize(train_x)

def to_matrix(x):
x0 = np.ones([x.shape[0], 1])
return np.hstack([x0, x])

X = to_matrix(train_z)

def f(x):
return 1 / (1 + np.exp(-np.dot(x, theta)))

def classify(x):
return (f(x) >= 0.5).astype(np.int)

EAT = 1e-3
epoch = 5000
count = 0
for _ in range(epoch):
theta = theta - EAT * np.dot(f(X) - train_y, X)
count += 1
print("第 {} 次:theta = {}".format(count, theta))

x0 = np.linspace(-2, 2, 100)
plt.plot(train_z[train_y == 1, 0], train_z[train_y == 1, 1], 'o')
plt.plot(train_z[train_y == 0, 0], train_z[train_y == 0, 1], 'x')
plt.plot(x0, -(theta[0] + theta[1] * x0) / theta[2], linestyle='dashed')
plt.show()
plt.savefig('logistic_regression.png')

如果相对线性不数据进行分类,过程也是类似的,只不过预测函数需要使用二次多项式,例如增加 x12x_1^2,同时增加参数 θ3\theta_3

如果将重复次数作为横轴、精度作为纵轴来绘图,应该可以看到随着重复次数的增加,精度逐渐提高。在代码实现中,我们直接指定重复次数为固定值 5000 次。我们也可以每次学习后都计算精度,当精度达到满意的程度后就停止学习。

另外,我们之前也介绍过正则化,我们可以通过比较 过拟合时 图的状态和应用了正则化后图的状态,来感受正则化的效果。应用正则化的代码实现和上面的代码是类似的,只不过更新参数时,需要添加正则化项。

小结

《白话机器学习的数学》一书介绍了回归和分类这两个机器学习领域的主要任务,并解释了如何建立创建预测函数、目标函数 以及如何求解目标函数,较为详细地解释了这背后所涉及的数学原理。通过学习本书,可以对机器学习的基本概念有个较为清晰的认识,为后续学习更复杂的机器学习算法打下基础。