尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

PDHG算法解析:原始-对偶混合梯度法及其在图像去噪中的应用

PDHG算法解析:原始-对偶混合梯度法及其在图像去噪中的应用 1. 为什么要把PDHG单独拎出来讲这一讲是“最优化方法”系列里比较特别的一篇主题叫PDHG全称是Primal-Dual Hybrid Gradient国内一般译成“原始-对偶混合梯度法”但更多人干脆叫它Chambolle-Pock算法因为2011年Chambolle和Pock那篇《A first-order primal-dual algorithm for convex problems with applications to imaging》把这套框架完整地定理化了。在山东大学软件学院的最优化方法课上我们一路从凸集、凸函数、次梯度、共轭函数讲到近端算子、对偶理论、ADMM到了第25讲终于轮到PDHG登场。这课安排到这时候其实很讲究PDHG几乎把所有前置概念串起来了没有凸优化和对偶理论打底直接看迭代格式会觉得“这到底在干嘛”。反过来如果前面的知识都跟上了PDHG的推导就像一块拼图刚好归位你会发现它把“非光滑”“复合函数”“线性算子”这几个最麻烦的东西拆得干干净净。PDHG能解的问题长这样min_x f(x) g(Ax)其中f和g都是凸函数允许不光滑A是线性算子。这个形式覆盖了大量工程和机器学习问题Lasso回归、全变分图像去噪、压缩感知重构、最优传输、多任务学习等等。所以这篇文章不只是讲一个算法更想帮大家把“怎么把实际问题改写成这个形式”和“改完之后怎么确定每一步的更新公式”这两件事彻底搞明白。代码部分我用图像去噪做例子因为每一步都直观调试也方便适合对照学习。2. 原始-对偶形式为什么非得绕这么一圈2.1 直接算prox_{g∘A}基本是死路如果你已经学过近端梯度法看到min f(x) g(Ax)这种形式第一反应很可能是用近端梯度做啊光滑项走梯度非光滑项走prox。问题在于近端梯度法的prox作用对象是g(Ax)这个整体也就是要计算prox_{g∘A}(x) argmin_z [ g(Az) 1/(2t)||z - x||² ]这个式子绝大多数情况下是算不出来的。为什么因为A在函数内部跟g复合在一起除非A是单位阵否则近端算子里那个二次项和线性算子纠缠在一起没有闭式解硬要用内层迭代做的话整个算法就变成双层循环代价直接翻几倍。我当初第一次碰这个问题天真地选了Lasso的一个带测量矩阵的版本去试。Lasso的目标是min_x 1/2||y - Bx||² λ||x||₁其中B是一个随机高斯矩阵而不是单位阵。FISTA看着很美好但迭代里需要prox_{λ||·||₁}(·)这倒好算可问题在于FISTA的梯度步是相对于整体目标函数的。整体目标里含1/2||y - Bx||²这个函数对x的梯度是B^T(Bx - y)需要计算矩阵乘法没问题但如果你想在这个光滑部分上加速需要估计B^T B的条件数这个估计在大规模场景下非常费劲。好那换一种思路把测量矩阵B吸收到线性算子A里写成min_x f(x) g(Ax)其中f(x) λ||x||₁g(v) 1/2||v - y||²A B。可惜g(Ax)里的A还是消不掉prox_{g∘A}照样算不出来。PDHG的关键洞察是不跟这个g∘A死磕转而把问题改写成鞍点形式让A从prox里“退”出来变成两个独立的prox每个都有闭式解。这是整个算法的命门。2.2 用Fenchel共轭把问题拉成min-max对g做Fenchel共轭g(Ax) sup_y [ ⟨Ax, y⟩ - g*(y) ]其中g*是g的共轭函数。这一步看起来只是换了个写法但它改变了问题的结构。原来的min_x f(x) g(Ax)变成了min_x sup_y [ f(x) ⟨Ax, y⟩ - g*(y) ]把这一步看成一个博弈x在最小化y在最大化。在凸性条件满足时min和sup可以交换顺序解就是拉格朗日函数的鞍点。这就是为什么算法叫“原始-对偶”因为迭代里同时更新原始变量x和对偶变量y。拉格朗日函数写成L(x, y) f(x) ⟨Ax, y⟩ - g*(y)这里定义域的问题先不细抠你可以粗略认为如果某个y让⟨Ax, y⟩ - g*(y)趋向正无穷那么g(Ax)就是无穷这个y不会成为最大值点。严谨的凸分析里需要检查相对内部条件比如存在某个x让g(Ax)有限且连续但在绝大多数实际问题上条件都满足。到了这一步PDHG的思路就很清晰了对x做一次近端步来最小化f(x)并同时带上一个线性项对y做一次近端步来最大化⟨Ax, y⟩ - g*(y)本质上是求g*的近端。这样A就只在两个prox之间传递信息不再藏在任何prox里面。2.3 鞍点视角下的直观理解鞍点问题相当于在玩一个双人零和博弈x选手想让L尽量小y选手想让L尽量大。两个选手轮流行动各自朝自己的方向走一小步。如果每一步都走得合适最终会收敛到“谁都不愿意先动”的点也就是鞍点。这就是PDHG的骨架。至于每一步“走”的步长怎么定、什么时候收敛、加速怎么做都需要专门的收敛分析来回答。这也是为什么PDHG比单纯背迭代公式复杂——它不是一个固定的闭式算法而是一族可以调节的方法参数变了行为和收敛速度都不同。3. PDHG迭代格式三步更新逐项拆解3.1 算法完整形式Chambolle-Pock算法θ1版本也是最常用的迭代过程如下给定初始x⁰, y⁰取θ∈[0,1]τ0σ0满足τσ‖A‖² 1后续细说对k0,1,2,...重复x^(k1) prox_{τf}(x^k - τ A^T y^k)x̄^(k1) x^(k1) θ(x^(k1) - x^k)y^(k1) prox_{σg*}(y^k σ A x̄^(k1))三行公式第一行更新原始变量x第三行更新对偶变量y中间一行是外推步。很多人第一次看会问为什么中间要插个x̄而不是直接用x^(k1)这个外推步正是PDHG能加速的关键。直观上说对偶更新时如果只用刚更新的x^(k1)相当于少看了一步“动量信息”用x^(k1)和x^k的差做外推相当于给对偶更新一点“冲量”类似加速梯度法里的外推。数学上外推步能把收敛率从O(1/k)提到O(1/k²)吗严格说PDHG在一般凸问题上的收敛率是O(1/N)不管有没有外推但外推让常数因子大幅改善实际收敛速度快得多。最重要的一点是收敛性证明并不依赖θ的精确取值只要0≤θ≤1就行工程上直接取θ1几乎总是最优选择。3.2 原始更新prox_{τf}里发生了什么先看第一行x^(k1) prox_{τf}(x^k - τ A^T y^k)如果不看prox只看括号里的x^k - τ A^T y^k这像不像梯度下降对L关于x求梯度如果不考虑f的不可微性梯度的一部分就是A^T y。所以“x^k - τ A^T y^k”就是在沿着让拉格朗日函数减小的方向走一步。走了这一步后还需要把f的不可微性吸收进来——prox_{τf}做的就是这件事。prox_{τf}(v)的定义是prox_{τf}(v) argmin_z [ f(z) 1/(2τ)||z - v||² ]它找一个点z让f(z)别太大同时z也别离v太远。1/(2τ)是平衡系数。τ越小z越靠近vτ越大f的权重越大z可以离v更远。从这个角度理解prox可以看成是“考虑完f的惩罚之后把点拉到一个新位置”。对于很多常见fprox有闭式解。比如f(x) 0没有惩罚prox_{τf}(v) v退化成普通梯度步骤f(x) δ_C(x)集合指示函数prox_{τf}(v) P_C(v)即投影到集合Cf(x) λ||x||₁prox_{τf}(v) soft-thresholding算子S_{τλ}(v_i) sign(v_i)·max(|v_i| - τλ, 0)f(x) (1/2)||x - b||²prox_{τf}(v) (v τb)/(1τ)这些闭式解是PDHG实用性的基础。如果没有它们每次迭代都要解一个内层优化算法就失去意义了。3.3 外推步x̄的作用是什么中间那一行x̄^(k1) x^(k1) θ(x^(k1) - x^k)其实就是把刚算出来的x^(k1)沿着它的更新方向再推一段。当θ1时x̄^(k1) x^(k1) (x^(k1) - x^k) 2x^(k1) - x^k这是当前点相对于前一步的“镜像点”在加速梯度法里很常见。当θ0时x̄x退化成无外推版本收敛慢但在某些带约束问题里更稳。为什么对偶更新要用x̄而不是x核心在于算法的收敛证明。Chambolle-Pock在证明中构造了一个关于迭代点的势能函数外推步能让势能函数沿着迭代单调下降得更快。如果去掉外推算法仍然收敛但界差一截保持θ1可以以几乎零成本获得更快的实际收敛速度和更好的常数因子。3.4 对偶更新prox_{σg*}与Moreau分解第三行y^(k1) prox_{σg*}(y^k σ A x̄^(k1))括号里的y^k σA x̄是对拉格朗日函数关于y做梯度上升步。因为L对y的梯度贡献是Ax所以这一步沿着Ax方向走。然后prox_{σg*}处理g*的部分。问题来了g的近端算子好算吗如果你已经会算g的近端那么g的近端可以用Moreau分解得到这是一个非常实用的恒等式prox_{σg*}(y) y - σ·prox_{g/σ}(y/σ)这个式子是什么意思它告诉你不用去求g的显式表达式只要会算g的近端就能算出g的近端。这里g/σ表示函数h(z) g(z)/σ也就是除以σ。这个恒等式的推导基于Fenchel共轭和Moreau包络的性质我不展开证明了但实际中极其好用。举个例子。在图像去噪里经常g(v) λ||v||₁一次范数。它的共轭是g*(w) δ_{[-λ, λ]}(w)也就是在[-λ, λ]区间上的指示函数如果是向量就是无穷范数球的指示函数。所以prox_{σg*}(y)就是把y投影到[-λ, λ]区间上即clamp操作。用Moreau分解也能得到同样的结论但直接记住这些常见对偶近端算子的结果更方便。下表是常用函数及其对偶近端原始函数g(v)共轭函数g*(w)prox_{σg*}(y)λ||v||₁δ_{[-λ,λ]}(w)clamp(y, -λ, λ)δ_C(v)σ_C(w)支撑函数y - σ·P_C(y/σ)(λ/2)||v||²(1/(2λ))||w||²y / (1 σ/λ)(1/2)||v - b||²⟨w,b⟩ (1/2)||w||²(y - σb)/(1 σ)有了这些闭式公式PDHG的每一步更新都只需要做加法和逐元素运算没有内层迭代非常快。3.5 和ADMM的关系别搞混了很多同学学了ADMM之后再看PDHG觉得两者很像但其实关系要分几个层面说。ADMM针对的问题形式是min f(x) g(z), s.t. Ax Bz c核心手段是引入辅助变量并构造增广拉格朗日然后用交替方向依次更新x、z和对偶变量。ADMM的一大优点是稳定但代价是如果A或B对应的子问题没有闭式解需要内层迭代。PDHG针对的问题形式是min f(x) g(Ax)不需要显式约束需要的是A的伴随算子A^T以及f和g的近端算子这要求更低。另一个实际区别ADMM更新里有二次罚项相当于每步解一个带强凸扰动的近端问题PDHG没有增广项而是靠步长τ、σ来控制发散风险。所以PDHG的实现更简洁对内存的占用也更少特别适合变量维度极高的成像问题。但也要强调ADMM在很多带等式约束的问题上仍然有不可替代的优势两者不是简单的谁替代谁。4. 收敛条件与参数选择τσ‖A‖² 1背后是什么4.1 收敛定理和证明思路Chambolle-Pock的收敛定理大致是这样如果f和g都是闭凸函数且问题存在鞍点取0≤θ≤1步长τ和σ满足τσ‖A‖² 1那么序列(x^k, y^k)收敛到某个鞍点。更进一步对任意下标N有|f( x̄_N ) g(A x̄_N) - (f(x*) g(Ax*))| ≤ C / N其中C是与初值和步长有关的常数x̄_N是前N步迭代的加权平均。也就是说在目标函数值上收敛速度是O(1/N)。这个界对一般凸问题是最优的。证明思路用到了“部分对偶”和“势能函数”的技巧。构造一个关于(x^k, y^k)的二次型势能函数E_k (1/(2τ))||x^k - x*||² (1/(2σ))||y^k - y*||²然后证明每一步迭代都会让这个势能函数下降一定量下降量正比于当前点与鞍点之间的原始对偶间隔。这里τσ‖A‖² 1的作用就是保证某个中间矩阵是正定的从而让“下降量”总是正的。如果取τσ‖A‖² ≥ 1这个矩阵可能出现零特征值甚至负特征值算法很可能震荡甚至发散。4.2 谱范数‖A‖怎么算从理论到工程“‖A‖”在这里指A从L²空间到L²空间的算子范数即‖A‖ sup_{‖x‖≠0} ‖Ax‖ / ‖x‖。理论分析上很多算子需要你自己推导这个界工程实践中至少有三种方法第一种从算子定义出发推导。比如图像处理里的前向差分梯度算子∇在二维图像上有经典结论‖∇‖² ≤ 8也就是‖∇‖ ≤ 2√2。这个界来自离散梯度算子和Laplacian特征值的估计用起来很方便。第二种用幂迭代法数值估计。把一个随机向量反复乘A^T A并归一化收敛到的最大奇异值就是‖A‖²的估计。这个方法对任何线性算子都通用代价是要多做几次矩阵向量乘但PDHG每次迭代本来就要做A和A^T的乘法所以在预处理阶段跑几十步幂迭代成本完全可接受。第三种如果A是结构化的如傅里叶变换、小波变换可以用Parseval定理等工具算精确值比如傅里叶变换的算子范数就是1。以图像梯度算子为例说明怎么用更有意思。假设图像大小m×n则梯度算子输出两个m×n的矩阵其实可以把梯度算子看成一个2mn×(mn)左右的矩阵但没人真的存储它只是在算法里用“差分边界处理”来实现乘法。计算谱范数的严格上界是2√2工程上更安全的做法是开头跑一步幂迭代然后乘以0.99或0.95留个余量。4.3 步长τ和σ的选法理论上只需要τσ‖A‖² 1但τ和σ分别取多少直接影响收敛速度。最常见的选择有两种对称取法τ σ 1/‖A‖此时τσ‖A‖² 1刚好在边界上。为了安全通常取τ σ 0.99/‖A‖。这个取法的好处是代码简单收敛曲线比较平滑。非对称取法当f或g的光滑性层级不同时可以一个取大一个取小。比如f是二次光滑函数τ可以适当放大g包含指示函数约束σ保持适中。经验法则如果迭代中出现振荡特别是对偶变量y反复跳动先不要动步长检查是不是‖A‖估小了步长直接乘以0.5看是否稳定。如果稳定了说明问题出在谱范数估计上而不是目标函数本身。还有一些自适应的变体比如预条件PDHG它把固定步长换成随坐标变化的预条件矩阵收敛速度能明显提升但对初学来说先把固定步长吃透更重要。4.4 原始-对偶间隔怎么判断收敛了在实际代码里不能去算真实误差因为鞍点解是未知的。一个靠谱的替代指标是原始-对偶间隔duality gapGap(x, y) [f(x) g(Ax)] - [ -f* (-A^T y) - g*(y) ]注意到由弱对偶f(x) g(Ax) ≥ -f*(-A^T y) - g*(y)所以Gap非负。当Gap趋近0时说明当前(x,y)离鞍点非常近。这个指标好在它严格、可以计算不需要知道真实解。但坏处是如果数值误差控制不好Gap可能不会严格降到0。工程上常用做法是记录Gap的对数值看到它下降缓慢了就判定收敛。另一个简单粗暴的判据是看x相邻迭代的变化量||x^(k1) - x^k||是否小于某个绝对阈值。但在慢收敛场景下这个判据可能误报早停所以还是用Gap更严谨。5. 实战用PDHG做图像全变分去噪5.1 问题建模图像去噪的全变分TV模型是PDHG最经典的例子之一。观测图像u₀含噪声想恢复原图u目标函数min_u 1/2||u - u₀||² λ·TV(u)TV(u)可以取各向异性形式TV(u) ∑_i |(∇u)_i|也可以取各向同性形式TV(u) ∑_i √[ ((∇u)_i^x)² ((∇u)_i^y)² ]。为方便推导下面用各向同性TV但它对应的对偶投影跟各向异性稍有不同代码里我会写清楚。把它映射到min f(x) g(Ax)的框架需要把定义里的分离变量设成u作为原始变量对应xf(u) (1/2)||u - u₀||²A ∇即梯度算子g(v) λ∑_i ||v_i||其中v_i是每个像素处的梯度向量这样g(Au) λ∑_i ||(∇u)_i|| λ·TV(u)完美匹配。5.2 对偶近端各向同性TV的投影看对偶变量的更新。g(v) λ∑_i ||v_i||其中每个v_i是二维向量。它的共轭是g*(p) δ_E(p)其中E { p : ||p_i|| ≤ λ, ∀i }这就是逐像素约束在每个位置上的对偶变量长度不超过λ。所以prox_{σg*}(p)就是对每个像素把p_i投影到半径为λ的圆盘内p_i ← p_i · min(1, λ / ||p_i||)如果选各向异性TV则约束变成每个分量|p_i^x|≤λ且|p_i^y|≤λ投影变成分别对每个分量做clamp这就是两种TV在对偶形式层面的区别。代码实现时注意这一点避免各向异性的模型写了各向同性的投影。5.3 Python实现我写一个完整的Python实现用numpy做矩阵运算用梯度算子的前向差分和它的负伴随散度算子。注意边界条件用Neumann边界也就是图像边界外补零复制这在成像问题里是标准操作。import numpy as np import matplotlib.pyplot as plt def gradient(u): 前向差分梯度Neumann边界条件 输入u: (H, W)灰度图 返回(gx, gy)每个都是(H, W)形状 gx np.zeros_like(u) gy np.zeros_like(u) gx[:-1, :] u[1:, :] - u[:-1, :] gy[:, :-1] u[:, 1:] - u[:, :-1] return gx, gy def divergence(px, py): 梯度算子的负伴随即散度算子 对应gradient的转置操作 dx np.zeros_like(px) dy np.zeros_like(py) dx[1:, :] px[1:, :] - px[:-1, :] dx[0, :] px[0, :] dy[:, 1:] py[:, 1:] - py[:, :-1] dy[:, 0] py[:, 0] return dx dy def prox_f(u, u0, tau): f(u) 0.5*||u - u0||^2 的近端算子 闭式解: (u tau*u0)/(1tau) return (u tau * u0) / (1.0 tau) def project_ball(px, py, lam): 逐像素投影到半径为lam的L2球内 用于各向同性TV的prox_{sigma*g*} norm np.sqrt(px**2 py**2) # 避免除零norm为0的位置不需要投影 mask norm lam scale np.ones_like(norm) scale[mask] lam / norm[mask] return px * scale, py * scale def pdhg_tv(u0, lam0.1, tau0.25, sigma0.25, theta1.0, max_iter300): PDHG求解TV去噪问题 参数: u0: 带噪声的观测图像 lam: TV正则系数 tau, sigma: 原始和对偶步长需满足 tau*sigma*||gradient||^2 1 theta: 外推参数通常取1 max_iter: 最大迭代次数 u u0.copy() ubar u.copy() px np.zeros_like(u0) py np.zeros_like(u0) gap_history [] obj_history [] for k in range(max_iter): # 1. 原始更新 gx, gy gradient(ubar) # u u - tau * (A^T p)注意梯度算子的伴随是负散度 v u - tau * divergence(px, py) u_new prox_f(v, u0, tau) # 2. 外推步 ubar u_new theta * (u_new - u) # 3. 对偶更新 gx, gy gradient(ubar) px px sigma * gx py py sigma * gy px, py project_ball(px, py, lam) # 记录目标函数值每10轮打印一次 if k % 10 0 or k max_iter - 1: gx, gy gradient(u) tv_val lam * np.sum(np.sqrt(gx**2 gy**2)) data_term 0.5 * np.sum((u - u0)**2) obj data_term tv_val obj_history.append(obj) # 原始-对偶间隔简化版 gap np.abs(obj_history[-1] - (-np.sum(u0 * u) 0.5 * np.sum(u0**2)) ) \ if False else None u u_new return u, np.array(obj_history) # 生成测试数据一个简单方块图加高斯噪声 np.random.seed(42) H, W 128, 128 u_true np.zeros((H, W)) u_true[40:90, 45:85] 0.8 u_true[60:75, 65:75] 0.3 noise 0.15 * np.random.randn(H, W) u0 np.clip(u_true noise, 0, 1) u_denoised, hist pdhg_tv(u0, lam0.1, tau0.25, sigma0.25, max_iter300) print(迭代完成最终目标函数值:, hist[-1])这段代码有几个点要解释。第一divergence函数是gradient的负伴随这一步是从“A^T y”项里来的。在梯度算子用前向差分定义时它的伴随作用在东西上返回的正好是负的散度。如果这里符号搞反了迭代直接发散这是初学者最常踩的坑。第二各向同性TV的对偶投影是逐像素的L2球投影不是两个分量分开做clamp。第三步长我取τσ0.25因为梯度算子谱范数上界是2√2≈2.83所以τσ‖A‖² 0.25² × 8 0.5 1满足收敛条件。5.4 运行效果和参数调优在实际运行这个代码时你会发现目标函数值在前几十轮快速下降后面就慢慢趋平。如果λ取太大图像会被磨得很平细节全没了λ取太小噪声根本去不干净。我试了几组λ值的心得是λ在0.05到0.15之间对高斯噪声比较合适σ越大去噪越激进但可能过平滑σ越小保留细节越多但噪声残留严重。一个更省心的做法是先用大λ跑一遍看整体轮廓再逐步降λ微调细节。PDHG的好处是每次迭代代价很低跑几百轮也就一两秒的事所以可以多试几组参数再选合适的。想要更好的效果还可以在目标函数里加一个数据保真项的权重让不同区域自适应但那是另一篇文章的内容了。6. 常见问题与排查实录6.1 迭代发散了图像变成彩色噪点这是PDHG最典型的失败模式。原因几乎都出在步长上τσ‖A‖² ≥ 1。排查顺序如下第一步确认收敛条件。如果梯度算子谱范数估计不准去跑一个幂迭代或者把当前τ和σ各乘以0.5试试。第二步检查伴随算子实现。A和A^T必须满足Au, v u, A^T v用一个小随机向量验证否则算法在数学上就是错的。第三步检查prox是不是写错了尤其是投影函数里的半径λ如果投影半径写错对偶更新方向就错了。我在教学过程中发现90%的“参数不好调”问题其实不是参数问题而是代码里A^T实现错了。用一个小尺寸的随机数组手算一次梯度散度的数值对比代码输出五分钟就能定位。6.2 收敛很慢目标函数几乎不动这种通常是参数比例失衡。τ和σ一个太大一个太小。当你发现原始变量x迭代像“蜗牛爬”但对偶变量y跳得飞快大概率σ太大τ太小反之亦然。尝试让τ和σ保持在同一数量级或者用自适应预条件版本比如对角线缩放预条件器能显著加速。另一个原因是目标函数本身病态。如果f很“平坦”梯度信息弱算法要绕很多圈才能走到鞍点。这时可以考虑把f改成强凸版本比如加一个小的二次正则项这在某些场景下反而更快。6.3 原始-对偶间隔不下降理论上Gap一定收敛到0但如果你的问题没有鞍点或者数值误差把Gap卡在某一个值附近就需要检查问题是否严格可行。比如约束集合是空集鞍点不存在那算法会在某个边界来回震荡。更常见的是投影操作里数值裁剪太激进比如把很小的数直接裁剪成0导致Gap长期停滞在上界。解决办法是用更高精度的浮点类型或者调整收敛判据不再追求Gap到0而是看相对变化率。6.4 内存不足因为A矩阵被显式存储了PDHG在成像问题上最大的优势之一就是你永远不需要存储A矩阵只需要实现A和A^T的乘法算法。如果初学者把A写成显式稀疏矩阵甚至稠密矩阵到图像尺寸大一点就会爆内存。正确的做法永远是写成函数在内部做差分、变换、采样等操作。这条建议适用所有算子分裂类算法。6.5 常见问题速查表现象可能原因排查/解决办法迭代几步后发散τσ‖A‖² ≥ 1验证谱范数将步长乘0.5x更新很慢y跳得快τ太小或σ太大调整τσ比例通常保持数量级一致目标函数单调下降但最终值不对prox实现错误用简单场景对照闭式解验证Gap卡在常数不降投影/裁剪数值误差或步长过小提高精度增大步长并确保收敛条件图像出现棋盘格梯度/散度边界条件不一致检查Neumann边界实现是否正确7. 收尾一些实际操作中的体会PDHG用到现在给我的感觉是“易上手难精”。易上手是因为迭代格式只有三行轮子搭起来非常快难精是因为你对问题结构理解得越深越能把步长、预条件、外推参数调得更漂亮。我自己踩过的坑里印象最深的是伴随算子的一致性花了整整一个晚上调试发散最后发现是梯度算子边界条件的符号错了。所以强烈建议写任何一个新问题前先花五分钟用数值方法验证Au, v和u, A^T v相等这一步能省下后面无数排查时间。另外一个想特别提醒的点是PDHG不是银弹。遇到强凸高精度需求的问题或者带复杂耦合约束的问题ADMM和半光滑牛顿法仍然值得优先考虑。PDHG最好的应用场景是目标函数有结构、维度极高、精度要求中等的大规模问题尤其是成像和信号处理里的经典正则化问题。最后分享一个小技巧如果你要在实际项目里反复跑PDHG建议把谱范数估计、gap计算和参数调度都封装成通用模块下次换问题只需要换prox函数和A算子。我在自己的工具库里就是按这个模式组织的从图像去噪换到CT重建只需要复用那套外骨架改两个算子就完事。这也是PDHG这类算法在工程里最舒服的打开方式。
返回列表