介绍
作为一个拟合常用的算法,对于我来说可以用来调参使得模拟程序生成的光谱尽可能的匹配上实验测得的光谱, L-M算法网上能找到的 python 代码有好多bug,没办法只能自己写一个啦。
先放代码,再聊聊对 Levenberg-Macquardt 算法、The Steepest Descent Method 以及 Newton Method 的理解。
代码
把L-M算法写成了一个类,相当于一个工具箱以后想用的时候直接import就行了。
import numpy as np
class Levenberg_Macquardt:
Experiment_Data = None # y
Experiment_Wavelength = None # x
Floating_Parameter = None
Function = None
k_max = 200
def __init__(self):
self.Experiment_Data = None
self.Experiment_Wavelength = None
self.Floating_Parameter = None # Parameters must be float-type numbers.
self.k_max = 200
self.Function = None
def Derivation(self, Wavelength, n):
X1 = np.array(self.Floating_Parameter.copy(), dtype=np.float64)
X2 = np.array(self.Floating_Parameter.copy(), dtype=np.float64)
X1[n][0] -= 1e-6 * X1[n][0]
X2[n][0] += 1e-6 * X2[n][0]
P1 = self.Function(X1, Wavelength)
P2 = self.Function(X2, Wavelength)
D = (P2 - P1) / (2e-6 * np.array(self.Floating_Parameter, dtype=np.float64)[n][0])
return D
def Jaccobi(self):
num_data = self.Experiment_Data.shape[1]
num_params = self.Floating_Parameter.shape[0]
J = np.mat(np.zeros((num_data, num_params)))
for i in range(num_data):
for j in range(num_params):
J[i, j] = self.Derivation(self.Experiment_Wavelength[i], j)
return J
def Residual(self, x):
num_data = self.Experiment_Data.shape[1]
Residual = np.mat(np.zeros((num_data, 1)))
for i in range(num_data):
Residual[i] = self.Function(np.array(x), self.Experiment_Wavelength[i]) - self.Experiment_Data[0, i]
return Residual
def Converge(self):
k, v = 0, 2
x = self.Floating_Parameter
tau = 1e-3
epsilon1 = 1e-8
epsilon2 = 1e-8
J = self.Jaccobi()
A = J.T * J
f = self.Residual(x)
g = J.T * f
found = np.linalg.norm(g) <= epsilon1
u = tau * A.max()
while (not found) and k < self.k_max:
H = A + u * np.eye(x.shape[0])
h = -H.I * g
if np.linalg.norm(h) <= epsilon2 * (np.linalg.norm(x) + epsilon2):
found = True
else:
x_new = x + h
f = self.Residual(x)
f_new = self.Residual(x_new)
F = 0.5 * np.linalg.norm(f) ** 2
F_new = 0.5 * np.linalg.norm(f_new) ** 2
gain_ratio = (F - F_new) / ((0.5 * h.T * (u * h - g))[0, 0])
if gain_ratio > 0:
x = x_new
J = self.Jaccobi()
A = J.T * J
f = self.Residual(x)
g = J.T * f
found = np.linalg.norm(g) <= epsilon1
u = u * max([1 / 3, 1 - (2 * gain_ratio - 1) ** 3])
v = 2
else:
u = u * v
v = 2 * v
self.Floating_Parameter = x
下面是一个简单的测试程序,验证一下我的代码是可行的。
from Levenberg_Macquardt import Levenberg_Macquardt
import numpy as np
n = 100
wl = np.linspace(0,10,n)
a_true,b_true,c_true,d_true = -3,-10,3,-2
y = [c_true*np.exp(a_true*i)+d_true*np.exp(b_true*i) for i in wl]
y = np.mat(y)
def Func(abcd:np.ndarray, iput):
a = abcd[0][0]
b = abcd[1][0]
c = abcd[2][0]
d = abcd[3][0]
return c*np.exp(a*iput)+d*np.exp(b*iput)
L = Levenberg_Macquardt()
L.Experiment_Wavelength = wl
L.Experiment_Data = y
L.Function = Func
L.Floating_Parameter = np.mat([[-4],[-5],[4],[-4]])
L.Converge()
print(L.Floating_Parameter)
下面是程序的运行结果
可以看到最后得到的参数结果接近真实值。
理解
本质上这种问题就是有一堆的数据点,然后给你一个函数让你调整函数中可以调整的参数来使得尽可能的匹配上这些数据点。其中衡量的标准就是
F
(
x
)
F(x)
F(x) mean square error
现在回归的方法都是要迭代的,相当于一步一步向着目标前进。其实很简单只需要在每步中想清楚两件事情就行了。
-
向什么方向走
-
走多远
目前有三种方法来解决这个问题,最陡下降方法、高斯-牛顿法、L-M方法。本质上讲就是他们选取的前进方向和步长不一样。
最陡下降法
沿着这一点下降最快的方向前进,其实就是沿着梯度的反方向下降,如下图所示。
上面就是常常被提到,以及神经网络中应用很广泛的梯度下降方法。这种方法的好处是一开始下降很快因为梯度很陡,到接近极小值点时,下降很慢,因为梯度基本上没有了。所以回归刚开始的时候这种方法很好,但是到末期时这种方法收敛很慢。
牛顿方法
除了梯度下降的方法外就没有其它方法了吗?牛爵爷(牛顿)不这么认为。牛爵爷是这么思考这个问题的。怎么一步(我只要一步)就直接到极小值点处。
这件事可能吗?我们能拿到的信息就只有出发点的一阶导数、二阶导数信息呀,后面要走的路都是未知呀。等等极小值点处是一阶导数等于0,而二阶导数衡量了一阶导数的变化。那就不可以了吗
h = − F ′ ( x ) F ′ ′ ( x ) h=-\frac{F'(x)}{F''(x)} h=−F′′(x)F′(x)
只要直接变化 h 就到极小值点了。(不得不说牛爵爷还是猛呀!)
但是也有问题,你现在在的这一点如果里极小值点很进那还好说,这个可以估计的很准。如果你离极小值点很远怎么办呀,估计相当不准?一开始那不是乱无目的地乱跳吗。
L-M 方法
思想很简单引入了一个调整的因子,当因子很小时,L-M 方法近似为牛顿的方法,当因子很大时,L-M 方法近似为最陡下降法。再引入一个判别条件,自动调整改因子的值。即可实现在离极小值点很远时,采用最陡下降法。在离极小值很近时,采用牛顿方法。两个方法的优点都有了。