[前言]
对于矩阵(Matrix)的特征值(Eigens)求解,采用数值分析(Number Analysis)的方法有一些,我熟知的是针对实对称矩阵(Real Symmetric Matrix)的特征值和特征向量(Characteristic Vectors)求解算法——雅克比算法(Jacobi)。Jacobi算法的原理和实现可以参考[https://blog.csdn.net/zhouxuguang236/article/details/40212143]。通过Jacobi算法可以以任意精度近似求解任意实对称矩阵的所有特征值和特征向量。Jacobi算法利用了实对称矩阵的特性得到了迭代求解公式,而对于一般方阵,不能利用Jacobi求解特征值和特征向量。因此更一般的求解方法是基于矩阵的特征值和特征向量的定义计算得到的。对于尚不清楚或者已经遗忘矩阵的特征值和特征向量的定义的读者,建议参考知乎[https://www.zhihu.com/question/21874816],每一个回答都从某个角度讲述了矩阵的特征根和特征向量的意义。需要注意的是,学习矩阵的性质和变换,不得不深入其内在的几何意义。同时,矩阵和群论(Galois Group Theory)之间也可以建立起联系:在求解矩阵特征值时,要求行列式的值为0。因为,所以,这样问题就转化为一个一元(该元为)高次多项式求零点的问题。而关于这个方程是否有根式解的判定,需要借助Galois群论知识,主要是通过置换转化为可解群的过程。当然,如果该矩阵不是实对称矩阵,那么该一元高次方程有可能在复数域存在解,意味着该矩阵可能存在复数的特征值和特征向量,其求解过程将会变得稍微复杂。
本文的研究问题是:如何采用近似最优化算法求解得到全局的所有可能解。
本文的实验对象是:一般方阵在实数域内的特征值和特征根的求解算法。
本文的研究思路是:对梯度下降算法进行改进,通过引入禁忌表、局部最优解的感知和跳出机制实现持续解空间搜索,进而得到全部全局最优解。我们顺带研究了如何避免梯度下降过慢的解决方案。
[实验]
采用梯度下降方法一般只能求得一个近似最优解,这无法满足我们寻求所有满足的和的要求,因此,我们需要对梯度下降方法进行优化,使之能迭代求得所有满足的解。另外,如果我们的方法幸运地成功了,那么意味着,我们不但能够扩展到一般多解问题的求解上,同时还能对全局最优解的近似求解方法进行改进。这样想的话,我们的工作将变得十分有趣!
那么接下来,我们采用Python语言在Tensorflow这个十分好用的机器学习框架下搭建我们的机器学习模型,并利用Tensorflow中已为我们集成的tf.train.GradientDescentOptimizer这个梯度下降优化器进行目标求解。代码如下:
# python2.7+, tensorflow 1.5rc+
import numpy as np import numpy.linalg as lin import tensorflow as tf ''' Only REAL NUMBER is considered as a solution, solutions in complex field is nonsense. For a real symmetric matrix, all the eigen values are real, and all the eigen vectors are real. Every pair of the eigen vector are orthogonal. ''' ''' Solving eigen values of a matrix using numpy linear algorithm tools. ''' def NP_Eigen(mat): assert mat.shape[0]==mat.shape[1] return lin.eig(mat) ''' Normalizing vector with L2 metrics. ''' def TF_L2_Norm(m): t_scale = tf.sqrt(tf.reduce_sum(m*m)) t_sign = m[0]/tf.abs(m[0]) t_scale = 1.0/tf.maximum(t_scale, 1e-7) tf.stop_gradient(t_scale) tf.stop_gradient(t_sign) return t_sign*t_scale*m ''' Solving eigen values of a matrix using stochastic gradient descent optimizer. mat: the matrix to solve N: the maximum number of iteration M: the maximum number of jump R: the effective radius of tabu spots ''' def GD_Eigen(mat, N, M, R): assert mat.shape[0]==mat.shape[1] eigens = np.zeros([mat.shape[0]]) vectors = np.zeros(mat.shape) t_A = tf.constant(mat) t_lambda = tf.Variable(tf.random_uniform([1])-0.5) t_x = tf.Variable(tf.random_uniform([mat.shape[1],1])-0.5) t_x = TF_L2_Norm(t_x) t_err_pure = tf.matmul(t_A, t_x) - (t_lambda*t_x) t_err_pure = tf.reduce_mean(t_err_pure*t_err_pure) t_reg = tf.constant(0, tf.float32) opt = tf.train.GradientDescentOptimizer(learning_rate=0.1) sess = tf.Session() sess.run(tf.global_variables_initializer()) for counter in xrange(mat.shape[0]): if counter>0: t_dist = tf.reduce_mean(tf.square(tf.reshape(t_x,[t_x.shape[0]*t_x.shape[1]])-vectors[counter-1])) t_cond = tf.cast(t_distR, tf.float32) t_reg_add = 1.0/tf.maximum(t_dist, 1e-7) t_reg_add = t_cond*t_reg_add t_reg = t_reg + t_reg_add t_err = t_err_pure + t_reg trainer = opt.minimize(t_err) err = 1e7 err_last = 0 i = 0 j = 0 while i and j<M: i += 1 err_last = err _, err, reg = sess.run([trainer, t_err, t_reg]) if err < 1e-12: print 'Root#', counter, ' found at iter#', i print 'Solution is: ', sess.run([t_lambda, t_x]) break elif abs(err_last - err) < 1e-15: print 'Trapped! Reinitialize vars and search again!' sess.run(tf.global_variables_initializer()) i = 0 j += 1 elif err/(err_last-err)>N-i: print 'Optimize too slow at speed: ', err_last-err sess.run(tf.global_variables_initializer()) i = 0 j += 1 #print err, reg # saving this solution eigens[counter], x = sess.run([t_lambda, t_x]) vectors[counter] = x.reshape([mat.shape[1]]) return eigens, vectors.T def main(): tf.set_random_seed(128) A = np.array([[1,3,-4],[3,-2,5],[-7,6,0]], dtype=np.float32) A = A.dot(A.T) A = A/np.sqrt(np.sum(A*A)) s, u = GD_Eigen(A, 10000, 100, 1e-1) print s print u s, u = NP_Eigen(A) print s print u main()
注意到,在上述代码中,我们采用了numpy.linalg(Numpy Linear Algorithm Library)这一个工具库中的eigen函数对同一目标矩阵求解了特征值和特征向量,eigen函数采用的精确解的求解方法,因此其结果总是正确的,为此,我们将我们的代码运行得到的结果和eigen函数的结果对比发现,我们得到的所有特征值和特征向量都是正确的,虽然他们都或多或少存在1e-5精度上的误差,但这不影响我们的正确率。另外,即便你更改上述程序中的初始随机种子(tf.set_random_seed(128)),依然能够观测到类似结果。这表明我们的算法是正确的。
那么,接下来我将慢慢解释上述代码的算法由来。
1. 梯度下降算法和一些近似最优求解算法一样,无法一次性求得全部满足最优化目标的解,而且,有时候得到的解并非全局最优,而仅仅是局部最优。其原因是梯度下降算法只是朝着当前最佳的误差下降的方向搜索解空间。除了精确解算法,在非凸目标函数的最优化过程中,贪婪规则下的算法很容易就陷入到局部最优解,而且接下来会困死在那里,因为局部最优解所在的空间附近是平坦的(在这一点上对误差的梯度的模趋近0),而且稍微动一下就会出现上坡路(在该点的邻域内任意点,其对误差的梯度方向背离该点,这意味着误差要想下降,必须沿着梯度反方向——也就是朝着该点的方向运动,这使得该点成为了一个局部稳定点,小扰动根本不会使之逃离该点),这使得Agent就仿佛陷入到了一个陷阱里,按照贪婪规则再也爬不出来了。 为此,我们提出局部最优解的跳出机制,但在这之前,我们需要一个方法能监测Agent是否陷入到了局部最优解中。其次我们才能针对这种情形设计迫使Agent跳出的机制。
2. 为了搜索得到全部满足优化目标的解,我们需要设计持续搜索的机制,并且为了避免陷入循环或浪费搜索时间,我们应该设计避免Agent回到已搜索过的最优解上。
3. 有时候,Agent的出发点不一样会导致陷入不同的局部最优解,也可能会导致梯度下降的坡度太缓,优化速度过慢。因此,需要设计检测下降速度的机制并针对该情形设计跳出机制。
4. 对于优化目标,我们需要设计正规化(Normalization)方法使得误差的值域是合理的。而且这一点对于包含正则项的优化目标来说尤为重要,因为它决定正则系数的选取。
5. 由于持续搜索机制,我们需要考虑如何避免由之而来的新问题:新增部分对优化目标的最终解是否有影响?影响多大?能否完全避免?
幸运的是,上述的代码中,这些问题我们已经全部解答了,而且在实验中已经证实上述问题都得到了不错的解决。
可以先看下运行日志:
Root# 0 found at iter# 1076 Solution is: [array([0.05244394], dtype=float32), array([[0.6913288 ], [0.6959271 ], [0.19429319]], dtype=float32)] Trapped! Reinitialize vars and search again! Optimize too slow at speed: 6.117625e-08 Trapped! Reinitialize vars and search again! Root# 1 found at iter# 403 Solution is: [array([0.31607816], dtype=float32), array([[ 0.6778219 ], [-0.53152364], [-0.5079764 ]], dtype=float32)] Trapped! Reinitialize vars and search again! Optimize too slow at speed: 3.1590462e-06 Optimize too slow at speed: 6.239861e-08 Optimize too slow at speed: 6.187474e-08 Optimize too slow at speed: 3.874302e-06 Optimize too slow at speed: 6.367918e-08 Optimize too slow at speed: 6.7055225e-08 Optimize too slow at speed: 4.425645e-06 Optimize too slow at speed: 3.3862889e-06 Optimize too slow at speed: 3.0882657e-06 Optimize too slow at speed: 6.030314e-08 Optimize too slow at speed: 6.1816536e-08 Optimize too slow at speed: 1.356937e-06 Optimize too slow at speed: 6.088521e-08 Optimize too slow at speed: 6.251503e-08 Optimize too slow at speed: 9.071082e-07 Optimize too slow at speed: 6.6822395e-08 Optimize too slow at speed: 6.0070306e-08 Optimize too slow at speed: 4.5895576e-06 Optimize too slow at speed: 3.571622e-07 Optimize too slow at speed: 9.313226e-10 Optimize too slow at speed: 3.3304095e-06 Optimize too slow at speed: 6.589107e-08 Optimize too slow at speed: 6.0498714e-06 Optimize too slow at speed: 6.164191e-08 Optimize too slow at speed: 5.9138983e-08 Optimize too slow at speed: 6.0594175e-08 Optimize too slow at speed: 6.0827006e-08 Root# 2 found at iter# 382 Solution is: [array([0.94728214], dtype=float32), array([[ 0.25024486], [-0.48287624], [ 0.839171 ]], dtype=float32)] [0.05244394 0.31607816 0.94728214] [[ 0.69132882 0.67782187 0.25024486] [ 0.69592708 -0.53152364 -0.48287624] [ 0.19429319 -0.50797641 0.83917099]] [0.9472825 0.05244393 0.3160783 ] [[-0.2502432 0.69133323 -0.6778176 ] [ 0.48287496 0.6959237 0.53152794] [-0.8391723 0.19428991 0.50797766]]
从上述运行日志中可以看到,Agent随着持续搜索进程的进行,发现的解越多,以后搜索到新解的难度越大,这一方面是因为优化目标本身的解空间中:容易发现的解具有更强大的吸引子,它能把绝大多数随机出发点上的Agent吸引到该解上。而不易被发现的解的吸引子的引力场较弱,大部分随机出发点上的Agent难以找寻到通往该解的最快的路径。
在我们设计的算法中,每找到一个解,我们都会将该解添加到禁忌表中,并且将该解作为禁区,设置一个反引力子迫使任何进入该禁区的Agent因为误差梯度突然变得极大而被排挤出。该引力子的添加其实就是在优化目标上添加一项新的正则项,该正则项在有限范围内(R的设置,在本实验中取0.1)给Agent一个很大的惩罚。这样Agent将不会停留在该禁区中,从而实现了持续搜索以及跳出机制。