在上一篇文章《机器学习算法推导&实现——逻辑斯蒂回归》中,我们分别使用了梯度下降法和牛顿法来求解对数似然函数的最优化问题,从实例中我们也能发现牛顿法的收敛速度远快于梯度下降法,但是在上文中,直接使用了牛顿法的结果,并没有进行相应推导,故本文一方面是补上牛顿法的推导,另一方面是展开讨论下拟牛顿法。
牛顿法推导牛顿法的推导得先从泰勒公式说起:泰勒公式假设现在函数f(x)迭代了k次的值为X(k),则在X(k)上进行二阶泰勒展开可近似得到以下公式:泰勒二阶展开我们要求得f(x)的极小值,则必要条件是f(x)在极值点处的一阶导数为0,即:因为我们把每轮迭代求得的满足目标函数极小值的x作为下一轮迭代的值,因此我们可以假设第k+1轮的值就是最优解:代入二阶泰勒展开并求导可得:令:一阶导数二阶导数可得最终的优化公式为:牛顿法迭代虽然根据推导,我们已经知道了牛顿法的迭代方法,但是在实际应用过程中,我们会发现海塞矩阵的逆矩阵往往计算比较复杂,于是又有了拟牛顿法来简化这一过程。
拟牛顿法的原理在拟牛顿法中,我们考虑优化出一个n阶矩阵D来代替海塞矩阵的逆矩阵,首先我们得先来看看替代矩阵D要满足什么条件:
首先根据二阶泰勒展开,我们令:代入f(x),并求导,可得:泰勒二阶展开再令:最终可得矩阵D需满足的条件:替代矩阵Dk需满足的条件另外在每次迭代中可以更新矩阵D,故可以假设以下更新条件:由此我们可以发现海塞矩阵逆矩阵的近似矩阵D(x)的选择条件比较灵活,可以有多种具体的实现方法。
1、DFP算法(Davidon-Fletcher-Powell)推导在DFP算法中,我们假设:*在这里我们知道凸函数的二阶导数海塞矩阵(Hessian)是对称矩阵,因此我们假设想要替代的D, P, Q矩阵也是对称矩阵,即矩阵的转置等于矩阵本身。两边乘以
可得:为了满足上式要求,我们不妨令:我们先来求P矩阵,最简单的就是令:接着两边乘以
可得:求得
,并代入P矩阵,可求得:同样的方法我们求解下Q矩阵:最终,DFP算法的迭代公式为:DFP算法替代海塞矩阵逆矩阵的迭代公式
python实现DFP算法我们继承上一篇文章逻辑回归算法的类,并且增加拟牛顿法的优化算法。
我们先看下算法的主函数部分,在继承逻辑回归类的基础上主要有2点改动:一是把"method"参数提前,在实例化的时候就要求定位好优化算法;二是重写了train方法,衍生出其他拟牛顿算法。
from ML_LogisticRegression import LogisticRegressionSelf as logit
import numpy as np
class allLogitMethod(logit):
def __init__(self, method):
"""init class"""
super().__init__()
self.method = method
self.graList = [] #记录梯度模长的列表
#重写下train方法
def train(self, X, y, n_iters=1000, learning_rate=0.01):
"""fit model"""
if X.ndim < 2:
raise ValueError("X must be 2D array-like!")
self.trainSet = X
self.label = y
if self.method.lower() == "gradient":
self._LogisticRegressionSelf__train_gradient(n_iters, learning_rate)
elif self.method.lower() == "newton":
self._LogisticRegressionSelf__train_newton(n_iters)
elif self.method.lower() == "dfp":
self.__train_dfp(n_iters, learning_rate)
else:
raise ValueError("method value not found!")
return在写算法主体前,我们先看下DFP的简易优化过程:1. 初始化参数W0,初始化替代矩阵Dk0,计算初始梯度Gk0,迭代次数k;
2. While ( k < n_iters ) and ( ||Gk0|| > e ):
- 2.1 计算出W0需要优化的方向Pk = Dk*Gk0
- 2.2 更新参数W1如下:
- for n in N:
- 尝试用一维搜索方法改变Pk的学习率,
- 求得最小似然值,
- 求出W1和deltaW(W1 - W0)
- 2.3 更新梯度Gk1,并求deltaG(Gk1 - Gk0)
- 2.4 更新DK1,利用上述推导的DFP的迭代公式
- 2.5 重新赋值W0,Dk0,Gk0
- 2.6 k += 1
3. end.我们可以大致看到DFP算法的主体部分有外循环和内循环两个部分组成,所以我们也是将内外循环分开来写。
我们先看内循环部分。内循环是已经知道了参数W的优化方向的基础上,想要找到最合适的学习率,使得更新后的W求得的似然值最小。实际有很多方法,在这里为了简单,笔者只是对学习率进行了i次方的操作。具体函数如下:
#一维搜索法求出最优lambdak,更新W后,使得似然值最小
def __updateW(self, X, Y, lambdak, W0, Pk):
"""此处对lambdak的处理仅简单用1~i次方来逐步变小,以选取到最小似然值的lambdak"""
min_LLvalue = np.inf
W1 = np.zeros(W0.shape)
for i in range(10):
Wi = W0 - (lambdak**i)*Pk
Ypreprob, LLvalue = self.PVandLLV(X, Y, Wi)
if LLvalue < min_LLvalue:
min_LLvalue = LLvalue
W1 = np.copy(Wi)
deltaW = - (lambdak**i)*Pk
bestYpreprob = Ypreprob
return W1, deltaW, min_LLvalue, bestYpreprob再看外循环部分。
#新增拟牛顿法-DFP优化算法
def __train_dfp(self, n_iters, learning_rate):
"""Quasi-Newton Method DFP(Davidon-Fletcher-Powell)"""
n_samples, n_features = self.trainSet.shape
X = self.trainSet
y = self.label
#合并w和b,在X尾部添加一列全是1的特征
X2 = np.hstack((X, np.ones((n_samples, 1))))
#将y转置变为(n_samples,1)的矩阵
Y = np.expand_dims(y, axis=1)
#初始化特征系数W,初始化替代对称矩阵
W = np.zeros((1, n_features+1))
Dk0 = np.eye(n_features+1)
#计算初始的预测值、似然值,并记录似然值
Ypreprob, LL0 = self.PVandLLV(X2, Y, W)
self.llList.append(LL0)
#根据初始的预测值计算初始梯度,并记录梯度的模长
Gk0 = self._LogisticRegressionSelf__calGradient(X2, Y, Ypreprob)
graLength = np.linalg.norm(Gk0)
self.graList.append(graLength)
#初始化迭代次数
k = 0
while (kself.tol):
#计算优化方向的值Pk=Gk0.Dk0
Pk = np.dot(Gk0, Dk0)
#一维搜索更新参数,并保存求得的最小似然值
W, deltaW, min_LLvalue, Ypreprob = self.__updateW(X2, Y, learning_rate, W, Pk)
self.llList.append(min_LLvalue)
#更新梯度Gk和deltaG,同时求得梯度的模长和更新前后的模长差值
Gk1 = self._LogisticRegressionSelf__calGradient(X2, Y, Ypreprob)
graLength = np.linalg.norm(Gk1)
self.graList.append(graLength)
deltaG = Gk1 - Gk0
Gk0 = Gk1
#更新替代矩阵Dk
Dk1 = Dk0 + np.dot(deltaW.T, deltaW)/np.dot(deltaW, deltaG.T) - \
np.dot(np.dot(np.dot(Dk0, deltaG.T), deltaG), Dk0)/np.dot(np.dot(deltaG, Dk0), deltaG.T)
Dk0 = Dk1
k += 1
self.n_iters = k
self.w = W.flatten()[:-1]
self.b = W.flatten()[-1]
Ypre = np.argmax(np.column_stack((1-Ypreprob,Ypreprob)), axis=1)
self.accurancy = sum(Ypre==y)/n_samples
print("第{}次停止迭代,梯度模长为{},似然值为{},准确率为{}".format(self.n_iters, self.graList[-1], self.llList[-1], self.accurancy))
print("w:{};\nb:{}".format(self.w, self.b))
return在这里还有一点小小的提示:笔者在实测过程中,发现DFP算法经常会导致求解似然值和P(y=1|X)的时候报值溢出的错误。主要是python的numpy.exp(wx)中的wx过大导致的,比如看下图的报错:值溢出报错信息所以在这里我们对求解似然值和P(y=1|X)的公式进行了一些调整。当wx为负数时,使用原公式;当wx为正数时,使用转换公式替换。其实质是一致的。原公式和替换公式最后我们用拟牛顿法-DFP算法和牛顿法实测比较下。
牛顿法:迭代了7次,迭代时长0.18s,似然值176.81,准确率0.94,模型参数如下图所示:
if __name__ == "__main__":
import matplotlib.pyplot as plt
from sklearn.datasets import make_classification
import time
X, y = make_classification(n_samples=1000, n_features=4)
#1、自编的牛顿法进行拟合
time_nt = time.time()
logit_nt = allLogitMethod("newton")
logit_nt.train(X, y, n_iters=100)
print("迭代时长:", time.time()-time_nt)
plt.plot(range(logit_nt.n_iters+1), logit_nt.llList)
plt.show()牛顿法迭代结果
拟牛顿法-DFP算法:迭代了18次,迭代时长0.038s,似然值176.81,准确率0.94,模型参数如下图所示:
#3、自编的拟牛顿法-DFP算法进行拟合
time_dfp = time.time()
logit_dfp = allLogitMethod("DFP")
logit_dfp.train(X, y, n_iters=20, learning_rate=0.5)
print("迭代时长:", time.time()-time_dfp)
fig = plt.figure(figsize=(10,6))
ax1 = fig.add_subplot(1,2,1)
ax1.plot(range(logit_dfp.n_iters+1), logit_dfp.llList)
ax2 = fig.add_subplot(1,2,2)
ax2.plot(range(logit_dfp.n_iters+1), logit_dfp.graList)
plt.show()拟牛顿法迭代结果经过两种方法的对比,我们发现两者最终训练出来的模型参数基本一致,似然值也下降到同一水平;DFP虽然迭代18次比牛顿法多,但是训练总时间只有0.038s,远小于牛顿法的训练总时间。
可见,DFP算法中的Dk矩阵完全可以逼近于牛顿法中的海塞矩阵的逆矩阵,而且DFP算法中的Dk矩阵的训练也比二阶导数矩阵的训练来得方便与快速。
以上就是本次的全部内容,谢谢阅读。(今天先写到这,写的好累,拟牛顿法的其他算法我们下次再接着推导与实现。)
全部代码可前往以下地址下载:shoucangjia1qu/ML_gzhgithub.com
往期回顾:
学无止境,欢迎关注笔者公众号、知乎号,互相学习!