模型优化-梯度下降算法

梯度下降(Gradient Descent)算法是机器学习中使用非常广泛的优化算法。当前流行的机器学习库或者深度学习库都会包括梯度下降算法的不同变种实现。

【思想】:要找到某函数的最小值,最好的方法是沿着该函数的梯度方向探寻,例如物理学上的加速度与速度的关系。当加速度为零时,此时速度可能是最大,也有可能是最小,这取决于函数曲线。

【步骤】:

  1. 随机取一个自变量的值 x0x_0
  2. 对应该自变量算出对应点的因变量值:f(x0)f(x_0)
  3. 计算 f(x0)f(x_0) 处目标函数 f(x) 的导数;
  4. f(x0)f(x_0) 开始,沿着该处目标函数导数的方向,按一个指定的步长 α,向前“走一步”,走到的位置对应自变量取值为 x1x_1。换言之,x0x1/α=f(x)|x_0 - x_1| / α = f(x)f(x0)f(x_0) 处的斜率;
  5. 继续重复步骤 2 - 4,直至退出迭代(达到指定迭代次数,或 f(x) 近似收敛到最优解)。

超参数 α

超参数 α 又叫做步长,用于确定找到最小值点而尝试在目标函数上前进的步伐到底走多大。如果该参数设置的大小不合适,则会导致最终无法找到最小值点。

比如下就是因为步幅太大,几个迭代后反而取值越来越大。

模型优化-梯度下降算法

改成如下那样的小步伐就可以顺利找到最低点了。

模型优化-梯度下降算法

不过大步伐也不是没有优点。步伐越大,每一次前进得越多。步伐太小,虽然不容易“跨过”极值点,但需要的迭代次数也多,相应需要的运算时间也就越多。

为了平衡大小步伐的优缺点,也可以在一开始的时候先大步走,当所到达点斜率逐渐下降——函数梯度下降的趋势越来越缓和——以后,逐步调整,缩小步伐。比如下图这样:

模型优化-梯度下降算法

算法难点

即使步伐合适,也不一定能够找到最小值点。如果目标函数有多个极小值点(多个向下的“弯儿”),那么如果开始位置不妥,很可能导致最终是走到了一个局部极小值就无法前进了。

模型优化-梯度下降算法

【解决方案】:如果目标函数不能确定只有一个极小值,而获得的模型结果又不令人满意时,就该考虑是否是在学习的过程中,优化算法进入了局部而非全局最小值。这种情况下,可以尝试几个不同的起始点。甚至尝试一下大步长,说不定反而能够跨出局部最小值点所在的凸域。

迭代结束的条件

梯度下降法(梯度上升法应该也适用)迭代结束的条件,常用的有两种:

  • 定义一个合理的阈值,当两次迭代之间的差值小于该阈值时,迭代结束。
  • 设置一个大概的迭代步数,比如 1000 或 500,梯度下降法最终的迭代肯定会收敛,只要达到相应迭代次数,多了也没关系。因为迭代次数多了后,在到达极值点时,函数对变量的导数已近乎为 0,即使过了极值点,导数就变为正数了,之前的导数为负数。这个时候,变量 x 的值减去步长与导数的乘积反倒变小了。所以即使步数多了,结果也基本上就在极值点处左右徘徊,几乎等于极值点,因此没有问题。

案例:求解线性回归的目标函数

已知线性回归的目标函数为:
J(a,b)=12mi=1m(y(i)y(i))2=12mi=1m(a+bx(i)y(i))2 J(a, b) = \frac{1}{2m}\sum_{i=1}^m(y'^{(i)} - y^{(i)})^2 = \frac{1}{2m}\sum_{i=1}^m(a + bx^{(i)} - y^{(i)})^2
J(a, b) 是一个二元函数。

  • 要求解的对象:两个参数 a 和 b 的值。
  • 要满足的条件:a 和 b 取特定值,使得 J(a, b) 的值达到最小。

函数 J 分别对自变量 a 和 b 取偏微分。
J(a,b)a=1(m)i=1m((a+bx(i))y(i))J(a,b)b=1(m)i=1mx(i)((a+bx(i))y(i)) \frac{\partial{J(a,b)}}{\partial{a}} = \frac{1}{(m)}\sum_{i=1}^{m}((a+bx^{(i)}) - y^{(i)}) \\ \frac{\partial{J(a,b)}}{\partial{b}} = \frac{1}{(m)}\sum_{i=1}^{m}x^{(i)}((a+bx^{(i)}) - y^{(i)})

【步骤】:

  1. 任意给定 a 和 b 的初值:a = 0; b = 0。
  2. 用梯度下降法求解 a 和 b,伪代码:

repeat  until  convergence{a=aαJ(a,b)ab=bαJ(a,b)b} repeat\,\,until\,\,convergence \{ \\ \hspace{1cm} a = a - \alpha \frac{\partial{J(a,b)}}{\partial{a}} \\ \hspace{1cm} b = b - \alpha \frac{\partial{J(a,b)}}{\partial{b}} \\ \}
当下降的高度小于某个指定的阈值(近似收敛至最优结果),则停止下降。

通用线性回归模型的目标函数求解

y = a + bx 是一个线性回归模型,这个没问题。不过,反过来,线性回归模型只能是 y = a + bx 形式吗?当然不是。

y = a + bx => f(x) = a + bx 实际上是线性回归模型的一个特例——自变量只有一个维度的特例,在这个模型中,自变量 x 是一个一维向量,可写作 [x]。

通用的线性回归模型,是接受 n 维自变量的,也就是说自变量可以写作 [x1, x2, …, xn] 形式。于是,相应的模型函数写出来就是这样的:f(x1,x2,...,xn)=a+b1x1+b2x2+...+bnxnf(x1, x2, ..., xn) = a + b1x1 + b2x2 + ... + bnxn

这样写参数有点混乱,我们用 θ0 来代替 a,用 θ1 到 θn 来代替 b1 到 bn ,那么写出来就是这样的:
f(1,x1,x2,...,xn)=θ0+θ1x1+θ2x2+...+θnxnf(1, x_1, x_2, ..., x_n) = \theta_0 + \theta_1 x_1 + \theta_2 x_2 + ... + \theta_n x_n

我们设 x0 = 1,因此:
f(x0,x1,x2,...,xn)=θ0x0+θ1x1+θ2x2+...+θnxnf(x_0, x_1, x_2, ..., x_n) = \theta_0 x_0 + \theta_1 x_1 + \theta_2 x_2 + ... + \theta_n x_n

那么对应的,n 维自变量的线性回归模型对应的目标函数就是:
J(θ0,θ1,...,θn)=1(2m)i=1m(y(i)y(i))2=1(2m)i=1m(θ0+θ1x1(i)+θ2x2(i)+...+θnxn(i)y(i))2J(\theta_0,\theta_1, ..., \theta_n) = \frac{1}{(2m)}\sum_{i=1}^{m} (y'^{(i)}-y^{(i)})^{2} = \frac{1}{(2m)}\sum_{i=1}^{m}(\theta_0+\theta_1x_1^{(i)}+\theta_2x_2^{(i)} + ... + \theta_n x_n^{(i)} -y^{(i)})^{2}

再设:
X=[x0,x1,x2,...,xn],Θ=[θ0,θ1,θ2,...,θn]X=[x_0, x_1, x_2, ..., x_n],\Theta = [\theta_0, \theta_1, \theta_2, ..., \theta_n]

然后将模型函数简写成:
f(X)=ΘXf(X) = \Theta X

慑于习惯,我们在这里将 f(X) 写作 h(X),因此,模型函数就成了:
h(X)=ΘXh(X) = \Theta X

相应的目标函数就是:
J(Θ)=1(2m)i=1m(hθ(X(i))y(i))2J(\Theta) = \frac{1}{(2m)}\sum_{i=1}^{m}(h_\theta(X^{(i)})-y^{(i)})^{2}

线性回归的超参数

作为一个线性回归模型,本身的参数是 Θ ,在开始训练之前,Θ(无论是多少维),具体的数值都是不知道,训练过程就是求解 Θ 中各维度数值的过程。

当我们使用梯度下降求解时,梯度下降算法中的步长参数:α ,就是训练线性回归模型的超参数。

训练程序通过梯度下降的计算,自动求出了 Θ 的值。而 α 却是无法求解的,必须人来手工指定。反之,如果没有指定 α ,梯度下降运算则根本无法进行。

  • 对于线性回归而言,只要用到梯度下降,就会有步长参数 alpha 这个超参数。

如果训练结果偏差较大,可以尝试调小步长;如果模型质量不错但是训练效率太低,可以适当放大步长;也可以尝试使用动态步长,开始步长较大,随着梯度的缩小,步长同样缩小……

  • 如果训练程序是通过人工指定迭代次数来确定退出条件,则迭代次数也是一个超参数。
  • 如果训练程序以模型结果与真实结果的整体差值小于某一个阈值为退出条件,则这个阈值就是超参数。

在模型类型和训练数据确定的情况下,超参数的设置就成了影响模型最终质量的关键。

而往往一个模型会涉及到多个超参数,如何制定策略在最少尝试的情况下让所有超参数设置的结果达到最佳,是一个在实践中非常重要又没有统一方法可以解决的问题。

在实际应用中,能够在调参方面有章法,而不是乱试一气,就有赖于大家对于模型原理和数据的掌握了。

Python 代码

下面的例子就对应最开始的经验和工资的问题。我们用前 6 个数据作为训练集,后面 5 个作为测试集。

import matplotlib.pyplot as plt
import numpy as np
from sklearn import datasets, linear_model
from sklearn.metrics import mean_squared_error, r2_score

experiences = np.array([0,1,2,3,4,5,6,7,8,9,10])
salaries = np.array([103100, 104900, 106800, 108700, 110400, 112300, 114200, 116100, 117800, 119700, 121600])

# 将特征数据集分为训练集和测试集,除了最后5个作为测试用例,其他都用于训练
X_train = experiences[:7]
X_train = X_train.reshape(-1,1)
X_test = experiences[7:]
X_test = X_test.reshape(-1,1)

# 把目标数据(特征对应的真实值)也分为训练集和测试集
y_train = salaries[:7]
y_test = salaries[7:]

# 创建线性回归模型
regr = linear_model.LinearRegression()

# 用训练集训练模型——看就这么简单,一行搞定训练过程
regr.fit(X_train, y_train)

# 用训练得出的模型进行预测
diabetes_y_pred = regr.predict(X_test)

# 将测试结果以图标的方式显示出来
plt.scatter(X_test, y_test,  color='black')
plt.plot(X_test, diabetes_y_pred, color='blue', linewidth=3)

plt.xticks(())
plt.yticks(())

plt.show()

【自身代码实现】:

def gradient_descent(self, source, target, type=True, **param):
    """
    梯度下降算法
    :param source: 自变量 
    :param target: 因变量
    :param type:   终止条件类型
    :param param:  超参数 
    """
    a = 0
    b = 0
    m = len(source)
    step = param['step']

    # 终止条件类型:迭代次数
    if type:
        count = 500
        if 'count' in param:
            count = param['count']

        for loop in range(count):
            sum_a = 0
            sum_b = 0

            for i in range(0, m):
                sum_a += (a + b * source[i] - target[i])
                sum_b += source[i] * (a + b * source[i] - target[i])

            a = a - step * (sum_a / m)
            b = b - step * (sum_b / m)
            
    # 终止条件类型:小于阈值
    else:
        last = 0
        while True:
            sum_a = 0
            sum_b = 0

            try:
                for i in range(0, m):
                    sum_a += (a + b * source[i] - target[i])
                    sum_b += source[i] * (a + b * source[i] - target[i])

                a = a - step * (sum_a / m)
                b = b - step * (sum_b / m)
                threshold = abs(b - last)
                if threshold < param['min']:
                    break

                last = b
                if str(threshold) == 'nan':
                    raise InvalidStepError("The value of the step parameter is too large, you should set it smaller.")
            except InvalidStepError as error:
                print(error)
                break

    self.a = a
    self.b = b

问题

当目标函数存在多元时,那么如何通过阈值之差来作为梯度下降算法的终止条件?

参考