简介:针对LMS与RLS自适应均衡算法,资源包提供MATLAB 2021a及更高版本下的仿真对比实现。面向通信工程、信号处理方向的学生与工程师,可用于理解多径信道与频率选择性衰落下的均衡器设计,比较两种算法在收敛速度、稳态误差和计算复杂度上的差异,也适用于无线通信基带处理、水声通信等需要自适应均衡的场景。包内共4个文件,包括2个MATLAB脚本、1份算法分析文档和1份FPGA与MATLAB协同设计说明,整体压缩包约74KB,结构简洁,便于直接运行与查阅。目前已有271人学习下载。通过仿真源码可调整信道参数与滤波器阶数,直观对比两类算法在不同信噪比下的误码率与收敛曲线;配套文档补充了LMS与RLS的数学推导、滤波器阶次与步长因子的参数优化建议,以及从MATLAB仿真模型到FPGA硬件实现的映射方法,适合从仿真走向实际部署时参考。
1. 为什么拿 LMS 和 RLS 比:自适应均衡器的“收敛-稳态”天平
信道衰落和多径造成的码间干扰(ISI)是通信系统里绕不开的黑匣子。要把它压下去,最经济的做法就是在接收端挂一个自适应均衡器,用已知训练序列让抽头系数自动收敛到逆信道响应。LMS 结构简单、每步只需两次乘法,但步长选小了收敛慢、选大了稳态误差大;RLS 借助矩阵递推,收敛速度快一个量级以上,但计算量多一个维度,遗忘因子稍微调错就可能发散。这篇内容我用 MATLAB 2021a+ 把两个算法做在同一组信道数据上,给出可直接跑的脚本、收敛曲线对比和参数设置的血泪经验,适合做通信基带仿真、毕业设计或者换算法选型的同学照着复现、对比、改参数。
2. 理论先行:LMS 与 RLS 的迭代公式和收敛特性差异
自适应均衡的核心是把均衡器抽头向量 w 看成待优化的变量,用误差信号 e(n) 驱动更新。LMS 和 RLS 的区别在于更新方向和步长怎么算。一个用的是瞬时梯度,一个用的是递推最小二乘;一个只需要几个标量运算,一个要维护一个矩阵。这两种不同的更新策略,直接决定了它们在收敛速度、稳态误差和复杂度上的表现。
2.1 LMS 的最小均方误差迭代:μ 是唯一的步长旋钮
LMS 对代价函数 J(w)=E[|e(n)|^2] 做最陡下降,实际实现时用瞬时梯度代替统计梯度,更新式写为:
w(n+1) = w(n) + μ · e(n) · x(n)
这里 x(n) 是均衡器输入向量,e(n)=d(n)-y(n),d(n) 是期望符号,y(n) 是均衡器输出。μ 是步长因子,它同时决定收敛速度和稳态波动。为了保证均方意义下收敛,μ 需要满足 0 < μ < 2/λ_max,λ_max 是输入自相关矩阵的最大特征值。实际工程中不会去算特征值,而是从输入信号二阶矩粗略估计,通常取 μ 为 0.001~0.05 这个量级。
LMS 的优点是每一轮迭代只有一次矩阵乘向量和一次向量乘标量,存储量只有抽头个数 M。缺点也很明显:当输入信号自相关矩阵的特征值扩展度较大时,收敛速度会被拉得很慢。均匀谱的信道还好,若信道深度衰落导致频谱出现凹点,LMS 要跑几千步才能达到稳态。这个“慢”不是实现问题,而是算法本身对输入信号二阶统计量的敏感——这也就是为什么在信道失真严重的场景下,人们更愿意考虑 RLS。
2.2 RLS 的递推最小二乘:遗忘因子 λ 与逆相关矩阵 P
RLS 的目标是最小化带指数加权的最小二乘累积误差:
J(n) = Σᵏ₌₁ⁿ λⁿ⁻ᵏ |e(k)|²
其中 0 < λ ≤ 1,λ 称为遗忘因子。当 λ=1 时,所有历史误差权重相同,就是标准最小二乘;λ 越小,过去误差衰减得越快,算法越能跟踪信道变化,但也会被噪声牵着走。RLS 的迭代可以写成:
g(n) = P(n-1)·x(n) / (λ + xᴴ(n)·P(n-1)·x(n))
w(n) = w(n-1) + g(n)·e(n)
P(n) = (P(n-1) - g(n)·xᴴ(n)·P(n-1)) / λ
这里的 P(n) 是输入自相关矩阵逆的估计,g(n) 是增益向量,形式上跟卡尔曼滤波类似。由于它直接估计了二阶统计量并用于更新,RLS 对输入相关矩阵的特征值扩展不敏感,所以即使在严重 ISI 信道下也能快速收敛。代价是每一轮要更新一个 M×M 矩阵,计算量约 O(M²),存储量也是 O(M²)。在抽头数 M=32 以上时,RLS 的乘法次数会比 LMS 多一个量级,实时硬件实现时要特别注意资源占用。
P 矩阵的初值 δI 也会影响收敛过程。δ 取得大,代表初始信任输入自相关逆很大,算法前期收敛激进,但可能带来较大振荡;δ 取太小,前期冲得慢。一般实数 BPSK 场景可以取 δ=1 或更大的值,复杂调制或低信噪比时再根据仿真结果调整。
2.3 两者核心参数与代价对比:一张表说清选谁
| 对比项 | LMS | RLS |
|---|---|---|
| 每步计算量 | O(M) | O(M²) |
| 存储量 | O(M) | O(M²) |
| 收敛速度 | 慢,受特征值扩展影响 | 快,几乎不受特征值扩展影响 |
| 稳态误差 | 与 μ 正相关 | 与 1-λ 正相关 |
| 跟踪能力 | 弱,需要重新收敛 | 强,可通过调 λ 适应时变信道 |
| 调参敏感度 | 只有一个 μ,较直观 | λ 和 δ 组合,调参更微妙 |
| 数值稳定性 | 稳定 | 高精度要求下 P 矩阵易发散 |
从这张表可以看出,LMS 适合信道变化不快、算力受限的场景;RLS 适合信道时变、要求快速跟踪的场景。标题里的对比仿真,核心就是把这种差异用同一套路数据量化出来:让一个静态信道和一个突变信道分别跑两种算法,看它们收敛速度差多少,稳态误差差多少,最后落脚到“什么时候该用哪种算法”。
3. 用 MATLAB 2021a+ 搭建一个最小可跑的对比仿真
3.1 仿真模型:BPSK 信道、横向均衡器与训练序列
我这里用的模型是基带等效收发:发送端产生 BPSK 符号(+1/-1),经过一个三抽头 FIR 信道产生 ISI,再加高斯白噪声。接收端用一个 M 阶横向均衡器,期望信号是已知的训练序列。整个过程是符号间隔采样,不考虑定时恢复和过采样。
信道我设成h_chan = [0.8, 0.3, 0.1],其中 0.8 是主径,后面两个是反射路径。这样接收信号rx(n)主要包含了当前符号tx(n),但也混入了前两个符号的能量,正好制造了可移除的码间干扰。均衡器输入向量取当前接收采样和它之前的 M-1 个采样,即rx(n:-1:n-M+1),目标是从这一段观察向量里把tx(n)恢复出来。
训练序列用 2000 个 BPSK 符号。前 200 个符号足够两种算法进入收敛区,后面一段用于统计误码率。为了让对比公平,两种算法共享同一组发送符号、同一组信道增益、同一组噪声样本,唯一区别是算法本身。
3.2 主脚本:完整可运行代码与参数说明
下面这个脚本直接放到 MATLAB 2021a 及以上版本即可运行,我用rng(2024)固定了随机种子,每次运行结果一致,方便你调参数时做横向对比。
% LMS vs RLS 自适应均衡仿真(MATLAB R2021a+) clear; close all; clc; rng(2024); % 固定随机种子,结果可复现 % ---------- 系统参数 ---------- N = 2000; % 训练段符号数 M = 4; % 均衡器抽头数 mu = 0.01; % LMS 步长 lambda = 0.98; % RLS 遗忘因子 delta = 1; % RLS P 矩阵初始化系数 SNRdB = 20; % 信噪比 dB h_chan = [0.8, 0.3, 0.1]; % 三抽头基带信道 % ---------- 发送与接收 ---------- tx = 2 * randi([0 1], N, 1) - 1; % BPSK: -1 / +1 rx_no_noise = filter(h_chan, 1, tx); % 经信道,含 ISI signal_power = mean(abs(rx_no_noise).^2); noise_power = signal_power / 10^(SNRdB/10); noise = sqrt(noise_power) * randn(N, 1); rx = rx_no_noise + noise; % 加噪声后接收信号 % ---------- 初始化 ---------- w_lms = zeros(M, 1); % LMS 抽头向量 w_rls = zeros(M, 1); % RLS 抽头向量 P = delta * eye(M); % RLS 逆相关矩阵初值 e_lms = zeros(N, 1); e_rls = zeros(N, 1); y_lms = zeros(N, 1); y_rls = zeros(N, 1); % ---------- 训练循环 ---------- for n = M:N x = rx(n:-1:n-M+1); % 当前接收向量,M×1 % ---- LMS ---- y_lms(n) = w_lms.' * x; e_lms(n) = tx(n) - y_lms(n); w_lms = w_lms + mu * e_lms(n) * x; % ---- RLS ---- y_rls(n) = w_rls.' * x; e_rls(n) = tx(n) - y_rls(n); gain = P * x / (lambda + x' * P * x); w_rls = w_rls + gain * e_rls(n); P = (P - gain * x' * P) / lambda; end % ---------- 后段误码率统计(最后500个符号) ---------- idx = N-499:N; sym_lms = sign(real(y_lms(idx))); sym_rls = sign(real(y_rls(idx))); ber_lms = mean(sym_lms ~= tx(idx)); ber_rls = mean(sym_rls ~= tx(idx)); fprintf('LMS 后500符号误码率: %.4f\n', ber_lms); fprintf('RLS 后500符号误码率: %.4f\n', ber_rls); % ---------- 误差平方学习曲线(100点滑动平均) ---------- e2_lms = e_lms.^2; e2_rls = e_rls.^2; win = 100; sm_lms = movmean(e2_lms, win); sm_rls = movmean(e2_rls, win); % ---------- 绘图 ---------- figure; semilogy(1:N, sm_lms, 'b', 'LineWidth', 1.2); hold on; semilogy(1:N, sm_rls, 'r', 'LineWidth', 1.2); grid on; xlabel('迭代次数 n'); ylabel('误差平方(滑动平均)'); legend('LMS', 'RLS'); title('LMS vs RLS 自适应均衡收敛曲线');这段代码的逻辑说明如下:tx是发送的 BPSK 符号,filter(h_chan,1,tx)得到无噪接收序列rx_no_noise,再按信噪比折算噪声功率后叠加噪声。训练循环从n=M开始,因为输入向量要用到当前及前 M-1 个接收样点,前 M-1 个样点无法凑满向量。LMS 部分就是最基本的三个公式:输出、误差、更新。RLS 部分则额外维护P矩阵,增益向量gain的计算里lambda + x'*P*x是一个标量,保证除法安全。
参数说明:M=4是抽头数,对三抽头信道来说足够覆盖 ISI 径,设少了均衡能力不足,设太多会放大噪声;mu=0.01是 LMS 步长,输入信号功率大约在 0.1 量级,这个步长不至于发散,也不会太慢;lambda=0.98是 RLS 遗忘因子,表示 100 个符号前的历史误差权重衰减到约 13.5%,用于静态信道绰绰有余。delta=1是 P 矩阵的初始化加权,BPSK 场景下取 1 比较通用。
3.3 跑通后的输出:收敛曲线与误码率怎么看
运行脚本后,命令行会打印两个误码率,图形窗口会画出两条学习曲线。正常情况下,RLS 的红色曲线会比 LMS 的蓝色曲线更快掉下来,也就是在更小的迭代次数就降到 10⁻² 量级。LMS 因为步长只有 0.01,会经历一个较长的线性收敛区,可能要到 300~500 次迭代才进入稳态。
需要注意的坑是:如果只盯这张图,很容易被“谁先掉下来”带偏。误码率才是最终目标。我调参时见过 LSM 在图上看起来收敛快,但稳态误差高出 RLS 一个数量级,最终误码率反而更差。所以第三部分的代码把最后 500 个符号的误码率也打印出来,就是提醒自己:收敛速度是过程,误码率才是结果。
4. 核心指标怎么量化:收敛速度、稳态误差与抽头系数对比
只是画两条曲线,说服力不够。要把 LMS 和 RLS 的差异写成数字,在论文或项目报告中才能站得住。以下三个指标是我必看的:收敛速度、稳态误差、抽头系数轨迹。它们分别对应“多快到位”“到位后抖多大”“走的什么路径”。
4.1 收敛速度:以误差首次进入阈值所需的迭代次数为准
收敛速度的度量不是看曲线“好像平了”,而是看误差平方滑动平均第一次低于某个阈值。阈值怎么定?我常用绝对阈值,比如误差平方 10⁻²,对应误差幅度 0.1,对 BPSK 来说已经接近可靠判决。如果你想让阈值更自适应,也可以取最终稳态误差的 1.1 倍,但那样前期的判决会牵扯进来,不如下固定阈值直观。
在主脚本后面加这几行:
threshold = 1e-2; % 误差平方阈值 first_lms = find(sm_lms < threshold, 1); first_rls = find(sm_rls < threshold, 1); fprintf('LMS 首次低于阈值: %d 次迭代\n', first_lms); fprintf('RLS 首次低于阈值: %d 次迭代\n', first_rls);find(..., 1)返回第一个满足条件的索引,也就是首次进入稳态的迭代次数。注意这里的sm_lms和sm_rls是滑动平均后的误差平方,直接用原始误差平方容易被单个噪声尖刺误导。阈值的选择可以结合信噪比调整,信噪比低时稳态误差本身就大,阈值要放宽,否则可能一次都找不到。
4.2 稳态误差:取末尾一段误差平方均值,别被曲线图骗了
稳态误差是收敛之后误差功率的平均,反映算法的最终精度。取主脚本里已经算好的e2_lms和e2_rls,截最后 500 个点:
emse_lms = mean(e2_lms(end-499:end)); emse_rls = mean(e2_rls(end-499:end)); fprintf('LMS 稳态误差(EMSE): %.6f\n', emse_lms); fprintf('RLS 稳态误差(EMSE): %.6f\n', emse_rls);end-499:end是去掉收敛过程,只看已稳定区域。稳态误差跟参数高度相关:LMS 的 EMSE 大约正比于 μ,μ 翻倍,稳态抖动也接近翻倍;RLS 的 EMSE 大约正比于 1-λ,λ 越接近 1,稳态误差越小,但跟踪信道变化就越慢。这在参数扫描时能直接看到。
另外,稳态误差和误码率不是简单线性关系。在 BPSK 下,误码率主要由误差概率密度在判决边界附近的积分决定,所以单看 EMSE 可能不够,还要结合星座图统计错误数。这也是为什么我在主脚本里同时打印了误码率。
4.3 抽头系数轨迹:看 LMS 和 RLS 的“到达路径”
除了误差曲线,抽头系数的变化轨迹也很有信息量。它告诉你算法是如何逼近最优解的:LMS 往往是一步步爬,RLS 则会先大幅跳变再快速修正。要记录轨迹,在主脚本循环里加上:
w_lms_history = zeros(N, M); % 在循环前分配 w_rls_history = zeros(N, M); % 循环内 LMS 更新后加: w_lms_history(n, :) = w_lms.'; % 循环内 RLS 更新后加: w_rls_history(n, :) = w_rls.';画图:
figure; plot(1:N, w_lms_history(:, 1:3), 'b', 'LineWidth', 1); hold on; plot(1:N, w_rls_history(:, 1:3), 'r', 'LineWidth', 1); xlabel('迭代次数'); ylabel('抽头系数'); title('抽头系数收敛轨迹(蓝=LMS 红=RLS)');可以看到几组抽头迅速从 0 逼近最优解。RLS 的轨迹通常在前 50 次迭代就有较大幅度变化,而 LMS 的轨迹则平缓许多。需要注意的是,如果抽头系数在收敛后持续缓慢漂移,说明稳态噪声在持续更新权重,这也是 LMS 稳态误差偏大的直接表现。
5. 仿真中的五个高频坑:从翻车到稳定复现
5.1 现象:LMS 误差不降反升
我见过不少同学把mu直接设成 0.1,结果误差曲线像过山车,越跑越大。原因是步长超过了收敛边界。输入向量 x 的功率大约是几个信道的能量之和,0.1 在低功率时看似安全,但遇到噪声峰值会瞬间破坏权重。解决办法是把步长改成对输入功率归一化的形式,也就是用 NLMS:
mu_eff = mu / (x' * x + 1e-6); % 归一化步长 w_lms = w_lms + mu_eff * e_lms(n) * x;x'*x是输入向量能量的瞬时估计,加 1e-6 防止除零。这样即使输入幅度突然增大,有效步长也自动缩小。NLMS 的收敛速度对输入功率不敏感,调参比 LMS 好调。
5.2 现象:RLS 的 P 矩阵很快变成 NaN/Inf
RLS 在高信噪比或信道条件较好时,P 矩阵数值容易溢出。原因是P = (P - gain*x'*P)/lambda每次都要做矩阵乘法,反复迭代后舍入误差累积,λ 越接近 1,P 越容易失去正定性,最终出现 Inf。解决思路之一是加对角加载:
P = (P - gain * x' * P) / lambda + 1e-6 * eye(M);每次更新后在 P 的对角线上加一个极小常数,等价于给最小二乘问题加正则项,能有效阻止 P 病态发散。另一种办法是改用数据域直接传递 RLS 算法(如 QR-RLS),但代码复杂度高,仿真阶段加对角加载已经够用。
5.3 现象:训练序列与均衡输出对不齐,误差始终很大
均衡器输入向量用的是rx(n:-1:n-M+1),期望用的是tx(n)。如果信道传播导致主径延迟不是 0,比如信道抽头第一位不是主径,那么rx(n)对应的主要符号其实是tx(n-d),d 为正延迟。此时用tx(n)做期望,误差里永远混着一个符号延迟,算法再怎么调都收不到接近零。解决办法是把期望信号也做延迟:
d = 1; % 需要根据信道主径位置估计 tx_delayed = [zeros(d,1); tx(1:end-d)]; e = tx_delayed(n) - y(n);多数信道模型的第一个抽头是主径时不需延迟调整,但遇到信道首径很小、次径很大的场景,记得检查max(abs(h_chan))的下标位置。
5.4 现象:随机种子一换,算法优劣排序就变
单次实验里 LMS 可能因为噪声峰值少而显得比 RLS 还好,但换个rng种子结论就反转。自适应均衡的收敛过程是随机的,不能拿一次曲线定结论。我做对比至少跑 50 次独立实验,每次重新生成噪声(对随机信道场景也可以每次重生成信道),然后对收敛曲线和稳态误差取平均:
for trial = 1:50 rng(trial); % 重新生成噪声和信道(可选) % 运行整个训练循环 emse_lms_all(trial) = emse_lms; emse_rls_all(trial) = emse_rls; end mean_emse_lms = mean(emse_lms_all); mean_emse_rls = mean(emse_rls_all);固定rng(trial)是为了保证试验序列可复现,50 次平均后的曲线会平滑很多,算法优劣势一目了然。
5.5 现象:R2021a 里运行报错,函数版本不一致
主脚本里用了movmean,这个函数在 R2016a 开始就有,正常 R2021a 都能跑。如果遇到旧代码是filter(ones(1,win)/win, 1, e2),结果会有一点相位延迟,但画学习曲线无伤大雅。更常见的问题是x'与x.'混用。在实数 BPSK 场景两者一样,但一旦以后换成 QPSK,复数信号必须用'做共轭转置,不能省。另外,randi在 R2021a 没问题,老版本 R2011 之前的randint才需要改。总之版本问题优先看函数名是否在“函数浏览器”里能查到,查不到就换成替代写法。
6. 从仿真到验证:信道突变对比与参数调优技巧
静态信道收敛只是第一步,实际信道的多径结构是随时间变化的。要验证两种算法的跟踪能力,我建议在训练序列中段让信道突变:比如第 1000 个符号处把h_chan从[0.8, 0.3, 0.1]换成[0.5, 0.6, -0.2]。注意不能简单在循环里切换滤波器系数,因为rx是提前离线生成的;要做分段滤波,再把两段拼接:
tx1 = tx(1:999); tx2 = tx(1000:end); rx1 = filter([0.8 0.3 0.1], 1, tx1); rx2 = filter([0.5 0.6 -0.2], 1, tx2); rx = [rx1; rx2];这样前 999 个符号走旧信道,后面走新信道。跑完看误差曲线:RLS 依靠 P 矩阵记忆,突变初期可能因为旧信道统计信息残留而剧烈跳动,但几轮后就能重新收敛;LMS 没有历史累计结构,反而可能更快摆脱旧信道的影响。这个对比能很明显地展现出 RLS“跟踪快但代价大”的实质。
参数调优的关键是扫描。我常见做法是让 mu 在[0.005 0.01 0.02 0.05]里扫,lambda 在[0.9 0.95 0.98 0.99]里扫,每组参数跑 50 次取平均:
mus = [0.005 0.01 0.02 0.05]; lambdas = [0.9 0.95 0.98 0.99]; for i = 1:length(mus) for j = 1:length(lambdas) mu = mus(i); lambda = lambdas(j); % 运行完整仿真,记录 emse_lms 和 emse_rls end end扫描结果通常会形成一张表:mu 越大,LMS 收敛越快但稳态误差越大;lambda 越大,RLS 稳态误差越小但重新收敛越慢。这个表最终会告诉你,在目标信道下到底牺牲哪一边更划算。
我自己的教训是:一开始只看静态信道曲线,觉得 mu=0.02 收敛快、误码率也低,就定了这个参数。后来做信道突变实验,发现 LMS 在这个参数下重新收敛要 600 多个符号,而 RLS 只用 50 多个符号。从此以后我固定地把“静态稳态误差、突变重收敛时间、后段误码率”三个指标一起列,参数从表格里选,不再靠曲线图拍脑袋。这个习惯帮我躲过了不少论文数据翻车的坑,希望帮到你。
本文还有配套的精品资源,点击获取