Multigrid Method - Python实现
- 算法特征:
①. pre-smoothing提取低频残差; ②. 向下插值计算残差补偿; ③.向上插值填充残差补偿; ④. post-smoothing降低整体残差 - 算法推导:
Part Ⅰ: 算法原理
考虑一般线性系统:
\begin{equation}
Ax = b
\label{eq_1}
\end{equation}
给定某初始值$x^{0}$, 残差为:
\begin{equation}
r^{0} = b - Ax^{0}
\label{eq_2}
\end{equation}
以该残差为基础, 计算残差补偿:
\begin{equation}
\delta x^{0} = A^{-1}r^{0}
\label{eq_3}
\end{equation}
填充残差补偿, 式(\ref{eq_1})解表示如下:
\begin{equation}
x = x^{0} + \delta x^{0}
\label{eq_4}
\end{equation}
Part Ⅱ: 误差(即残差补偿)特征
现以Jacobi迭代为例, 分解系数矩阵$A$, 式(\ref{eq_1})转换为
\begin{equation}
(L + D + U)x = b
\label{eq_5}
\end{equation}
其中, $L$、$D$、$U$分别为系数矩阵$A$的下三角矩阵、对角矩阵及上三角矩阵. Jacobi迭代公式表示如下:
\begin{equation}
x^{k+1} = Jx^k + D^{-1}b
\label{eq_6}
\end{equation}
其中, $J = -D^{-1}(L + U)$, 代表Jacobi迭代矩阵. 定义第$k$步迭代误差:
\begin{equation}
e^k = x^k - x
\label{eq_7}
\end{equation}
根据式(\ref{eq_6})可知, 迭代误差满足如下条件:
\begin{equation}
e^{k+1} = Je^k
\label{eq_8}
\end{equation}
结合上式, 对矩阵$J$进行特征分解, 则有
\begin{equation}
e^k = V\Lambda^kV^{-1}e^0
\label{eq_9}
\end{equation}
其中, 矩阵$V$和$\Lambda$分别由矩阵$J$的本征矢和本征值组成. 显然, Jacobi迭代的收敛性由相应迭代矩阵的本征值决定. 若矩阵$J$所有本征值绝对值均小于1, 则Jacobi迭代收敛; 否则, 不收敛.
现以有限差分法离散化1维Poisson's equation($\Delta u = f$)为例, 采用Jacobi迭代求解该线性系统, 并分析误差成分:
\begin{equation}
\begin{split}
A &= \begin{bmatrix}-2 & 1 & & \\ 1 & \ddots & \ddots & \\ & \ddots & \ddots & 1 \\ & & 1 & -2 \end{bmatrix} \frac{1}{\Delta x^2} \\
D &= -2I\frac{1}{\Delta x^2} \\
L + U &= \begin{bmatrix} 0 & 1 & & \\ 1 & \ddots & \ddots & \\ & \ddots & \ddots & 1 \\ & & 1 & 0 \end{bmatrix} \frac{1}{\Delta x^2} \\
J &= \begin{bmatrix} 0 & 1/2 & & \\ 1/2 & \ddots & \ddots & \\ & \ddots & \ddots & 1/2 \\ & & 1/2 & 0 \end{bmatrix}
\end{split}
\label{eq_10}
\end{equation}
矩阵$J$之本征矢与本征值分别为:
\begin{equation}
\begin{split}
V_{ij} &= \sin\frac{ij\pi}{n+1} \\
\lambda_j &= \cos\frac{j\pi}{n+1}
\end{split}
\label{eq_11}
\end{equation}
其中, $i$、$j$取$[1, n]$之间的整数. 显然, 矩阵$J$之本征值$\lambda \in (-1, 1)$, Jacobi迭代在该线性系统上收敛.
观察本征矢取值, 当$j$逐渐增大时, 频率逐渐升高. 由此可将误差大致分为低频组分与高频组分. 低频组分, 误差分布平滑, 利于近似表达; 高频组分, 误差分布不平滑, 不利于近似表达.
观察本征值取值, 当$j$逐渐增大时, 其变化趋势为: $1 \rightarrow 0 \rightarrow -1$. 低频部分, 误差收敛速度慢; 中频部分, 误差收敛速度快; 高频部分, 误差收敛速度慢.
对于低频部分, 误差分布平滑, 可采用向下插值技术将离散点上的残差由精细网格过渡至粗糙网格(即降低$n + 1$的取值), 以提升误差组分的频率, 达到快速收敛之目的, 再由向上插值将离散点上的误差由粗糙网格过渡至精细网格, 获取一个可以接受的残差补偿; 对于中频部分, 误差收敛速度快, 无需特殊处理; 对于高频部分, 由于误差收敛速度慢且分布不平滑, 需要引入under-relaxation技术.
Part Ⅲ: under-relaxation技术
结合式(\ref{eq_6}), 采用如下凸组合改写Jacobi迭代:
\begin{equation}
x^{k+1} = (1 - \alpha)x^k + \alpha [-D^{-1}(L + U)x^k + D^{-1}b], \quad \text{where }\alpha \in (0, 1)
\label{eq_12}
\end{equation}
根据式(\ref{eq_7})之定义, 误差方程转换如下:
\begin{equation}
e^{k+1} = [(1 - \alpha )I - \alpha D^{-1}(L + U)]e^k
\label{eq_13}
\end{equation}
此时, 迭代矩阵及其本征值分别为:
\begin{equation}
\begin{cases}
J_\alpha = (1 - \alpha)I - \alpha D^{-1}(L + U) \\
\lambda_\alpha = 1 - \alpha + \alpha\lambda
\end{cases}
\label{eq_14}
\end{equation}
根据式(\ref{eq_11}), 本征值具体取值为:
\begin{equation}
\lambda_j^\alpha = 1 - \alpha + \alpha \cdot \cos\frac{j\pi}{n+1} \in (1 - 2\alpha, 1)
\label{eq_15}
\end{equation}
若$\alpha = 0.5$, 则本征值取值区间为$(0, 1)$. 此时已成功消除分布不平滑且收敛速度慢的误差组分, 使Jacobi迭代在多重网格上求解Poisson's equation时快速收敛. - 代码实现:
Part Ⅰ:
现以Poisson's equation为例进行算法实施, 原始图像、Laplace变换图像及边界条件图像分别如下方左、中、右三图所示:
Part Ⅱ:
Multigrid实现如下:1 # 多重网格法求解Poisson's equation 2 3 import copy 4 import numpy 5 from PIL import Image 6 from matplotlib import pyplot as plt 7 from matplotlib import animation 8 9 10 11 def get_F(picName): 12 ''' 13 根据图片数据获取Poisson's equation等式右侧值 14 ''' 15 img = Image.open(picName) 16 arr = numpy.array(img).astype(float) 17 img.close() 18 19 dx = 1 / (arr.shape[1] - 1) 20 21 F = numpy.zeros(arr.shape) 22 for i in range(1, F.shape[0]-1): 23 for j in range(1, F.shape[1]-1): 24 F[i, j] = ((arr[i-1, j] + arr[i+1, j] + arr[i, j-1] + arr[i, j+1]) - 4 * arr[i, j]) / dx ** 2 25 return F 26 27 28 29 def get_B(picName): 30 ''' 31 根据图片数据获取Poisson's equation边界条件, 非边界值填充0 32 ''' 33 img = Image.open(picName) 34 B = numpy.array(img).astype(float) 35 img.close() 36 B[1:-1, 1:-1] = 0 37 return B 38 39 40 41 def jacobi(U, F): 42 ''' 43 Jacobi迭代法求解Poisson's equation: ΔU = F 44 U: 迭代解初始值, 含边界条件 45 F: Poisson's equation右侧值, 边界值无效 46 横轴长度固定取1 47 ''' 48 dx = 1 / (U.shape[1] - 1) 49 UNew = copy.deepcopy(U) 50 for i in range(1, UNew.shape[0]-1): 51 for j in range(1, UNew.shape[1]-1): 52 UNew[i, j] = (U[i-1, j] + U[i+1, j] + U[i, j-1] + U[i, j+1] - dx ** 2 * F[i, j]) / 4 53 return UNew 54 55 56 57 class Multigrid(object): 58 ''' 59 多重网格法之实现 60 ''' 61 def __init__(self, B, F, alpha=0.5): 62 self.__B = B # 边界条件, 矩阵非边界值无效 63 self.__F = F # Poisson's equation等式右侧 64 self.__alpha = alpha # under-relaxation凸组合因子 65 66 self.__UList = list() # 记录每帧迭代解 67 68 69 def get_solu(self): 70 return self.__UList 71 72 73 def optimize(self, maxIter=3000, epsilon=1.e-1): 74 ''' 75 maxIter: 最大迭代次数 76 epsilon: 收敛判据, 迭代解趋于稳定则收敛 77 ''' 78 self.__UList.clear() 79 80 U0 = copy.deepcopy(self.__B) 81 F0 = copy.deepcopy(self.__F) 82 alpha = self.__alpha 83 84 self.__UList.append(numpy.rint(U0).astype("int")) 85 for idx in range(maxIter): 86 R0 = self.__calc_residual(U0, F0) 87 R0Norm = numpy.linalg.norm(R0) 88 print("iterCnt: {:3d}, ResidualNorm: {}".format(idx, R0Norm)) 89 if R0Norm <= epsilon: 90 break 91 92 U0 = self.__do_multigrid(U0, F0, alpha) 93 self.__UList.append(numpy.rint(U0).astype("int")) 94 95 return U0 96 97 98 def __do_multigrid(self, U0, F0, alpha): 99 # pre-smoothing 100 Uf = self.__smoothing(U0, F0, alpha, 10) 101 Rf = self.__calc_residual(Uf, F0) # 残差计算 102 # downward interpolation - find grid to coarse grid 103 Rc = self.__downward_interpolate(Rf) # 向下插值 - 残差计算 104 deltaU0 = numpy.zeros(Rc.shape) 105 if (Rc.shape[0] >= 5 and Rc.shape[1] >= 5): 106 deltaUc = self.__do_multigrid(deltaU0, Rc, alpha) 107 else: 108 deltaUc = self.__smoothing(deltaU0, Rf, alpha, 10) 109 # upward interpolation - coarse grid to fine grid 110 deltaUf = self.__upward_interpolate(deltaUc, Uf) # 向上插值 - 残差补偿计算 111 Uf += deltaUf 112 # post-smoothing 113 U0 = self.__smoothing(Uf, F0, alpha, 10) 114 return U0 115 116 117 def __upward_interpolate(self, deltaUc, Uf): 118 deltaUf = numpy.zeros(Uf.shape) 119 deltaUf[2:-2:2, 2:-2:2] = deltaUc[1:-1, 1:-1] 120 deltaUf[1:-1:2, 2:-2:2] = (deltaUf[0:-2:2, 2:-2:2] + deltaUf[2::2, 2:-2:2]) / 2 121 deltaUf[:, 1:-1:2] = (deltaUf[:, 0:-2:2] + deltaUf[:, 2::2]) / 2 122 return deltaUf 123 124 125 def __downward_interpolate(self, Rf): 126 ''' 127 向下插值 - R中边界值无效 128 ''' 129 RcOri = Rf[2:-2:2, 2:-2:2] 130 RcTra = numpy.zeros((RcOri.shape[0]+2, RcOri.shape[1]+2)) 131 RcTra[1:-1, 1:-1] = RcOri 132 return RcTra 133 134 135 def __calc_residual(self, U, F): 136 ''' 137 残差计算 138 ''' 139 dx = 1 / (U.shape[1] - 1) 140 R = numpy.zeros(U.shape) 141 142 for i in range(1, U.shape[0]-1): 143 for j in range(1, U.shape[1]-1): 144 R[i, j] = F[i, j] - (U[i-1, j] + U[i+1, j] + U[i, j-1] + U[i, j+1] - 4 * U[i, j]) / dx ** 2 145 return R 146 147 148 def __smoothing(self, U, F, alpha, iterNum=5): 149 for idx in range(iterNum): 150 U = (1 - alpha) * U + alpha * jacobi(U, F) # under-relaxation 151 return U 152 153 154 155 class MultigridPlot(object): 156 157 fig = None 158 ax1 = None 159 line = None 160 txt = None 161 UList = None 162 163 @classmethod 164 def plot_ani(cls, mgObj): 165 mgObj.optimize() 166 167 cls.UList = mgObj.get_solu() 168 cls.fig = plt.figure(figsize=(5, 4)) 169 cls.ax1 = plt.subplot() 170 cls.line = cls.ax1.imshow(cls.UList[0], cmap="gray") 171 cls.txt = cls.ax1.text(x=100, y=100, s="") 172 173 ani = animation.FuncAnimation(cls.fig, cls.update, frames=numpy.arange(len(cls.UList)), blit=True, interval=1000, repeat=True) 174 175 cls.ax1.xaxis.set_visible(False) 176 cls.ax1.yaxis.set_visible(False) 177 cls.fig.tight_layout() 178 ani.save("plot_ani.gif", writer="PillowWriter", fps=3, dpi=1000) 179 plt.close() 180 181 182 @classmethod 183 def update(cls, frame): 184 cls.line.set_data(cls.UList[frame]) 185 cls.txt.set_text("iterCnt: {}".format(frame)) 186 return cls.line, cls.txt 187 188 189 @staticmethod 190 def plot_fig(mgObj): 191 U = mgObj.optimize() 192 img = Image.fromarray(U).convert("L") 193 img.save("plot_fig.png") 194 img.close() 195 196 197 198 if __name__ == "__main__": 199 picName = "./telescope-gray.jpg" 200 F = get_F(picName) 201 B = get_B(picName) 202 203 img = Image.fromarray(F).convert("L") 204 img.save("F.png") 205 img.close() 206 207 img2 = Image.fromarray(B).convert("L") 208 img2.save("B.png") 209 img2.close() 210 211 mgObj = Multigrid(B, F) 212 MultigridPlot.plot_fig(mgObj) 213 # MultigridPlot.plot_ani(mgObj)
-
结果展示:
显然, 通过Multigrid方法, 根据中间Laplace变换图像及右侧边界条件图像即可实现对左侧原始图像的快速求解, 迭代速度较常规迭代方法(Jacobi迭代、Gauss-Seidel迭代等)提升明显. -
使用建议:
①. 适用于结构化网格, 计算过程在精细网格与粗糙网格间多次变换;
②. 适用于求解椭圆型方程, 尤其是线性椭圆型方程. -
参考文档:
Numerical Methods for PDE 2016 by Professor Qiqi Wang & Jacob White from MIT