MENU

Numerical Analysis: 共轭梯度法(1)--基本原理

• October 29, 2019 • Read: 9524 • Knowledge,Python,Numerical Analysis,深度学习

共轭梯度法(1)--的基本原理

之前已经搞明白了,梯度下降法的基本原理,当然解释的调度是从求函数极值的角度出发的,事实上从这个角度来理解,个人感觉是一个最为直接的理解角度,其完完全全是建立在多变量函数的微分系统中的。事实上这个方法(思想)在实际中是被用于求解线性方程的,当然单纯的梯度下降形式并没有被直接采用,被广泛用于求解对称、正系数矩阵方程组的方法是,基于梯度下降原理实现的共轭梯度法,这一方法在大型稀疏线性方程组的求解中,有极高的效率。但是,之前对于梯度下降思想的描述,好像认为这个方法是用于求函数极值的方法,似乎与线性方程组求解问题无关。因此,对于对称、正定系数矩阵的二次型的理解是很必要的,它能让我们明白矩阵系统中线性方程组与函数极值的关系,能够解释为什么一个求函数极值的思想可以被用于求解线性方程组系统。

$Ax = b$对应的二次型

这里就不严格去给出严格的数学表示了,因为对于非数学专业的我们来说,好像没啥必要,知晓核心的思想更为重要。注意本次针对的系数矩阵$A$都是对称、正定的,线性方程组系统$Ax = b$对应的是二次型对于自变量导数为0时的表达式,不严谨地可将其看作是函数吧,即二次型为

$$ f(x) = \frac{1}{2} x^{T} A x - b^{T} x + c \tag{1} $$

对于$Ax = b$来说,$c$是一个0向量。我们知道求$f(x)$极小值的方法最直接的一种就是利用其导数为0,获得极值点从而获得极小值,因此,求导得到

$$ f'(x) = \frac{1}{2}A^T x + \frac{1}{2}A x - b = 0 \tag{2} $$

因此,求解线性方程组$Ax = b$就等价于求解$f'(x) = 0$这个表达式,那么一阶导数为0这个表达换一种说法,不就是去求二次型$f(x)$的函数的极值嘛!这也就解释了为什么一个求解函数极值的方法却可以应用到线性方程组的求解当中来。

为了更好的理解线性方程组$Ax = b$的求解与函数极值之间的关系,这里举一个简单的,可以被可视化的例子,若方程组为:

$$ \begin{bmatrix} 2 & 2\\ 2 & 5 \end{bmatrix} \begin{bmatrix} x\\ y \end{bmatrix} = \begin{bmatrix} 6\\ 3 \end{bmatrix} \tag{3} $$

这样一个对称正定的方程组,为了待求的未知数为了更符合函数表达,这里用了$x, y$来表示,依据二次型公式,得到

$$ f(x, y) = \frac{1}{2} \begin{bmatrix} x\\ y \end{bmatrix} \begin{bmatrix} 2 & 2\\ 2 & 5 \end{bmatrix} \begin{bmatrix} x & y \end{bmatrix} - \begin{bmatrix} 6 & 3 \end{bmatrix} \begin{bmatrix} x\\ y \end{bmatrix} $$

这样一个$2 \times 2$的线性方程组对应的二次型就是一个二元函数,为了得到表达式,这里利用sympy库进行符号运算。

import numpy as np
from sympy import *

# 先定义符号变量x, y
x, y = symbols('x y')

A = np.array([[2, 2], [2, 5]])
b = np.array([6, 3])
# 自变量向量这里用x1表示
x1 = np.array([x, y])
f_xy = 1/2 * x1.T @ A @ x1 - b.T @ x1
init_printing(use_unicode = True)  # 更美观的显示
pprint(f_xy.expand())
     2                        2      
1.0⋅x  + 2.0⋅x⋅y - 6⋅x + 2.5⋅y  - 3⋅y

利用函数图像来直观地可视化函数极值情况。

import matplotlib.pyplot as plt
import numpy as np
from mpl_toolkits.mplot3d import Axes3D

x = np.linspace(-10, 10, 100)
y = np.linspace(-10, 10, 100)
xx, yy = np.meshgrid(x, y)
f_xy = xx * (xx + yy) - 6 * xx + yy * (xx + 2.5 * yy) - 3 * yy

# 这里用等值线云图与伪色彩图来可视化,也顺便对比下两者的区别
fig, ax = plt.subplots(1, 2)

cf1 = ax[0].pcolormesh(xx, yy, f_xy, cmap='RdBu_r')
cs1 = ax[0].contour(x, y, f_xy, colors='gray', levels=15)
ax[0].clabel(cs1, inline=True, colors='k')
fig.colorbar(cf1, orientation='vertical')
ax[0].set_title('function by pcolormesh')

cf2 = ax[1].contourf(x, y, f_xy, cmap='RdBu_r')
cs2 = ax[1].contour(x, y, f_xy, colors='gray', levels=15)
ax[1].clabel(cs2, inline=True, colors='k')
ax[1].set_title('function by contourf')
fig.savefig('function2.jpg', dpi=600)
plt.show()

# 三维的函数图像
fig1 = plt.figure()
ax = Axes3D(fig1)
cd = ax.plot_surface(xx, yy, f_xy, cmap='RdBu_r')
ax.set_title('the function of 3D image')
fig1.colorbar(cd)
fig1.savefig('image3d.jpg', dpi=600)
plt.show()

png

png

通过函数图像的可视化,可以看到线性方程组本身是对称正定的,那么对应的二次型的函数图像则必定都位于在$z= 0 $平面以上,函数均是大于0的,且从函数的等值线及三维图像中可以看到是存在极小值的。明白了这个关系之后,对于梯度下降法或者共轭梯度法应用于方程组(系数矩阵为对称正定)的求解就不足为奇了。

Gradient descent method求解方程组

共轭梯度法的实现全来自于或者说是基于梯度下降法,因此,理解梯度下降法求解线性方程系统很有必要。在之前的梯度下降法的原理中,已经明白了这个方法对于函数极值的求解,是通过沿着梯度方向负向进行的,每一次的计算实质上获得是自变量的增量向量,而现在面对方程组时,由于之前的二次型的内容已经得出了线性方程组与多元函数极值之间的关系,即$f'(x_{i}) = Ax_{i} - b = 0$。那么梯度下降法可以被这样描述与构建:每一次产生的近似值$x_{i}$向量,近似解自然会有一个残差向量(residual vector)为$r_{i}$, 下表$i$表示第几步的迭代。

$$ r_{i} = b - A x_{i}\\ r_{i} = - f'(x_{i}) \tag{4} $$

式(4)中的$r_{i}$不就是梯度向量嘛,表示了点下一步搜索或者移动的方向,当然要想确定自变量的增量大小,还需要知道步长,这一点在梯度下降原理中已经给出了解释;在这里步长设定为$\alpha_{i}$,所以,产生的新的点$x_{i + 1}$为:

$$ x_{i + 1} = x_{i} + \alpha_{i} r_{i} \tag{5} $$

到这里好像似乎与之前的原理中阐述并无大的区别,实际上现在问题在于如何确定步长$\alpha_i$,而在之前的介绍中步长是一个固定的,对于迭代计算来说,固定的步长的计算效率显然是不能够令人满意的。对于函数的极小值的确定来说,在这里每一步获得的新的函数值为$f(x_{i+1})$,对于整个求解过来说,只要这个更新的函数值达到最小值那么任务就算是完成了,依据式(5),函数值可以看做是关于步长的函数,为了获得极小值,通过导数为0来完成,即

$$ \frac{\mathrm{d}f'(x_{i + 1})}{\mathrm{d} \alpha_{i}} = 0 \tag{6} $$

结合式(5)来看,这是一个复合函数,利用链式求导法则,得到

$$ 式(6) = f'(x_{i+1}) ^{\mathrm{T}} \cdot \frac{\mathrm{d}x_{i + 1}}{\mathrm{d}\alpha_{i}} = 0 \tag{7} $$

这个式子中,$f'(x)$不就是梯度向量嘛,依据式(4)给出的定义,在这里它也称作残差向量$-r_{i + 1}$,$x_{i + 1}$求导的结果可以根据式(5)为$r_{i}$,所以,要保证函数取得极值,就要使得前后两步的梯度向量(也就是残差向量$r$)保证正交!这里写为内积形式:

$$ r_{i + 1} ^ {\mathrm{T}} \cdot r_{i} = 0 \tag{8} $$

那么依据这个关系,带入残差向量的表达,得到:

$$ (b- A x_{i + 1}) ^ {\mathrm{T}} \cdot r_{i} = 0\\ (b - A (x + \alpha_{i} r_{i}))^{\mathrm{T}} \cdot r_{i} = 0 \tag{9} $$

通过这个式子就可以得到步长$\alpha_{i}$的计算公式:

$$ \alpha_{i} = \frac{r_{i} ^{\mathrm{T}} r_{i}}{r_{i} ^{\mathrm{T}} A r_{i}} \tag{10} $$

到这里,所有量的表达都得到了解决,那么对于线性方程组系统来说,梯度下降法的迭代过程为:

$$ repeat:\\ r_{i} = b - A x_{i}\\ \alpha_{i} = \frac{r_{i}^{\mathrm{T}} r_{i}}{r_{i} ^{\mathrm{T}} A r_{i}}\\ x_{i + 1} = x_{i} + \alpha_{i} r_{i}\\ until: x_{i}的增量幅度满足设定精度\\ end \tag{11} $$

迭代计算的代码如下:

import numpy as np

# 每一步的残差向量r0都会被以行向量的形式存放在r矩阵中
def gd_method(A, b, x0, tol, max_time):
    r0 = b - np.dot(A, x0)
    number = 0
    r = np.zeros([max_time, b.shape[0]])
    x = np.zeros([max_time, b.shape[0]])
    while np.linalg.norm(r0, np.inf) > tol:
        r0 = b - np.dot(A, x0)
        r[number, :] = r0
        alpha = np.dot(r0.T, r0) / np.dot(np.dot(r0,A), r0)
        x0 = x0 + alpha * r0
        x[number, :] = x0
        number += 1
        if number > max_time:
            print('warning:the result is not stable!')
            return
    return x, number, r

这里对一个$3\times3$大小的对称正定矩阵进行验算,梯度下降算法迭代了67次得到结果,$[0.99983945,0.99976565, 1.99978575]$,而这个方程组的精确解为$[1,1,2]$。可以迭代结果还是比较可信与可靠的。

A = np.array([[4, -2, -1], [-2, 4, -2], [-1, -2, 3]])
b = np.array([0, -2, 3])
x0 = np.array([1, 1, 1])
x_gd = gd_method(A,b, x0, 0.0001,500)
print(x_gd[0][x_gd[1]-1,:])
print(x_gd[1])
print(x_gd[2])
    [0.99983945 0.99976565 1.99978575]
    67
    [[-1.         -2.          3.        ]
     [-0.39130435  0.43478261  0.15942029]
     [ 0.09201888  0.02436289  0.15942029]
     ...
     [ 0.          0.          0.        ]
     [ 0.          0.          0.        ]
     [ 0.          0.          0.        ]]
x = np.linalg.solve(A,b)
print(x)
    [1. 1. 2.]

依据上述的分析以及这个算例的计算,实际上对于线性方程组迭代的迭代计算会有一个新的认识。首先,从最为原始的迭代的角度来看,通过每一迭代步中都会产生一个残差向量,似乎在方程组的求解角度来看,残差向量仅仅也就表示一个误差吧,好像也并没有很直观的一个认识。但是从梯度下降的算法角度来看,线性方程组迭代步中的残差向量实际上就是二次型(函数)的负梯度向量,那么也就是说,这个向量$r$不断地在修正点移动的方向,使得移动方向不断的向函数的极小值点处靠近!由于gd_method函数中会记录每一步的参差向量,因为这个是一个3个未知数的方程组,因此残差向量$r$本身就是一个$R^{3}$空间中的向量,同时返回得到的$x$矩阵中存放着每一步的近似解,这样进行可视化,可以看出梯度下降中前后的参差向量确实是正交的。因为三元函数无法进行可视化,不是很容易看出梯度下降的搜寻过程。可能二元函数的化对于理解更方便吧。

xs = x_gd[0][0:x_gd[1]-1,:]
plt.style.use('ggplot')
fig = plt.figure()
ax = Axes3D(fig)
ax.plot(xs[:,0], xs[:,1], xs[:,2], marker='o')
ax.set_title('point in every step')
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_zlabel('z')
fig.savefig('gdimage.jpg', dpi=600)
plt.show()

png

在上述的分析中,我们会发现实际好像似乎梯度下降这个迭代算法的效率并不是很高,很简单的方程组都要迭代67次,显然这样的效率去求解大型的稀疏方程组是不能令人满意的。这个方法之所以没有被广泛地应用,主要原因是因为每一步的残差向量也就是梯度向量,都必须与相邻步的残差向量保持正交,如果为了便于理解,以二维空间为例,就是每一次的搜索(移动)方向都是相互垂直的,那么间隔的残差向量必定平行,也就说从初始值向精确解移动是呈折线的方式,一个搜索方向会被重复了好多次,这样使得迭代效率并不高。但是梯度下降法实现的思想确是很有意义的,为函数极值的确定与线性方程组的求解之间建立了联系。真正有意义的方法是在这个方法基础上实现的共轭梯度法,它对搜索方向进行了共轭处理,在理解了梯度下降的原理后,再对共轭梯度法进行学习可能更好一点。

Last Modified: May 3, 2020
Archives Tip
QR Code for this page
Tipping QR Code