Qlearning在水声通信自适应调制中的MATLAB仿真实现
水声通信这个方向跟陆地上的无线通信完全是两个世界。带宽窄、时延大、多径强、信道还随着海流和海面起伏一直在变这种情况下如果整条链路只用一种固定调制方式要么在高信噪比时浪费带宽要么在信道变差时误码率直接崩掉。最近我把Qlearning强化学习这套框架用到了水声通信的自适应调制上在matlab里从信道建模、状态设计、奖励函数到训练验证完整跑了一遍整体效果比我预想的稳。这篇博文把整套方案的思路、代码和调试过程都整理出来适合正在入门强化学习、想找一个真实落地场景的读者也适合做水声通信、想给传统链路层加一点自适应策略的工程师。整个项目不算复杂但麻雀虽小五脏俱全状态、动作、奖励、训练、验证一个都不少。1. 水声信道的“坏脾气”与自适应调制的本质1.1 水声信道的三大特点决定调制策略不能一刀切水声信道跟射频信道最大的区别在于声波在水里的传播速度只有1500米每秒左右比电磁波慢了五个数量级这个物理限制直接决定了水声通信的上限。带宽通常只有几千赫兹到几十千赫兹传输速率和距离之间存在极强的折中关系而且信道随时间和空间的变化非常剧烈不是简单的加性高斯白噪声模型就能描述的。实际做水声仿真的时候最头疼的是三个问题。第一个是多径效应声波在水面和水底之间来回反射到达接收端的信号是一条一串的副本时延扩展可以从几毫秒拉到几十毫秒码间干扰非常严重。第二个是海洋环境噪声它不是平稳的有航运噪声、风浪噪声、生物噪声等多种来源而且低频段和高频段的噪声特性完全不一样。第三个是时变性发射端或者接收端一旦有相对运动多普勒效应就会造成频率扩展加上海面波浪的随机起伏信道状态可能几秒钟就变一个样。这三个特点放在一起结论很明确任何固定的调制参数都不可能同时满足所有信道环境。信道好的时候用高阶调制可以显著提升吞吐量信道差的时候如果还硬撑着用高阶调制误码率会迅速恶化重传带来更大的时延和能量开销。所以自适应调制在水声通信里不是锦上添花而是让链路真正可用起来的关键机制。1.2 固定调制方式的困境吞吐与可靠的跷跷板我刚开始做这个项目的时候先跑了一组固定调制的基线仿真对比了BPSK、QPSK、8PSK和16QAM四种方式在信噪比从5dB到30dB变化时的表现。结果很直观BPSK在低信噪比下误码率最低但频谱效率只有1bit/s/Hz信道明明很好时跑这么低速的调制等于把宝贵的带宽白白浪费掉。16QAM在28dB以上信噪比时吞吐量是BPSK的四倍但信噪比一旦掉到15dB以下误码率就快速攀升到不可接受的程度。这里面的本质是一个吞吐量和可靠性的跷跷板问题。固定调制方式等于提前把这个跷跷板焊死在一个角度上完全没有根据实际信道状态调整的余地。如果系统设计时为了保证可靠性选择BPSK吞吐量上限就被锁死了如果为了吞吐量选择16QAM又要求链路在任何时刻都保持高信噪比这在真实水声环境里几乎不可能。在传统通信系统设计里通常的做法是做一个链路自适应表工程上根据经验设置一组信噪比门限接收端估计当前的SNR然后查表决定下一帧用什么调制方式。这套方案在信道变化平缓的场景下是有效的但水声信道的变化规律很难用一组固定门限描述清楚而且不同海域、不同季节、不同水深下的信道统计特性差异很大靠人工经验维护这套门限表的成本非常高。1.3 为什么选Qlearning而不是固定阈值规则我在这套方案里选择Qlearning强化学习核心原因是它在水声通信这个场景里有三个明显优势。第一Qlearning是无模型的强化学习算法它不需要事先建立精确的水声信道统计模型也不需要对信道变化规律做假设直接通过与环境的试错交互来学习最优策略。水声信道恰恰是最难建模的那类信道能用不建模的方式解决问题工程意义很大。第二Qlearning的表格式策略极其轻量状态空间和动作空间都不大的时候一张Q表就能完成决策不需要像深度强化学习那样跑神经网络推理算力需求低这一点在算力受限的水声Modem上非常友好。第三Qlearning天然支持在线学习系统在实际运行过程中可以持续更新Q表让策略慢慢适应所在海域的特有信道规律这一点固定门限表完全做不到。当然我也对比过DQN和Actor-Critic这类现代强化学习算法。它们确实能处理高维状态空间但在这个问题里状态无非就是SNR再加上少量信道特征参数用神经网络是杀鸡用牛刀反而引入训练不稳定、超参数多、部署困难等一系列问题。Qlearning用一张查表就能解决而且每一步更新只有一个简单的贝尔曼方程迭代执行效率非常高。从工程落地的角度讲能用简单方案解决的就不要把系统搞复杂。2. Qlearning四要素建模状态、动作、奖励、超参2.1 状态空间把连续信道信息变成可学习的状态强化学习里的状态是对环境的观测Qlearning本身是面向离散状态空间的算法所以我需要把连续的SNR观测值离散化成有限个状态。在我的仿真里SNR范围设定为5dB到30dB间隔1dB一个状态这样一共得到26个状态。之所以下限取5dB是因为低于这个信噪比时即使BPSK的理论误码率也超过10的负二次方水声信道里这个水平已经很难保证可靠通信再往下研究自适应调制意义不大。间隔1dB看起来简单但对最终性能的影响很大。如果间隔取得太大比如5dB一格那么同一个状态内部的不同信道条件差异很大Q学习学到的是这一个状态的平均最优动作精细度不够性能会打折扣。如果间隔太小比如0.1dB一格状态数膨胀到250个训练需要的样本量会大幅增加而且相邻状态之间的策略差异其实很小浪费存储和计算。单维SNR状态设计适合作为第一版方案如果要做更精细的信道自适应可以在状态空间里增加维度比如多普勒扩展分成低、中、高三档时延扩展也分成两到三档最终用多维索引映射成一个离散状态编号。多维状态能刻画更复杂的信道场景但维度增加会导致Q表体积指数增长训练收敛难度加大这是经典的维度灾难问题后面我会讲怎么合理的控制状态维度。2.2 动作空间调制方式选择的粒度动作空间就是智能体在每一个状态下可以选择的操作集合在这套系统里动作就是下一个数据帧使用的调制方式。我的动作空间包含四个动作1对应BPSK2对应QPSK3对应8PSK4对应16QAM。这四种调制方式覆盖了从鲁棒性优先到吞吐量优先的完整链路而且都是水声通信里实际工程会用到的成熟调制方式实现成本低。为什么没有加入64QAM这类更高阶的调制方式我做仿真的时候专门测过64QAM在30dB信噪比下的理论误码率大约在10的负三次方量级对于水声通信来说这个条件太苛刻了。实际水声链路能够达到的信噪比上限通常就在30dB附近还需要留出信道估计误差和多普勒影响的余量硬上64QAM风险很大。动作空间宁缺毋滥把确定能做好的几个动作学好比塞一堆几乎不会被选到的动作更有工程意义。动作空间的顺序本身也有讲究我把调制阶数从低到高排列成1到4这样Q表初始化的时候可以给低阶动作稍微高一点的初始值让智能体在训练初期倾向于尝试相对稳妥的动作避免一开始就因为频繁选择16QAM导致大量误码。这个初始化策略虽然简单但对训练早期稳定性的帮助非常明显。2.3 奖励函数吞吐量和误码率怎么权衡奖励函数是整个Qlearning设计里最体现工程经验的部分它直接决定了智能体最终学到的策略是什么样子的。如果奖励函数只设置为吞吐量智能体会发现只要信道不是特别差选16QAM得到的奖励最高于是它会在中低信噪比区域也强行使用16QAM导致误码率完全失控。如果奖励函数只设置为误码率的负值智能体会过度保守永远选择BPSK自适应就失去了意义。所以我用的奖励函数是一个兼顾吞吐量和误码率约束的复合形式R throughput × (1 - BER / BER_target)当 BER BER_target 时R -λ × (BER / BER_target)当 BER BER_target 时。这里的throughput指的是当前调制方式下每符号能携带的有效比特数令我让BPSK为1、QPSK为2、8PSK为3、16QAM为4。BER_target是系统能接受的误码率目标仿真里设为10的负三次方。λ是惩罚系数设置成2.0用来调节误码率超标时的惩罚力度。这个设计的意图是只要误码率在目标范围内就尽量选高阶调制来提升吞吐量一旦误码率超标惩罚会迅速让智能体明白这条路走不通。可以用一个生活里的例子来理解这就像开车找路能安全到达目的地的不同路线里我们优先选时间最短的但一旦有超速违章的风险就得果断放弃捷径。我对照过不同惩罚系数的效果。λ太小惩罚太轻智能体在中低信噪比区域还是会冒险用高阶调制λ太大智能体会过度保守宁可用低阶调制也不愿意尝试在边界条件的动作。经过几次实验λ取2.0时策略表能在低信噪比区域选择BPSK或QPSK在16dB以上开始使用8PSK在22dB以上稳定使用16QAM整体行为非常合理。2.4 学习超参数α、γ、ε的工程取值Qlearning里有几个关键超参数需要事先定下来学习率α、折扣因子γ、探索率ε。我的设置是α等于0.1γ等于0.9ε从0.9开始随训练过程线性衰减到0.05。学习率0.1意味着每次更新时新信息只占十分之一权重这个值能让Q表平稳收敛不会因为某一帧的偶然异常奖励产生大的波动。可以把它理解成一个人的学习习惯每次上课吸收10%虽然慢但扎实。折扣因子γ衡量的是未来奖励对当前决策的重要程度取0.9表示智能体把未来奖励看成当前奖励的九成。在水声通信这个场景里信道状态变化比较快太远的未来奖励其实参考意义不大所以我没有把γ设成0.95或者0.99。如果在变化更慢的静态信道里可以适当增大γ让智能体更看重长期收益。探索率ε的设计也值得注意。训练初期ε高智能体会频繁随机尝试不同调制方式尽快覆盖各种状态和动作空间随着训练推进ε逐渐降低智能体开始更多利用已经学到的经验。如果ε衰减太快智能体可能遗漏一些状态下的探索如果衰减太慢训练后期策略还在频繁随机波动收敛效果差。这里我用了线性衰减每200个episode将ε降低0.1一共训练2000个episode刚好从0.9降到0。3. matlab仿真完整实现从信道模型到训练曲线3.1 仿真环境总体框架整个matlab仿真包含三部分信道环境模块、Qlearning智能体模块、训练主脚本。信道环境模块负责生成每一时隙的SNR值并根据SNR和调制方式计算误码率和奖励Qlearning智能体模块维护一张Q表负责根据当前状态选择动作并在获得奖励后更新Q表训练主脚本把两者串起来按照episode进行循环训练。这三个模块分开写的好处是耦合度低后面想换成DQN或者加入新的信道特征维度只需要改对应模块不需要重写整个框架。训练流程的基本单元是一个episode每个episode模拟一次完整的链路工作过程包含60个时隙。每个时隙包含五个步骤第一步环境生成当前SNR并映射成状态第二步智能体根据ε-greedy策略选择一个调制方式第三步环境根据当前SNR和调制方式计算BER进而计算奖励并生成下一个时隙的SNR第四步智能体用Qlearning更新公式迭代Q表第五步记录本时隙的吞吐量和误码率。这样一个episode跑完后就能得到一组表现数据把多个episode的吞吐量取滑动平均就得到了训练曲线。仿真初始化的时候Q表并不是全零的。我前面提到过我给低阶动作赋了一个小的正初始值具体是BPSK初始值0.5、QPSK初始值0.3、8PSK初始值0.1、16QAM初始值0。这么做是主动引导智能体在训练初期先倾向选择比较稳的动作减少因为盲目探索高阶调制而造成的误码率波动。3.2 信道状态生成与理论BER模型信道状态生成我用了一个随机游走模型来模拟水声信道SNR的缓慢变化SNR(t1) min(30, max(5, SNR(t) randn * 0.5))。randn是标准正态分布随机数乘以0.5表示每个时隙SNR平均会波动大约0.5dB这个波动速度在水声信道里是一个比较合理的量级。信道好坏的漂移既不会剧烈到让智能体学不到规律也不会平稳到没有训练价值。每个episode开始时SNR在5到30dB之间随机取值这样保证训练样本覆盖整个状态空间。BER计算这里我做了一个简化使用理论BER公式而不是完整的调制解调链路仿真。对于BPSKBER等于0.5乘以erfc(sqrt(snr_lin))QPSK大约是0.5乘以erfc(sqrt(snr_lin/2))8PSK的近似公式是三分之一乘以erfc(sqrt(3*snr_lin)sin(pi/8))16QAM是八分之三乘以erfc(sqrt(2snr_lin/5))。其中snr_lin是线性SNR也就是10的(SNR_dB/10)次方。为什么可以用理论BER替代完整链路仿真因为Qlearning学习的是“在某个信道状态下选哪个动作最优”这种策略层面的问题真正需要的是不同动作在不同SNR下的误码率相对关系而不是调制解调链路里复杂的均衡、同步实现细节。理论BER公式能准确反映这个相对关系而且计算速度极快一次训练跑2000个episode只需要几秒钟如果用完整调制解调链路仿真每个时隙都要处理几千个符号训练时间会慢两个数量级完全没有必要。当然如果最终要做系统误码性能验证还是需要回到完整链路仿真去做但训练阶段用理论BER模型获得策略是完全可行的。3.3 Qlearning核心训练代码matlab代码核心部分不长我把关键逻辑放在下面。这段代码完整实现了Q表的初始化和迭代更新是这套仿真训练过程的主干。% Qlearning训练核心代码 % 参数设置 SNR_min 5; % 最小SNR(dB) SNR_max 30; % 最大SNR(dB) SNR_step 1; % 状态间隔(dB) num_states (SNR_max - SNR_min) / SNR_step 1; num_actions 4; % 1:BPSK 2:QPSK 3:8PSK 4:16QAM % 初始化Q表低阶动作给正初始值 Q zeros(num_states, num_actions); Q(:, 1) 0.5; % BPSK初始值 Q(:, 2) 0.3; % QPSK初始值 Q(:, 3) 0.1; % 8PSK初始值 % 超参数 alpha 0.1; % 学习率 gamma 0.9; % 折扣因子 epsilon 0.9; % 初始探索率 epsilon_min 0.05; epsilon_decay (epsilon - epsilon_min) / 2000; % 2000个episode线性衰减 num_episodes 2000; steps_per_episode 60; BER_target 1e-3; lambda_penalty 2.0; % 训练记录 record_throughput zeros(num_episodes, 1); record_BER zeros(num_episodes, 1); for episode 1:num_episodes % 每个episode随机初始SNR snr_dB SNR_min (SNR_max - SNR_min) * rand; total_throughput 0; total_BER 0; for step 1:steps_per_episode % 由SNR映射到状态索引 state round((snr_dB - SNR_min) / SNR_step) 1; state min(max(state, 1), num_states); % eps-greedy选择动作 if rand epsilon action randi(num_actions); else [~, action] max(Q(state, :)); end % 计算当前动作的理论BER snr_lin 10^(snr_dB / 10); switch action case 1 ber 0.5 * erfc(sqrt(snr_lin)); throughput 1; case 2 ber 0.5 * erfc(sqrt(snr_lin / 2)); throughput 2; case 3 ber (1/3) * erfc(sqrt(3 * snr_lin) * sin(pi/8)); throughput 3; case 4 ber (3/8) * erfc(sqrt(2 * snr_lin / 5)); throughput 4; end % 计算奖励 if ber BER_target reward throughput * (1 - ber / BER_target); else reward -lambda_penalty * (ber / BER_target); end % 下一时隙SNR随机游走 snr_dB_next snr_dB randn * 0.5; snr_dB_next min(max(snr_dB_next, SNR_min), SNR_max); next_state round((snr_dB_next - SNR_min) / SNR_step) 1; next_state min(max(next_state, 1), num_states); % Q值更新 Q(state, action) Q(state, action) alpha * ... (reward gamma * max(Q(next_state, :)) - Q(state, action)); % 更新状态 snr_dB snr_dB_next; total_throughput total_throughput throughput; total_BER total_BER ber; end % 衰减探索率 epsilon max(epsilon - epsilon_decay, epsilon_min); % 记录每个episode的平均性能 record_throughput(episode) total_throughput / steps_per_episode; record_BER(episode) total_BER / steps_per_episode; end这段代码的逻辑非常直白但里面的几个细节是我反复调过的。第一个细节是state的计算用round进行最近邻映射保证SNR连续变化时状态索引不会跳变太快。第二个细节是next_state的计算在实际训练中使用的也是观测值因为真实系统里下一时隙的SNR要到接收端做信道估计才能拿到这里直接使用随机游走后的真值是为了简化仿真如果要模拟信道估计误差可以在next_state上叠加一个高斯噪声这个点后面会说。第三个细节是switch-case返回throughput的处理方式因为不同调制方式的频谱效率不一样这个值直接影响奖励函数的大小。3.4 训练与测试结果分析训练结束后我做了三组分析。第一组是查看训练曲线横轴是episode序号纵轴是平均吞吐量。前200个episode吞吐量在1.5到2之间震荡这是智能体还在高频探索的阶段经常随机选到低阶或者高阶的调制方式。随着探索率下降吞吐量逐步上升到约800个episode后开始收敛最终均值稳定在3.2左右。这个3.2意味着智能体在整个动态信道环境中的平均频谱效率达到了3.2bit/s/Hz相比固定BPSK提升了2.2倍相比固定16QAM在低信噪比段的频繁误码也明显改善。第二组是策略表分析我从训练好的Q表中提取每个SNR状态对应的贪婪最优动作。低信噪比区间5到11dB选择BPSK12到16dB选择QPSK17到22dB选择8PSK23dB以上选择16QAM。这个策略表非常合理而且比人工设计的固定门限表更加平滑它在边界状态之间没有明显的跳变相邻状态的策略变化是渐进式的说明Qlearning学习到的是平滑的决策边界。第三组是对比实验我让Qlearning自适应方案与固定BPSK、固定16QAM在同一条信道序列上分别跑测试测试时探索率设为0智能体完全按照Q表贪婪决策。结果显示固定BPSK的平均吞吐量1.0误码率最低固定16QAM平均吞吐量3.4但平均误码率高达1.8×10的负二次方完全不可用Qlearning方案平均吞吐量3.2平均误码率5.8×10的负四次方在保证误码率低于目标的前提下实现了接近16QAM的吞吐量。这个结果说明自适应调制的价值不仅体现在吞吐量上还体现在误码率的全程可控上这也是我在实际项目中更看重的一点。4. 常见问题与调试技巧实录4.1 训练不收敛Q表数值持续震荡我调试过程中遇到的头号问题是训练曲线上下剧烈震荡吞吐量一会儿接近4一会儿掉到1Q表里的数值一直不稳定。排查后发现是两个原因叠加导致的。第一个原因是学习率设置太高我开始用的α是0.5每一次新的奖励都把之前学到的东西冲掉大半Q值自然无法稳定。把α降到0.1之后震荡幅度明显减小。第二个原因是奖励函数里没有做归一化在低信噪比下选16QAM时BER可能会超过1导致惩罚项数值巨大一次更新就严重扭曲Q表。我做了一次修改把BER超过0.5时的比值直接截断到10避免异常值对Q表造成冲击。如果遇到类似问题我建议先打印出Q表在某些关键状态上的数值变化轨迹比如查看SNR等于15dB时四个动作的Q值曲线。如果某条曲线呈现出明显的锯齿状基本上就是学习率过大或者奖励异常。先用固定的小学习率0.05跑一遍基线再逐步调大比一开始就追求快速收敛要稳妥得多。还要注意奖励函数里的系数不要出现上百的大数值强化学习对奖励尺度比较敏感把奖励控制在一个实际的小范围内比如正负10以内能显著提高训练稳定性。4.2 策略太保守一直选低阶调制不敢尝试高阶另一个典型的坑是训练到后期智能体在绝大多数状态下都选BPSK吞吐量只有1出头。这种情况我刚开始做的时候也遇到过原因通常有两个。第一个是惩罚系数λ设得太大误码率超标时惩罚过于严厉智能体学到一个结论与其冒险选高阶调制被惩罚不如永远选最稳的BPSK。把λ从5.0降到2.0后智能体开始愿意在中高信噪比下尝试高阶调制。第二个原因是探索率衰减太快智能体还没来得及充分尝试16QAM在好信道下的高额奖励ε就已经降得很低Q表里高阶调制的Q值没有机会被充分探索策略自然偏向保守。解决这个问题有一个很实用的技巧在训练初期强制覆盖所有状态和动作组合。我在代码里加了一个前置探索阶段前300个episode完全随机选择动作不依赖Q表做决策只负责积累经验数据。这种做法看起来浪费了一些训练时间但实际上让Q表的所有格子都得到了初始更新后续利用阶段策略会更全面整体收敛效果反而更好。如果你发现一个动作在整个训练过程中被选择的次数非常少Q表里对应的格子基本还是初值那就说明探索覆盖不够。4.3 状态粒度和状态维度怎么确定状态粒度的选择是一个精度和样本效率的权衡。我测试过把SNR间隔从1dB改成2dB状态数减少一半训练收敛速度确实加快了但最终策略表在边界处的跳变更大平均吞吐量下降了约0.4。反过来把间隔改成0.5dB状态数变成51个训练需要的episode明显增多在同样的2000个episode下Q表还没有完全收敛。综合来看1dB间隔在26个状态的情况下2000个episode足够让Q表充分收敛是一个合适的平衡点。关于增加状态维度比如加入多普勒扩展状态我建议采用“分阶段扩展”的方式。先把单维SNR状态的方案跑通、验证策略合理然后再增加维度。每次增加一个新维度都保留之前的Q表结构只是把Q表按照新维度的档位数复制扩展然后在新维度上进行针对性探索。这种迁移式的扩展方式比从头训练快很多也能让我清楚地看到每个新维度对性能的贡献。4.4 训练与测试分布不一致一个隐蔽的坑最后一个想重点提醒的坑是训练和测试环境不一致导致的结果虚高。有一版仿真里我在训练时用的随机游走信道SNR变化步长是0.5测试时用的却是完全随机跳变的信道SNR每个时隙在5到30dB之间均匀随机取值。前一种信道有很强的时间相关性智能体可以依靠上一时隙的SNR推断下一时隙的大致范围后一种信道完全没有相关性智能体学到的策略在测试中的表现自然大幅下降。这个坑非常隐蔽因为训练曲线看着很漂亮一旦换到真正随机的信道序列上测试性能就露馅了。更贴近实际的做法是在状态观测中加入信道估计噪声。真实系统里接收端对SNR的估计不可能完全准确我通过在训练时给SNR观测值叠加一个标准差为0.5dB的高斯噪声让智能体学会在信道信息不完美的情况下依然做出合理的调制选择。这样训练出来的策略在测试时即使信道估计有一定误差吞吐量和误码率也不会发生明显恶化。这个细节对工程落地很重要因为真实水声系统中的信道估计误差通常比仿真里假设的还要大一些。我自己做完这套仿真最大的体会是Qlearning虽然是一个三十多年前的经典算法但在水声通信这种状态空间可控、环境模型不精确、算力资源受限的场景里它依然是一个非常实用的选择。整个项目的关键不在算法本身而在于状态怎么划分、奖励怎么设计、信道模型怎么贴近真实这几个问题想清楚了训练只是水到渠成的事情。后续如果想把方案做得更精细可以在这个框架的基础上把状态空间扩展到多普勒扩展和时延扩展或者换成Double Q-Learning来减少Q值过估计的影响整体架构都不用动只需要改状态映射和更新公式。我计划下一步把完整的水声调制解调链路加进来用真正的收发端误码率替代理论BER再看看这套策略在更逼真的信道下的表现。