# 学习算法剖析

译者：[P3n9W31](https://github.com/P3n9W31)

## 一个学习算法的组成模块

通过对先前章节的阅读，你可能已经注意到了，我们看到的每个学习算法都是由三个部分组成的：

1. 一个损失函数
2. 一个建立在损失函数之上的优化准则（例如，一个代价函数，cost function）
3. 一个能够利用训练数据集来使优化准则不断优化的优化过程

以上的这三个部分是任何一个学习算法都包含的组成模块。你在之前的章节中可以看到，某些算法旨在对明确地对特定的一些优化准则进行不断的优化（包括线性回归、逻辑回归、SVM）。而此外其他的一些算法，包括决策树以及kNN算法，并没有对相应的优化准则进行明确地不断优化。决策树算法以及kNN算法几乎是历史最久远的机器学习算法，它们在被发明的时候更多的是实验性的基于直觉来构建的，并没有一个特定的全局优化的准则，它们的优化准则在晚些时候才发展而来（就像在科学的历史中常常发生的那样），用于解释这些算法的工作原理。

在阅读关于机器学习的一些现代的文献的时候，你常常都会遇到 **梯度下降法** 与 **随机梯度下降法** 的概念。它们是在优化准则可微分的情况下使用的两种最常用的优化算法。

梯度下降法是一个用于寻找函数最小值的迭代优化算法。使用梯度下降法是为了寻找函数的一个 *局部* 最小值，一次梯度下降法从一个随机点开始，并移动与函数当前点的梯度（或近似梯度）的负值成比例的距离。

梯度下降可用于找到线性和逻辑回归，SVM以及我们稍后考虑的神经网络的最佳参数。对于许多模型来说，例如逻辑回归或SVM，优化准则是凸的（convex）。凸函数只有一个全局最小值。而神经网络的优化准则不是凸函数，但是在实际应用中，找到一个局部最小值就已经能够满足优化的需求了。

接下来让我们看看梯度下降法到底是怎么工作的。

## 梯度下降法

在本节中，我将演示梯度下降法是如何找到线性回归问题的解的（脚注1：正如你所知的那样，线性回归具有闭合形式的解。这意味着这种特定类型的问题其实并不需要梯度下降来解决。 然而，出于说明目的，线性回归是解释梯度下降的一类完美问题）。我将使用Python的源代码以及图像来说明在一定数量的迭代运算之后，梯度下降是如何让解进行改进的。

我使用的数据集只包含一个特征。然而，优化准则将有两个参数： $w$ 与 $b$ 。拓展到多维的训练数据是也是十分直接的：对于二维数据来说，你会有参数 $w^{(1)}$, $w^{(2)}$ 与 $b$ ，对于三维数据来说，你会有参数 $w^{(1)}$, $w^{(2)}$ , $w^{(3)}$ 与 $b$ ，以此类推。

![图1：原始数据。Y轴对应于单元内的销售额（我们想要预测的数量），X轴对应于样本的特征：以M$为单位的广播广告支出。](https://github.com/apachecn/ml-book-100-zh/blob/master/doc/img/4-1.png)

图1：原始数据。Y轴对应于单元内的销售额（我们想要预测的数量），X轴对应于样本的特征：以M$为单位的广播广告支出。

为了给出一个实际的例子，我使用了包含以下列的真实数据集：每年广播广告的各公司的支出和按销售量计算的年度销售额。我们想要构建一个能够根据一家公司在广播广告上的支出来预测销售量的模型。数据集中的每一行代表了一个特定的公司：

| Company | Spendinngs, M$ | Sales, Units |
| :-----: | :------------: | :----------: |
|    1    |      37.8      |     22.1     |
|    2    |      39.3      |     10.4     |
|    3    |      45.9      |     9.3      |
|    4    |      41.3      |     18.5     |
|   ...   |      ...       |     ...      |

我们的数据包含了200家公司，所以我们就有200条训练样本。图1在2维图像上展示了所有的样本。

要记得，线性回归模型看起来有着这样的形式： $f(x)=w x+b$ 。我们不知道 $w$ 与 $b$ 的最佳值是多少，我们希望能够从数据中学习得到。为了实现这个目标，我们为 $w$ 与 $b$ 寻找能够最小化均方误差的值：

$$l=\frac{1}{N} \sum_{i=1}^{N}\left(y_{i}-\left(w x_{i}+b\right)\right)^{2}$$

梯度下降从计算每个参数的偏导数开始：

$$\begin{aligned} \frac{\partial l}{\partial w} &=\frac{1}{N} \sum_{i=1}^{N}-2 x_{i}\left(y_{i}-\left(w x_{i}+b\right)\right) \\ \frac{\partial l}{\partial b} &=\frac{1}{N} \sum_{i=1}^{N}-2\left(y_{i}-\left(w x_{i}+b\right)\right) \end{aligned}$$

为了找到 $\left(y_{i}-(w x+b)\right)^{2}$ 项关于 $w$ 的偏导数，我们应用 *链式法则* 。在这里，我们具有 $f=f_{2}\left(f_{1}\right)$ 这样形式的一条链，其中， $f_{1}=y_{i}-(w x+b)$ ， $f_{2}=f_{1}^{2}$ 。为了找到 $f$ 关于 $w$ 的偏导数，我们首先必须要找到 $f$ 关于 $f_2$ 的，与 $2(y_i-(w x+b))$ 相等的偏导数（通过计算，我们知道导数 $\frac{\partial f}{\partial x} x^{2}=2 x$ ）然后我们必须将其与 $y_{i}-(w x+b)$ 关于 $w$ 并与 $-x$ 相等的偏导数相乘。综上所述， $\frac{\partial l}{\partial w}=\frac{1}{N} \sum_{i=1}^{N}-2 x_{i}\left(y_{i}-\left(w x_{i}+b\right)\right)$ 。同样的，也可以计算出 $l$ 关于 $b$ , $\frac{\partial l}{\partial b}$ 的偏导数。

我们初始化 $w_0 = 0$ 与 $b_0 = 0$ （脚注2：在一些复杂的模型中，例如神经网络，将有着数以千计的参数，对参数的初始化将极大的影响使用梯度下降法进行优化时找到的解。有着不同的初始化方法（随机初始化，全部初始化为0，初始化为0附近小的数值，以及其它），这是数据分析师必须做出的重要选择），然后通过训练样本进行迭代，每个样本都有如 $\left(x_{i}, y_{i}\right)=\left(\text {Spendings}_{i}, \text {Sales}_{i}\right)$ 的形式。对于每一个训练样本来说，我们使用偏导数来更新 $w$ 与 $b$ 。学习率 $\alpha$ 控制了一次参数更新的幅度：

$$\begin{aligned} w_{i} & \leftarrow \alpha \frac{-2 x_{i}\left(y_{i}-\left(w_{i-1} x_{i}+b_{i-1}\right)\right)}{N} \\ b_{i} & \leftarrow \alpha \frac{-2\left(y_{i}-\left(w_{i-1} x_{i}+b_{i-1}\right)\right)}{N} \end{aligned}$$

其中，$w_i$ 与 $b_i$ 表示 $w$ 与 $b$ 在利用 $\left(x_{i}, y_{i}\right)$ 进行更新之后的值。

一次对所有训练样本的遍历称为1 epoch（代）。一般情况下，我们进行许多代更新，直到我们开始发现 $w$ 与 $b$ 的值不再发生大的变化，我们就停止。

很难想象一个机器学习工程师不使用Python。所以，如果你真正寻找一个合适的时机来开始学习Python的话，那就是现在了。在下面，我们说明了如何在Python中编写梯度下降算法。

用于在每一代中更新参数 $w$ 与 $b$ 的函数如下所示：

```python
def update_w_and_b(spendings, sales, w, b, alpha):
    dl_dw = 0.0
    dl_db = 0.0
    N = len(spendings)
  
    for i in range(N):
        dl_dw += -2*spendings[i]*(sales[i] - (w*spendings[i] + b))
        dl_db += -2*(sales[i] - (w*spendings[i] + b))
        
    # update w and b
    w = w - (1/float(N))*dl_dw*alpha
    b = b - (1/float(N))*dl_db*alpha
    
    return w, b
```

在多代训练中进行循环的函数如下所示：

```python
def train(spendings, sales, w, b, alpha, epochs):
    for e in range(epochs):
        w, b = update_w_and_b(spendings, sales, w, b, alpha)
    
        # log the progress
        if e % 400 == 0:
            print("epoch:", e, "loss: ", avg_loss(spendings, sales, w, b))
            
    return w, b
```

下面的 *avg_loss* 函数适用于计算均方误差，其被定义为：

```python
def avg_loss(spendings, sales, w, b):
    N = len(spendings)
    total_error = 0.0
    for i in range(N):
        total_error += (sales[i] - (w*spendings[i] + b))**2
        
    return total_error / float(N)
```

如果我们将参数设置为 $\alpha = 0.001, w = 0.0, b = 0.0, epochs = 15000$ 来运行 *train* 函数，我们将看到如下的输出结果（部分）：

```
epoch: 0    loss: 92.32078294903626
epoch: 400  loss: 33.79131790081576
epoch: 800  loss: 27.9918542960729
epoch: 1200 loss: 24.33481690722147
epoch: 1600 loss: 22.028754937538633
...
epoch: 2800 loss: 19.07940244306619
```

![图2：回归线随着梯度下降法代数变化的演变过程](https://github.com/apachecn/ml-book-100-zh/blob/master/doc/img/4-2.png)

图2：回归线随着梯度下降法代数变化的演变过程

从图中可以发现平均损失值随着 *train* 函数的循环与epoch增加正在降低。图2展示了回归线随着代数（epoch）变化的演变过程。

最终，一旦我们找到了参数 $w$ 与 $b$ 的最佳值，我们唯一缺少的东西就是一个能够用于预测的函数了：

```python
def predict(x, w, b):
  return w*x + b
```

尝试执行以下的代码：

```python
w, b = train(x, y, 0.0, 0.0, 0.001, 15000)
x_new = 23.0
y_new = predict(x_new, w, b)
print(y_new)
```

输出的结果为13.97。

梯度下降法对于学习率 $\alpha$ 的选择是十分敏感的。且对于大型的数据集来说，梯度下降法是比较慢的。幸运的是，有好几种十分重要的针对梯度下降法的优化方式已经被提出了。

**随即梯度下降**（SGD）是梯度下降法的一种改进，其通过估算训练样本中的更小的批次（子集）的梯度来加速了计算的过程。SGD本身就存在许多版本的升级，**Adagrad** 是SGD的其中一个版本，能够根据梯度的变化历史来对参数的学习率 $\alpha$ 进行缩放调整。 **Momentum** 是一种可以通过确定梯度下降在相关方向上的方向并减少震荡的一种能够对SGD加速的方法。在神经网络的训练中，SGD的变体，如 **RESprop** 与 **Adam** 都是使用的十分频繁的。

注意，梯度下降法及其变体都不是机器学习算法。它们是最小化问题的解决方案，其中需要最小化的目标函数在其值域的大多数点处都具有梯度。

## 机器学习工程师会怎么做

除非你是研究科学家或为拥有大量研发预算的大公司工作，否则你通常情况下都不会自己实现机器学习算法。你不会去实现梯度下降或其他的求解器。你使用库，其中大部分都是开源的。库是一些算法以及支持工具的一个集合，其同时考虑了程序的稳定性与运行效率。在现实中，使用的最频繁的开源机器学习库是 *scikit-learn* 。它是用Python与C语言写的。下面一个实例，告诉了你如何在scikit-learn中来完成线性回归：

```python
def train(x, y):
    from sklearn.linear_model import LinearRegression
    model = LinearRegression().fit(x,y)
    return model

model = train(x,y)
x_new = 23.0
y_new = model.predict(x_new)
print(y_new)
```

输出的结果。再一次的，为13.97。这很简单，对吧？你可以将其中的LinearRegression换为其它类型的回归学习算法，而其余部分不需要进行额外的改动。马上就正常工作了。同样的方式也可以使用在分类问题中。你可以简单的将 *LogisticRegression* 算法替换为 *SVC* 算法（这是支持向量机算法在scikit-learn中的名字）， *DecisionTreeClassiﬁer* ，*NearestNeighbors* 或着是许多其它的scikit-learn中已经实现的分类学习算法。

## 学习算法的特点

在这里，我们概述了一些可以将一种学习算法与另一种学习算法区分开来的实用特性。你可能已经知道了，不同的学习算法可以有着不同的超参数（SVM中的 $C$ ，ID3中的 $\epsilon$ 与 $d$ ）。类似于梯度下降法之类的求解器也可以有着超参数，例如其中的 $\alpha$ 。

某些算法，如决策树学习，可以接受分类型特征。举个例子，如果你有一个特征“颜色”，可能的取值为“红”，“黄”，“绿”，那你可以不用对这个“颜色”特征的取值进行额外的调整，直接保持原样即可。SVM，逻辑回归，线性回归与kNN（具有余弦相似性或欧几里德距离度量），则希望所有的特征都是数字型的。在scikit-learn中实现的所有算法所期望的特征都是数字型的。在下一章我将介绍如何将分类型的特征转化为数字型的特征。

一些算法，如SVM，允许数据分析师为每个类别提供权重。这些权重影响了决策边界的绘制。如果某些类别的权重很高，那么所采用的学习算法在预测该类别的训练样本时则会尽可能的试图不出现错误（典型的，考虑到在其他类别处出错的代价）。这可能很重要，如果某些类别的实例在你的训练数据中占少数，但您希望尽可能避免错误地分类该类的样本。

一些分类模型，如SVM与kNN，给出了直接输出类别的特征向量。其他的，如逻辑回归或决策树，则也会返回一个位于0到1之间的可以解释模型对结果的置信程度或是作为预测为某一个具体类别的实际概率的数。（脚注3：如果真的有必要，可以使用一些简单的技术合成创建SVM和kNN预测的分数）

一些分类算法（如决策树，逻辑回顾，SVM）在建立模型的时候一次性的使用了数据集中所有的数据。如果在模型完成建立之后你又有了一些额外的带标签的数据，那么你必须从头开始重建模型。另外一些算法（如scikit-learn中的朴素贝叶斯，多层感知器，SGDClassiﬁer/SGDRegressor， PassiveAggressiveClassiﬁer/PassiveAggressiveRegressor）可以迭代的训练，一次只训练其中的一批。一旦新的训练样本可以使用了，你可以在仅使用新数据的情况下完成模型的更新。

最后，如决策树， SVM与kNN算法在分类问题与回归问题中都是适用的，而其他的算法只能够解决其中的一种问题：要么是分类，要么是回归，无法两类都解决。

通常情况下，每个库提供的文档都解释了每种算法解决的问题类型，允许的输入值以及模型产生的输出值类型。该文档还提供了有关超参数的信息。