简介:面向多输入多输出(MIMO)下行链路中的线性预编码算法比较场景,这份MATLAB源码包系统实现了奇异值分解(SVD)、块对角化(BD)、迫零(ZF)、匹配滤波(MF)、信漏噪比(SLNR)、最小均方误差(MMSE)等多种算法的性能对比,适合通信工程专业学生、算法研究人员以及预编码技术初学者进行仿真验证与算法理解。资源包共34个文件,其中14个M脚本负责主程序、各算法函数、调制映射与注水功率分配,20个FIG图形文件直观呈现和速率、误码率等性能随信噪比或天线数变化的对比曲线,整个压缩包仅207KB,轻量紧凑、便于下载。目前已有228人学习,验证了该资源在预编码仿真中的参考价值。通过运行主脚本,可快速复现4用户2天线MIMO系统下的和速率与误码率性能,并支持动态调整信噪比、发射天线数等参数,观察不同算法的对比结果;代码结构清晰、模块化程度较高,便于移植到自有仿真框架,也可为理解线性预编码原理和论文图表绘制提供有力支撑。
1. 六种线性预编码放在同一套源码里对比:这套代码在解决什么、适合谁
做 MIMO 下行链路仿真时,几乎每个人都会撞上这样一份清单:SVD、BD、ZF、MF、SLNR、MMSE,六种预编码方案摆在面前,文献里每篇论文都说自己的算法最优。可真把代码跑起来就会发现,排序经常变——低信噪比下匹配滤波占优,高信噪比下迫零和块对角化才显本事,天线数不够时 SLNR 又成了那个"怎么都能用"的方案。这套线性预编码性能对比源码,就是解决这个问题的:把六种算法放进同一信道模型、同一功率归一化规则、同一 BER 统计框架里公平比一轮。适合正在做 MIMO 预编码课程设计、物理层算法验证或者基站预编码选型的从业者。看完你能得到一张可以解释"为什么性能排序会变"的曲线,而不是又多一份堆公式的理论笔记。
2. 预编码的数学模型选型:为什么 ZF 会放大噪声,SVD 只在单用户最优
2.1 系统模型:单用户和多用户必须先分清
预编码公式本身不复杂,但六种算法适用的场景差异极大,选型前第一件事是问一句:你的系统是单用户 MIMO 还是多用户 MIMO?
先写系统模型。发射端有 Nt 根天线,服务 K 个用户,第 k 个用户有 Nr,k 根天线。发送符号向量 x_k 经过预编码矩阵 F_k(形状 Nt×Ns,Ns 是发送流数)后,第 k 个用户的接收信号可以写成:
y_k = H_k F_k x_k + Σ_{j≠k} H_k F_j x_j + n_k
前面两项里,第一项是有用信号,第二项是其他用户信号泄漏到本用户的干扰,叫用户间干扰(IUI,Inter-User Interference),n_k 是高斯白噪声,方差为 σ²。
单用户 MIMO 只有 H、F、x、y,没有第二项,问题简化为怎么把一个 Nt×Nr 的信道拆成多个并行子信道。多用户 MIMO 则多了一个维度:除了要照顾每个用户自己的多流复用,还得处理用户之间的干扰。六种算法在这个坐标系里的位置完全不同——MF、ZF、MMSE 单用户和多用户都能用,但多用户下必须用整体信道矩阵求逆;SVD 是单用户的理论最优,直接搬到多用户会因干扰崩溃;BD 和 SLNR 则是典型的多用户方案。选型第一步不是比性能,是比场景,否则后面全是白做。
2.2 单用户家族:SVD 最优,MF/ZF/MMSE 的边界在哪
单用户 MIMO 里,信道矩阵 H 的 SVD 分解 H = UΣV^H 给出了最漂亮的答案:发射端用 V 做预编码(取前 Ns 列),接收端乘 U^H,等效信道变成对角矩阵 Σ,一组并行子信道互不干扰。这就是 SVD 预编码,容量上是最优的,没有争议。
但工程上 SVD 需要接收端把完整的信道状态信息反馈给发射端,且每次信道变化都要重新分解,代价不小。于是就有了更廉价的线性方案:
MF(匹配滤波,也叫 MRT)直接用信道的共轭转置做预编码 F = H^H。它的物理含义是把信号能量集中到目标信道方向上,完全不考虑干扰。接收端信噪比低时,MF 性能反而好,因为它不放大噪声;但信噪比一高,干扰变得无法忽略,曲线会明显掉头。
ZF 迫零预编码取 F = H^H (H H^H)^{-1},目标是让等效信道 HF 变成单位阵,彻底消除干扰。代价是当 H 接近奇异时,预编码矩阵的范数会变得很大,把噪声成倍放大——这就是常说的"ZF 放大噪声"。低信噪比下 ZF 往往比 MF 还差,高信噪比下才追回来。
MMSE 是折中,F = H^H (H H^H + αI)^{-1},其中 α 与噪声方差和发送功率的比值相关。多出来的 αI 起到了正则化作用,防止求逆时放大噪声。当信噪比趋近无穷,MMSE 趋近 ZF;信噪比趋近 0,MMSE 趋近 MF。工程上 MMSE 是默认选择,原因就是它在各种信噪比下都不会太差。
2.3 多用户家族:BD 的零空间与 SLNR 的泄漏
多用户 MIMO 预编码要在每一根发射天线上同时向多个用户发信号,干扰管理成了主角。
BD(块对角化)的核心思想是把其他所有用户的信道构成一个干扰信道矩阵 H̃_k = [H_1^T, ..., H_{k-1}^T, H_{k+1}^T, ..., H_K^T]^T,然后找到这个矩阵的零空间,让预编码矩阵 F_k 完全落在零空间里。这样 H_j F_k ≈ 0(j ≠ k),用户间干扰从物理上被消除。F_k 的求解分两级:先对 H̃_k 做 SVD,取右奇异矩阵中零空间对应的列;再在该零空间内对 H_k 做一次 SVD,挑出最优的 Ns 个方向。BD 性能好且能支持多流,但有一个硬约束:所有用户接收天线总数必须小于发射天线数,否则零空间不存在,公式直接崩掉。
SLNR(信漏噪比)换了个思路,不追求完全消除干扰,最大化"有用信号功率 /(泄漏给其他用户的功率 + 噪声功率)"。对第 k 个用户,SLNR 的优化目标是:
max f^H H_k^H H_k f / f^H (σ²I + Σ_{j≠k} H_j^H H_j) f
这是一个广义瑞利商问题,最优解是广义特征分解中最大广义特征值对应的特征向量。SLNR 的好处是不要求零空间存在,天线数紧张时照样能用;同时对信道误差的鲁棒性比 BD 好。代价是闭式解要解一个 Nt×Nt 的广义特征问题,数值稳定性需要小心处理。
2.4 选型坐标:一张表看六种算法的适用边界
整理成一张选型表,方便照着参数选方案:
| 算法 | 干扰处理 | 核心思路 | 最佳适用位置 | 主要代价 |
|---|---|---|---|---|
| MF | 不处理 | 共轭转置,能量集中 | 低信噪比、单用户基线 | 高信噪比受干扰限制 |
| ZF | 完全消除 | 信道求逆 | 天线富余、高信噪比 | 病态信道放大噪声 |
| MMSE | 折中抑制 | 正则化求逆 | 工程默认、未知信道 | 需要估计噪声方差 |
| SVD | 不处理(单用户) | 信道对角化 | 单用户最优、多流 | 多用户场景需配合 BD |
| BD | 完全消除 | 零空间投影 | 多用户、天线充足 | 总接收天线必须小于 Nt |
| SLNR | 按泄漏最小化 | 广义特征分解 | 天线紧张、信道相关 | 广义特征求解复杂度高 |
这张表是后面所有仿真判断的依据。比如你发现 ZF 的 BER 曲线比 MF 还高,先别怀疑代码,先看信噪比区间;再比如你发现 BD 报"矩阵维度错误",先数一数总接收天线是不是超过 Nt 了。
3. 搭建可复现的对比仿真:信道模型、参数表与最小 Python 框架
3.1 参数怎么定:天线数决定你一半的结论
仿真参数不是随手填的,天线数直接决定哪些算法能跑、哪些算法不能跑。本文采用的是一套多用户配置:Nt = 8 发射天线,K = 4 个用户,每用户 Nr = 2 根接收天线,Ns = 2 条数据流。这样每用户 2 流、4 个用户共 8 条流,总接收天线数等于发射天线数,BD 和 ZF 的维度约束恰好满足。
为什么强调这一点?因为如果总接收天线数大于 Nt,ZF 的 (H H^H) 矩阵就会奇异,BD 也找不到零空间,仿真直接报错。反过来,如果天线数太富余,所有算法性能都会趋同,看不出区别。这套参数是论文和工程里最常见的一组,对比结果最有代表性。
其余参数按常见做法设置:
| 参数 | 取值 | 说明 |
|---|---|---|
| 调制方式 | QPSK | 每符号 2 bit,BER 统计方便 |
| SNR 范围 | 0~20 dB | 步进 2 dB,覆盖典型工作区 |
| 信道模型 | i.i.d. Rayleigh | 每用户独立生成复高斯矩阵 |
| 每用户发射功率 | 归一化为 1 | 逐用户归一化,保证公平比较 |
| 蒙特卡洛帧数 | 视误码率自动增加 | 每个 SNR 点至少统计 50 个错误比特 |
3.2 信道模型与 SNR 定义:瑞利衰落不是随便生成个复数
i.i.d. Rayleigh 信道的生成方法是 H = (randn + j·randn) / √2,这样每个元素的方差是 1。但要提醒一句:生成完信道后最好做一次功率归一化,把信道矩阵的期望功率归一,否则 SNR 的定义会和实际不符。
SNR 的定义直接影响噪声方差:总发射功率和有用信号功率都归一化为 1 后,噪声方差 σ² = 10^{-SNR/10}。这里有个容易绕晕的地方:每个用户发射功率都是 1,四个用户叠加后总发射功率其实变大了。本文刻意采用逐用户归一化,目的是聚焦"算法对干扰的抑制能力",而不是让功率分配差异干扰结论。实际系统里如果需要公平功率分配,还要额外加注水算法或等功率分配,那是另一个话题。
3.3 最小 Python 框架:一套代码跑遍所有系统配置
接下来是一个可直接运行的仿真骨架。先把系统参数、QPSK 调制解调和 BER 统计的主循环搭起来,后面每一章实现的预编码函数都可以直接塞进这个循环:
import numpy as np Nt, K, Nr, Ns = 8, 4, 2, 2 snr_db_range = np.arange(0, 22, 2) # QPSK 调制:把 bit 映射成能量为 1 的复数符号 def bits_to_qpsk(bits): bits = bits.reshape(-1, 2) real = (bits[:, 0] * 2 - 1) / np.sqrt(2) imag = (bits[:, 1] * 2 - 1) / np.sqrt(2) return (real + 1j * imag).astype(complex) # QPSK 硬判决解调 def qpsk_to_bits(syms): hard_real = (syms.real > 0).astype(int) hard_imag = (syms.imag > 0).astype(int) return np.stack([hard_real, hard_imag], axis=1).reshape(-1) # 生成一组多用户信道,每个用户一个 H_k,形状为 (Nr, Nt) def generate_channels(): H_list = [] for _ in range(K): H = (np.random.randn(Nr, Nt) + 1j * np.random.randn(Nr, Nt)) / np.sqrt(2) H_list.append(H) return H_list # 蒙特卡洛主循环:precoder_func 接收 H_list 和 sigma2,返回 F_list def run_ber_simulation(precoder_func): ber_per_snr = [] for snr_db in snr_db_range: sigma2 = 10 ** (-snr_db / 10) total_errors, total_bits = 0, 0 while total_errors < 50 and total_bits < 5e6: H_list = generate_channels() F_list = precoder_func(H_list, sigma2) for k in range(K): bits = np.random.randint(0, 2, Ns * 2) x = bits_to_qpsk(bits).reshape(Ns, 1) Fk = F_list[k] # 有用信号 + 其他用户干扰 + 噪声 y = H_list[k] @ Fk @ x + sigma2 ** 0.5 * ( np.random.randn(Nr, 1) + 1j * np.random.randn(Nr, 1) ) / np.sqrt(2) for j in range(K): if j != k: xj = bits_to_qpsk(np.random.randint(0, 2, Ns * 2)).reshape(Ns, 1) y += H_list[k] @ F_list[j] @ xj Heff = H_list[k] @ Fk # 接收端 MMSE 检测 W = np.linalg.solve(Heff.conj().T @ Heff + sigma2 * np.eye(Ns), Heff.conj().T) est = W @ y rx_bits = qpsk_to_bits(est) total_errors += np.sum(rx_bits != bits) total_bits += len(bits) ber_per_snr.append(total_errors / total_bits) return ber_per_snr这段代码的逻辑说明如下:bits_to_qpsk 把二进制比特映射为 ±1/√2 ± j/√2,保证每个符号的平均能量为 1;run_ber_simulation 是蒙特卡洛主循环,外层遍历 SNR,内层不断产生信道、发送数据、计算预编码、叠加噪声和干扰,最后统计误比特率。
循环终止条件用的是"至少统计 50 个错误比特",而不是固定帧数。为什么这么做?因为高信噪比下误码率很低,如果固定帧数,BER 曲线后半段会抖动得很厉害;按错误比特数控制,可以让每个 SNR 点的统计精度基本一致。上限 5×10^6 bit 是为了防止极低误码率时仿真时间过长。
3.4 预编码矩阵的功率归一化:这个坑不踩,后面全是幻觉
预编码函数返回的 F_k 不能直接用,必须做功率归一化。原因在于 MF 和 ZF 的矩阵范数差异可以差出几个数量级,如果不归一化,MF 的曲线会因为发送功率小得可怜而"看起来"差到离谱,ZF 又会因为范数太大而表现得异常优秀。这不是算法本身的差异,是功率没对齐。
本文统一用逐用户 Frobenius 归一化,在每个预编码函数返回前加一行:
Fk = Fk / np.linalg.norm(Fk, 'fro')这样一来每个用户的发射功率恒为 1,各 SNR 点的噪声方差 σ² 可以直接用 10^{-SNR/10}。需要说明的是,逐用户归一化的代价是总发射功率会随用户数增长,但既然我们的目标是算法干扰抑制能力的对比,这是最公平的基准。如果将来要模拟真实基站功率受限场景,就要改成整体归一化,并配合用户间的功率分配策略,那属于系统级仿真的范畴了。
4. 写对比脚本:六种预编码的实现要点、关键函数与参数校准
4.1 MF/ZF/MMSE 实现:整体信道矩阵与逐用户归一化的次序
MF、ZF、MMSE 在多用户场景下的标准做法是把所有用户的信道堆叠成一个总矩阵 H_all,形状为 (K·Nr) × Nt,然后统一求预编码矩阵 F_all,再按用户拆开、逐用户归一化。
def mf_precoder(H_list, sigma2): H_all = np.vstack(H_list) # (K*Nr, Nt) F_all = H_all.conj().T # (Nt, K*Nr) return split_and_normalize(F_all, Ns) def zf_precoder(H_list, sigma2): H_all = np.vstack(H_list) # 用 solve 代替 inv,数值稳定性更好 F_all = H_all.conj().T @ np.linalg.solve(H_all @ H_all.conj().T, np.eye(H_all.shape[0])) return split_and_normalize(F_all, Ns) def mmse_precoder(H_list, sigma2): H_all = np.vstack(H_list) alpha = sigma2 # 每用户功率归一为 1,故 alpha = sigma2 F_all = H_all.conj().T @ np.linalg.solve( H_all @ H_all.conj().T + alpha * np.eye(H_all.shape[0]), np.eye(H_all.shape[0]) ) return split_and_normalize(F_all, Ns) def split_and_normalize(F_all, Ns): F_list = [] for k in range(K): Fk = F_all[:, k * Nr:(k + 1) * Nr][:, :Ns] F_list.append(Fk / np.linalg.norm(Fk, 'fro')) return F_list三个函数的区别只在一行:ZF 用的是 H_all @ H_all^H 求逆,MMSE 在求逆前加了 αI,MF 干脆不求逆。这段代码里有两个参数需要注意。
第一个是 α 的取值。由于前面做了逐用户功率归一化,每个用户的发射功率是 1,所以 α = σ²。如果改成整体功率归一化,α 必须跟着发射功率缩放,否则 MMSE 会退化成 ZF。第二个是切分方式:第 k 个用户占据 F_all 的第 k·Nr 到 (k+1)·Nr 列,若每用户只发 Ns 条流,就取前 Ns 列。这里最常犯的错是把 Ns 和 Nr 弄混,导致矩阵维度对不上,或者在 BER 统计时直接报广播错误。
4.2 SVD 预编码:只在单用户才成立的最优
SVD 预编码在单用户 MIMO 里的实现非常简洁,发射端取 V 的前 Ns 列,接收端乘 U^H 即可把信道拆成并行子信道。在多用户场景里,经典做法是把 SVD 作为 BD 的内部步骤,而不是独立算法。所以这一小节先给单用户的完整实现:
def svd_single_user_precoder(H, sigma2, Ns=2): U, S, Vh = np.linalg.svd(H, full_matrices=False) F = Vh[:Ns, :].T # V 的前 Ns 列 return F / np.linalg.norm(F, 'fro')代码本身没什么玄学,但有一个认知要掰正:SVD 在多用户场景下如果直接对每个用户分别做 SVD,完全不处理 IUI,用户间干扰会成为主要矛盾,BER 曲线会表现出明显的错误平台。文献里常看到的"SVD 性能最优"几乎都限定在单用户或者配合 BD 使用的场景,单独拿 SVD 去做多用户对比属于刻舟求剑。
4.3 BD 的实现:两级 SVD 与零空间维度判断
BD 是六个算法里实现最容易出 bug 的一个。核心逻辑是对每个用户构造干扰信道矩阵 H̃_k 并做 SVD,取零空间基,再在零空间内对 H_k 做第二次 SVD。
def bd_precoder(H_list, sigma2): F_list = [] for k in range(K): # 第一步:堆叠其他所有用户的信道 H_others = np.vstack([H_list[j] for j in range(K) if j != k]) _, _, Vh_others = np.linalg.svd(H_others, full_matrices=True) null_dim = Nt - H_others.shape[0] # 零空间维数 = Nt - (K-1)*Nr V_null = Vh_others[-null_dim:, :].T # 零空间基,形状 (Nt, null_dim) # 第二步:在零空间内对目标用户做第二次 SVD H_eff = H_list[k] @ V_null _, _, Vh_eff = np.linalg.svd(H_eff, full_matrices=False) Fk = V_null @ Vh_eff[:Ns, :].T F_list.append(Fk / np.linalg.norm(Fk, 'fro')) return F_list逻辑说明:第一步 Vh_others 的形状是 (Nt, Nt),由 SVD 的性质,最后 null_dim 行对应 H_others 的零空间方向,经过转置后成为零空间基;第二步把 H_k 投影到零空间,再取前 Ns 个右奇异向量作为最终的预编码方向。
一个关键判断:null_dim = Nt - (K-1)·Nr。在本文参数下是 8 - 6 = 2,恰好等于每用户流数 Ns,因此每个用户的预编码矩阵是方阵。如果 zero dimension 小于 Ns,BD 就无解,后面 5.2 会详细展开这种情况的处理。另外注意全 SVD 和瘦 SVD 的区别:这里必须用 full_matrices=True 才能拿到完整的零空间,用默认的瘦 SVD 会直接丢维度报错。
4.4 SLNR 的实现:广义特征分解的取法
SLNR 的实现核心是求解广义特征值问题。每个用户的优化目标涉及两个 Nt×Nt 的 Hermitian 矩阵,A_k = H_k^H H_k 对应有用信号,B_k = σ²I + Σ_{j≠k} H_j^H H_j 对应泄漏加噪声。最优预编码是最大广义特征值对应的特征向量。
from scipy.linalg import eigh def slnr_precoder(H_list, sigma2): F_list = [] for k in range(K): A_k = H_list[k].conj().T @ H_list[k] # 有用信号相关矩阵 B_k = sigma2 * np.eye(Nt) # 噪声项,保证 B 正定 for j in range(K): if j != k: B_k += H_list[j].conj().T @ H_list[j] # 泄漏项 # 广义特征分解,特征值升序排列 _, eigvecs = eigh(A_k, B_k) Fk = eigvecs[:, -Ns:] # 取最大的 Ns 个特征向量 F_list.append(Fk / np.linalg.norm(Fk, 'fro')) return F_listscipy.linalg.eigh 的第二个位置参数传入 B_k,返回的特征值默认按升序排列,最大特征值在最后,所以取 -Ns 到最后列。这里有两个容易翻车的点:一是 B_k 必须正定,好在 σ²I 作为正则项保证了这一点;二是如果误取前 Ns 列(最小特征值),SLNR 的曲线会比 MF 还难看,这种错误从数值上完全不会报错,只能靠对比曲线位置发现。
如果环境里没有 scipy,可以用 numpy 替代:对 B_k 做 Cholesky 分解 B = L L^H,通过变换把广义特征问题转为普通特征问题,但数值稳定性稍差,仍建议优先 eigh。
4.5 串起来跑完整轮:五个算法全部接入主循环
把上面三个函数和 SVD 单用户实现都接入 run_ber_simulation,就能得到完整的对比图。实际跑的时候我建议每换一个算法都单独确认一次曲线形状:MF 的 BER 在低 SNR 应该和中高 SNR 的 ZF 有交叉;MMSE 的曲线应该全程贴着 ZF 或更优;BD 和 SLNR 在高 SNR 下应该明显优于 MF。如果这些"常识位置"对不上,优先检查归一化和接收端检测器,而不是先怀疑理论。
5. 预编码对比的五个常见翻车点:现象、根因与修复
5.1 功率归一化不统一,MMSE 在低 SNR"好"得离谱
现象:MMSE 的 BER 在低信噪比下显著优于 SVD,甚至逼近无干扰理论上限,怎么看都不合理。
原因:预编码矩阵没有做逐用户功率归一化,或者归一化方式不一致。MF 的 F 范数天然比其他算法小,ZF 的范数又偏大,如果直接用未归一化的矩阵仿真,实际上每用户发射功率相差十倍以上,BER 曲线反映的是功率差异而不是算法差异。
解决:统一在预编码函数末尾执行 Fk = Fk / np.linalg.norm(Fk, 'fro')。同时检查主循环里的 x 符号能量是否为 1,QPSK 映射必须用 ±1/√2 而不是 ±1,否则 SNR 的定义整体偏移 3 dB。这一步做完,各算法的相对位置才会回到正常范围。
5.2 天线维度违反约束,BD 和 ZF 直接矩阵奇异
现象:把用户数从 4 改成 5,或者每用户天线数从 2 改成 3,BD 报 svd 维度错误,ZF 报 LinAlgError,而 SLNR 和 MF 还能正常出数。
原因:BD 的零空间维度 = Nt - (K-1)·Nr,ZF 要求总接收天线数 ≤ Nt。当用户天线总数超过发射天线时,H̃_k 的零空间不存在,数学上无解。代码不会提示"约束不满足",只会表现为矩阵奇异或维度对不上。
解决:在仿真脚本入口加一个断言。Nt ≥ K·Nr 是 ZF 和 BD 的必要条件;如果 Nt 不够,要么减少用户数或单用户天线数,要么改用 SLNR。SLNR 不要求零空间,天线紧张时它是少数还能工作的线性方案,这也是它的一大实用价值。
5.3 接收端检测器和预编码混为一谈,曲线高得没有意义
现象:同一套预编码代码,有些人跑出来的 BER 比论文里的还好,仔细看是接收端用了所谓的"理想判决"或者把整条链路当成单用户等效来检测。
原因:接收端后处理方式没有统一。预编码性能对比应该固定接收机,否则 ZF 预编码配合 ZF 检测、MMSE 预编码配合 MMSE 检测,会让结果变成"预编码+检测器"的联合性能,谁占便宜完全说不清。
解决:统一用 MMSE 检测器,公式是 W = (H_eff^H H_eff + σ²I)^{-1} H_eff^H,即前文主循环里的写法。这样对 ZF 和 BD 这类已经消除干扰的方案,检测器退化为匹配滤波;对 MF 和 SLNR 这类残留干扰的方案,检测器还能起到一定补偿作用。需要对比接收机影响时,再单独把检测器作为变量。
5.4 蒙特卡洛统计帧数太少,BER 曲线抖到没法判断
现象:低 SNR 区域 BER 两条曲线纠缠在一起,高 SNR 区域曲线抖得像是噪声信号,同一个点每次跑结果都不一样。
原因:BER 统计是靠随机事件撑起来的。高 SNR 下误码事件很稀疏,如果固定只跑 500 帧,可能一个错误比特都没有,BER 记成 0,曲线直接断掉;低 SNR 下如果只统计了几百个比特,误码率的方差非常大。
解决:按"每个 SNR 点至少统计 50 个错误比特"作为循环退出条件,同时设定一个最大比特数上限防止算法陷入死循环。实际仿真时还可以给高 SNR 点增加帧数,比如从 0 dB 到 20 dB 每个点单独设置统计量,这是最稳的提速手段。
5.5 SLNR 广义特征分解取错方向,性能比 MF 还差
现象:SLNR 的 BER 曲线在高 SNR 下不仅没有收敛,反而比 MF 差,且差得很稳定——不是随机抖动,是稳定的差。
原因:scipy.linalg.eigh 返回的特征值默认升序排列,第一次写 SLNR 的人极容易自然取前 Ns 列,也就是最小特征值对应的特征向量。这些方向完全不匹配信号子空间,预编码等于把能量发到了错误方向。
解决:取特征向量矩阵的倒数 Ns 列,即 eigvecs[:, -Ns:]。如果还是不放心,可以打印广义特征值看一眼:最大特征值应该显著大于其他特征值,并且随 SNR 升高而增大。这一步属于典型的"代码不报错、曲线让你怀疑人生"的坑,排查时先怀疑方向再怀疑公式。
6. 换一种信道再跑一遍:相关性信道下性能排序为什么会变
前面所有仿真用的都是 i.i.d. Rayleigh 信道,这个模型假设各天线之间的信道完全不相关。但真实基站天线间距有限,发射端往往存在相关性,信道不再满足 i.i.d. 假设。要验证预编码方案的鲁棒性,常见的做法是用 Kronecker 模型产生发射端相关信道:H = H_iid · R_t^{1/2},其中 H_iid 是原来的复高斯矩阵,R_t 是发射端相关矩阵。
指数相关模型生成 R_t 的方式很直观:R_t[i,j] = ρ^{|i-j|},ρ 是相邻天线间的相关系数,取 0.6 左右就能明显改变结论。对应代码只有几行:
def generate_correlated_channel(Nr, Nt, rho=0.6): R_t = np.array([[rho ** abs(i - j) for j in range(Nt)] for i in range(Nt)]) L = np.linalg.cholesky(R_t) H_iid = (np.random.randn(Nr, Nt) + 1j * np.random.randn(Nr, Nt)) / np.sqrt(2) H = H_iid @ L.T # 归一化使发射端相关矩阵的迹为 Nt,信道功率不随 rho 改变 return H / np.sqrt(np.trace(R_t) / Nt)把 generate_channels 换成这个函数,其他代码一行都不用改。跑完后你会发现性能排序发生了三个值得注意的变化:第一,ZF 的高 SNR 优势被明显压缩,因为相关信道导致 H_all @ H_all^H 的条件数变大,求逆放大了噪声;第二,BD 的零空间虽然还存在,但有效子信道的增益变小,BER 曲线整体上移;第三,MMSE 和 SLNR 的相对位置提前了,α 和泄漏项的正则化作用在这种病态信道下反而成了优势。
我自己的一个习惯是:做任何预编码对比前,先写一个只含 MF 和 ZF 的基线脚本,分别跑一遍 i.i.d. 信道和相关性信道,确认两条曲线的相对位置符合理论预期——MF 低 SNR 好、ZF 高 SNR 好、相关信道下差距缩小——再往上加 BD 和 SLNR。这个习惯帮我挡掉过多次实现写错导致的翻车,比任何公式验证都直接。希望帮到你。
本文还有配套的精品资源,点击获取