@fsfzp888
2019-01-23T21:27:22.000000Z
字数 17128
阅读 2686
数值优化
线性代数
本文介绍和总结共轭梯度(conjugate gradient)算法的如下内容:
共轭梯度算法的背景和由来
梯度算法的几何理解和共轭梯度算法的流程
基于SSOR预条件子的共轭梯度算法
非线性共轭梯度算法
共轭梯度算法的由来,源于求解如下所示的二次型问题:
如果对于问题的求解,可以被转化为如上所示的二次型表达式(Quadratic Form)的极值,那么求解这个问题的方法也称为二次规划(Quadratic Programming)
二次规划问题,有一个极好的性质,这个性质直接导致了该问题的求解有较好的几何解释,这个性质就是如果矩阵为对称矩阵,那么把这个函数对列向量求导取极值,得到的梯度方向向量满足:
这个是经典的线性代数求解线性方程组的问题,求极值即是求解线性方程组,而矩阵的性质,直接决定了梯度方向的情况。
对一个从维到维的函数映射,对列向量求导即:
共轭梯度算法来源于二次型问题的求解,二次型问题可以在几何上直观的理解,即使是最速梯度下降(Steepest Descent),也可以这样来理解。
二次型的几何图解完全取决于矩阵,如果是正定的矩阵,那么图像将会是:
也就是是一个碗状朝上的结构,因为对于正定矩阵来说,任何非零向量,都有:
对于最速梯度下降(Steepest Descent)而言,选定了初始的方向向量后,后续每一次迭代所选择的方向向量由当前的梯度方向决定:
import numpy as np
def steepest_descent(A, b, x_initial, max_step, thresh=0.00001):
assert(isinstance(A, np.matrix))
assert (isinstance(b, np.matrix))
assert (isinstance(x_initial, np.matrix))
x = x_initial
for _ in range(max_step):
r = b - A * x
alpha = (r.transpose() * r) / (r.transpose() * A * r)
x = x + r * alpha
dist = np.sqrt(np.sum(np.square(b - A * x)))
if dist < thresh:
break
return x
if __name__ == '__main__':
N = 100
Ar = np.mat(np.random.rand(N, N))
As = Ar * Ar.transpose() # get positive definite matrix
bn = np.mat(np.random.rand(N, 1))
xi = np.mat(np.zeros((N, 1)))
xr = steepest_descent(As, bn, xi, 1000)
print('1000:', np.sqrt(np.sum(np.square(bn - As * xr))))
xr = steepest_descent(As, bn, xi, 10000)
print('10000:', np.sqrt(np.sum(np.square(bn - As * xr))))
xr = steepest_descent(As, bn, xi, 20000)
print('20000:', np.sqrt(np.sum(np.square(bn - As * xr))))
上边的Python代码体现了SD的最直接流程,梯度都是直接计算的,实际真的要应用,还要考虑稀疏矩阵,计算效率问题,否则效率太低了,也没有使用价值。当然,这个只用于二次型问题。
对一些非线性函数问题,就需要通过别的方式求导,而且步长也需要确定,或者设定个固定值!如下所示是该代码的一次运行结果:
1000: 1.9453821317304896
10000: 0.9116628408830175
20000: 0.6950563935826073
运行SD的例子,会发现,随机生成的一个对称矩阵,迭代了几万次仍然不收敛,可以见得,按照SD的理论公式求解效果有些不理想,收敛过慢。在数值优化里边,一个矩阵迭代求解方法是否收敛,取决于这个矩阵是否严格对角占优,即
在CG中,引入了共轭方向的概念。这个概念本质上就是对正交的推广,任何向量在欧几里得空间内正交意味着:
上边的过程容易让人联想起线性代数中的二次型标准化过程,二次型标准化,首先通过平移变换,让二次型变成了如下的形式:
二次型的基本迭代流程还是不变的:
条件1:各个方向向量和之间两两正交
条件2:通过求导让每一次迭代的步长因子取得最优
那么每一次迭代求解的过程,其实就是相当于减少了这个方向向量上的误差,每一步的误差向量可以被表示为方向向量的连加的形式,这样只需要迭代步后就会直接收敛到极值点。这个和我们在几何上边的理解是完全一致的。
只是,我们该如何选择这些方向向量呢?
如果仅仅只是随便选择一组线性无关的构造基,然后如同等式17那样构造方向向量,并满足正交,那么:
for loop until reach max_step or cost_function below threshold:
return
使用Python代码实现:
import numpy as np
def conjugate_gradient(A, b, x_initial, max_step, threshold=0.00001):
assert(isinstance(A, np.matrix))
assert(isinstance(b, np.matrix))
assert(isinstance(x_initial, np.matrix))
r_old = b - A * x_initial
d = r_old
x = x_initial
for i in range(max_step):
alpha = (r_old.transpose() * r_old) / (d.transpose() * A * d)
x = x + d * alpha
# r_new = b - A * x
r_new = r_old - A * d * alpha
beta = (r_new.transpose() * r_new) / (r_old.transpose() * r_old)
d = r_new + d * beta
r_old = r_new
cf = np.sqrt(np.sum(np.square(b - A * x)))
if cf < threshold:
print("Using step: ", i)
break
return x
if __name__ == '__main__':
N = 200
Ar = np.mat(np.random.rand(N, N))
As = Ar * Ar.transpose()
bn = np.mat(np.random.rand(N, 1))
xi = np.mat(np.random.rand(N, 1))
xr = conjugate_gradient(As, bn, xi, 1000)
print('1000:', np.sqrt(np.sum(np.square(bn - As * xr))))
xr = conjugate_gradient(As, bn, xi, 10000)
print('10000:', np.sqrt(np.sum(np.square(bn - As * xr))))
运行上述程序的一个输出:
Using step: 410
1000: 6.43707958798e-06
Using step: 410
10000: 6.43707958798e-06
大约需要两倍于维度的迭代次数才可以收敛到要求的范围,虽然理论上100维的变量值需要100次迭代,但是实际上各种浮点数运算误差导致不可能实现这样理想化的结果。通过对比会发现,迭代效果要比SD要好。当然了,实际使用的时候,判断损失函数其实没必要通过那样的形式。而且可以几十次迭代后才计算一下等等。
上边的迭代算法,本质上其实还是在求解形如的方程。毕竟最终的结果,都是让尽可能的接近于零。一般情况下,对于的求解,通常直接通过求逆得到,这就存在求矩阵的逆可能不存在等等问题。如果不是方阵,且,那么就是在求解超定方程组,方程组可能没有解,求解的过程就会等效于最小二乘,因为,为了让误差最小,其实就是让最终的的解得到的离最近,那么只能是到的列空间的投影,所以它们之间的差和的列空间正交,所以有
在数值分析里边,矩阵迭代求解的收敛速度取决于矩阵条件数:
for loop until reach max_step or cost_function below threshold:
return
对应的Python代码:
import numpy as np
def get_ssor_precondition_matrix(A, w):
UD = np.triu(A)
LD = np.tril(A)
dim = A.shape[0]
D = np.mat(np.zeros((dim, dim)))
for i in range(dim):
D[i, i] = A[i, i]
for i in range(dim):
for j in range(i+1, dim):
UD[i, j] = w * UD[i, j]
for i in range(dim):
for j in range(0, i):
LD[i, j] = w * LD[i, j]
# 对上下三角矩阵求逆矩阵,其实不必用通用的求逆方法,不停回代即可
return np.mat(np.linalg.inv(UD)) * D * np.mat(np.linalg.inv(LD))
def precondition_conjugate_gradient(A, b, x_initial, max_step,
threshold=0.00001, w=0.2):
assert(isinstance(A, np.matrix))
assert(isinstance(b, np.matrix))
assert(isinstance(x_initial, np.matrix))
r_old = b - A * x_initial
M_inv = get_ssor_precondition_matrix(A, w)
z_old = M_inv * r_old
d = z_old
x = x_initial
for i in range(max_step):
alpha = (z_old.transpose() * r_old) / (d.transpose() * A * d)
x = x + d * alpha
# r_new = b - A * x
r_new = r_old - A * d * alpha
z_new = M_inv * r_new
beta = (z_new.transpose() * r_new) / (z_old.transpose() * r_old)
d = z_new + d * beta
r_old = r_new
z_old = z_new
cf = np.sqrt(np.sum(np.square(b - A * x)))
if cf < threshold:
print("Using step: ", i)
break
return x
if __name__ == '__main__':
N = 200
Ar = np.mat(np.random.rand(N, N))
As = Ar * Ar.transpose()
bn = np.mat(np.random.rand(N, 1))
xi = np.mat(np.random.rand(N, 1))
xr = precondition_conjugate_gradient(As, bn, xi, 1000, 0.00001, 0.05)
print('w=0.05:', np.sqrt(np.sum(np.square(bn - As * xr))))
xr = precondition_conjugate_gradient(As, bn, xi, 1000, 0.00001, 0.5)
print('w=0.5:', np.sqrt(np.sum(np.square(bn - As * xr))))
xr = precondition_conjugate_gradient(As, bn, xi, 1000, 0.00001, 1)
print('w=1:', np.sqrt(np.sum(np.square(bn - As * xr))))
运行三次的结果:
runfile('C:/Users/zczx1/.spyder-py3/temp.py', wdir='C:/Users/zczx1/.spyder-py3')
Using step: 378
w=0.05: 6.71578034331e-06
Using step: 405
w=0.5: 9.67223926338e-06
Using step: 573
w=1: 8.09438315554e-06
runfile('C:/Users/zczx1/.spyder-py3/temp.py', wdir='C:/Users/zczx1/.spyder-py3')
Using step: 371
w=0.05: 6.1508910381e-06
Using step: 401
w=0.5: 8.51261715479e-06
Using step: 602
w=1: 7.57385104633e-06
runfile('C:/Users/zczx1/.spyder-py3/temp.py', wdir='C:/Users/zczx1/.spyder-py3')
Using step: 373
w=0.05: 6.44798434081e-06
Using step: 406
w=0.5: 6.55294163482e-06
Using step: 580
w=1: 8.13783429322e-06
虽然随机生成的矩阵不太能说明问题,但是这个只能说,在松弛系数比较小的时候,PCG和CG相比,相对来说减少了迭代的次数。但是当松弛系数较大的时候,还不如CG。
上边的总结,都是在二次型问题上进行的分析。可以看到,虽然算法本身的实现比较简单,但是算法背后的数学思路如果进行严谨地推导的话,还是有些复杂的,不过因为有了几何的理解,所以也还是比较直观。
但是如果这个问题的表达式函数是非线性的,共轭梯度的思想仍然可以被推广,只是形式上不那么精确。而事实上,就算严格按照上边的算法流程,通过上边的实践也会发现,由于round off误差,其实不能达到理论的计算效果,所以在某种程度上近似求解也是可以的。回顾共轭梯度算法和核心,其实就两个:
1. 依据步长和方向向量更新解向量,即
2. 利用梯度更新方向向量,即
其中,,我们只需要考虑怎么选择和即可
对,是希望找到这样的步长因子使得,本质上还是希望下一个梯度方向和当前的方向向量正交,这个时候应该就是最优的解,和SD中的线搜索是类似的。不过,在实际的实现中,可能只是设置一个固定值,然后乘以一个比例,这个比例随着迭代的进行可能会变得越来越小。所以:
for loop until max step or :
寻求最优的步长因子的过程其实就是广义线搜索,根据泰勒级数展开,有:
非线性CG需要对函数进行求导,而求导的方法,目前有:
1. 公式法
2. 符号微分法
3. 自动微分法
这些根据具体场景来实现,只是非线性CG迭代流程就是如上所示的过程。
值得注意的是,非线性函数的这个过程,最终收敛到的可能只是局部极值,和初始值的选择相关。如果初始值在一个类二次型的区域,那么就可以收敛到这块区域的极值点。
本文总结了本人学习EDA软件中布局算法中使用到的共轭梯度相关的内容。共轭梯度算法,应用到实际工程当中,可能还是使用非线性的部分内容,只是由于也是近似的求解,所以和随机梯度下降相比也没有太多的优势,毕竟都是和初始点相关,所以引入更多的随机性,也许可以取得更好的效果!
1. An Introduction to the Conjugate Gradient Method Without the Agonizing Pain
2. Numerical Analysis
@fsfzp888
2019 年 01月