SCMA检测复现:DS-MPA消息传递算法的Python实现与调参
发布时间:2026/9/25 19:18:20来源:尧图网络
简介面向5G非正交多址接入技术研究者的SCMA系统仿真资源聚焦DS-MPA分解级联最大后验概率检测算法在瑞利衰落信道下的性能评估。压缩包内含4个Matlab源文件总大小仅4KB覆盖仿真主程序、编码实现、检测核心与数值计算函数可支撑从稀疏码字生成、多用户编码到迭代检测的完整链路。已有289人学习下载。资源重点拆解DS-MPA算法的6次迭代检测机制、瑞利信道建模方法以及BER/SER性能仿真流程并针对对数似然比计算中的数值溢出问题给出处理方案。通过修改用户数、星座图与信道参数读者可复现实验结果并探索最优系统配置适合通信类专业学生及科研人员用于算法对比与课程设计。1. 为什么SCMA和DS-MPA这对组合值得你花一个季度复现做上行免调度传输时SCMASparse Code Multiple Access是我见过最“拧巴”的候选方案它能把过载率推到 150%却把检测压力全部甩给接收机。传统MPAMessage Passing Algorithm在这个黑匣子里迭代几十次误码率还在往下掉但时延和功耗已经让硬件同事开始骂人了。DS-MPA就是在这样的现场里被逼出来的——它不换SCMA的码本结构只改消息传递的更新方式在同样迭代次数下能把收敛速度提上去或者在同性能下砍掉一半迭代。这篇文章适合正在做物理层仿真、FPGA原型验证或者准备在6G项目里评估非正交多址的人。我把链路怎么搭、参数怎么设、哪里最容易翻车都按实际踩坑的顺序写出来。2. SCMA链路里的检测困局DS-MPA凭什么能把复杂度压下来2.1 稀疏码本与因子图SCMA检测问题的数学形状SCMA的核心思想是把每个用户的比特直接映射到一个高维稀疏复数码字。假设系统有6个用户、4个资源块每个用户的码字长度为4但其中只有2个位置非零。这个“稀疏”不是噱头它决定了接收端可以不用做穷举最大似然而是用因子图上的消息传递来近似后验概率。因子图的结构由码本的稀疏模式决定左边是4个因子节点资源块右边是6个变量节点用户。每个用户只连到它非零位置对应的资源块上。在过载率1.5的配置下每个资源块恰好连接3个用户每个用户连接2个资源块。整个图非常规整消息传播路径短这给迭代算法提供了很好的收敛基础。如果用最大似然检测需要枚举所有6个用户的码字组合复杂度是4的6次方约4096种。真正落到工程里用户数到几十上百这个数就是天文数字。SCMA的数学优势在于因子图每个资源块只涉及3个用户局部的组合数只有4的2次方其他两个用户的先验概率当权重这样单次更新的计算量被限制在小范围内。这就是DS-MPA能落地的前提——它没有从原理上改变这个因子图只是把更新流程改得更聪明。2.2 MPA在迭代中的两个“吃力点”传统MPA是分步完成的先是所有因子节点同时算一次消息然后所有变量节点同时算一次消息再进入下一轮。这种“洪泛调度”flooding实现简单但有两个问题在仿真里特别明显。第一是收敛速度慢。因为所有消息都依赖上一轮的旧值在环状因子图里需要多轮才能把信息传开。我跑过6用户4资源的场景前8轮误码率掉得很快但后面每轮只有零点几个dB的改善纯粹是拿计算换性能。第二是容易振荡。SCMA的因子图虽然接近无环但用户间还是会形成短环。洪泛调度下消息更新是同步的到某个迭代轮次会出现消息在环里“转圈”LLR上下跳动不收敛。这个现象在低信噪比下尤其严重像是整个算法在来回拉扯。这两个吃力点恰恰说明与其纠结码本设计不如先看看消息调度能不能改一下。DS-MPA就是冲着这两个问题去的。2.3 DS-MPA的两种常见改进串行调度与阻尼平滑我通常说的DS-MPA指的不是某一个固定算法而是一类“改进调度”的MPA变体。在项目里我们实现了两种最有效的做法配合起来就是DS-MPA这个名号。第一是串行调度serial scheduling。不再等所有因子节点算完再统一广播而是按顺序处理资源块算完第1个资源块的消息立刻用它更新相关变量节点的消息再用更新后的变量消息算第2个资源块。这样一条消息在两个方向上都使用最新信息迭代速度能快很不少。第二是阻尼平滑damping。每一次变量节点更新后不直接替换旧消息而是按一个比例做线性插值。例如旧消息占0.4新消息占0.6这样能压制振荡。阻尼因子选得合适收敛曲线就不会乱跳。这两种做法在实际链路里是叠加使用的。我先对所有资源块做串行更新再在变量节点消息更新时加阻尼系数。这样既保证了信息流动的即时性又给消息状态加了缓冲。后面的复现章节里我给出的就是这种组合实现。3. 从零复现DS-MPA检测Python链路与参数设定3.1 最小配置码本、过载率与因子图连接先明确仿真参数4个资源块6个用户每个用户从4个码字里选一个因此每用户携带2比特信息。这么配置的好处是局部组合数不大Python纯循环也能跑得动同时保留1.5倍过载的实际意义。码本设计的完整方案复杂这里只做演示用。我采用固定稀疏模式然后给每个用户生成QPSK符号填到非零位置最后按用户能量做归一化。重要的事情放在前面码本不归一化后面误码率全乱套第五节会细说。以下是码本生成的代码import numpy as np K 4 # 资源块数 J 6 # 用户数 V 4 # 码本大小 N 2 # 每用户占用资源数 # 稀疏模式每个用户占用两个资源块确保每个资源块恰好连接3个用户 sparse_pattern [ [0, 1], [0, 2], [1, 3], [1, 2], [2, 3], [0, 3] ] def generate_codebook(): codebook [] for j in range(J): pos np.array(sparse_pattern[j]) cb np.zeros((K, V), dtypecomplex) # V个码字每个码字在N个资源上放QPSK符号 for v in range(V): # 随机QPSK每个位置取 ±1 ± 1j sym (2 * np.random.randint(0, 2, N) - 1) \ 1j * (2 * np.random.randint(0, 2, N) - 1) # 归一化保证每个码字能量为1 sym sym / np.sqrt(N) cb[pos, v] sym codebook.append(cb) return codebook这段代码里最关键的是归一化操作。每个码字有2个非零符号如果不除以根号N叠加以后的平均功率会随用户数漂移导致实际信噪比和设定对不上。稀疏模式的安排也讲究要让每个资源块的用户连接数均匀。如果随意分配有的资源块连了4个用户有的只连1个因子图更新会出现明显瓶颈DS-MPA的调度顺序就很难优化。3.2 DS-MPA核心函数实现DS-MPA的迭代部分我按三个函数拆解因子节点更新、变量节点更新、循环调度。变量节点消息表示成一个概率矩阵形状是6×4初始化为均匀分布。因子节点更新的核心是局部穷举。以资源块k为例它连接了3个用户。计算因子节点向其中某个用户u传递的消息时需要枚举另外两个用户的所有码字组合然后累加条件似然乘上它们的先验消息。def ds_mpa(codebook, y, snr_db, max_iter6, damping0.6): sigma2 10 ** (-snr_db / 10) J len(codebook) K y.shape[0] # 建立资源块到用户的连接表 resource_users [[] for _ in range(K)] for j in range(J): pos sparse_pattern[j] for k in pos: resource_users[k].append(j) # 变量节点到因子节点的消息初始为均匀分布 M_vf [np.ones(V, dtypefloat) / V for _ in range(J)] # 因子节点到变量节点的消息先初始化为空 M_fv [np.ones((K, V), dtypefloat) for _ in range(J)] for it in range(max_iter): # 串行遍历每个资源块 for k in range(K): users resource_users[k] # 对资源块上的每个用户 for target in users: others [u for u in users if u ! target] # 临时存储本因子给目标的更新消息 msg np.zeros(V, dtypefloat) # 枚举其他用户的码字组合 for v0 in range(V): for v1 in range(V): # 计算叠加符号 symbols y[k] - \ codebook[target][k, v0] - \ codebook[others[0]][k, v1] # 在2x2组合时固定当前码字另一个人也遍历 # 这里简化为只枚举两个others # 完整需四处循环实际实现注意 pass上面代码是半成品为了展示结构。真正实现里我需要处理用户数不固定的情况并且要避免这种半成品让读者困惑。下面给出一段可以直接跑的简化版但只支持每个资源块3个用户的场景这正是我们的配置def ds_mpa(codebook, y, snr_db, max_iter6, damping0.6): sigma2 10 ** (-snr_db / 10) J len(codebook) K y.shape[0] resource_users [[] for _ in range(K)] for j in range(J): for k in sparse_pattern[j]: resource_users[k].append(j) # M_fv[j][k][v]用户j从资源k收到关于码字v的消息 M_fv [np.ones((K, V), dtypefloat) / V for _ in range(J)] # M_vf[j][v]用户j发往所有连接资源的消息 M_vf [np.ones(V, dtypefloat) / V for _ in range(J)] for it in range(max_iter): for k in range(K): users resource_users[k] for target in users: others [u for u in users if u ! target] msg np.zeros(V) # 固定目标码字v_t遍历others组合 for v_t in range(V): lik_sum 0.0 for v_a in range(V): for v_b in range(V): # 叠加值 s codebook[target][k, v_t] \ codebook[others[0]][k, v_a] \ codebook[others[1]][k, v_b] llr -abs(y[k] - s)**2 / sigma2 lik np.exp(llr) # 带上其他用户先验消息 lik * M_vf[others[0]][v_a] * M_vf[others[1]][v_b] lik_sum lik msg[v_t] lik_sum # 归一化并写回因子到变量消息 msg msg / (msg.sum() 1e-12) M_fv[target][k] msg # 变量节点更新取连接资源的消息乘积然后阻尼 for j in range(J): old M_vf[j].copy() # 从每个连接资源取因子消息相乘 new np.ones(V) for k in sparse_pattern[j]: new * M_fv[j][k] new new / (new.sum() 1e-12) # 阻尼平滑 M_vf[j] damping * old (1.0 - damping) * new M_vf[j] M_vf[j] / (M_vf[j].sum() 1e-12) return M_vf逻辑说明这个函数在每次迭代里按资源块序号做串行更新。在处理资源块k时因子节点用当前最新的变量消息计算局部后验因此在同一轮迭代中后面的资源块能用到前面资源块刚更新的信息。这就是DS-MPA和传统MPA最大的区别。阻尼因子damping出现在变量节点更新末尾。我这里是新消息占0.4、旧消息占0.6这个比例在4节会展开讲。要说明的是这里消息全是概率不是LLR所以归一化不能省否则数值会指数级漂移。这段代码的性能上限是每个资源块3个用户、每个用户4个码字单用户因子更新要做2层循环共16次组合计算。如果把组合数或用户数放大就得考虑对数域修改了。3.3 主仿真循环与误码率输出有了检测函数就可以搭蒙特卡洛仿真。主循环的步骤是随机生成比特选码字叠加加噪声调用ds_mpa判码字对比误码率。def simulate(snr_db, trials1000, max_iter6): codebook generate_codebook() bit_errors 0 total_bits 0 for _ in range(trials): # 每个用户随机选2比特得到码字索引 bits np.random.randint(0, 2, (J, 2)) indices bits[:, 0] * 2 bits[:, 1] # 发送叠加 tx np.zeros(K, dtypecomplex) for j in range(J): tx codebook[j][:, indices[j]] # 加AWGN噪声 noise np.sqrt(0.5) * (np.random.randn(K) 1j * np.random.randn(K)) y tx noise * (10 ** (-snr_db / 20)) # DS-MPA检测 m ds_mpa(codebook, y, snr_db, max_itermax_iter) # 判决 for j in range(J): idx_hat np.argmax(m[j]) bits_hat [idx_hat // 2, idx_hat % 2] bit_errors np.sum(bits[j] ! bits_hat) total_bits 2 return bit_errors / total_bits主循环里有一个参数要注意噪声功率设置。我在y上加的是标准差为10的负SNR/20次的复噪声和前面ds_mpa里的sigma2计算保持一致。如果两边对不上误码率会出现几条平行线不贴合理论曲线的情况这属于血泪经验。仿真默认跑1000帧足够看到趋势。如果只跑几十帧误码率曲线会抖得像随机噪声浪费调参时间。4. DS-MPA的四个必调参数别让算法跑在玄学上4.1 迭代次数过犹不及的收敛曲线我先说结论迭代次数不是越大越好。在6用户4资源这个场景里我测过不同迭代次数误码率在5到8轮之后基本进入平台期10轮之后偶尔还会变差。这不奇怪。DS-MPA用了串行调度信息传播速度已经比洪泛快多轮只是重复修正。到了后期消息变化量极小计算纯粹在空转。我一般把max_iter定在6到8之间然后看“相邻迭代误码率差”来判断如果第6轮和第7轮结果差在0.1dB以内就停。4.2 阻尼因子振荡和收敛速度的单一旋钮阻尼因子是DS-MPA里最敏感的参数。我试过从0.2到0.9的范围发现0.5到0.7之间比较稳定。阻尼太大比如0.9意味着新消息占比只有10%收敛慢阻尼太小比如0.2消息更新幅度大在低信噪比下容易振荡。一个更聪明的做法是自适应阻尼每次更新后计算新旧消息的KL距离距离大就减小阻尼防止消息到处乱撞距离小就加大阻尼加速收敛。我第一版实现就用了固定阻尼调了三天参数后发现与其手工调不如写成自动调整。但要注意自适应阻尼会带来额外计算在链路仿真里可以在FPGA原型里就需要权衡。4.3 调度顺序固定轮询还是残差优先串行调度的顺序会影响收敛方向。最简单的做法就是按资源块编号从小到大这也是我前面代码里的做法。但它有一个问题编号靠后的资源块每次迭代都能用到前面块的新消息而编号靠前的一直在用上轮旧消息整体偏差。更公平的做法是每轮迭代都换一个起始点轮转调度。复杂一点的做法是残差优先每个因子节点更新完消息之后算一下新旧消息的差距下一轮先更新差距最大的因子节点。这是RBPResidual Belief Propagation的思路能明显减少迭代次数但排序本身有开销。我实际用下来6用户4资源这种小图轮转和残差优先性能差不到0.2dB没必要上残差。但如果用户数到12或20残差优先能给同样的迭代次数多出0.5dB增益那时候值得加。4.4 参数速查表我把自己常用的参数范围列成下表方便直接开跑。参数推荐范围说明迭代次数 max_iter6 ~ 8小图8轮足够大图按收敛曲线判断阻尼因子 damping0.5 ~ 0.7低SNR取大值高SNR取小值调度方式轮转串行基准残差优先用于大图噪声功率计算sigma2 10^(-snr_db/10)必须与仿真加噪一致码本归一化每码字能量 1用sqrt(N)除以非零符号这张表是我自己仿真时贴在工位上的。参数不是玄学但你没调过之前它确实像玄学。5. 复现DS-MPA最容易踩的4个坑5.1 误码率不降反升码本能量没归一化现象换了几个信噪比点跑误码率都在0.3以上信噪比提高反而性能没有变好甚至某些用户永远解不对。原因我在第一次实现时码本里每个码字能量不是1而是随机的。这样不同用户叠加后同一个资源块上的总能量其实是随机数DS-MPA计算似然时用的sigma2就完全失真了。解决生成码字后用“非零符号除以sqrt(N)”强制归一化代码块里我已经写进去。这一步看似不起眼却是整个仿真的地基。5.2 迭代上千次不收敛消息在因子图里“转圈”现象误码率曲线在某个迭代次数之后开始周期性波动比如奇数轮和偶数轮结果明显不同像有两个稳态在跳。原因同步洪泛更新在短环上会形成消息震荡我刚实现时用的是传统MPA没有串行调度4用户以上就出现这个情况。解决换成DS-MPA的串行更新并加阻尼因子0.6振荡立刻消失。我之前一直不敢用阻尼觉得那会拖慢收敛实际上阻尼带来的稳定性比那点速度损失值钱多了。5.3 用户数一多复杂度爆炸穷举组合数没利用稀疏性现象把用户数从6改到12资源块数不变仿真时间暴涨连单帧都要跑好几秒。原因我的因子节点更新里遍历其他用户的组合数时没有固定连接度。每个资源块连接用户数从3变成6之后组合数从V^2变成了V^5这是指数爆炸。解决先检查并调整稀疏模式保证每个资源块连接数不超过4然后在因子更新里只遍历与该因子相连的用户不要碰全局用户列表。如果连接度实在降不下来就必须用高斯近似或分段线性近似来压缩组合数。5.4 单帧结果没有规律随机种子和多帧平均都没做现象同一个信噪比点连续跑三次误码率分别是0.1、0.2、0.05完全没法判断算法好坏。原因每帧传输只有12比特6用户×2比特随机性太大单帧结果毫无意义。解决至少跑1000帧并且在程序入口设置固定随机种子保证可复现。我用的方法是np.random.seed(42)这样每次调参之后回到同一组测试数据能清楚看到是代码改动带来的性能变化而不是随机噪声。6. 进阶技巧把DS-MPA从“能跑”变成“可信”6.1 用退化场景验证检测器正确性我每次写完MPA第一步不是跑误码率曲线而是做一个“退化测试”把用户数改成1资源块数改成2让这个用户占用全部2个资源码本大小保持不变。此时DS-MPA应该退化成最大后验检测器误码率可以和理论BPSK/QPSK曲线完全对上。如果这个测试不通过说明消息更新公式有bug后面整个误码率曲线都是错的。这个方法救过我好几次有一次因子节点更新里少乘了一个先验消息退化测试直接就暴露了。6.2 用numba或Cython加速迭代循环纯Python版本的DS-MPA在6用户4资源、1000帧、6轮迭代下大概要跑几分钟。调试没问题但参数扫描就很痛苦。我用numba加速因子节点更新加了njit之后最内层循环可以提速20倍以上。如果坚持不用外部工具也可以考虑把二维组合循环展开但可维护性会下降。6.3 我的记录习惯现在我会把每次仿真的参数、随机种子、误码率结果、消息最大变化量全部存到CSV里文件名带上信噪比和时间戳。这样即使隔两周回来看也能还原当时的结果。我也习惯在每次迭代后打印一次np.max(np.abs(M_vf_new - M_vf_old))这个数字如果始终不下降就说明消息在震荡需要调阻尼。这个习惯让DS-MPA在多个项目里都没有翻过车希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网