经过前面几节的介绍,你们能够直观的感觉到:对于大部分机器学习模型,咱们一般会将其转化为一个优化问题,因为模型一般较为复杂,难以直接计算其解析解,咱们会采用迭代式的优化手段,用数学语言描述以下:python
这里目标函数为\(f(x)\),当前优化变量为\(v^k\),目标便是找到一个\(v^k\)对当前的\(x^k\)进行更新,使得函数值尽量的下降,若是目标函数一阶可微,对其做一阶泰勒展开便可获得以下梯度降低的更新公式:算法
对目标函数做一阶泰勒展开:机器学习
因此要使得\(f(x^k+v^k)<f(x^k)\)只须要使\(\triangledown f(x^k)^Tv^k<0\)便可,而若是取:函数
则必定能使\(\triangledown f(x^k)^Tv^k<0\),因此,咱们就获得了梯度降低的更新公式:学习
这里\(\lambda_k\)通常能够设置一个较小的定值,或者初始设置一个定值并使其随着迭代次数增长而递减;另外更好的作法是利用一维搜索:\(min_{\lambda_k}f(x^k-\lambda_k\triangledown f(x^k))\)求一个最优的\(\lambda_k\),接下来咱们想一下下面两个问题:优化
(1)梯度降低法必定能使得函数值降低吗?spa
(2)若它能使函数值降低,则它是最优的方向吗?code
对于第一个问题,泰勒展开实际上是有一个条件的,那就是\(v^k\rightarrow 0\),再结合上面的更新公式,若是\(\lambda_k\)取得过大时,咱们是不能省略泰勒展开后面的项的,并且后面项的取值也不必定能保证小于0,因此有时咱们设置的学习率\(\lambda_k\)较大时,函数值反而会上升。orm
因此,当\(v^k\)的取值大到不能忽略后面的项时,泰勒展开的二阶项取值就必需要考虑其中了,因此这时梯度降低法未必时最优的方向,接下来咱们看下二阶展开的状况:blog
对其做二阶泰勒展开:
这里为了方便,记\(g_k=g(x^k)=\triangledown f(x^k),H_k=H(x^k)=\triangledown^2 f(x^k)\),\(H_k\)表示Hessian矩阵在\(x^k\)处的取值,Hessian矩阵的定义:
对于大部分机器学习模型,一般目标函数是凸的,因此\(H(x)\)半正定,即对于\(\forall v^k\),都有\({v^k}^TH_kv^k\geq0\),此时,\(f(x^k)+g_k^Tv^k+\frac{1}{2}{v^k}^TH_kv^k\)是关于\(v^k\)的凸二次规划问题,因此最优的\(v^k\)在其梯度为0处取得:
能够发现牛顿法对比梯度降低法,其实牛顿法是对梯度法的方向进行了一个改变\(H_k^{-1}\),因此,咱们能够获得牛顿法的更新公式:
这里记\(p_k=-H_k^-1g_k\);
能够发现牛顿法的复杂有点高,由于要求解\(H_k^{-1}\),那么有没有方便一点的方法呢?好比构建一个矩阵去近似\(H_k\)或者\(H_k^{-1}\),这即是拟牛顿法的基本思想
上面说到了利用一个矩阵去近似Hessian矩阵或者Hessian矩阵的逆,那么这个近似矩阵须要知足怎样的条件呢?咱们仍是从二阶泰勒展开出发,稍微变换一下:
两边对\(x^{k+1}\)求偏导可得:
这即是拟牛顿条件,为了方便,记\(y_k=g_{k+1}-g_k,\delta_k=x^{k+1}-x^k\),因此:
因此,拟牛顿法也要知足和\(H_k\)同样的性质:
(1)正定性;
(2)知足拟牛顿条件
接下来,简单证实一下若是知足性质(1):正定性,更新时能够知足函数值降低,假设\(G_k\)去近似\(H_k^{-1}\),因此:\(G_k\succ 0\),那么迭代公式为:
将其带入二阶泰勒展开式中:
一般\(\lambda_k^2<<\lambda_k\),因此能够省略第三项,而第二项因为\(G_k\succ 0\),因此\(-\lambda_kg_k^TG_kg_k< 0\),因此\(f(x^{k+1})<f(x^k)\)
DFP算法即是利用\(G_k\)近似\(H_k^{-1}\)的一种算法,它的构造很tricky,它假设每一步迭代中矩阵\(G_{k+1}\)是由\(G_k\)加上两个附加项构成的,即:
这里\(P_k,Q_k\)是待定项,因为须要知足拟牛顿条件,因此:
这里作一个tricky的假设:
这样的\(P_k,Q_k\)不难找到:
因此,矩阵\(G_{k+1}\)的迭代公式:
能够证实,只要初始矩阵\(G_0\)正定对称,则迭代过程当中的每一个矩阵\(G_k\)均正定对称,接下来对其进行代码实现:
import numpy as np """ DPF拟牛顿法,封装到ml_models.optimization模块,与梯度降低法配合使用 """ class DFP(object): def __init__(self, x0, g0): """ :param x0: 初始的x :param g0: 初始x对应的梯度 """ self.x0 = x0 self.g0 = g0 # 初始化G0 self.G0 = np.eye(len(x0)) def update_quasi_newton_matrix(self, x1, g1): """ 更新拟牛顿矩阵 :param x1: :param g1: :return: """ # 进行一步更新 y0 = g1 - self.g0 delta0 = x1 - self.x0 self.G0 = self.G0 + delta0.dot(delta0.T) / delta0.T.dot(y0)[0][0] - self.G0.dot(y0).dot(y0.T).dot(self.G0) / y0.T.dot( self.G0).dot(y0)[0][0] def adjust_gradient(self, gradient): """ 对原始的梯度作调整 :param gradient: :return: """ return self.G0.dot(gradient)
咱们试一试将DFP算法应用到LogisticRegression,修改的地方以下:
fit
函数追加以下的一个判断:
elif self.solver == 'dfp': self.dfp = None self._fit_sgd(x, y)
_fit_sgd
函数中,在梯度更新前作以下调整:
if self.solver == 'dfp': if self.dfp is None: self.dfp = optimization.DFP(x0=self.w, g0=dw) else: # 更新一次拟牛顿矩阵 self.dfp.update_quasi_newton_matrix(self.w, dw) # 调整梯度方向 dw = self.dfp.adjust_gradient(dw)
""" 梯度降低和DFP作一下对比 """ from sklearn.datasets import make_classification import matplotlib.pyplot as plt %matplotlib inline data, target = make_classification(n_samples=200, n_features=2, n_classes=2, n_informative=1, n_redundant=0, n_repeated=0, n_clusters_per_class=1) target = target.reshape(200, 1)
import os os.chdir('../') from ml_models.linear_model import LogisticRegression sgd_model = LogisticRegression(epochs=50) sgd_model.fit(data, target) dfp_model = LogisticRegression(solver='dfp',epochs=50) dfp_model.fit(data,target)
#损失函数对比 plt.plot(range(0, len(sgd_model.losses)), sgd_model.losses,'b') plt.plot(range(0, len(dfp_model.losses)), dfp_model.losses,'r')
[<matplotlib.lines.Line2D at 0x169fb529588>]
能够发现,大部分状况下DFP比SGD收敛的更快,且收敛效果更好
#分类效果对比 sgd_model.plot_decision_boundary(data,target) dfp_model.plot_decision_boundary(data,target)
BFGS算法是用一个矩阵\(B_k\)去模拟海瑟矩阵\(H_k\),它的更新公式一样假设有两个附加项:
固然,它须要知足拟牛顿条件:
因此:
考虑,使\(P_k\)和\(Q_k\)知足下面两个条件:
能够获得知足条件的解:
因此更新公式:
一样能够证实,若是\(B_0\)正定对称,那么迭代过程当中的每一个矩阵\(B_k\)都是正定对称的,因为这里是对\(H_k\)的近似,因此每次更新梯度时,还须要对\(B_k\)作求逆计算,咱们可使用两次以下的Sherman-Morrison公式:
获得BFGS算法关于\(G_k\)的迭代公式:
接下来,进行代码实现:
""" BFGS拟牛顿法,封装到ml_models.optimization模块,与梯度降低法配合使用 """ class BFGS(object): def __init__(self, x0, g0): """ :param x0: 初始的x :param g0: 初始x对应的梯度 """ self.x0 = x0 self.g0 = g0 # 初始化B0 self.B0 = np.eye(len(x0)) def update_quasi_newton_matrix(self, x1, g1): """ 更新拟牛顿矩阵 :param x1: :param g1: :return: """ # 进行一步更新 y0 = g1 - self.g0 delta0 = x1 - self.x0 self.B0 = self.B0 + y0.dot(y0.T) / y0.T.dot(delta0)[0][0] - self.B0.dot(delta0).dot(delta0.T).dot(self.B0) / \ delta0.T.dot(self.B0).dot(delta0)[0][0] def adjust_gradient(self, gradient): """ 对原始的梯度作调整 :param gradient: :return: """ return np.linalg.pinv(self.B0).dot(gradient)
fit
函数追加以下的一个判断:
elif self.solver == 'bfgs': self.bfgs = None self._fit_sgd(x, y)
_fit_sgd
函数中,在梯度更新前作以下调整:
if self.solver == 'bfgs': if self.bfgs is None: self.bfgs = optimization.BFGS(x0=self.w, g0=dw) else: # 更新一次拟牛顿矩阵 self.bfgs.update_quasi_newton_matrix(self.w, dw) # 调整梯度方向 dw = self.bfgs.adjust_gradient(dw)
#训练模型 bfgs_model = LogisticRegression(solver='bfgs',epochs=50) bfgs_model.fit(data,target)
#损失函数对比 plt.plot(range(0, len(sgd_model.losses)), sgd_model.losses,'b') plt.plot(range(0, len(dfp_model.losses)), dfp_model.losses,'r') plt.plot(range(0, len(bfgs_model.losses)), bfgs_model.losses,'y')
[<matplotlib.lines.Line2D at 0x169fd6e7b38>]
能够发现大部分状况下BFGS会比DFS收敛更快
#查看效果 bfgs_model.plot_decision_boundary(data,target)