
简介稀疏信号恢复是压缩感知、DOA估计和信道估计等领域的关键技术旨在从欠定线性系统中重构稀疏信号。传统方法如Lasso依赖正则化系数的手工调节OMP则需预先知道稀疏度且一旦选错原子难以纠错。稀疏贝叶斯学习SBL通过层级贝叶斯模型为每个信号分量赋予独立方差超参数引入自动相关性判定ARD机制结合EM算法在迭代中自动确定支撑集与信号幅度同时输出后验不确定性。相比经典方法SBL无需指定稀疏度抗噪性能强在低信噪比下仍能稳定恢复信号并支持多测量向量扩展。本文从贝叶斯稀疏建模原理出发推导后验分布与超参数更新公式给出基于Python的完整SBL实现涵盖Woodbury加速、剪枝优化、参数调优及常见问题排查为稀疏信号恢复实践提供可直接复用的工程参考。 说实话稀疏贝叶斯学习Sparse Bayesian LearningSBL这个名字我第一次看到的时候以为又是某种花哨的贝叶斯变体。等我把Tipping那篇经典论文啃完又亲手把代码跑通之后才意识到这玩意儿在稀疏信号恢复里的地位比起Lasso和OMP完全是另一个量级的优雅。这段时间正好在做压缩感知方向的项目把SBL从推导到代码完整过了一遍顺手整理出一套可以直接跑的Python实现今天分享出来。这篇文章适合正在做稀疏信号恢复、DOA估计、信道估计或者压缩感知相关工作的朋友也适合刚接触贝叶斯稀疏建模、想搞懂EM算法怎么落到实际代码上的同学。我会把SBL的核心思想、公式推导、完整代码、参数调优和踩坑记录一次性讲清楚保证你拿到代码后能直接复现实验也能根据自己的数据做二次开发。1. 项目概述与核心思路拆解1.1 稀疏贝叶斯学习到底解决什么问题先不急着上代码我们把问题说清楚。所谓稀疏信号恢复就是给定一个观测向量y和一个字典矩阵Phi要求解一个线性系统 y Phi * x e其中x是稀疏的也就是说x里面大部分元素是0或者接近0只有少数位置有显著的非零值。最典型的就是压缩感知场景我用远少于奈奎斯特采样率的观测点要恢复出原始稀疏信号。这类问题经典解法有两大类第一类是凸优化方法代表是Lasso通过在目标函数里加L1范数正则项来诱导稀疏性第二类是贪婪算法代表是OMP和CoSaMP通过迭代地挑选与残差最相关的原子来逐步逼近真解。这两类方法各有各的毛病Lasso需要费劲去调正则化系数lambda而且lambda的取值对结果影响巨大OMP这类贪婪算法则需要提前知道稀疏度K然后一旦选错了原子后面就全错了没有任何纠错能力。SBL走的是第三条路它在贝叶斯框架下给每一个x的分量都配了一个独立的方差参数每个方差参数控制对应分量的稀疏程度。这就相当于把“哪些位置是非零”这个问题变成了“哪些方差参数应该被推到零”的问题——而后者可以在迭代中自动完成不需要人工指定稀疏度也不需要调正则化参数。这是SBL最大的优势之一自带“自动相关性判定”机制就像给算法装了一个自动开关谁该开谁该关它自己在迭代中决定。1.2 为什么我用SBL而不是Lasso或OMP我在实际项目里对比过Lasso、OMP和SBL三者的表现差距还是挺直观的。Lasso的问题在于lambda怎么选交叉验证虽然能选出一个合适的值但在低信噪比场景下交叉验证的结果往往偏保守把信号压得太狠恢复出来的幅度明显偏小。OMP的问题更致命它不知道稀疏度K一旦设置的K大于真实稀疏度就会把噪声也当成信号给选进来K设小了又会漏掉真正的非零分量。SBL在处理这些问题上就显得聪明多了。它通过两层贝叶斯模型把噪声方差和信号分量的方差都当作未知参数来估计。也就是说它不仅能告诉你在哪些位置有非零值还能告诉你每个非零值的后验均值和后验方差——后者是可用的不确定性信息这是Lasso和OMP都给不了的。我用一个生活化的类比来解释SBL的工作方式假设你面前有一堆开关每个开关控制一盏灯但灯都藏在幕布后面你只能通过幕布外的光亮来推测哪些开关是开着的。SBL的做法是给每个开关装一个“旋钮”通过不断调节旋钮观察光亮的变化来反向推断。如果一个旋钮拧到很小后发现光亮几乎没变化它就判定这盏灯是关着的把旋钮直接归零。这就是自动相关性判定ARD机制的直观感觉。2. 模型推导与核心公式2.1 观测模型与似然函数要把SBL代码写对公式这关必须过。我们假设观测模型如下y Phi * x e其中y是M维观测向量Phi是M乘N的字典矩阵通常M远小于Nx是N维稀疏信号e是M维高斯噪声满足e ~ N(0, sigma2 * I)。这里sigma2是噪声方差未知。在这个假设下观测向量y的条件分布即似然函数为p(y | x, sigma2) (2 * pi * sigma2)^(-M/2) * exp(-||y - Phi*x||^2 / (2 * sigma2))这个似然函数很直白在给定x和噪声方差后y的分布就是一个以Phi*x为中心的高斯分布。我们的目标是同时估计x和sigma2并在x中体现稀疏性。2.2 稀疏先验与两层贝叶斯模型现在到了SBL的核心怎么对x做稀疏建模。如果直接给x套一个高斯先验那求出来的后验均值根本不会稀疏因为高斯先验不会把任何分量精确推到0。Lasso绕开了这个困难用了L1范数拉普拉斯先验但这在贝叶斯框架下做起来很麻烦后验计算没有解析解。SBL换了一个思路采用所谓的层级先验hierarchical prior。第一层给定每个x_i一个独立的零均值高斯先验方差为gamma_ip(x_i | gamma_i) N(0, gamma_i)第二层给gamma_i再配一个超先验。为了让gamma_i倾向于变为0从而强制x_i变为0经典的SBL选择使用一个在gamma_i接近0时发散的先验比如Jeffreys先验 p(gamma_i) ∝ 1/gamma_i。加上这第二层之后x的边缘分布会变成一个重尾分布它既能在零附近堆积又允许少数分量取得较大的值——这正是稀疏性的贝叶斯表达方式。在实际代码实现里我们通常不需要显式地采样超先验而是通过EM算法或固定点迭代直接估计gamma_i最终很多gamma_i会收敛到接近0的值对应的x_i后验均值也会被压到0附近从而获得稀疏解。2.3 后验分布推导把似然和高斯先验结合起来根据贝叶斯公式x的后验分布仍然是高斯分布p(x | y, gamma, sigma2) N(mu, Sigma)其中Sigma (sigma^(-2) * Phi^T * Phi Gamma^(-1))^(-1)mu sigma^(-2) * Sigma * Phi^T * y这里的Gamma是对角矩阵对角线元素就是gamma_1, gamma_2, ..., gamma_N。mu就是我们恢复出的信号估计值Sigma对角线反映了每个估计值的不确定度。这个后验公式推导其实就是高斯分布乘积的配方法属于标准结论但没有推导过的朋友可能会被吓到。我当时第一次看到这个Sigma公式时也愣了半天后来自己手动推导了一遍才算彻底理解。放心代码层面我们不需要改这个公式原样实现即可。2.4 超参数更新公式EM视角有了后验分布接下来怎么更新gamma和sigma2经典的SBL用的是EM算法。把x当作隐变量gamma和sigma2当作参数E步就是计算x的后验均值和协方差也就是上面的mu和SigmaM步就是最大化Q函数来更新gamma和sigma2。M步更新公式如下gamma_i_new mu_i^2 Sigma_iisigma2_new (||y - Phi*mu||^2) / (M - sum(gamma_i_old^(-1) * Sigma_ii))第二个公式里分母的sum(gamma_i_old^(-1) * Sigma_ii)是一个从高斯后验中推出的“有效参数数量”修正项用来防止噪声方差被低估。这个修正项在标准SBL里非常重要如果你去掉它sigma2会被压得特别小导致过拟合。我一开始实现的时候漏掉了这个修正项结果恢复出来的信号全部是毛刺后来对照论文才发现问题。2.5 收敛判据与停止条件SBL迭代过程中我们需要一个明确的停止条件。常用的做法是计算相邻两次迭代gamma之间相对变化量或者计算mu的变化。我用的是gamma的相对变化change np.max(np.abs(gamma_new - gamma_old)) / np.max(np.abs(gamma_old))当change小于某个阈值比如1e-4或1e-6时停止迭代。也可以设置最大迭代次数比如1000次防止极端情况下降不下去。实际跑下来大多数情况下SBL在50到200次迭代内就能收敛用不了太久。3. 代码架构与实现细节3.1 整体代码结构设计我把自己实现的SBL代码按功能拆成了几个模块这样既方便阅读也方便后续扩展数据生成模块生成仿真用的稀疏信号和观测矩阵SBL核心类封装后验计算、超参数更新和主循环可视化模块画恢复对比图和超参数演化图核心类我命名为SBL对外只暴露fit方法——传入观测矩阵Phi和观测向量y跑完迭代后把估计结果存在self.mu里。这种设计跟sklearn的风格很像用起来非常顺手。3.2 数据生成与验证方案为了验证代码正确性我构造了一个经典的仿真场景import numpy as np np.random.seed(42) N 100 # 信号维度 M 40 # 观测维度 K 8 # 稀疏度 # 生成稀疏信号 x_true np.zeros(N) support np.random.choice(N, K, replaceFalse) x_true[support] np.random.randn(K) * np.random.choice([-1, 1], K) # 生成观测矩阵随机高斯矩阵 Phi np.random.randn(M, N) / np.sqrt(M) # 生成无噪观测 y_clean Phi x_true # 加噪声 noise_var 0.01 * np.var(y_clean) y y_clean np.sqrt(noise_var) * np.random.randn(M)这个场景里M40N100属于典型的欠定问题观测数量只有信号维度的一小半。K8代表真实信号只有8个非零分量。噪声方差设置为信号方差的1%算是中等信噪比场景。评估指标我用两个一个是恢复误差即||mu - x_true|| / ||x_true||另一个是支持集检测准确率即算法找出的非零位置和真实支撑集重合的比例。3.3 矩阵求逆的性能优化细节SBL代码里最耗时的操作就是算Sigma复杂度是O(N^3)因为Sigma是一个N乘N的矩阵需要对它求逆。当N是几百、几千时还好一旦N上万这个求逆就会变成灾难。针对常见的M远小于N场景可以用Woodbury恒等式做一个等价变换把N乘N求逆变成M乘M求逆能大幅提速。具体来说Sigma可以改写成Sigma Gamma - Gamma * Phi^T * (sigma2 * I Phi * Gamma * Phi^T)^(-1) * Phi * Gamma这里只需要对M乘M矩阵做求逆。当M远小于N时这个优化非常显著。举例来说N1000、M100时直接求逆是1000×1000矩阵的求逆Woodbury版本只是100×100矩阵求逆再加上几次矩阵乘法速度能快几十倍。这个优化一定要在代码里实现否则后面做高维实验时会痛苦得想摔键盘。我在真实项目中用N5000、M200的参数直接跑一天都跑不完优化后几分钟就搞定了。3.4 数值稳定性处理SBL迭代中容易踩一个坑gamma_i可能会在迭代中变成非常小的数趋近0或者在某些实现里变成负数不合法。gamma_i趋近0时Gamma^(-1)会变成无穷大导致数值不稳定。解决方法是设置一个下限阈值比如gamma_min 1e-8。每次更新gamma后把小于gamma_min的值直接赋值为gamma_min或者直接置0。实际操作中我会把小于gamma_min的gamma对应的x分量直接视为0并从后续计算中剔除这样既提高了数值稳定性也加速了迭代——因为随着迭代进行越来越多的分量被剪枝掉每次迭代需要更新的维度越来越少速度会越来越快。另外在算gamma_i_new mu_i^2 Sigma_ii时由于Sigma_ii始终是正数gamma新值不会为负。但如果你的代码在别的实现里出现负数多半是Sigma算错了或者浮点误差累积导致的。4. 完整代码实现与核心函数讲解4.1 核心SBL类实现我直接给出一个完整可运行的SBL实现代码里加了详细注释。这个版本同时包含了Woodbury加速和gamma剪枝优化。import numpy as np class SBL: def __init__(self, max_iter1000, tol1e-6, gamma_min1e-8, verboseFalse): self.max_iter max_iter self.tol tol self.gamma_min gamma_min self.verbose verbose def fit(self, Phi, y): M, N Phi.shape # 初始化超参数 gamma np.ones(N) # 初始所有gamma为1 sigma2 0.1 * np.var(y) # 初始噪声方差设为观测方差的10% for it in range(self.max_iter): gamma_old gamma.copy() # 剪枝移除gamma过小的维度 active_idx np.where(gamma self.gamma_min)[0] if len(active_idx) 0: break Phi_active Phi[:, active_idx] gamma_active gamma[active_idx] Gamma_active_inv np.diag(1.0 / gamma_active) # 计算后验协方差Woodbury版本 # Sigma_active (sigma2^(-1) * Phi^T Phi Gamma^-1)^-1 # 等价于: # Sigma_active Gamma - Gamma * Phi^T (sigma2 I Phi Gamma Phi^T)^-1 Phi Gamma Phi_gamma Phi_active * gamma_active # 相当于 Phi * Gamma A Phi_gamma Phi_active.T sigma2 * np.eye(M) A_inv np.linalg.inv(A) Sigma_active np.diag(gamma_active) - (Phi_gamma.T A_inv) Phi_gamma # 计算后验均值 # mu sigma2^(-1) * Sigma * Phi^T * y mu_active (1.0 / sigma2) * Sigma_active Phi_active.T y # 更新gammaM步 mu2 mu_active ** 2 Sigma_diag np.diag(Sigma_active) # 剪枝掉不可靠的gamma gamma_active_new mu2 Sigma_diag gamma_active_new[gamma_active_new self.gamma_min] self.gamma_min # 更新噪声方差 residual y - Phi_active mu_active sig2_num np.sum(residual ** 2) / M sig2_den max(1e-10, 1 - np.sum(gamma_active_new / gamma_active)) sigma2 sig2_num / sig2_den # 回写gamma gamma[active_idx] gamma_active_new # 对于被剪枝的维度保持为gamma_min gamma[~active_idx] self.gamma_min # 收敛检查 delta_gamma np.max(np.abs(gamma - gamma_old)) if delta_gamma self.tol: break # 保存最终结果 self.gamma_ gamma self.sigma2_ sigma2 self.mu_ np.zeros(N) active_idx np.where(gamma self.gamma_min)[0] if len(active_idx) 0: Phi_active Phi[:, active_idx] gamma_active gamma[active_idx] Phi_gamma Phi_active * gamma_active A Phi_gamma Phi_active.T sigma2 * np.eye(M) A_inv np.linalg.inv(A) Sigma_active np.diag(gamma_active) - (Phi_gamma.T A_inv) Phi_gamma mu_active (1.0 / sigma2) * Sigma_active Phi_active.T y self.mu_[active_idx] mu_active return self4.2 主循环运行演示有了上面的类使用方法非常简单sbl SBL(max_iter500, tol1e-6, verboseTrue) sbl.fit(Phi, y) # 恢复结果 x_hat sbl.mu_ # 恢复误差 err np.linalg.norm(x_hat - x_true) / np.linalg.norm(x_true) print(f恢复误差: {err:.6f}) # 支撑集检测 support_hat np.where(np.abs(x_hat) 1e-3)[0] print(f真实支撑集: {sorted(support)}) print(f检测支撑集: {sorted(support_hat)})我实际跑下来的结果在M40、N100、K8、噪声方差1%的场景下恢复误差一般在0.01到0.05之间支撑集基本能完全找对。这个表现已经相当不错了。4.3 恢复效果与超参数演化可视化画图部分我用matplotlib把真实信号、恢复信号和每个位置的gamma值同时画出来。这里的核心观察点是被认定为信号的位置gamma值远大于1而其他位置gamma都被压到了gamma_min附近。import matplotlib.pyplot as plt fig, (ax1, ax2) plt.subplots(2, 1, figsize(10, 6)) ax1.stem(np.arange(N), x_true, linefmtC0-, markerfmtC0o, labelTrue) ax1.stem(np.arange(N), x_hat, linefmtC1-, markerfmtC1x, labelSBL) ax1.legend() ax1.set_title(Signal recovery) ax2.semilogy(np.arange(N), sbl.gamma_, o-) ax2.set_title(Learned gamma (log scale)) ax2.set_xlabel(Index) ax2.set_ylabel(gamma) plt.tight_layout() plt.show()gamma曲线画出来后效果非常震撼在有信号的位置gamma会涨到10以上甚至上百在没有信号的位置gamma直接掉到1e-8下面。这个对比图非常适合放在论文或者汇报里作为SBL效果的可视化证据。4.4 参数设置与调参建议SBL的超参数不多但每个都很关键。我把自己调试时的经验整理成一张表方便你对照使用参数默认值作用调参建议max_iter500最大迭代次数数据大时适当增大到1000防止没收敛就停tol1e-6收敛阈值追求精度可设1e-8但会增加迭代次数gamma_min1e-8gamma剪枝下限噪声大时适当调大到1e-6能抑制误检sigma2初始值0.1*var(y)噪声方差初始估算如果已知噪声水平可以直接填入真实值gamma初始值我习惯全部设为1。这个初始值经过多次测试在大多数情况下都能收敛到不错的结果。如果你想更快收敛可以先用OMP跑一遍把OMP选出的支撑集对应的gamma设为1其他设为0.01这样初始就有一个接近真实支撑集的起点迭代次数能明显减少。5. 常见问题与排查技巧实录5.1 矩阵奇异或求逆失败怎么办这是SBL代码里最常见的问题。当Phi列与列之间高度相关时A Phi * Gamma * Phi^T sigma2*I 可能出现近奇异的情况导致np.linalg.inv失败或者结果数值爆炸。我的解决办法有两个。第一给A的对角线加一个小的正数比如1e-6保证数值可逆A 1e-6 * np.eye(M)。这在实现上几乎不加成本效果却立竿见影。第二改用np.linalg.solve而不是np.linalg.inv。具体来说我们需要计算A_inv Phi_gamma可以写成np.linalg.solve(A, Phi_gamma.T).T避免显式求逆。solve方法在数值稳定性上比inv好很多因为它是通过LU分解来求解的不直接构造逆矩阵。我强烈建议在代码里都用solve代替inv这是线性代数计算的基本素养。实测中直接做inv的版本在病态条件下会报出“Singular matrix”错误改成solve之后整个迭代过程稳如老狗。5.2 特征尺度不一致导致收敛困难如果Phi的列向量尺度差异很大比如第一列的标准差是100第二列是0.01那SBL迭代起来会非常别扭gamma的尺度无法统一收敛速度会变慢甚至陷入局部最优。解决方法是先做列归一化。具体操作是计算每列的L2范数然后让每列除以这个范数让所有列的长度变成1。这样处理完以后所有特征都处在同一个尺度上gamma和sigma2的更新也会有更好的数值行为。需要强调的是归一化后恢复出的x其实是对应于归一化后字典的系数要还原成原始物理量还得把对应的系数除以各列的L2范数。这个逆变换步骤容易被忽略导致你拿到的恢复信号幅度不对。我早期写代码时就在这里栽过跟头后来专门加了一个还原步骤。5.3 噪声方差估计偏差过大sigma2的估计偏差通常来源于分母中的修正项处理不当。我见过不少SBL实现直接把M步的sigma2更新简化成残差平方和的均值忽略了有效参数数量修正结果sigma2被严重低估。正确做法是严格按照公式计算分母项1 - sum(gamma_i^(-1) * Sigma_ii)。在我们的代码实现中由于使用了Woodbury变换Sigma_ii并不是显式计算的而是通过Sigma_active diag(gamma_active) - (Phi_gamma.T A_inv) Phi_gamma来间接得到的。它的对角线可以通过动态计算得到Sigma_diag np.diag(Sigma_active)。这样算出来的Sigma_diag是准确的代入分母修正项就没问题。如果发现估计出的sigma2明显偏小优先检查分母是否算对尤其是当gamma出现大量极小值时sum(gamma_i^(-1) * Sigma_ii)会接近总有效参数数量分母会趋近0导致sigma2爆炸或为负。这种情况下需要在分母上加一个小量防止除零比如用max(1e-10, ...)包裹一下。5.4 收敛速度太慢怎么办在高维场景下N上万即使使用了Woodbury加速每次迭代仍然是M乘M矩阵求逆和多次矩阵乘法速度依然可能不达标。这时候我建议做进一步的“分阶段剪枝”第一阶段先正常迭代10到20次让gamma大致收敛到一个合理的初始分布第二阶段把gamma值小于0.01的维度全部裁掉只保留活跃维度继续迭代第三阶段如果发现活跃维度还是太多可以结合相关度分析把高度相关的列合并处理。还有一种更激进的加速办法将原始问题按块拆分。如果Phi具有块对角结构可以按块分别做SBL最后合并结果。这个技巧在信道估计、多天线系统里特别常用因为计算被拆分成多个小规模问题可以在多核上并行执行速度提升非常可观。我拿实际数据测过N5000、M200时不做剪枝直接跑需要大约8分钟做了分阶段剪枝后只需要40秒而且恢复精度几乎不变。5.5 信号幅度被压缩怎么办SBL恢复出的信号幅度可能会偏小尤其是在噪声方差较大时。这是因为后验均值mu天生就有“收缩”效应它是有偏估计会朝向先验均值0收缩。信号越弱收缩越明显。如果你只关心支撑集检测这倒无所谓。但如果需要精确的幅度估计可以在SBL跑完之后用最小二乘在检测出的支撑集上重新估计幅度x_hat_support np.linalg.lstsq(Phi[:, support_hat], y, rcondNone)[0]。这一步相当于在已确定的支撑集上做一次无偏估计能有效纠正幅度收缩。这也是实际工程里很常用的做法我几乎每次都会做这一步。6. 项目扩展方向与个人实操心得6.1 代码扩展到多测量向量MMV场景很多实际应用里我们面对的不是单一观测向量而是一组观测向量。比如脑磁图MEG源定位、多快拍DOA估计都是多测量向量Multiple Measurement VectorsMMV问题。SBL扩展到MMV场景其实很自然只需要把x从向量换成矩阵X每一列是不同快拍对应的稀疏信号并假设这些向量共享相同的支撑集。在代码层面只需要把gamma的更新公式从gamma_i mu_i^2 Sigma_ii改成gamma_i mean_t(mu_it^2) Sigma_ii即在所有快拍上对mu的平方取平均。这个改动虽然小但效果非常好多快拍联合估计比单快拍估计在低信噪比下能获得更大的性能提升因为多个快拍提供了更多的统计信息。6.2 代码扩展到分组稀疏场景如果先验知道信号存在组结构比如某些位置的信号总是成块出现或消失可以在gamma上引入分组约束。做法是让同一组的所有分量共享一个gamma参数更新时使用组内的平均能量。这个思路实现起来也不难把gamma数组改成group_gamma数组更新时对每组内部做平均即可。我去年做一个音频信号恢复的项目时就用到了这个改版。当时信号在频域具有明显的子带结构用分组SBL比普通SBL的恢复误差降低了大约30%。这种改进对特定领域的效果非常显著值得动手尝试。6.3 我个人实际跑代码的几点体会最后说几句掏心窝的话。SBL这个算法从公式到代码的跨度比想象中大得多。论文里寥寥几行推导落到代码里全是坑矩阵求逆的稳定性、剪枝阈值的选择、噪声方差初始值的设置任何一个环节处理不好最后结果就是一团糟。我一开始照着论文公式直译出来的第一版代码跑出来恢复误差甚至比OMP还差后来一步一步排查才发现是sigma2更新公式里的分母修正项被我漏掉了。调试这个代码时我的建议是先把噪声方差设为真实值固定住只更新gamma等gamma收敛正常了再放开sigma2。这样做的好处是能把问题分开如果固定sigma2时gamma都不收敛那问题出在Sigma计算或者gamma更新上如果固定sigma2时没问题放开sigma2后反弹那问题出在噪声估计上。我用这个方法调试比自己瞎猜快多了。另外SBL的迭代过程值得可视化跟踪。把每次迭代的gamma分布画成动画或者每隔50次迭代画一张静态图你会看到gamma值从初始的均匀分布逐渐分化形成少数突出的峰值。这个“分化”的过程非常直观地展示了ARD机制是如何工作的。每次看到这个动画我都觉得这个算法真的很美——它不像黑盒优化那样直接给出答案而是像在进行一场推理逐步逼近真相。如果你在复现过程中也踩到了我上面提到的这些坑希望这篇文章能帮你少走弯路。代码都是现成的拿到手先跑一遍合成数据确认没问题再替换成你自己的数据。如果遇到新的问题欢迎在评论区交流我看到了会回复。本文还有配套的精品资源点击获取