ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

稀疏表示与字典学习:DCT/K-SVD、OMP/LASSO实战

稀疏表示与字典学习:DCT/K-SVD、OMP/LASSO实战

简介:稀疏表示主题的MATLAB实现包,面向信号处理、机器学习方向的学生与研究者,以及希望把稀疏编码理论落到代码中的开发者,可用于理解用少量非零原子组合表达信号、揭示数据内在结构的基本思路。压缩包为rar格式,共6个文件,均为m脚本,整体约4KB,体量轻便,内容围绕主流程脚本、主成分分析、L1范数约束求解、样本读取、准确率计算与预处理等环节组织,便于按模块阅读和调试。已有6567人学习下载,说明其在稀疏表示入门与代码复现方面具有一定参考价值。材料涉及LASSO、BPDN等稀疏优化问题,以及字典学习与K-SVD迭代思想,并延伸到图像去噪、压缩感知重建等典型应用;读者可据此搭建实验框架,对照算法步骤理解稀疏编码、字典更新与结果评估的实现细节,也可作为课程作业或科研预研的代码底稿。

1. 稀疏表示到底在解什么问题:一个 8×8 图像块为什么只用 5 个原子就能拼回来

稀疏表示这个词第一次进入视野时,场景很朴素:一个 8×8 的图像块有 64 个自由度,却只用字典里挑出的 5 个原子的加权和,就重建到肉眼几乎看不出差别。核心假设只有一句——信号在某个过完备字典下,可以用极少数原子的线性组合逼近;工程上就是求解min ||x||_0 s.t. ||Dx − y||₂ ≤ ε。它能解决的问题很具体:强噪声下的图像去噪、传感器丢点后的像素补全、采样率不足时的重建、特征降维前的字典学习。适合谁?做图像处理、压缩感知、稀疏编码方向,手里有 MATLAB,并且愿意把矩阵拆开看每一步在做什么的人。

2. 字典怎么来:DCT 过完备字典与 K-SVD 训练的最小可跑实现

稀疏表示里有条明确的分界线:字典是固定的还是学出来的,决定了你后面所有参数怎么调。固定字典不用训练、随时可用、可解释;学习型字典更贴合数据,但多了训练集构造、迭代次数、初始化三个新变量。两者我都用,选择标准大概是这样:数据量小于几千块、或者只是先验证思路,直接用 DCT 过完备字典;样本上万块、并且知道数据有固定纹理结构,再上 K-SVD。

2.1 过完备字典为什么能稀疏:正交基给不了的自由度

正交基(DCT、小波)的列数等于行数,M = N,任何信号只有唯一一组系数,稀疏与否全看运气——一个只含两条竖边的块,在 DCT 域里系数会铺开一大片。把字典做成 M×N 且 N > M,方程组欠定,解有无穷多个,才有可能从中挑出系数最稀疏的那一个。这就是稀疏表示的全部出发点:表达方式变多,而不是让信号变小。

代价是相干性。定义mu = max_{i≠j} |dᵢᵀdⱼ| / (||dᵢ||·||dⱼ||),mu 越大,两个原子越像,OMP 这类贪心算法越容易选错支撑集。我踩过的经验区间大致是:mu < 0.3 时 OMP 一般能收敛到合理支撑;0.3~0.6 之间要降 K、加正则或者换 LASSO;超过 0.7 还硬用 OMP,现象就是反复选同一批原子、重建出块状伪影。字典冗余度 N/M 一般取 2~4,再往上稀疏度的收益递减,而相干性和内存开销是线性上涨的。所以「字典越大越好」是个错觉,冗余度 4 之后我基本不再往上加。

另一个常被忽略的点是字典的列必须归一化。稀疏惩罚的是系数幅值,不是原子的能量;如果一个原子的范数是另一个的 3 倍,OMP 的内积排序就会系统性地偏向它,跟真正的相关性无关。这一步漏掉,后面所有调参都是在错误的基础上做的。

2.2 用二十行 MATLAB 构造一个 DCT 过完备字典

先上构造代码。这里用 DCT-II 基在频域上复制 R 份、每份加不同偏移的方式构造冗余字典,简单、可复现,不依赖任何工具箱。

function [D, mu] = dct_dict(M, R) % 生成 M x (R*M) 的过完备 DCT 字典 % M : 原子长度(8x8 的 patch 就是 64) % R : 冗余度倍数,常用 2~4 % D : 列归一化的字典 % mu: 相干性指标,越接近 0 越好 N = R * M; D = zeros(M, N); for r = 0:R-1 for k = 0:M-1 col = r*M + k + 1; basis = cos(pi * k * (0:M-1)' / M); % DCT-II 的一列 if k == 0 basis = basis / sqrt(2); % DC 分量归一化 end D(:, col) = basis; end end D = D ./ sqrt(sum(D.^2, 1)); % 列归一化,必须做 G = abs(D' * D); % Gram 矩阵 G(1:size(G,1)+1:end) = 0; % 去对角线 mu = max(G(:)); end

逻辑说明:外层r控制冗余副本,内层k遍历频点,cos(pi*k*(0:M-1)'/M)就是 M 点 DCT-II 的第 k 个基向量。k == 0时基向量是常数向量,除以sqrt(2)保证归一化后能量一致。最后一行算的是最大非对角相关系数,也就是前面说的 mu,后面调参时会反复用到它。

参数怎么改:M跟 patch 尺寸绑定,8×8 就是 64,16×16 是 256,后者对细节的拟合更好但求解慢一个数量级;R建议从 2 起步,先跑通再试 4。用sqrt(sum(D.^2,1))而非vecnorm,是为了兼容没有该函数的 MATLAB 版本,两者等价。

构造完别急着用,先做一次合成验证:随机造一个 K-稀疏的系数向量,乘出观测,再用 OMP 反解,看支撑集命中率和相对误差。这一步花 30 秒,能省掉后面两小时的排查。

M = 64; R = 4; K = 6; [D, mu] = dct_dict(M, R); N = size(D, 2); x0 = zeros(N, 1); supp = randperm(N, K); x0(supp) = randn(K, 1); y = D * x0; % 无噪合成观测 [xh, sh] = omp_solve(D, y, K, 1e-8); fprintf('mu=%.3f 命中 %d/%d 相对误差 %.2e\n', ... mu, numel(intersect(sh, supp)), K, norm(D*xh - y)/norm(y));

如果命中率不是满分,先别怀疑算法,八成是字典没归一化或者 K 设得比真实稀疏度小。相对误差应当落在 1e-12 量级(无噪、K 准确的前提下),做不到就说明支撑集错了。

2.3 K-SVD 训练字典:训练块怎么切、迭代怎么停

DCT 字典的问题是跟数据无关。医疗影像的纹理和工业表面缺陷的纹理完全不同,拿同一个 DCT 字典去逼近,稀疏度会明显变差。K-SVD 的思路是交替优化:固定字典做稀疏编码,再逐个原子更新(用残差的 SVD 第一主成分替换原子,同时更新对应系数)。这套流程没有随机性,结果可复现。

训练集的构造比算法本身更容易出事。我一般这么做:从训练图上以步长 1 抽 8×8 块,每块减去自身均值(这个均值单独存下来,重建时加回去),再除以所有块的全局标准差。千万注意是「全局」而不是「逐块」——逐块除以自身标准差,等于把噪声块放大成边缘块,训练出来的字典全是噪声结构。块数控制在 2 万到 5 万之间,太少字典过拟合训练图,太多训练时间线性上涨但收益很小。

function [D, X] = ksvd_train(Y, K, nIter, D0) % Y : M x P 训练样本,每列一个 patch(已去均值、已按全局 std 缩放) % K : 每列编码的稀疏度 % nIter: 外层迭代次数,10~15 足够 % D0 : 初始字典,建议用 dct_dict(M, 2) [M, P] = size(Y); if nargin < 4 || isempty(D0) D0 = dct_dict(M, 2); end D = D0; X = zeros(size(D, 2), P); for it = 1:nIter % ---- 1) 稀疏编码:固定 D,逐列求稀疏系数 ---- for p = 1:P X(:, p) = omp_solve(D, Y(:, p), K, 1e-6); end % ---- 2) 字典更新:逐原子重估 ---- for j = 1:size(D, 2) idx = find(X(j, :) ~= 0); % 用到第 j 个原子的样本 if isempty(idx), continue; end Ej = Y(:, idx) - D * X(:, idx) ... + D(:, j) * X(j, idx); % 剔除第 j 个原子的贡献 [U, S, V] = svd(Ej, 'econ'); D(:, j) = U(:, 1); % 新原子,自动单位范数 X(j, idx) = S(1,1) * V(:, 1)'; % 同步更新对应系数 end fprintf('iter %d, 平均非零系数 %.2f\n', it, mean(sum(X ~= 0, 1))); end end

逻辑说明:第 2 步的Ej是关键,它把当前原子对残差的贡献减掉,剩下的残差矩阵做 SVD,第一左奇异向量作为新原子,第一奇异值乘第一右奇异向量作为新系数。这样每更新一次原子,重建误差单调不增。注意D(:, j) = U(:, 1)天然满足单位范数,所以不需要额外归一化——这也反过来说明,只要初始字典归一化了,后续永远保持归一化。

参数怎么改:K取 4~8,8×8 块的经验值;nIter10~15,超过 15 之后平均非零系数和重建误差基本不动;D0一定用 DCT 初始化,随机初始化的 K-SVD 收敛慢且容易退化到只用少数几个原子。P超过 5 万时把内层for换成parfor(需要并行工具箱),加速比大概 4~6 倍。

参数建议取值影响
M(原子长度)64(8×8)越大细节越好,求解复杂度 M²N
N(字典大小)2M~4M越大越稀疏,相干性同步升高
K(稀疏度)4~8过小欠拟合,过大开始拟合噪声
nIter10~15超过后收益迅速衰减
P(训练块数)2e4~5e4影响泛化,不线性提升

3. OMP 还是 LASSO:两条求解路线的 MATLAB 手写实现与选型

字典搭好之后,问题变成「给定 D 和 y,怎么把 x 求出来」。0-范数最小化是 NP 难,工程上只有两条路:贪心(OMP、CoSaMP)和凸松弛(LASSO、基追踪)。前者快、无参数、支撑集明确;后者慢一点,但噪声下表现好得多。选错的代价很直接——用 OMP 去处理 SNR 低于 20 dB 的场景,重建结果会随噪声抖得一塌糊涂。

3.1 把 OMP 主循环拆成四步:内积选原子、最小二乘重估、残差更新

OMP 的每一步都能手算,这是它最大的好处:出错时你可以把中间量打印出来逐位核对,不像深度学习方法那样只能当黑匣子看。

function [x, supp] = omp_solve(D, y, K, tol) % 正交匹配追踪(OMP) % D : M x N 字典,约定每列已归一化 % y : M x 1 观测 % K : 最大稀疏度 % tol : 残差 2-范数阈值,达到即提前退出 % x : N x 1 稀疏系数 % supp: 被选中的原子下标(按选择顺序) [M, N] = size(D); x = zeros(N, 1); r = y; % 初始残差 = 观测本身 supp = zeros(K, 1); a = []; for k = 1:K c = abs(D' * r); % 1) 残差与全部原子的内积 if k > 1 c(supp(1:k-1)) = -inf; % 已选原子不重复入选 end [~, idx] = max(c); % 2) 相关性最大的原子 supp(k) = idx; a = D(:, supp(1:k)) \ y; % 3) 在支撑集上做最小二乘重估 r = y - D(:, supp(1:k)) * a; % 4) 更新残差 if norm(r) < tol break; end end supp = supp(supp > 0); x(supp) = a; end

逻辑说明:第 1 步的内积衡量残差与每个原子的相关性,归一化之后就是余弦相似度;第 2 步取最大者,是贪心的核心。第 3 步是最小二乘,也是最关键的一步——OMP 之所以叫「正交」,就是因为每轮都重新求解整个支撑集上的系数,保证残差与已选原子张成的子空间正交。第 4 步更新残差。如果不做第 3 步、只做单原子投影,那就是 MP(匹配追踪),收敛慢且系数不唯一,这是新手最常混淆的地方。

参数怎么改:K是硬上限还是提前停止的分界,取决于你信不信tol。无噪场景把tol设成 1e-8,让 K 起作用;有噪场景反过来,tol设成sqrt(M) * sigma(sigma 是噪声标准差),K 设成 2~3 倍预期稀疏度当兜底。M, N只影响复杂度,每轮内积是M*N次乘法,最小二乘是O(k²M),总共O(K·M·N)。64×256 的字典、K=6,单次求解在毫秒量级,足够对一张 512×512 图的 26 万块做处理(实际上要用重叠块加速,见第 4 章)。

注意:D(:, supp(1:k)) \ y用的是反斜杠最小二乘,MATLAB 会自动选择 QR 分解。支撑集里出现近共线原子时矩阵会病态,此时会出现警告或结果发散,这正是后面要查相干性的原因。

3.2 噪声一上来 OMP 就飘:LASSO 的软阈值写法

OMP 的贪心是「一旦选了就不撤销」,噪声让内积排序出错时,错误原子会一直留在支撑集里,最终表现为重建图上出现不属于原图的纹理。LASSO 把 0-范数换成 1-范数,变成凸问题,没有「选了不可撤销」这回事:

min 0.5·||Dx − y||₂² + λ||x||₁

ISTA(迭代软阈值)是最容易手写的解法,每一步就是一次梯度下降加一次软阈值收缩:

function x = ista_lasso(D, y, lambda, nIter) % 迭代软阈值解 LASSO % lambda: 正则强度,越大越稀疏 % nIter : 迭代次数,通常几百到几千 L = norm(D, 2)^2; % 梯度步长上界 = 最大奇异值平方 x = zeros(size(D, 2), 1); for k = 1:nIter g = D' * (D * x - y); % 数据项梯度 x = soft_thresh(x - g / L, lambda / L); end end function s = soft_thresh(v, tau) % 软阈值算子 S_tau(v) = sign(v)·max(|v| - tau, 0) s = sign(v) .* max(abs(v) - tau, 0); end

逻辑说明:L = ||D||₂²是数据项梯度的 Lipschitz 常数,步长取1/L保证收敛,不需要手调学习率。soft_thresh把小于阈值的系数直接压成 0,这是 1-范数产生稀疏性的机制——它不像 OMP 那样显式选原子,而是让不重要的系数自己归零。lambda决定稀疏程度,lambda越大,非零系数越少。

选型判据我给三条:噪声明显(SNR < 25 dB)、字典相干性偏高(mu > 0.4)、或者你根本不知道真实稀疏度 K——这三条中任意一条成立,就别用 OMP。反过来,需要明确支撑集(比如做故障诊断要指出是哪几个原子对应哪类故障)、数据量极大要快、或者做教学演示,OMP 更合适。ISTA 的缺点是收敛慢,nIter上千是常态,实务中会换 FISTA,第 6 章给代码。

3.3 停止条件与复杂度:K、tol、迭代上限怎么定

这三类参数是新手最容易糊弄过去的,但它们的取值直接决定结果好坏。

K(稀疏度)——不要凭感觉。合法做法是先做一次无约束最小二乘或伪逆,看系数幅值的衰减曲线,取「幅值掉到噪声水平」的位置作为 K 的量级。8×8 块的图像任务里,K = 6 是个保守起点。

tol(残差阈值)——和噪声直接挂钩。观测噪声标准差是 sigma 时,M 维残差的期望范数是sqrt(M)*sigma,所以tol = 1.1 * sqrt(M) * sigma比较合理。设太小会逼着算法用噪声去拟合观测,重建反而更脏;设太大会欠拟合,块内出现平斑。

迭代上限——OMP 就是 K;ISTA 用相对变化量判停比固定次数更稳:norm(x_new - x_old) / norm(x_old) < 1e-4就退出。

方法单次复杂度需要 K抗噪支撑集明确
OMPO(K·M·N)是弱是
ISTA/LASSOO(nIter·M·N)否强否
FISTAO(nIter·M·N),nIter 更小否强否

4. 三类落地:图像去噪、缺失像素修复与压缩感知重建的完整脚本

原理讲完就该动手了。这三类任务共用同一套「字典 + 求解器」,差别只在观测模型:去噪是y = x + n(观测满维但有噪)、修复是y = M ⊙ x(观测有缺)、压缩感知是y = Φx(观测维度比信号低)。看清楚这个差别,代码就能复用。

4.1 分块稀疏去噪:重叠块、均值分离与加权融合

整幅图直接做稀疏表示是不现实的,512×512 就是 26 万维,字典根本存不下。标准做法是切成重叠小块,逐块稀疏编码,再按位置加权平均拼回原尺寸。重叠的意义在于抑制块边界的不连续,代价是计算量翻几倍,一般重叠一半(步长 = patch/2)就够。

%% 分块稀疏去噪:训练好的 D + OMP patch = 8; K = 6; sigma = 0.1; step = 1; tol = 1.15 * sqrt(patch^2) * sigma; % 与噪声水平挂钩的残差阈值 noisy = img + sigma * randn(size(img)); acc = zeros(size(noisy)); % 累加重建结果 wsum = zeros(size(noisy)); % 累加权重 win = ones(patch); % 矩形窗,可换高斯窗 for i = 1:step:(size(noisy,1)-patch+1) for j = 1:step:(size(noisy,2)-patch+1) blk = noisy(i:i+patch-1, j:j+patch-1); mu = mean(blk(:)); % 单独处理 DC 分量 y = blk(:) - mu; x = omp_solve(D, y, K, tol); % 稀疏编码 rec = reshape(D * x + mu, patch, patch); acc(i:i+patch-1, j:j+patch-1) = acc(i:i+patch-1, j:j+patch-1) + win.*rec; wsum(i:i+patch-1, j:j+patch-1) = wsum(i:i+patch-1, j:j+patch-1) + win; end end denoised = acc ./ max(wsum, eps); % 加权平均还原 psnr_v = psnr(denoised, img);

逻辑说明:mu是先减掉的块均值。DCT 字典的 DC 原子跟其他原子的相干性很高,不分离出来,OMP 会花掉一个稀疏度名额去拟合亮度,剩下的名额就不够表达纹理了。acc/wsum是标准的重叠块加权融合,max(wsum, eps)防除零。win换成二维高斯窗能进一步压制块效应,代价是边缘的权重变小。

参数怎么改:sigma必须知道或者估出来,估不准就把tol设成1.5*sqrt(64)*sigma_hat,宁可欠拟合也别过拟合。K在去噪任务里不宜超过 8,因为噪声会被当成稀疏结构编码进去。step取 1 最慢最好,取patch/2是常见的折中。8×8 块、512×512 图、K=6,单核大概跑十几秒,可接受。

4.2 观测不全时的修复:mask 版 OMP 与压缩感知重建

像素丢失的建模方式很直接:只有观测到的位置参与残差计算,没观测到的位置不贡献误差。实现上就是把字典和观测同时按 mask 取行,其它一行不改。

function x = omp_masked(D, y, mask, K, tol) % 带观测掩膜的 OMP:只在 mask==true 的位置计算残差 % 注意:这里不要对 Dm 再归一化,否则会改变原子的相对权重 Dm = D(mask, :); % 只保留有观测的行 ym = y(mask); x = omp_solve(Dm, ym, K, tol); end

逻辑说明:mask是 logical 向量,D(mask, :)按行取子集,相当于把观测算子Φ塞进了字典里。这个过程有个前提——字典在「被采样后的空间」里仍然是过完备的,也就是sum(mask) < N要成立。丢失率超过 60% 之后,修复质量会明显下滑,70% 基本是这类方法的边界。

压缩感知是同一个思路的正向版本:不丢像素,而是用比信号维度更少的随机测量。此时等效字典是A = Φ·Ψ,Ψ是稀疏基。

n = 256; m = 96; % 信号 256 维,只测 96 次 Phi = randn(m, n) / sqrt(m); % 高斯测量矩阵 Psi = dctmtx(n)'; % 正交 DCT 基,列即原子 A = Phi * Psi; % 等效字典 A = A ./ sqrt(sum(A.^2, 1)); % 归一化后再求解 s = zeros(n, 1); supp = randperm(n, 10); % 合成一个 10-稀疏信号 s(supp) = randn(10, 1); y = A * s; % 观测 sh = omp_solve(A, y, 12, 1e-6); % K 给一点余量 xh = Psi * sh; % 回到原域 fprintf('CS 重建相对误差 %.2e\n', norm(xh - Psi*s)/norm(Psi*s));

逻辑说明:A = Phi*Psi是「测量算子 + 稀疏基」的复合,稀疏表示在这个复合字典上做。归一化这一步不能省,高斯随机矩阵乘上 DCT 基之后,各列范数不一致,不归一化 OMP 会偏向范数大的原子。K给 12 而不是 10,是给噪声和数值误差留余量,这是压缩感知里很常见的做法。

4.3 参数表:patch 尺寸、K、lambda、字典规模怎么配

下面这张表是这些年反复试出来的一组起点,不是最优解,但照着填至少不会一开始就翻车。

任务patch字典规模Ktol / lambda备注
图像去噪(sigma=0.1)8×864×2566tol=1.15·√M·sigma先减块均值
图像去噪(sigma=0.05)8×864×2564同上,sigma 换掉K 要跟着降
像素修复(丢失 30%)8×864×2568tol=1e-6mask 版 OMP
压缩感知(m/n=0.4)整段n×n DCT稀疏度+2tol=1e-6等效字典要归一化
纹理分类特征16×16256×51210训练用 LASSOK-SVD 训练

5. 避坑与排查:五个让重建结果直接报废的细节

这一章的内容基本都是血泪经验,每条都对应一个曾经让我对着屏幕怀疑人生的现象。

5.1 重建图整体发灰、细节全丢:字典列没归一化

现象:PSNR 比噪声图还低,重建结果像被低通滤波过一遍,块内几乎没有纹理。

原因:字典没有列归一化,或者训练时归一化了、推理时换了另一种。稀疏惩罚的是系数幅值,字典列范数不一致时,OMP 的内积排序和 LASSO 的软阈值都会系统性偏移,最终所有块都只用少数几个范数大的原子表达。

解决:字典构造和训练之后各加一行D = D ./ sqrt(sum(D.^2,1));如果训练和推理分成两个脚本,把归一化写进读取字典的函数里,别靠人记。K-SVD 更新出的原子天然单位范数,不需要额外处理,这一点可以用来反查是不是自己写错了。

5.2 块边界出现棋盘格:DC 分量没分离

现象:放大看,8×8 的网格线清晰可见,块与块的交界处有台阶。

原因:没有减块均值,DC 原子占掉了一个稀疏度名额,纹理表达被压缩;同时相邻块的均值不同,重建后块间亮度跳变,重叠区域平均也压不住。

解决:块内减均值再编码,重建时加回去。在上面的去噪脚本里就是mu = mean(blk(:))那两行。另一种做法是把 DC 原子单独拿出来作为第 1 列、永不参与 OMP 选择,效果等价但代码更绕。

5.3 去噪越去越脏:K 设得比信号稀疏度大太多

现象:sigma 从 0.1 加到 0.15,去噪结果的 PSNR 不升反降,图上出现原本不存在的斑点纹理。

原因:OMP 的 K 是硬上限,只要没到tol就一直选原子,噪声在字典里总能找到相关性不错的方向。K=12 的时候,一个 64 维块用 12 个原子拟合,参数量已经接近观测数,过拟合就发生了。

解决:去噪任务的 K 压到 4~8,同时把tol和噪声水平绑定。真正的判据不是 PSNR 涨没涨,而是残差范数是否接近sqrt(M)*sigma——如果远小于这个值,说明在拟合噪声。

5.4 内存爆掉、MATLAB 直接卡死:整幅图建字典

现象:跑压缩感知的脚本,刚执行到D' * D就报内存不足,或者风扇狂转到无响应。

原因:把整幅图当成一个原子来建字典,256×256 的图像就是 65536 维,字典哪怕只做 2 倍冗余也是 65536×131072,单精度存一次就是 34 GB。

解决:图像任务一律分块,patch 不超过 16×16;压缩感知里如果信号本身就是整段(比如一维光谱),字典规模控制在 n×4n 以内,并且用single精度。另外 Gram 矩阵只在算相干性时需要,算完立刻clear。

5.5 训练时好好的,换张图精度掉一半:训练集和测试集预处理不一致

现象:K-SVD 在训练图上重建误差 0.02,换到测试集就变成 0.15,肉眼可见结构错乱。

原因:训练时除以了全局标准差并减了块均值,测试时忘了做同样处理;或者训练用正态化、测试用 min-max 归一化。稀疏表示对尺度极其敏感,尺度一变,等价于换了一个字典。

解决:把预处理封装成一对函数preprocess_patch和postprocess_patch,训练和推理都调用同一份代码,绝不复制粘贴。另外把训练时的sigma_global存进字典的.mat里一起保存,加载时直接读。

6. 把 OMP 换成 FISTA:收敛速度、相干性与一个我常用的验收动作

ISTA 稳,但慢。当字典到 256×1024 这个量级、要对 26 万个块做处理时,ISTA 上千次迭代是跑不完的。FISTA 就是加了个动量项,同样的精度下迭代数能压到 ISTA 的三分之一到五分之一,代码只多五行。

function x = fista_lasso(D, y, lambda, nIter) L = norm(D, 2)^2; % Lipschitz 常数 x = zeros(size(D, 2), 1); z = x; t = 1; % 动量变量 for k = 1:nIter g = D' * (D * z - y); % 在动量点上算梯度 xn = soft_thresh(z - g / L, lambda / L); tn = (1 + sqrt(1 + 4*t^2)) / 2; z = xn + ((t - 1) / tn) * (xn - x); % 外推一步 x = xn; t = tn; end end

动量项(t-1)/tn的作用是把上一步的更新方向带进来,走出一点「惯性」,从而在平坦区域加速。注意梯度是在z而不是x上算的,这是 FISTA 和 ISTA 唯一的本质区别,写错这一点就等于白加。

换掉求解器之后,仍要确定lambda。我不喜欢网格搜索,太慢,常用的做法是先用lambda_max = max(abs(D'*y))定上界——这个值对应的解是全零,再取lambda = 1e-3 * lambda_max到1e-2 * lambda_max之间,观察非零系数个数,落在预期稀疏度的 1~1.5 倍就停。这个方法不用知道真值,也不依赖训练集。

验收动作只有一个:算字典的相干性,并且和上一次对比。

G = abs(D' * D); G(1:size(G,1)+1:end) = 0; mu = max(G(:)); fprintf('相干性 mu = %.3f,非零系数均值 %.1f\n', mu, mean(sum(abs(X) > 1e-6, 1)));

mu涨到 0.5 以上时,无论 OMP 还是 FISTA 的重建质量都会出现肉眼可见的退化。这时候不要调lambda,应该降字典冗余度或者加多样性约束——我吃过一次亏,死磕正则参数调了一整天,最后发现是训练集里同一类纹理的块占了 80%,字典自然向着那一类塌缩。重采样训练集之后,同样的参数下 PSNR 直接涨了 1.8 dB。

如果只让我给一条建议:先把mu和残差范数这两个数打印出来再谈调参,其余的都可以往后放。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进