@fsfzp888
2019-01-23T13:27:22.000000Z
字数 17128
阅读 3199
数值优化 线性代数
本文介绍和总结共轭梯度(conjugate gradient)算法的如下内容:
共轭梯度算法的背景和由来
梯度算法的几何理解和共轭梯度算法的流程
基于SSOR预条件子的共轭梯度算法
非线性共轭梯度算法
共轭梯度算法的由来,源于求解如下所示的二次型问题:
如果对于问题的求解,可以被转化为如上所示的二次型表达式(Quadratic Form)的极值,那么求解这个问题的方法也称为二次规划(Quadratic Programming)
二次规划问题,有一个极好的性质,这个性质直接导致了该问题的求解有较好的几何解释,这个性质就是如果矩阵为对称矩阵,那么把这个函数对列向量求导取极值,得到的梯度方向向量满足:
这个是经典的线性代数求解线性方程组的问题,求极值即是求解线性方程组,而矩阵的性质,直接决定了梯度方向的情况。
对一个从维到维的函数映射,对列向量求导即:
共轭梯度算法来源于二次型问题的求解,二次型问题可以在几何上直观的理解,即使是最速梯度下降(Steepest Descent),也可以这样来理解。
二次型的几何图解完全取决于矩阵,如果是正定的矩阵,那么图像将会是:
也就是是一个碗状朝上的结构,因为对于正定矩阵来说,任何非零向量,都有:
对于最速梯度下降(Steepest Descent)而言,选定了初始的方向向量后,后续每一次迭代所选择的方向向量由当前的梯度方向决定:
import numpy as npdef 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_initialfor _ in range(max_step):r = b - A * xalpha = (r.transpose() * r) / (r.transpose() * A * r)x = x + r * alphadist = np.sqrt(np.sum(np.square(b - A * x)))if dist < thresh:breakreturn xif __name__ == '__main__':N = 100Ar = np.mat(np.random.rand(N, N))As = Ar * Ar.transpose() # get positive definite matrixbn = 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.945382131730489610000: 0.911662840883017520000: 0.6950563935826073
运行SD的例子,会发现,随机生成的一个对称矩阵,迭代了几万次仍然不收敛,可以见得,按照SD的理论公式求解效果有些不理想,收敛过慢。在数值优化里边,一个矩阵迭代求解方法是否收敛,取决于这个矩阵是否严格对角占优,即
在CG中,引入了共轭方向的概念。这个概念本质上就是对正交的推广,任何向量在欧几里得空间内正交意味着:
上边的过程容易让人联想起线性代数中的二次型标准化过程,二次型标准化,首先通过平移变换,让二次型变成了如下的形式:
二次型的基本迭代流程还是不变的:
条件1:各个方向向量和之间两两正交
条件2:通过求导让每一次迭代的步长因子取得最优
那么每一次迭代求解的过程,其实就是相当于减少了这个方向向量上的误差,每一步的误差向量可以被表示为方向向量的连加的形式,这样只需要迭代步后就会直接收敛到极值点。这个和我们在几何上边的理解是完全一致的。
只是,我们该如何选择这些方向向量呢?
如果仅仅只是随便选择一组线性无关的构造基,然后如同等式17那样构造方向向量,并满足正交,那么:
for loop until reach max_step or cost_function below threshold:
return
使用Python代码实现:
import numpy as npdef 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_initiald = r_oldx = x_initialfor i in range(max_step):alpha = (r_old.transpose() * r_old) / (d.transpose() * A * d)x = x + d * alpha# r_new = b - A * xr_new = r_old - A * d * alphabeta = (r_new.transpose() * r_new) / (r_old.transpose() * r_old)d = r_new + d * betar_old = r_newcf = np.sqrt(np.sum(np.square(b - A * x)))if cf < threshold:print("Using step: ", i)breakreturn xif __name__ == '__main__':N = 200Ar = 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: 4101000: 6.43707958798e-06Using step: 41010000: 6.43707958798e-06
大约需要两倍于维度的迭代次数才可以收敛到要求的范围,虽然理论上100维的变量值需要100次迭代,但是实际上各种浮点数运算误差导致不可能实现这样理想化的结果。通过对比会发现,迭代效果要比SD要好。当然了,实际使用的时候,判断损失函数其实没必要通过那样的形式。而且可以几十次迭代后才计算一下等等。
上边的迭代算法,本质上其实还是在求解形如的方程。毕竟最终的结果,都是让尽可能的接近于零。一般情况下,对于的求解,通常直接通过求逆得到,这就存在求矩阵的逆可能不存在等等问题。如果不是方阵,且,那么就是在求解超定方程组,方程组可能没有解,求解的过程就会等效于最小二乘,因为,为了让误差最小,其实就是让最终的的解得到的离最近,那么只能是到的列空间的投影,所以它们之间的差和的列空间正交,所以有
在数值分析里边,矩阵迭代求解的收敛速度取决于矩阵条件数:
for loop until reach max_step or cost_function below threshold:
return
对应的Python代码:
import numpy as npdef 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_initialM_inv = get_ssor_precondition_matrix(A, w)z_old = M_inv * r_oldd = z_oldx = x_initialfor i in range(max_step):alpha = (z_old.transpose() * r_old) / (d.transpose() * A * d)x = x + d * alpha# r_new = b - A * xr_new = r_old - A * d * alphaz_new = M_inv * r_newbeta = (z_new.transpose() * r_new) / (z_old.transpose() * r_old)d = z_new + d * betar_old = r_newz_old = z_newcf = np.sqrt(np.sum(np.square(b - A * x)))if cf < threshold:print("Using step: ", i)breakreturn xif __name__ == '__main__':N = 200Ar = 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: 378w=0.05: 6.71578034331e-06Using step: 405w=0.5: 9.67223926338e-06Using step: 573w=1: 8.09438315554e-06runfile('C:/Users/zczx1/.spyder-py3/temp.py', wdir='C:/Users/zczx1/.spyder-py3')Using step: 371w=0.05: 6.1508910381e-06Using step: 401w=0.5: 8.51261715479e-06Using step: 602w=1: 7.57385104633e-06runfile('C:/Users/zczx1/.spyder-py3/temp.py', wdir='C:/Users/zczx1/.spyder-py3')Using step: 373w=0.05: 6.44798434081e-06Using step: 406w=0.5: 6.55294163482e-06Using step: 580w=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月