协作认知无线电Q-Learning频谱分配仿真:原理、代码与复现
简介:针对深度强化学习中的Q学习算法在协作认知无线电网络中的应用,这份资源面向通信工程与机器学习交叉方向的研究者,尤其适合希望复现动态频谱分配、协作感知与干扰管理实验的中高级学习者。压缩包内共十四个文件,以十二个脚本文件为主,辅以一段操作录像和一个说明文本,整体仅约823KB,轻量且便于按模块研读。目前已有三百零四人学习或下载。内容包括状态观测、状态集合构建、通信速率设置、干扰计算、Q值更新与信道选择等核心算法模块,整理为多个可调用函数;使用者只需运行主程序即可走通完整流程,切勿直接执行子函数,附带的录像演示了在较新版本环境下的正确操作步骤、当前文件夹路径设置与排错要点,可对照完成实验并理解算法迭代细节。对于希望快速掌握强化学习在认知无线电中落地实现的学习者,这一资源结构清晰、可直接运行,便于快速参照与二次开发。
1. 深度强化学习Q-Learning在协作认知无线电网络中的应用:一个能直接复现的仿真工程
认知无线电网络里最棘手的问题不是感知频谱空洞,而是当主用户(PU)和次用户(SU)共享同一段频谱时,系统必须实时决定“哪个次用户在哪个信道上接入、以什么功率发射”。这个决策空间随用户数和信道数指数膨胀,信道增益又随时在变,传统查表法和固定优先级策略根本追不上环境变化。这套深度强化学习Q-Learning在协作认知无线电网络中的仿真工程,把动态频谱接入建模成马尔可夫决策过程,用Q表学习最优分配策略,代码覆盖状态观测、状态集构建、SINR更新、Q表决策全链路,还带一份操作演示视频。运行环境是Matlab 2021a或更高版本,主脚本是Runme.m。正在做认知无线电方向仿真、需要跑通基线算法或准备论文对比实验的研究生和工程师,跟着录像动手,半小时就能跑出第一轮分配结果。
2. 从频谱接入到Q表更新:为什么协作认知无线电选Q-Learning
2.1 协作认知无线电的问题定义:PU、SU与频谱空洞
先把网络角色对齐。主用户PU是频谱的授权持有者,次用户SU只能在不对PU造成有害干扰的前提下机会式接入。所谓“协作”,指的是SU主动帮助PU转发数据或参与协作感知,改善PU收到的SINR,作为交换,PU允许SU在满足速率约束的前提下共享频谱。这个协作关系一旦成立,系统的核心优化问题就变成:SU以什么策略选择信道和功率,才能在满足PU所需传输速率的同时,把自身接入造成的畸变和失真压到最低。
工程文件里set_reqired_trans_rate.m负责定义这个速率需求,update_pu_sinr_level.m负责在每次动作后刷新PU的SINR水平,get_distortion.m则计算SU接入给链路带来的失真代价。这三者构成了环境侧的反馈回路:动作影响SINR,SINR变化带来新的失真代价,代价又驱动下一次决策。这个回路本质上是动态的、多步的,正好落入强化学习的建模范围。
2.2 状态、动作、代价三元组如何映射到代码
Q-Learning的第一步是把物理网络状态抽象成离散状态、动作和即时代价。本工程的状态向量由两部分拼接而成:信道增益矩阵G的展开值,加上当前PU的SINR水平。由于信道增益是连续量,直接进Q表会维度爆炸,create_state_set.m会预先枚举或采样一组代表性的状态组合,observe_state.m再把实时观测到的连续状态向量投影到状态集里最接近的那一行。这个离散化过程我在代码里见过很多种写法,有的是等间隔量化,有的用聚类中心,这个工程用的是最近邻匹配:
function state_idx = observe_state(G, pu_sinr_level, state_set) % 将连续网络观测映射到离散状态索引 % G: 信道增益矩阵,行对应SU,列对应信道 % pu_sinr_level: 当前主用户信干噪比水平(标量) % state_set: 预先生成的状态集,每行是一个候选状态 state_vec = [G(:)' , pu_sinr_level]; % 拼成完整状态向量 state_idx = findnearest(state_set, state_vec); % 最近邻匹配 end这里要注意state_vec的拼接顺序必须和create_state_set.m生成状态集时的列顺序完全一致,否则最近邻匹配会错位。findnearest.m内部用欧氏距离找最近状态,如果状态集中元素非常多,这段会成为性能瓶颈,我一般会建议改成先对状态集做归一化、再用向量化距离计算,能省掉一层循环。
动作空间则是信道分配选项。工程里q_learning_allocation2.m在每个状态下从Q表选动作,决策函数叫argmin_Q.m而不是常见的argmax_Q.m,这说明Q表里存的是期望累计代价而不是期望累计奖励。值越小代表这个动作越好。这个差异是理解整套代码的钥匙,如果按教科书的“奖励最大化”逻辑去读,会发现学习方向完全是反的。
2.3 为什么不用凸优化而用Q-Learning
有读者会问:频谱分配不是可以用凸优化或博弈论做吗?确实可以,但前提是知道精确的信道模型和实时CSI。协作认知无线电里,SU对PU链路的干扰信道是动态变化的,随机数发生器randomize_G.m和randomize_G2.m的存在就是模拟这种时变信道。凸优化在这种非平稳环境下每时隙都要重新求解,计算量和建模误差都不可控。
Q-Learning是无模型的时序差分方法,它不假设信道服从什么分布,只靠反复试错累积代价经验。每轮迭代用贝尔曼方程把当前动作的即时代价和下一步最优代价加权合并,逐步逼近真实的最优策略。对离散动作空间的问题,Q-Learning实现简单、收敛行为可解释,相比策略梯度类深度强化学习算法,不需要调神经网络结构,在Matlab里用纯m函数就能跑,这也是这个工程选择它的直接原因。
3. 代码包拆解:Runme.m与12个m文件的调用关系和参数细节
3.1 先看文件清单,建立整体调用地图
拿到压缩包先别急着双击运行,把文件分类看一遍。整个工程共12个m文件、1个操作录像和1个FPGA衔接说明txt。
| 文件 | 角色 | 主要职责 |
|---|---|---|
| Runme.m | 主脚本 | 初始化参数、构建状态集、启动Q学习循环、输出分配结果 |
| observe_state.m | 状态观测 | 把信道增益与PU SINR映射为离散状态索引 |
| create_state_set.m | 状态集构建 | 生成所有代表性状态组合 |
| q_learning_allocation2.m | 学习主流程 | 迭代更新Q表并执行动作选择 |
| argmin_Q.m | 决策函数 | 从Q表取累计代价最小的动作 |
| update_pu_sinr_level.m | 环境更新 | 根据动作刷新PU的SINR水平 |
| get_distortion.m | 代价计算 | 计算SU接入造成的链路失真 |
| set_reqired_trans_rate.m | 约束设定 | 定义PU与SU的所需传输速率 |
| randomize_G.m / randomize_G2.m | 信道随机化 | 生成和扰动信道增益矩阵 |
| find_idx.m / findnearest.m | 工具函数 | 索引查找与最近邻匹配 |
3.2 Runme.m主流程:从初始化到出图的完整顺序
Runme.m是整个工程的入口。摘要里特意强调“不要直接运行子函数”,原因就在于子函数依赖主脚本工作区里的变量,单独运行会报“未定义函数或变量”。我按这类频谱分配工程最常见的组织方式梳理出主流程:先清空工作区、固定随机种子、设置网络规模和Q学习超参数,然后依次调用create_state_set构建状态集、初始化Q表、进入q_learning_allocation2叠加学习轮次,最后输出收敛后的分配结果和代价曲线。
% Runme.m 主脚本(结构示意) clear; clc; rng(42); % 固定随机种子,保证可复现 params.n_su = 4; % 次用户数量 params.n_ch = 4; % 可用信道数量 params.alpha = 0.1; % Q学习率 params.gamma = 0.9; % 折扣因子 params.epsilon = 0.2; % 探索率 params.n_episodes = 500; % 学习轮数 G = randomize_G(params); % 初始化信道增益矩阵 state_set = create_state_set(params); Q = zeros(size(state_set, 1), params.n_actions); [Q, cost_history] = q_learning_allocation2(Q, state_set, G, params);固定随机种子这一步很多人会忽略,但对于复现论文结果极其重要。randomize_G里有随机数生成,epsilon-greedy探索也依赖随机数,不固定种子的话,同一套参数两次运行的结果差异会很大,后面第5章会专门展开讲。alpha、gamma、epsilon这三个值强烈建议先跑默认的,再逐步调。
3.3 决策侧:argmin_Q与q_learning_allocation2的更新规则
q_learning_allocation2.m里每个学习回合重新随机化信道矩阵,模拟信道时变,然后在每个时隙里执行“观测状态→选动作→算代价→更新Q表→转移状态”五步。
function [Q, cost_history] = q_learning_allocation2(Q, state_set, G, params) % Q-Learning频谱分配主流程 % 返回训练完成后的Q表和每轮平均代价历史 n_actions = params.n_actions; for ep = 1:params.n_episodes G = randomize_G2(G, params); % 每轮重新扰动信道 state = observe_state(G, 0, state_set); episode_cost = zeros(1, params.n_steps); for t = 1:params.n_steps if rand() < params.epsilon action = randi(n_actions); % 探索:随机动作 else action = argmin_Q(Q, state, n_actions); % 利用:代价最小 end pu_sinr = update_pu_sinr_level(action, G, params); distortion = get_distortion(G, action, params); next_state = observe_state(G, pu_sinr, state_set); % 贝尔曼更新:目标值 = 即时代价 + 折扣因子 * 下状态最小代价 target = distortion + params.gamma * min(Q(next_state, :)); Q(state, action) = (1 - params.alpha) * Q(state, action) ... + params.alpha * target; state = next_state; episode_cost(t) = distortion; end cost_history(ep) = mean(episode_cost); end end核心更新一行用了min而不是max,这是整套代码的命门。min(Q(next_state, :))取的是下一状态所有动作里累计代价最小的那个,意味着智能体在规划未来时总是假定自己会选最优动作。如果误写成max,算法会去学“最差情况”,分配结果会完全发散。argmin_Q的内部实现一般是先对第state行做全局搜索,找到最小值对应的列索引,若同时存在多个等值动作,取第一个即可,这种近似不影响收敛。
3.4 环境侧:SINR更新、失真计算与速率约束
环境侧函数决定了Q-Learning收到的反馈是否合理。update_pu_sinr_level.m的输入是当前动作和信道增益,输出是PU更新后的SINR,内部逻辑通常是先把SU接入带来的干扰加到PU的噪声项里,再和PU的目标SINR门限做比较。get_distortion.m则把“实际速率低于需求速率的程度”映射成代价。这两个函数合起来构成了即时代价的核心来源。
function distortion = get_distortion(G, action, params) % 计算当前动作下的链路失真代价 % 失真定义:实际传输速率低于所需速率时的惩罚量 req_rate = set_reqired_trans_rate(params); % 读取速率需求 achieved = compute_achieved_rate(G, action); % 根据信道增益估算可达速率 if achieved >= req_rate distortion = 0; % 满足需求,零代价 else distortion = (req_rate - achieved) / req_rate; % 缺额占比作为代价 end end这个代价函数的设计思路值得学习:满足需求时代价为0,不满足时代价随缺口线性增长。要注意set_reqired_trans_rate.m如果返回的是一个向量而params里没指定SU下标,get_distortion里直接做标量比较会报维度错误,这是子函数独立调试时最容易撞上的坑。GPU加速在Matlab R2021a里对纯m函数意义不大,真正耗时在最近邻匹配那步,第6章会给出优化思路。
4. 在Matlab 2021a上跑通Runme.m:环境、步骤与结果判读
4.1 环境检查:版本、路径与工作区三件事
运行前的环境检查决定了能否一次跑通。Matlab版本要求是2021a或更高,低版本在函数语法上大概率不兼容,比如某些用到arguments块或隐式扩展的写法在R2020b会直接红字报错。路径检查是第二优先级:Matlab左侧的当前文件夹窗口必须切到工程根目录,否则调用子函数时搜索路径里找不到对应m文件,报错信息往往是“未定义函数或变量observe_state”。第三件事是确认工作区干净,最好先执行clear,避免之前调试残留的变量把params结构体覆盖掉。
4.2 从Runme.m开始跑通全流程
操作录像里演示的顺序和我实际复现的路径一致:解压压缩包到纯英文路径(不要带中文和空格)→ 打开Matlab 2021a → 左侧目录切到工程根目录 → 打开Runme.m → 直接点击运行。录像里那几个关键节点值得对照着暂停,尤其是第一次运行控制台打印状态集维度的地方。
cd /d D:\Projects\CRN_Qlearning matlab -nosplash -nodesktop -r "Runme"命令行方式适合批量实验,但日常调试还是推荐在Matlab GUI里跑。运行过程中控制台会逐轮输出当前Episode和平均代价。q_learning_allocation2内部如果写了进度显示,你会看到代价先震荡后缓慢下降。整个500轮训练在普通台式机上大概几分钟量级,具体看状态集大小和SU数量。如果你看到代价从头到尾纹丝不动,优先检查Q表是否被意外初始化成全零且epsilon恒定导致只做随机探索,这个后面第5章专门说。
4.3 结果输出与收敛性判读
训练结束后,Runme.m会绘制收敛曲线和最终分配结果图。判读时有一个重要原则:不要只看最后一轮代价,要看曲线的整体形态。正常的训练曲线应该是前期快速下降、中期震荡收窄、后期趋于平缓。如果曲线呈锯齿状且幅度不减小,常见原因是epsilon没有衰减,智能体永远在探索,无法收敛到最优策略。如果曲线下降后又反弹,要怀疑gamma过大导致的价值传播不稳定。
% 绘制平均代价收敛曲线 figure('Color', 'w'); plot(1:length(cost_history), cost_history, 'LineWidth', 1.5); xlabel('Episode'); ylabel('Average Distortion'); title('Q-Learning 收敛曲线'); grid on;这张图可以直接进论文的baseline对比图。我一般会同时记录学习结束后的最终分配矩阵,对比状态集里不同初始条件下SU选择的信道是否稳定。如果同一初始状态多次复现给出的分配动作一致,说明策略已经收敛到确定性策略。
4.4 关键超参数的调整顺序
alpha、gamma、epsilon三者的调整顺序有讲究。先调alpha,把学习率从0.1逐步降到0.05、0.01,观察收敛曲线是否提前稳定;再调gamma,从0.9升到0.95会增强远期代价的考量,但gamma太接近1容易产生价值振荡;epsilon相对独立,作用是平衡探索与利用,固定探索率的工程我从没见过,多数会做线性衰减或指数衰减。调整时每次只动一个变量,否则干扰混淆严重,很难定位是哪个参数的影响。
5. 避坑清单:路径、版本与四个高频运行错误
5.1 双击Runme.m报“未定义函数或变量”
现象:从Matlab编辑器直接点运行Runme.m,控制台立刻报“未定义函数或变量observe_state”或类似的子函数找不到错误。
原因:Matlab当前文件夹不在工程根目录。编辑器会运行脚本,但函数搜索路径只包含当前文件夹和addpath添加的路径,子函数m文件不在其中就一概找不到。
解决:看Matlab工具条下方的“当前文件夹”窗口,手动切到解压后的工程目录。如果还报错,检查是不是加过addpath覆盖了同名函数,执行path看看搜索顺序里有没有其他目录的observe_state.m干扰。从那以后我拿到任何Matlab工程的第一件事就是先用pwd确认位置,再点运行。
5.2 单独运行子函数报维度错误
现象:想“调试一下”q_learning_allocation2.m,单独点运行,报矩阵维度不匹配或者提示Q未定义。
原因:子函数只是被主脚本调用的模块,它依赖主脚本构造的params、state_set、G这些变量。单独运行时这些变量不在工作区,函数入口参数不满足调用约定,必然报错。
解决:始终从Runme.m进入整个执行流程。想调试某个子函数,就在Runme.m里调用它的那一行设置断点,然后点击“运行”进入调试模式,工作区里能看到所有上下文变量,这才是正确的调试姿势。操作录像里演示的也是这个流程。
5.3 同一参数跑两次结果差异很大
现象:不修改任何参数,连续运行两次Runme.m,画出来的收敛曲线形态差不多,但最终平均代价和分配矩阵有明显差异。
原因:整套代码里存在三类随机源,randomize_G/G2初始化信道是随机的,epsilon-greedy探索是随机的,如果环境反馈里还混了噪声方差那也是随机的。强化学习在线交互本身方差就大,不固定随机种子等于在野外做不可控实验。
解决:主脚本里加rng(42)或者rng('default')固定全局随机流。做论文对比实验时,每种参数组合至少跑5次取均值±标准差,不要拿单次结果当结论。这个坑算得上强化学习复现的最常见翻车点。
5.4 训练很久但Q表基本没变
现象:控制台一直在打印Episode,但cost_history稳定在某条水平线上,Q表更新量趋近于零,最终分配结果看起来像随机选的。
原因:状态离散化粒度太粗,多个完全不同的物理状态被映射到同一个状态索引,Q表感知不到环境差异;或者学习率alpha太小,更新权重几乎为零,学习被“冻住”了。
解决:先验证状态集覆盖范围。把create_state_set生成的状态集画出来,检查是否覆盖了observe_state在训练中实际观测到的向量区间。增加状态集密度能解决大部分“白学”问题,但要注意状态集变大后Q表尺寸同步膨胀,训练时长增加,这是复杂度换准确度。alpha低于0.01就可以排除是学习率冻住了,通常0.05到0.1之间比较合理。
6. 进阶验证与FPGA衔接:确认Q表收敛后再改参数
6.1 画Q变化量曲线,判断是否真正收敛
平均代价曲线只能看到外在表现,想确认Q表本身是否还在地动山摇,最直接的办法是记录相邻两次迭代的Q差值的最大绝对值。随着训练推进,这个差值应当单调递减,接近机器精度说明Q表已经完全稳定。把这个量加进训练记录里,比只看代价曲线更早暴露不收敛问题。
dq = max(abs(Q - Q_prev), [], 'all');6.2 从仿真到FPGA的衔接思路
工程里那个fpga&matlab.txt文件点出了一个重要方向:Matlab仿真验证Q表策略后,真正上线往往要落成FPGA决策模块。我的习惯是把训练收敛后的Q表用save('Q_table.mat','Q')导出,再转成coe或mif文件,FPGA里用Block RAM存Q表,决策时根据状态索引查表取argmin,这样把在线学习变成在线查表,规避了FPGA上实现强化学习更新的复杂逻辑。查表决策是纯组合逻辑,时延可控,适合对实时性敏感的频谱接入场景。
6.3 多用户场景的维度控制技巧
最后分享一个扩展经验。把SU数量从4个增加到8个时,状态向量里G(:)'的长度会翻倍,状态集会指数膨胀,训练时长完全不可控。我一般先把信道增益量化成低比特等级再用状态索引组合,或者只保留对分配结果影响最大的前几条主路径增益进状态向量,牺牲一点精度换取可训练性。深度强化学习算法在这里可以做降维替代,但要引入网络训练,复杂度高一个量级。先用Q-Learning把线性规模场景吃透,再考虑往深度网络迁移,是性价比最高的路线。
这套工程的可贵之处在于完整性和可运行性,它把协作认知无线电中感、传、算、策的全流程都落到了具体m文件里。从那以后我每次拿到这类Matlab仿真包,都强制先跑通主脚本再看子函数,评估算法效果前先固定随机种子,确认收敛前绝不改超参。希望帮到你。
本文还有配套的精品资源,点击获取