简介:SCMA-ML-master 是一份面向无线通信与机器学习交叉方向研究者的开源代码资源,聚焦稀疏码多址(SCMA)技术与机器学习方法的结合。SCMA 作为码域非正交多用户接入方案,可提升频谱效率与抗干扰能力,而该资源尝试用机器学习模型优化其编码、检测与解码流程,适合具备通信原理与 Python 编程基础的学习者研读。压缩包共 9 个文件,以 .m 脚本为主体,辅以 LICENSE、README.md 及一个 .s 文件,整体约 6KB,体量轻巧但结构完整,涵盖码本生成、SCMA 编解码、ML 检测及四进制与二进制映射转换等核心模块。目前已有 227 人学习下载。读者可从中获取一套可运行的 SCMA 仿真链路,理解码本设计、信号检测与机器学习融合的实现思路,并借助 README 快速上手,为后续模型调优与性能评估提供参考。
1. 从一份 SCMA 稀疏码多址的 ML 检测工程说起
如果你手上正好有一份名为SCMA-ML-master的工程目录,里面散落着SCMA、ML、morehsq这类命名,第一反应大概率是:这是一套用最大似然(Maximum Likelihood, ML)做 SCMA 多用户检测的 MATLAB 实现。SCMA 是稀疏码多址(Sparse Code Multiple Access),5G 非正交多址里被讨论得最多的一支;ML 检测则是它性能上界的那把尺子。问题在于,ML 检测的复杂度随用户数和码本维度指数爆炸,工程里真正能跑起来的,往往是加了球形译码、码本剪枝或者近似消息传递的变体。这份工程的价值不在于它多完美,而在于它把「码本 → 信道 → 接收信号 → ML 判决」这条链路完整摆出来了,你能顺着它把 SCMA 的检测逻辑从公式落到矩阵。这篇笔记就按这个思路走:先讲清 SCMA 和 ML 检测到底在算什么,再拆工程里最可能的结构和关键参数,最后把踩过的坑和验证方法交给你。适合正在做多址接入仿真、想复现 SCMA 链路、或者被 ML 复杂度卡住的人。
2. SCMA 码本与 ML 检测:先把数学对象对齐
2.1 SCMA 到底稀疏在哪,码本怎么定义
SCMA 的核心是把比特流映射到一个多维稀疏码字上。假设系统有K个资源单元(RE,比如子载波)和J个用户层,每个用户占用N个非零资源,通常N < K,这就是「稀疏」的来源。过载因子λ = J / K,典型配置是K = 4, J = 6, N = 2,过载 150%。每个用户有一本码本CB_j,大小是K × M,M是码本码字数,等于2^{log2(M)},常见M = 4或8。
码本不是随便填的,它由母星座图经过相位旋转、资源映射和功率分配生成。工程里你看到的SCMA_codebook或类似变量,本质就是一个三维数组[K, M, J]。理解这一点很关键:后面 ML 检测的搜索空间,就是所有用户码字组合的笛卡尔积,规模是M^J。M = 4, J = 6时是 4096 种组合,还能暴力搜;M = 8, J = 6就是 262144 种,单次符号检测就够呛,这就是为什么工程里必须做剪枝或近似。
2.2 ML 检测的目标函数与复杂度来源
接收信号模型写成y = Σ_j diag(h_j) x_j + n,h_j是用户j的信道向量,x_j是从码本里选出的码字,n是噪声。ML 检测就是在所有可能的码字组合上最小化欧氏距离:
[x_1*, ..., x_J*] = argmin || y - Σ_j diag(h_j) x_j ||^2这个式子看着简单,复杂度却是O(M^J)。工程里SCMA-ML这个名字直接点明了它用的是穷举 ML,所以它的定位是「性能基准」而不是「可部署接收机」。你在读这份工程时,要带着这个判断:它的检测结果可以用来验证 MP(消息传递)或 MPA(消息传递算法)近似得够不够好,而不是拿去对标实时吞吐。
2.3 为什么工程里常见 ML 而不是直接上 MPA
MPA 是 SCMA 的标准低复杂度检测,基于因子图做迭代消息传递,复杂度随J和M线性增长。但 MPA 有几个工程上绕不开的问题:迭代次数、阻尼因子、消息初始化都会影响收敛,调不好就出现误码平台。ML 没有这些玄学参数,它给的是硬下界。所以一份叫SCMA-ML的工程,通常承担两个角色:一是给 MPA 提供 BER 对比曲线,二是用来验证码本设计本身好不好——如果 ML 都跑不出低误码,那码本或信道模型一定有问题。这也是我建议你先跑通 ML 再碰 MPA 的原因,先有一个可信的参照系。
2.4 从目录名推断工程结构
SCMA-ML-master这种命名,大概率是 GitHub 上直接下载的 zip 解压结果,morehsq可能是作者 ID 或分支标记。常见目录会包含:码本生成脚本、主仿真脚本、ML 检测函数、BER 统计脚本、可能的 MPA 对比脚本。你拿到手第一件事不是跑主脚本,而是先确认码本维度和用户数配置,因为后面所有矩阵索引都依赖它。如果码本是硬编码的.mat文件,先load出来看size,确认是[K, M, J]还是[J, M, K],这个顺序搞反了,检测结果会全错但又不报错,属于最隐蔽的翻车点。
3. 把 SCMA-ML 工程在本地跑通的最小路径
3.1 环境准备与目录确认
MATLAB 版本建议 R2018b 以上,因为工程里可能用到argmin类的隐式扩展或者pagemtimes。不需要额外工具箱,SCMA 仿真基本是纯矩阵运算。如果你只有 Python,也能复现,后面会给 NumPy 版本。先确认目录结构:
# 进入工程根目录,列出所有 .m 和 .mat 文件 cd SCMA-ML-master find . -maxdepth 2 -name "*.m" -o -name "*.mat" | sort这一步的目的是找到主入口和码本文件。常见主脚本叫main.m、SCMA_sim.m或test_ML.m。如果目录里同时有ML和MPA两个文件夹,先只跑 ML,别混着调。
3.2 码本加载与维度校验
% 加载码本,假设文件名为 codebook.mat load('codebook.mat'); % 变量名可能是 CB 或 codebook K = 4; J = 6; M = 4; % 典型配置,按实际改 % 校验维度,兼容两种常见存储顺序 if size(CB,1) == K && size(CB,2) == M && size(CB,3) == J fprintf('码本维度 [K,M,J] = [%d,%d,%d] 正确\n', K, M, J); elseif size(CB,1) == J && size(CB,2) == M && size(CB,3) == K CB = permute(CB, [3 2 1]); % 转成 [K,M,J] fprintf('已从 [J,M,K] 转为 [K,M,J]\n'); else error('码本维度不符合预期,检查 size(CB)'); end维度校验是整条链路的地基。K是资源数,M是每用户码字数,J是用户层数。permute那行是后悔药,很多下载来的工程存储顺序和你的索引习惯不一致,不转就等着 BER 恒为 0.5。
3.3 生成接收信号与穷举 ML 检测
N = 2; % 每用户非零资源数 numSym = 1000; % 仿真符号数 bitsPerSym = log2(M); % 每用户每符号比特数 BER = zeros(1, length(1:2:16)); snrIdx = 1; for EbN0 = 1:2:16 errCount = 0; totalBits = 0; for s = 1:numSym % 随机比特 -> 每用户码字索引 txIdx = randi(M, 1, J); % 叠加所有用户码字,h 为信道(此处设全 1,可换成瑞利) y = zeros(K, 1); for j = 1:J y = y + CB(:, txIdx(j), j); end % 加高斯噪声 noiseVar = 10^(-EbN0/10); y = y + sqrt(noiseVar/2) * (randn(K,1) + 1i*randn(K,1)); % 穷举 ML:遍历 M^J 种组合 bestDist = inf; bestIdx = zeros(1, J); for c = 0:M^J-1 idx = dec2base(c, M, J) - '0' + 1; % 组合索引 yhat = zeros(K, 1); for j = 1:J yhat = yhat + CB(:, idx(j), j); end d = norm(y - yhat)^2; if d < bestDist bestDist = d; bestIdx = idx; end end % 统计误比特 errCount = errCount + sum(txIdx ~= bestIdx); totalBits = totalBits + J * bitsPerSym; end BER(snrIdx) = errCount / totalBits; snrIdx = snrIdx + 1; end semilogy(1:2:16, BER, 'o-'); grid on; xlabel('Eb/N0 (dB)'); ylabel('BER'); title('SCMA ML 检测 BER');这段代码是整个工程的心脏。dec2base把十进制组合号转成M进制索引向量,实现M^J穷举。noiseVar按Eb/N0换算,注意这里用的是符号级噪声方差,如果你要精确到比特级Eb/N0,还要除以bitsPerSym和码率。bestDist用平方欧氏距离,避免开根号。参数上,numSym至少 1000 才能让高 SNR 点的 BER 有统计意义,低 SNR 点 500 就够。J和M一改,内层循环次数指数变化,M=8, J=6时单符号 26 万次距离计算,MATLAB 里跑 1000 符号会非常慢,这时候要么减numSym,要么上向量化。
3.4 向量化加速:把 M^J 循环压成矩阵
% 预生成所有组合索引矩阵,大小 M^J x J allIdx = zeros(M^J, J); for c = 0:M^J-1 allIdx(c+1, :) = dec2base(c, M, J) - '0' + 1; end % 预生成所有组合的叠加码字,K x M^J allCodewords = zeros(K, M^J); for c = 1:M^J for j = 1:J allCodewords(:, c) = allCodewords(:, c) + CB(:, allIdx(c,j), j); end end % 检测时只需一次矩阵运算 dist = sum(abs(y - allCodewords).^2, 1); [~, bestC] = min(dist); bestIdx = allIdx(bestC, :);预生成allCodewords是拿空间换时间,K × M^J的复数矩阵,K=4, M^J=4096时约 256KB,完全可接受。M=8, J=6时是 4 × 262144,约 16MB 复数,也能扛。这样每个符号的检测从M^J次循环降到一次sum,速度提升几十倍。注意allCodewords只和码本有关,和y无关,所以放在 SNR 循环外面生成一次即可。
3.5 Python 复现的关键差异
import numpy as np K, J, M = 4, 6, 4 # CB 形状 [K, M, J] CB = np.load('codebook.npy') # 或从 mat 转 all_idx = np.array(np.unravel_index(np.arange(M**J), [M]*J)).T # M^J x J all_cw = np.zeros((K, M**J), dtype=complex) for c in range(M**J): for j in range(J): all_cw[:, c] += CB[:, all_idx[c, j], j] # 检测 dist = np.sum(np.abs(y[:, None] - all_cw)**2, axis=0) best = np.argmin(dist)Python 里np.unravel_index直接生成所有组合,比 MATLAB 的dec2base更干净。注意y[:, None]做广播,all_cw是K × M^J,相减后是K × M^J,沿 axis=0 求和得到每个组合的距离。MATLAB 用户转 Python 最容易错的是索引从 0 开始,all_idx直接就是 0-based,喂给CB时不用加 1。
4. 参数怎么设:码本、信道与 SNR 的实操边界
4.1 码本维度与过载因子的取舍
| 参数 | 典型值 | 影响 | 建议 |
|---|---|---|---|
| K | 4 | 资源单元数 | 固定 4,便于对比文献 |
| J | 6 | 用户层数 | 过载 150%,ML 复杂度 4^6 |
| M | 4 或 8 | 码字数 | 先 4 跑通,再 8 看复杂度 |
| N | 2 | 非零资源 | 稀疏度,影响 MPA 收敛 |
| λ | 1.5 | J/K | 越高越非正交,干扰越大 |
M=4时M^J=4096,MATLAB 单符号检测约几毫秒;M=8时M^J=262144,单符号可能上百毫秒。如果你只是验证码本,M=4足够;如果要发论文对比,M=8才有说服力,但必须向量化。N=2是 SCMA 稀疏性的体现,N越大码字越密,MPA 的因子图越复杂,但 ML 不受N影响,只受M^J影响。
4.2 信道模型:AWGN 与瑞利的切换
上面代码用的是全 1 信道,等价 AWGN。要切瑞利,把y = y + CB(:, txIdx(j), j)改成y = y + h(j) * CB(:, txIdx(j), j),h是每用户独立的复高斯。注意 ML 检测端必须知道h,否则距离计算里的yhat也要乘h。工程里常见错误是发端加了h,收端yhat没乘,结果 BER 曲线在高 SNR 下不降,查半天以为是码本问题。瑞利信道下 ML 检测的allCodewords不能预生成,因为每个符号的h不同,yhat要实时算,向量化时把h广播进去。
4.3 SNR 范围与统计样本数
Eb/N0从 1 到 16 dB,步进 2,是 SCMA 文献的常见画法。低 SNR 段 BER 接近 0.5,样本可以少;高 SNR 段 BER 到 1e-4 以下,需要至少 1e6 比特才能看到稳定点。numSym=1000, J=6, bitsPerSym=2时总比特 12000,高 SNR 下可能一个错误都没有,BER 显示为 0,semilogy会断线。解决办法是设一个错误数下限,比如累计 100 个错误就停,或者直接加大numSym到 10000。我一般先小样本跑通流程,再大样本出曲线,别一上来就 10000 符号干等。
4.4 码本功率归一化
码本每个码字的功率应该归一化到 1,否则不同用户的发射功率不一致,ML 距离会被功率大的用户主导。检查方法:mean(sum(abs(CB(:,:,j)).^2, 1))应该接近 1。如果工程里码本没归一化,检测前先除。这个坑很隐蔽,因为 BER 曲线形状可能看起来正常,只是整体偏移,你不对比就发现不了。
5. 避坑与排查:SCMA-ML 工程里最容易翻车的五件事
5.1 现象:BER 恒为 0.5,完全不随 SNR 下降
原因:码本维度顺序错了,[J,M,K]当成[K,M,J]用,导致CB(:, idx, j)取出的码字完全错位。或者dec2base的索引起点搞错,MATLAB 里dec2base(0,M,J)返回全 0,加 1 后才是 1-based,漏加就全取到第 0 列报错或取错。解决:先跑size(CB)校验,再用固定txIdx手动算一次y和yhat,确认距离最小的是真实索引。
5.2 现象:低 SNR 正常,高 SNR 出现误码平台
原因:噪声方差换算错了。Eb/N0转噪声方差时,如果忘了除以bitsPerSym或码率,高 SNR 段实际噪声比设定值大,BER 降不下去。另一个可能是码本未归一化,用户间功率不平衡。解决:打印实际noiseVar,和理论值对比;检查码本功率。
5.3 现象:MATLAB 报「索引超出矩阵维度」
原因:M^J组合遍历时dec2base返回的是字符数组,M=8时字符'7'减'0'得 7,加 1 得 8,没问题;但M>10时dec2base会用字母,减'0'就错了。SCMA 里M一般不超过 8,但如果你改到 16,必须换base2dec或手动进制转换。解决:用ndgrid或allcomb生成索引,别依赖dec2base。
5.4 现象:向量化后结果和循环版不一致
原因:allCodewords预生成时用了旧的CB,后面改了码本没重新生成。或者allIdx的行列顺序和allCodewords的列顺序没对齐。解决:把预生成封装成函数,每次改码本强制重算;用一个小规模M=2, J=2手动验证两种实现结果一致。
5.5 现象:跑M=8, J=6时内存爆掉或卡死
原因:allCodewords是K × M^J,M=8, J=6时 4 × 262144 复数,约 16MB,不该爆;但如果K=8或J=8,就是 8 × 16M,直接上 GB。解决:分块计算距离,每次只生成一部分组合,或者改用球形译码剪枝。ML 穷举本来就不适合大J,工程里如果硬要跑,先降numSym到 100 看趋势。
6. 进阶:用球形译码把 ML 复杂度压下来
穷举 ML 的M^J在J=6, M=8时已经吃力,再大就没法用。球形译码(Sphere Decoding, SD)的思路是只搜索落在以接收信号y为球心、半径r的球内的码字组合,把复杂度从指数降到接近多项式。在 SCMA 里做 SD,关键是把叠加码字写成树结构:第j层对应第j个用户的码字选择,每层累加部分欧氏距离,一旦部分距离超过r^2就剪枝。
实现上,先设一个初始半径,比如用迫零或最小均方误差检测的结果距离作为r。然后按用户逐层展开,每层遍历M个码字,累加||y_partial - sum||^2,超半径就回退。半径可以动态收紧:找到一个可行解后,把r更新为当前最小距离,后续剪枝更狠。这个策略在M=8, J=6时通常能把搜索节点数从 26 万降到几千,速度提升两个数量级。
验证 SD 是否正确,最直接的方法是和穷举 ML 对比 BER:在相同码本、相同信道下,SD 的 BER 应该和 ML 几乎重合,如果有偏差,说明剪枝把最优解剪掉了,半径设太小。我一般先用M=4, J=4这种小规模,穷举和 SD 各跑一遍,确认曲线重合,再上大配置。另一个技巧是记录平均搜索节点数,如果它随 SNR 升高而下降,说明剪枝在起作用;如果一直等于M^J,说明半径没收紧,SD 退化成穷举。
最后说个习惯:每次改码本或信道模型,先跑M=2, J=2的最小用例,手动算一遍期望结果,确认链路无误再放大。SCMA 的矩阵索引多,错一处不报错但结果全错,这个最小用例就是你的后悔药。希望帮到你。
本文还有配套的精品资源,点击获取