每次看到“OFDM + QPSK + 信道估计”这个组合,不用猜,八成是要在 Matlab 里搭一条完整收发链路,先把 LS、MMSE、LMMSE 跑通,再用深度学习做一个估计器,最后画几条误码率曲线证明“深度学习方法更有效”。这个题目听起来不复杂,真正动手做起来,反直觉的地方比想象中多得多。我去年把这条链路从头到尾完整走了一遍,包括信道模型怎么定、导频怎么插、LMMSE 的自相关矩阵怎么生成、DNN 输入为什么选 LS 粗估计而不是原始接收信号、训练数据怎么标定,以及 BER 曲线背后的那些“明明结果对但总感觉哪里不对”的小细节。这篇就当作一次完整复盘,适合正在复现这篇题目思路的同学,也适合想快速对比传统估计器和深度学习方法差距的入门研究者。
1. OFDM+QPSK这条链路里,信道估计到底卡在哪个环节
1.1 你收到的信号并不是“发什么就收什么”
OFDM 做的事情,是把一串想要发送的频域符号放到很多子载波上,然后用 IFFT 变成时域波形发出去。接收端再做 FFT,把时域波形还原成频域符号。听起来像是一个“无损封装”的过程,但无线信道不会配合你。多径传播会让同一个符号沿着不同延迟到达接收机,不同路径之间相互叠加,反映到频域上,就是每个子载波被乘上了一个复数信道增益,同时再加上了噪声。
用公式表示就是:
Y(k) = H(k) X(k) + W(k)
其中 k 是子载波索引,H(k) 是复信道系数,W(k) 是噪声。QPSK 解调本来对相位非常敏感,而 H(k) 会让星座点发生旋转和缩放,所以接收端必须先估计出 H(k),再把接收信号除以这个估计值,也就是做均衡,才能恢复出发送符号。这条链路的核心问题,就是怎么在导频有限、噪声存在的情况下,把 H(k) 估得尽量准。
1.2 为什么仿真都喜欢选 QPSK 而不是 16QAM 或 64QAM
一个很现实的原因是,QPSK 星座点模值固定,每个复符号的幅度都一样。这看起来是个很普通的细节,但它会直接影响后面的估计算法复杂度。比如 LMMSE 公式里有一个常数 β,它的定义是:
β = E{|X(k)|²} · E{1/|X(k)|²}
对 QPSK 来说,所有星座点模值都相同,β = 1,LMMSE 会退化成非常简洁的形式。如果换成 16QAM,星座点幅度有 3 种,β 不再是 1,公式里就要多算一个系数。很多课程设计和论文用 QPSK,不是因为 QPSK 性能好,而是因为它能让算法对比的焦点放在信道估计本身,而不是星座图带来的额外复杂性。
1.3 链路里的算法分工要先理清楚
信道估计和均衡是两件事,但经常被混在一起说。
- 信道估计解决的是:H(k) 是多少?
- 均衡解决的是:拿到 H(k) 的估计值之后,怎么把 X(k) 还原出来?
LS、MMSE、LMMSE 都是信道估计算法,它们输出的是一个信道矩阵估计值。均衡可以用 ZF,也可以用 MMSE,这是个相对独立的模块。我这套仿真里,为了公平对比,所有估计器后面都用同一个 MMSE 均衡器:
eq(k) = conj(H_est(k)) / (|H_est(k)|² + N0)
如果信道估计误差大,均衡器再优化也救不回来;反过来,信道估计很准,但均衡器放大噪声,BER 一样会崩。所以仿真时要有一个“理想信道估计”的对照组,这样才能看出性能损失到底是估计带来的,还是均衡带来的。
2. LS、MMSE、LMMSE:三种经典估计器为什么按这个顺序实现
2.1 LS估计:简单到像“除法”,但也不完全是除法
LS 估计的思路非常直观:在导频子载波上,发送符号是已知的,那么接收信号除以这个已知符号,就能得到该位置的频域信道响应。
H_LS(p) = Y_p / X_p
如果把导频符号功率归一化到 1,除法可以直接变成乘以导频符号的共轭。Matlab 里写起来就是:
H_ls_pilot = rxGrid(pilotIdx) ./ pilotSym;问题在于,导频只占了部分子载波,数据子载波上还得靠插值。我的仿真里 FFT 点数是 64,导频每隔 4 个子载波插一个,导频数 16,剩下的 48 个子载波都是数据。从 16 个导频位置的 LS 估计,插值到 64 个子载波,用线性插值就能得到一个完整的 H_LS:
H_ls = interp1(pilotIdx, H_ls_pilot, (1:Nfft)', 'linear', 'extrap');LS 的优点是几乎不需要信道统计信息,缺点也明显:噪声被原样保留在估计里。导频处信噪比越低,LS 估计越抖;导频间隔如果太大,插值还会带来额外误差。
2.2 MMSE:用信道相关性把噪声“平均掉”
既然 LS 估计噪声大,那能不能利用信道本身的统计规律来修一修?可以。M MSE 估计的思路是:已知导频处的 LS 估计,以及信道在频域上的自相关矩阵,找一个线性变换,使得估计结果与真实信道之间的均方误差最小。
H_MMSE = R_HH · (R_HH + (N0 / Es) · I)^(-1) · H_LS_pilot
其中 R_HH 是导频位置之间的信道频域自相关矩阵,它是从多径信道的功率延迟谱算出来的。延迟为 τ 的一条路径,在频域相隔 Δk 的两个子载波上,会产生 exp(-j·2π·Δk·τ/Nfft) 的相关性。路径功率越大,相关性贡献越大。把所有路径的贡献加起来,就得到整个矩阵。
这个矩阵对性能和复杂度的影响非常大。它需要知道信道的功率延迟谱,也就是信道路径的数目、延迟和平均功率。如果这些信息不准确,MMSE 的性能优势会明显缩水。复杂度方面,每个 OFDM 符号都要做一次 Np × Np 的矩阵求逆和矩阵乘法,Np 是导频数。在 64 子载波的小仿真里无所谓,子载波数到 1024、2048 之后,这个复杂度就很难承受。
2.3 LMMSE:把“严格最优”变成“工程可用”
严格意义上的 MMSE 公式里,噪声项其实应该是 N0 · (XX^H)^(-1),也就是说它依赖当前发送符号的星座结构。导频符号如果每个能量都固定,那这项可以写成常数。LMMSE 做的就是这件事:用 β/SNR 代替原来的符号相关项,把矩阵表达式变成一个和具体发送符号无关的形式。
对 QPSK,β = 1,LMMSE 和 MMSE 在实际计算结果上几乎一样,但在代码实现上,LMMSE 更偏工程化。仿真里可以这么写:
H_LMMSE = R_cross · (R_pilot + (N0/Es)·I)^(-1) · H_LS_pilot
这里的 R_pilot 是导频位置自相关矩阵,R_cross 是所有子载波和导频位置之间的互相关矩阵。直接用第一次生成的频域自相关矩阵取下标就可以,不需要重复求相关。优点是基于统计先验能显著压低噪声;缺点是需要信道相关矩阵,这是“看起来不需要导频外额外开销、实际等于默认你知道信道延迟功率谱”的一种假设。
三种经典方法放到一张表里看,差异很清楚:
| 估计算法 | 额外依赖信息 | 实现复杂度 | 性能特点 |
|---|---|---|---|
| LS | 只需要导频符号 | 低 | 噪声残留大,插值误差明显 |
| MMSE | 需要信道相关矩阵和噪声方差 | 高,需要矩阵求逆 | 线性最优,性能明显好于 LS |
| LMMSE | 需要信道相关矩阵,β 可约定 | 中等,适合 QPSK | 与 MMSE 接近,实现更简洁 |
3. 深度学习估计器:不学原始信号,先学LS估计的“残差”
3.1 为什么输入用LS估计结果,而不是直接输入接收信号
一开始我按很多论文的思路,尝试把接收信号实部虚部直接输入网络,期待网络自己学会信道估计和均衡。试过之后效果并不好,原因也很简单:直接输入受数据符号调制影响的接收信号,网络需要同时处理未知数据、导频结构、信道相位和噪声,这个问题太大、太不唯一了。
后来换了个思路:先用 LS 得到导频位置的粗估计,再插值成完整频带上的“含噪信道图”,把网络当成一个去噪和超分辨模块。这和图像处理里先做双线性插值再做 CNN 去噪非常像。多径信道在频域上存在相干性,相邻子载波之间信道不是完全独立的,深度学习能利用这种结构去恢复被噪声破坏的细节,而 LMMSE 只能利用线性相关。
输入和输出的设计就很清晰了:
- 输入:LS 插值后的完整频带信道估计,拆成实部和虚部,拼接成一个 2Nfft 维向量
- 输出:理想信道的实部和虚部拼接向量
- 标签:仿真时生成的真实信道 H_true
3.2 网络结构不需要很大,全连接就够了
很多初学者一上来就套 CNN、Transformer,把模型搞得很重。但这个任务的输入是一个一维复向量,子载波之间的局部相关性用两层全连接就能拟合得很好。我的网络结构是:
layers = [ featureInputLayer(2*Nfft, 'Normalization', 'zscore') fullyConnectedLayer(128) reluLayer fullyConnectedLayer(256) reluLayer fullyConnectedLayer(256) reluLayer fullyConnectedLayer(128) reluLayer fullyConnectedLayer(2*Nfft) regressionLayer ];中间隐藏层没有用 dropout,因为训练数据本身是带噪声的,随机信道生成器会天然提供正则化。如果发现训练损失和验证损失差距大,再补一层dropoutLayer(0.2)就行。全连接网络训练快,推理也就是几次矩阵乘法,在误码率仿真主循环里反复调用完全扛得住。
3.3 训练数据必须覆盖混合信噪比
这是整套方案里最容易“看起来对但完全错”的地方。我第一版只用 10 dB 单一信噪比生成训练数据,结果 20 dB 测试时,网络输出明显偏“软”,BER 不降反升。原因是网络只见过 10 dB 的噪声特征,看到更干净的输入反而不认识了。
所以训练时,每个样本的 SNR 要随机从 0 dB 到 25 dB 之间抽取。这样网络学到的不是一个特定噪声强度的映射,而是“从含噪 LS 到真实信道”的一般性去噪映射。测试时无论 SNR 落在哪个位置,输入特征分布都在训练范围内。
训练样本构造流程如下:
for idx = 1:numTrainSamples snrTrain = randi([0, 25]); [H_true, H_ls_interp] = generateOneSample(Nfft, snrTrain); X(:, idx) = [real(H_ls_interp); imag(H_ls_interp)]; Y(:, idx) = [real(H_true); imag(H_true)]; end options = trainingOptions('adam', ... 'MaxEpochs', 300, ... 'MiniBatchSize', 64, ... 'ValidationData', {Xval, Yval}, ... 'Plots', 'training-progress', ... 'Verbose', false); net = trainNetwork(X, Y, layers, options);训练集和验证集都用完全独立的随机信道生成,验证集不参与训练。这个“训练集和测试集是否同源”的问题,比网络结构本身更容易让最终结果失真,很多人在这里翻车。
4. Matlab仿真工程:从帧结构到BER曲线的一次完整搭建
4.1 仿真参数先固定好,后面才不会被数据扰动
我把核心参数列出来,这些参数决定了系统行为和 BER 曲线的走向:
| 参数 | 取值 | 说明 |
|---|---|---|
| 调制方式 | QPSK | 每个复符号 2 bit |
| FFT 点数 | 64 | 子载波总数 |
| 循环前缀长度 | 16 | 大于最大信道时延,避免符号间干扰 |
| 导频间隔 | 4 | 每隔 4 个子载波放置一个导频 |
| 导频数量 | 16 | 占总子载波 25% |
| 数据子载波 | 48 | 剩余 48 个子载波传输数据 |
| 信道抽头延迟 | 0, 1, 3, 5 个采样点 | 频率选择性信道 |
| 抽头平均功率 | 0, -3, -6, -9 dB | 指数衰减型功率延迟谱 |
| 蒙特卡洛次数 | 10000 | 每个信噪比下独立生成至少 10000 个 OFDM 符号 |
信道要归一化,让所有抽头总功率等于 1:
h = sqrt(10.^(pdp/10)) .* (randn(1,4) + 1j*randn(1,4))/sqrt(2); h = h / sqrt(sum(abs(h).^2));不归一化的话,同样的 SNR 定义下,接收信号功率会随随机信道变化,BER 曲线上下震荡得很厉害。
4.2 主循环代码的主体结构
发送端每轮随机生成数据比特,映射成 QPSK 符号,导频位置放固定导频,IFFT 加 CP 后过信道,再加高斯白噪声:
for snr = snrList for mc = 1:numMC bits = randi([0 1], length(dataIdx)*2, 1); dataSym = qpskMod(bits); txGrid = zeros(Nfft, 1); txGrid(dataIdx) = dataSym; txGrid(pilotIdx) = (1+1j)/sqrt(2); tx = ifft(txGrid, Nfft); tx = [tx(end-Ncp+1:end); tx]; rx = filter(h, 1, tx); rx = rx + sqrt(10^(-snr/10)/2) * (randn(size(rx)) + 1j*randn(size(rx))); rxGrid = fft(rx(Ncp+1:end), Nfft); % 信道估计 H_ls_pilot = rxGrid(pilotIdx) ./ pilotSym; H_ls = interp1(pilotIdx, H_ls_pilot, 1:Nfft, 'linear', 'extrap'); H_lmmse = lmmseEstimate(H_ls_pilot, ...); H_dnn = predict(net, [real(H_ls).'; imag(H_ls).']); % MMSE均衡 No = 10^(-snr/10); eqDnn = conj(H_dnn) ./ (abs(H_dnn).^2 + No); rxData = rxGrid(dataIdx) .* eqDnn(dataIdx); rxBits = qpskDemod(rxData); errorCount = errorCount + sum(rxBits ~= bits); end ber = errorCount / (numMC * length(bits)); end这里我没有直接用awgn函数,因为awgn默认认为输入信号功率是“measured”,也就是会实时测量时域信号功率再来配噪声。如果信道归一化做得不严格,SNR 的实际含义会漂移。直接按理论 N0 生成高斯白噪声,更稳。
4.3 LMMSE 相关矩阵的构造方式
LMMSE 最核心的代码是频域自相关矩阵。假设抽头延迟为 delays(单位是采样点),抽头线性功率为 p,则两个子载波 i 和 j 之间的信道相关性为:
R(i,j) = sum over paths of p_path · exp(-j·2π·(i-j)·delay_path/Nfft)
在 Matlab 里可以这样生成:
function R = channelCorrMatrix(Nfft, delays, pdpLinear) R = zeros(Nfft, Nfft); for i = 1:Nfft for j = 1:Nfft diff_idx = i - j; R(i, j) = sum(pdpLinear .* exp(-1j * 2 * pi * diff_idx * delays / Nfft)); end end end然后取导频位置和全频带位置的下标:
R_all = channelCorrMatrix(Nfft, delays, pdpLinear); R_pilot = R_all(pilotIdx, pilotIdx); R_cross = R_all(:, pilotIdx); H_lmmse = R_cross * ((R_pilot + No/Es * eye(length(pilotIdx))) \ H_ls_pilot);注意这里用的是“理想信道相关矩阵”,它不含噪声,只由多径功率延迟谱决定。仿真中我们是人为设定信道模型的,所以相关矩阵可以准确算出来,这相当于给了 MMSE/LMMSE 一个公平的先验。如果实际信道里多径结构未知,这个优势就不存在了。
5. BER曲线结果与那些容易“看起来对但实际错”的坑
5.1 我在这套参数下观察到的性能趋势
训练完成后,在同样的独立测试信道上比较 LS、LMMSE、DNN 和理想信道估计四条 BER 曲线。我的结果里,LS 插值方法在低信噪比下表现最差,因为噪声和插值误差叠加在一起。LMMSE 有明显提升,因为相关矩阵能进一步滤掉噪声。DNN 在大部分信噪比下都优于 LMMSE,特别是在 0 dB 到 12 dB 这段区间,直观上大约能带来 2 dB 左右的增益。随着 SNR 升高,所有方法的绝对误差都变小,曲线逐渐靠拢,理想信道估计和 DNN 的差距通常能控制在 1 dB 以内。
这个趋势符合直觉:DNN 训练目标就是学习“从含噪 LS 到真实信道”的映射,它不只是线性组合,还能利用多径信道的结构信息,在低信噪比时相当于做了一个更强的去噪器。高信噪比时 LS 本身已经比较准,DNN 的改进空间自然变小。
5.2 训练与测试信噪比不匹配,是深度学习信道估计最大的坑
我前面提过单一 SNR 训练会翻车,这里再强调一下。用 0 dB 训练的数据,网络学会的其实是“把周围一切可疑波动都平均掉”,拿到 25 dB 的高信噪比输入后,它会不分青红皂白把真实信道细节也抹掉。反过来,用 25 dB 训练,网络只会做微小修正,拿到 0 dB 输入时完全不知道该怎么降噪。
一个比较实用的做法是让训练 SNR 覆盖测试范围,并保持分布密度一致。如果你的目标是看 0~25 dB 的 BER 曲线,那训练 SNR 就从 0~25 dB 均匀随机取。如果想要网络在某个重点 SNR 附近表现更好,可以对那个 SNR 附近做 2 倍过采样,但不能只训该点。
5.3 三个容易出错的小问题:SNR定义、信道归一化、比特统计
第一,SNR 定义。标题里说“误码率仿真”,如果你画的是 BER vs Eb/N0,那 Es/N0 = Eb/N0 + 3 dB(QPSK 下每个符号两个比特)。我在仿真里用的是 Es/N0,所以代码里 N0 = 10^(-snr/10)。如果混用,BER 曲线整体会平移 3 dB,三个算法之间的相对差距倒是不变,但和理论曲线对比时会非常难看。
第二,信道归一化。多条路径平均功率相加如果不等于 1,接收信号的平均功率就不是 1,噪声功率按 Es/N0 加进去之后,实际 SNR 就和名义 SNR 不一致。最直接的验证方法是在某个固定 SNR 下加一个理想信道估计基线,如果理想 BER 能贴合理论 QPSK BER,那 SNR 标定基本就没问题。
第三,比特统计。一个 QPSK 符号是两个 bit,BER 应该是“错误 bit 数 / 总 bit 数”,不是“错误符号数 / 总符号数”。如果符号判决错了但两位 bit 中只有一位错,按符号统计就会少算一半错误。很多人在最后算出来 BER 偏低,就是这个原因。
5.4 想继续扩展的话,后续可以做这几个方向
这套链路跑通之后,近一步的空间还很大。比如把单天线 OFDM 换成 MIMO-OFDM,输入从一维信道向量变成二维信道矩阵,DNN 可以改用 CNN;也可以把线性插值这一步去掉,让网络直接吃导频位置的 LS 估计,输出全频带信道,这样能同时学习插值和去噪;如果信道是时变的,还能把相邻 OFDM 符号按时间维组织起来,用 LSTM 或简易 Transformer 捕捉时间相关性。
但我的建议是,项目初期不要急着换大模型。先把 LS、LMMSE、DNN 三者之间的差距、训练数据的影响、SNR 标定的问题搞清楚,比刷高几个点的性能重要得多。我自己在这个项目里后期改动最多的不是网络结构,而是数据生成和 SNR 定义,这些细节稳定之后,所有算法对比才会真正有意义。