资讯动态

基于因子图的和积算法LDPC译码MATLAB源码实现

发布时间:2026/10/26 2:44:16 来源:尧图企业网站定制
简介这份 MATLAB 源码包围绕因子图上的消息传递与和积算法展开面向具备概率图模型基础、希望把公式落实到代码中的学生与工程人员旨在弥合和积算法从抽象推导到可运行实现之间的断层。它以节点函数和因子图连接为主线给出可复用的消息更新框架并配套 LDPC 编解码、卡尔曼滤波、隐马尔可夫链、数独求解、与或及噪声或等示例便于对照理解置信传播的迭代过程。压缩包共 38 个文件、约 826KB其中 31 个 .m 脚本与函数构成核心实现和示例入口4 个 .mat 提供数独等实验数据2 个 PDF 为算法参考手册1 个 txt 说明授权信息文件按节点类型、消息调度与案例分列结构紧凑适合逐文件阅读和二次改造。已有 2911 人学习读者可从中获得节点函数模板、边缘概率计算、消息传递流程与多个可运行案例用于课程作业、算法复现或在自有模型中替换节点与调度策略。1. 消息传递算法和积算法因子图matlab源码一套能跑通LDPC译码的完整实现如果你在搜索“消息传递算法 和积算法 因子图 matlab源码”大概率不是想从头推导置信传播的数学公式而是想找一套能直接跑、能改参数、能对着波形看误码率的工程代码。这份资源给的正是这个用MATLAB把因子图上的和积算法Sum-Product AlgorithmSPA完整落成可执行脚本覆盖变量节点到校验节点的消息更新、LLR域计算、迭代终止判据配套LDPC码的校验矩阵与AWGN信道仿真。它更适合通信、信号处理方向的研究生和工程师尤其是需要快速验证新码型或对比不同译码迭代次数的那类人。理论谁都能翻书但把消息传递的数值稳定性、归一化因子、早停条件写对才是这份源码真正的价值区。2. 因子图与和积算法的MATLAB映射从公式到可执行变量2.1 和积算法在LDPC译码中的核心公式和积算法的本质是在因子图上做置信传播。LDPC码的Tanner图就是一种典型的因子图变量节点对应码字比特校验节点对应校验方程。每次迭代分两步走——变量节点把收到的信道先验和邻居校验节点传来的外信息求和校验节点则在每个校验方程内部做“除自己外其余变量”的置信汇总。对数域实现下校验节点更新通常写成双曲正切形式变量节点更新L(q_ij) L_channel_j Σ_{i≠i} L(r_ij)校验节点更新L(r_ij) 2·atanh( Π_{j≠j} tanh( L(q_ij)/2 ) )这套公式看起来简单直接照抄进MATLAB跑两下就会遇到两个现实问题tanh在LLR绝对值稍大时迅速饱和到±1连乘后atanh直接返回Inf以及校验矩阵的稀疏索引如果不用find而用双重循环帧长上千时迭代一轮就要几十秒。源码里对这两点都做了处理——引入归一化最小和近似Normalized Min-Sum作为可切换的降级路径并用稀疏行索引把校验节点更新向量化。我一般建议新手先用MS近似跑通流程确认因子图的消息流向和索引没错再开SPA精确模式看误码率差距。提示直接用tanh/atanh的SPA在LLR超过20左右就会溢出源码里对输入做了±19的裁剪这不是数学上的妥协是浮点实现的保命操作。2.2 校验矩阵H的稀疏存储与消息数组维度约定因子图在代码里最直接的映射就是两个大数组L_q变量节点到校验节点的消息和L_r校验节点到变量节点的消息。它们的维度不是M×N——那会把稀疏的Tanner图存成稠密矩阵内存直接炸。源码的做法是按H矩阵中非零元素的个数nnz来开数组用两个索引数组row_idx和col_idx记录每条边两端连的是哪个校验节点、哪个变量节点。这其实对应因子图中“边”的概念每一条边就是一次消息传递的通道。% 从稀疏校验矩阵H提取因子图的边列表 [M, N] size(H); [row_idx, col_idx] find(H); % row_idx: 校验节点编号, col_idx: 变量节点编号 E length(row_idx); % 因子图总边数 L_q zeros(E, 1); % 变量节点 - 校验节点 消息 L_r zeros(E, 1); % 校验节点 - 变量节点 消息find(H)返回的是H中非零元的位置row_idx(e)和col_idx(e)分别告诉你第e条边连接的校验节点和变量节点编号。后续所有消息更新都围绕这个边列表展开避免了双重循环遍历M×N矩阵。参数E就是因子图的边数LDPC码通常E N·d_v M·d_c稀疏度越高这个值越接近线性。这里容易翻车的地方是MATLAB的find默认按列扫描row_idx和col_idx的配对顺序虽然是对的但如果你后面自己写排序或分组逻辑得记住这个排列不是按行分组的——源码里用accumarray做校验节点分组更新时专门依赖了这一点。2.3 变量节点更新先验LLR与外信息求和变量节点的更新逻辑对应因子图上“变量节点向校验节点传消息”这一步。每个变量节点j发给校验节点i的消息等于信道先验L_ch加上除i以外所有邻居校验节点传来的L_r之和。源码里用accumarray按变量节点分组求和再减去当前边自己的贡献避免对每个节点写内层循环。% 变量节点更新LLR域 L_ch 2 * y / sigma^2; % AWGN信道下的先验LLR, y为接收符号 L_q repmat(L_ch, 1, 1); % 初始化先验直接作为初始消息 for iter 1:max_iter % 按变量节点分组求所有入边L_r之和 sum_Lr accumarray(col_idx, L_r, [N, 1]); % 每条边的L_q 先验 (该变量节点总外信息和 - 自己这条边的L_r) L_q L_ch(col_idx) sum_Lr(col_idx) - L_r; endL_ch(col_idx)把每个变量节点的先验LLR映射到对应的边上sum_Lr(col_idx)取出该变量节点所有入边消息的总和减去L_r自身就是“除自己以外”的外信息。这里的关键参数是sigma^2噪声方差直接决定先验LLR的尺度——sigma设大了先验不可靠设小了会过度自信导致早停误判。我一般会先用Eb/N0换算sigma^2 1/(2·R·10^(EbN0/10))其中R是码率。源码里把这步封装成了awgn_llr函数方便扫信噪比曲线时批量调用。3. 校验节点更新tanh域计算的数值稳定性与向量化3.1 标准SPA的tanh连乘实现与裁剪保护校验节点更新是整个和积算法里最容易翻车的一步。公式要求对每条入边做“除自己外所有入边tanh(L_q/2)的连乘再取atanh”直接实现就是两层循环加一个连乘。源码里用了一个技巧先算全部入边的tanh乘积再对每条边除以自己的tanh值这样把O(d_c²)的连乘降到了O(d_c)。但tanh在LLR绝对值大于15左右就饱和到±1除法和atanh会直接产生Inf或NaN所以源码在入口处对L_q做了±19的硬裁剪。% 校验节点更新标准SPA, tanh域 L_q_clipped max(min(L_q, 19), -19); % 裁剪防止tanh饱和溢出 T tanh(L_q_clipped / 2); % 每条边的tanh值 % 按校验节点分组求连乘用对数域求和再exp避免直接prod溢出 logT log(abs(T) eps); % 加eps防止log(0) signT sign(T); sum_logT accumarray(row_idx, logT, [M, 1]); prod_sign accumarray(row_idx, (signT 0), [M, 1]); % 每条边的外信息总连乘除以自己 prod_except exp(sum_logT(row_idx) - logT) .* ... ((-1).^(prod_sign(row_idx) - (signT 0))); L_r 2 * atanh(max(min(prod_except, 1-1e-12), -11e-12));这里把连乘转成对数域求和再exp是为了避免d_c较大时prod直接下溢到0。prod_sign统计每个校验节点下负号的个数(-1).^(...)还原符号——因为tanh是奇函数符号信息必须单独跟踪否则取绝对值再连乘会丢失负号。最后atanh的输入被夹在±(1-1e-12)之间这是浮点实现的最后一道保险。参数eps和1e-12不是随便选的eps保证log(0)不出现1e-12保证atanh不返回Inf。这套写法在帧长64800的LDPC码上跑50次迭代没出过数值问题但速度比MS近似慢三到四倍适合做误码率基准对比。3.2 归一化最小和近似用符号乘积与最小值替代tanh实际工程里更常用的是最小和Min-Sum及其归一化版本因为省掉了tanh/atanh的超越函数开销。校验节点更新简化为“取除自己外所有入边绝对值的最小值符号取所有入边符号的乘积”再乘一个归一化因子α通常0.75~0.9补偿MS近似带来的过估计。源码里把α做成了可配参数扫α从0.6到1.0看误码率拐点是我觉得这套代码最实用的功能之一。% 归一化最小和NMS校验节点更新 alpha 0.8; % 归一化因子, 典型0.75~0.9 abs_Lq abs(L_q); sign_Lq sign(L_q); % 按校验节点分组找最小值和次小值用于排除自己 [min1, idx1] accumarray(row_idx, abs_Lq, [M,1], min); % 简化实现用分组排序取前两小源码用循环局部排序这里示意逻辑 L_r zeros(E, 1); for m 1:M edges find(row_idx m); [sorted_abs, order] sort(abs_Lq(edges)); sign_prod prod(sign_Lq(edges)); for k 1:length(edges) if order(k) 1 min_except sorted_abs(2); % 自己是最小值, 取次小 else min_except sorted_abs(1); end L_r(edges(k)) alpha * sign_prod * sign_Lq(edges(k)) * min_except; end endmin1那行是示意实际源码里为了避免对每个校验节点做sort用了“找最小值和次小值”的两遍扫描第一遍记录最小值和对应边第二遍对每条边判断“如果自己是最小边则用次小值否则用最小值”。符号处理上sign_prod * sign_Lq(e)等价于“总符号除以自己符号”因为符号只有±1。参数α的物理意义是补偿MS算法对校验节点外信息的低估——α1就是纯MSα越小外信息越弱、迭代收敛越慢但稳态误差可能更低。我一般先在Eb/N02dB附近扫α看哪个值让BER曲线在10次迭代内基本走平再固定下来跑全信噪比范围。3.3 硬判决与迭代终止 syndrome校验和早停策略每次迭代完校验节点和变量节点更新后要对变量节点的后验LLR做硬判决然后算H·c^T mod 2看是否全零。全零说明当前码字满足所有校验方程可以提前退出迭代——这就是早停early termination。源码里还加了一个“消息收敛”判据如果连续两次迭代的硬判决结果不变也提前退出避免在不可纠正的错误帧上白跑满迭代次数。% 硬判决与syndrome校验 L_total L_ch accumarray(col_idx, L_r, [N, 1]); % 后验LLR c_hat (L_total 0); % 硬判决, LLR0判为1 syndrome mod(H * c_hat, 2); if all(syndrome 0) break; % 满足校验方程, 早停 end if iter 1 isequal(c_hat, c_hat_prev) break; % 硬判决不变, 提前退出 end c_hat_prev c_hat;L_total 0的判决约定对应“LLR定义为log(P(0)/P(1))”负值表示比特1的概率更大。H * c_hat在GF(2)上做矩阵乘再取模2就是校验方程的值。早停能省多少时间取决于信噪比高SNR下平均2~3次迭代就退出低SNR下可能跑满50次。参数max_iter一般设30~50再大收益很小。源码里还留了一个verbose开关打开后每次迭代打印当前syndrome重量调试索引错误时非常有用——我调自定义H矩阵时第一件事就是开verbose看syndrome重量是不是在单调下降如果来回震荡说明消息更新有符号或索引问题。4. 跑通第一个LDPC译码脚本结构、参数配置与结果验证4.1 源码文件构成与调用关系拿到一套MATLAB源码先别急着点运行。我习惯先看主脚本调了哪些函数、哪些是配置项、哪些是核心算法。这套代码的结构大概是主脚本main_ldpc_sim.m负责扫信噪比、循环帧、统计误码率gen_H.m生成或加载校验矩阵源码里给了一个随机构造的规则LDPC H矩阵示例列重3行重6spa_decode.m是译码主函数内部调用check_node_update.m和variable_node_update.mawgn_llr.m做信道先验计算ber_curve.m画误码率对比图。主脚本里所有可调参数集中在开头一个结构体cfg里包括N码长、R码率、max_iter、alpha、EbN0_range、frames每信噪比点帧数。这种“配置集中、算法分离”的写法对改参数很友好但也意味着你不能单独跑spa_decode而不先构造好cfg和H。4.2 参数配置与一次完整译码的代码走读下面这段是主脚本的核心配置和单帧译码调用我加了逐行说明。实际源码里扫信噪比用外层for循环这里缩成单点演示。% 主脚本参数配置对应源码cfg结构体 N 1024; % 码长, 影响因子图边数和单帧译码耗时 R 0.5; % 码率, 决定sigma^2换算和信息位长度 max_iter 50; % 最大迭代次数, 高SNR下早停会提前退出 alpha 0.8; % NMS归一化因子, 仅MS模式生效 EbN0_dB 2.0; % 信噪比, 扫曲线时改成向量 frames 100; % 每个信噪比点的仿真帧数, 统计BER用 % 生成校验矩阵和编码源码gen_H 简单编码示意 H gen_H(N, R); % 返回稀疏校验矩阵, 列重3行重6 K N - size(H,1); % 信息位长度 info_bits randi([0 1], K, 1); codeword ldpc_encode(H, info_bits); % 源码用系统码构造, 这里不展开 % AWGN信道 sigma2 1 / (2 * R * 10^(EbN0_dB/10)); y (1 - 2*codeword) sqrt(sigma2) * randn(N, 1); % BPSK调制加噪声 % 译码 [c_hat, iter_used] spa_decode(y, H, sigma2, max_iter, alpha); % 误码统计 ber mean(c_hat ~ codeword);1 - 2*codeword是BPSK映射比特0→1比特1→-1sigma2的换算里2*R来自复基带等效噪声方差公式。spa_decode返回译码后的比特和实际用的迭代次数后者可以用来统计平均迭代次数随SNR的变化——这个曲线能直观看出早停策略省了多少计算。frames设100意味着每个信噪比点跑100帧独立噪声码长1024时大概几秒出结果想画平滑的BER曲线到10^-5量级得设到10^4帧以上耗时线性增长。我一般调试阶段frames10看趋势确认索引和消息更新没翻车后再开大。注意源码里ldpc_encode用的是系统码构造codeword的前K位是信息位、后M位是校验位。如果你自己换H矩阵必须保证H是满秩且能构造出系统形式否则编码输出和H不匹配译码端syndrome永远不为零会误以为是译码算法写错了。4.3 用已知码字反查消息更新是否正确验证译码器有没有写对最直接的办法不是看BER而是“无噪声回灌”把编码后的码字直接当接收符号不加噪声LLR取±大值跑译码看是否一次迭代就输出全零syndrome和原始码字。这一步能排除信道模型和LLR尺度的影响单纯验证因子图的消息流和索引映射。如果无噪声下都译不对问题一定出在row_idx/col_idx的配对、L_q初始化的维度、或者硬判决的符号约定上。源码里提供了一个test_decode_noiseless.m做这个自检我觉得这是整套代码里最值得先跑的文件——比看BER曲线快十倍定位问题。5. 消息传递算法落地时的常见问题与排查清单5.1 syndrome重量不降反升符号约定或索引配对翻了现象打开verbose后每次迭代打印的syndrome重量从初始值开始震荡有时比第一轮还大最终BER和不译码差不多。原因通常有两个一是LLR的符号约定前后不一致——信道先验按log(P(0)/P(1))算硬判决却用了L_total 0判1整数翻转导致校验方程永远不满足二是accumarray分组时row_idx和col_idx在find(H)那步没有对应好某条边的消息被加到了错误的校验节点上。解决先跑无噪声自检如果无噪声也翻车就是索引问题然后检查c_hat (L_total 0)和L_ch 2*y/sigma2的符号是否自洽——BPSK下y1对应比特0先验LLR应为正硬判决0判1正好匹配。5.2 tanh/atanh返回Inf或NaN没做饱和裁剪或log(0)保护现象迭代几轮后L_r出现Inf下一轮L_q全变NaN整个译码输出随机。原因标准SPA里tanh(L_q/2)在|L_q|20时饱和到±1连乘后atanh(±1)返回Inf或者某条边的tanh恰好为0取log时没有加eps。解决在L_q进入tanh前硬裁剪到±19atanh输入夹到±(1-1e-12)对数域连乘时log(abs(T)eps)。源码里这三道保险都有但如果你自己改公式忘了加翻车是必然的。我一般会在check_node_update函数开头加一句assert(all(isfinite(L_q)))让问题在源头就暴露而不是扩散到后续迭代。5.3 大码长下译码慢到跑不完循环没向量化或早停没生效现象N64800时单帧译码要好几秒扫10个信噪比点×1000帧根本跑不完。原因校验节点更新里用了双重循环遍历M×N或者早停判据写成了“每轮都算完整syndrome但从不break”。解决确认校验节点更新走的是accumarray分组find(row_idxm)只遍历非零边而不是for i1:M for j1:N确认if all(syndrome0) break在迭代循环内部而不是外面。源码里还有个细节accumarray对row_idx的分组要求索引是正整数列向量如果find返回的是行向量accumarray会报错或静默出错误结果——用row_idx(:)强制转列。5.4 换了H矩阵后编码译码对不上没检查秩和系统形式现象用自己构造的稀疏H替换源码自带矩阵后无噪声回灌都译不对syndrome初始重量就不为零。原因源码的ldpc_encode假设H能通过高斯消元化成系统形式[I | P]如果你的H不满秩或者消元后主元列不连续编码输出根本不满足H·c^T0。解决换H后先跑rank(full(H))确认满秩再用gf2高斯消元检查系统形式是否可构造实在不行就用源码里的gen_H按指定列重行重重新生成别硬塞一个来路不明的矩阵。我踩过一次坑H的某两行线性相关编码出来的码字syndrome重量恒为2译码器跑满50次迭代也降不下去排查了大半天才发现是矩阵本身的问题。5.5 早停误判导致高SNR下BER平台现象Eb/N0超过4dB后BER曲线不再下降出现平底。原因早停判据用了“硬判决连续两次不变就退出”但低迭代次数下硬判决可能暂时稳定在一个错误码字上提前退出锁死了错误。解决源码里早停默认优先判syndrome全零硬判决不变只是次级条件且要求iter1。如果你把硬判决不变作为唯一早停条件高SNR下误码平台几乎必然出现。稳妥做法是只认syndrome全零或者把硬判决不变的连续次数阈值设到3以上。我现在的习惯是调试阶段关掉硬判决早停只留syndrome早停确认BER曲线正常后再开对比两次结果差异。6. 从能跑到好用归一化因子扫描与误码率基准对比把译码器跑通只是起点这套源码真正能帮你做决策的地方在于参数对比。我一般会做两件事扫归一化因子α看NMS和SPA的差距以及统计平均迭代次数评估早停的实际收益。α扫描的做法是固定Eb/N0比如2.5dB此时FER大概在10^-1量级几百帧就能看出差异让α从0.6步进0.05到1.0每个点跑200帧记录BER。典型结果是α在0.8附近BER最低α1纯MS比最优α差0.3~0.5dB而标准SPA比最优NMS好0.1~0.2dB但耗时翻倍——这个差距值不值得付三倍计算时间取决于你的应用场景。下面这段是扫描循环的骨架% 归一化因子alpha扫描 alpha_list 0.6:0.05:1.0; ber_list zeros(length(alpha_list), 1); for a 1:length(alpha_list) err 0; total_bits 0; for f 1:200 % ... 生成码字、加噪、译码 ... [c_hat, ~] spa_decode(y, H, sigma2, max_iter, alpha_list(a)); err err sum(c_hat ~ codeword); total_bits total_bits N; end ber_list(a) err / total_bits; end [best_ber, best_idx] min(ber_list); fprintf(最优alpha%.2f, BER%.2e\n, alpha_list(best_idx), best_ber);另一个值得看的指标是平均迭代次数。在spa_decode里多返回一个iter_used扫EbN0从1dB到5dB画mean(iter_used)随SNR的曲线——高SNR下应该快速降到2~3次如果一直贴着max_iter不降要么早停坏了要么LLR尺度不对导致译码器一直在“犹豫”。我还会把SPA和NMS的BER曲线画在同一张图上做基准对比确认NMS的损失在可接受范围内再定最终参数。从那以后我每次改因子图的消息更新逻辑都强制先跑无噪声自检、再扫α、最后看平均迭代次数三步走完才敢信BER曲线。希望这套流程帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑