RIS辅助NOMA-D2D系统:基于DDPG的联合资源分配与干扰管理
简介面向无线通信与深度强化学习交叉领域的研究人员和技术人员这份资源复现了智能超表面RIS辅助蜂窝NOMA-D2D通信系统中的资源分配算法。内容聚焦D2D与非正交多址NOMA技术共存下的干扰管理完整展示了系统建模、优化问题分解和求解流程使用二分图最大匹配算法完成D2D簇信道分配基于DDPG深度强化学习联合优化D2D发射功率与RIS相移矩阵并通过交替迭代收敛。动态优先级机制、混合奖励设计和LSTM增强等细节也有讲解。资源包为1个PDF文件总大小763KB正文包含可运行Python代码及逐步解释覆盖系统参数初始化、瑞利衰落信道生成、干扰矩阵计算、二分图匹配等核心模块。目前已有83人学习。通过阅读其中内容读者能深入理解“分解-协调”优化框架如何求解混合整数非线性问题同时获得可直接运行或修改的代码基座用于进一步实验比较或学术复现。1. RIS辅助D2D通信这套资源到底解决了什么问题做无线通信仿真的同行应该都有体会NOMA和D2D一叠加干扰关系立刻变成一团乱麻——D2D簇之间有同频干扰蜂窝用户会压D2D的速率D2D发射功率又会反过来干扰蜂窝用户的接收再加上RIS相移这个连续复数变量整个优化问题直接落在混合整数非线性规划上。传统解法要么用SDR做松弛要么靠穷举信道分配动态场景下每帧拓扑一变就要重新算复杂度根本扛不住。这份资源是一个完整的论文复现工程基于深度强化学习DDPG做RIS辅助NOMA-D2D系统的联合资源分配。它的核心思路是把原问题解耦成三步先用二分图最大匹配做D2D簇信道分配离散部分再用DDPG联合优化发射功率与RIS相移连续部分最后交替迭代收敛仿真能明显看出和速率和干扰抑制的提升。对正在做D2D、NOMA、RIS或者深度强化学习资源分配方向的人来说这套代码能直接当骨架改不用从头搭模型。2. 系统建模与信道生成参数怎么设、状态怎么算2.1 参数初始化哪些值影响结果、哪些值只是占位代码里SystemParameters类把场景参数全部集中管理这是复现时最值得先看的地方。里面的N_c5是蜂窝用户数量N_d10是D2D用户对数量M32是RIS反射单元数带宽10MHz载波频率2.4GHz路径损耗指数2.7最大发射功率23dBm最小速率门限1Mbps。这些参数直接决定信道矩阵的维度和后续的干扰计算范围。class SystemParameters: def __init__(self): self.N_c 5 # 蜂窝用户数量 self.N_d 10 # D2D用户对数量 self.M 32 # RIS反射单元数量 self.B 10 # 带宽(MHz) self.N0 -174 # 噪声功率谱密度(dBm/Hz) self.P_max 23 # 最大发射功率(dBm) self.R_min 1 # 最小速率要求(Mbps) self.alpha 2.7 # 路径损耗指数 self.fc 2.4 # 载波频率(GHz)几个参数的实际影响要分清。M直接决定DDPG动作维度和状态维度——相移矩阵的实部虚部加起来是64维这是训练收敛难度的重要来源N0配合带宽换算出的噪声功率会影响SINR的底噪水平SINR上不去的时候先查这个换算是否正确alpha是路径损耗指数2.7属于城区环境的中等偏上值如果你做室内场景1.8到2.2才合理不改这个值出来的绝对数值参考意义不大。2.2 信道生成逻辑瑞利衰落加路径损耗的组合ChannelModel的核心是生成五类信道基站到RIS、RIS到蜂窝用户、RIS到D2D发射端/接收端、基站到蜂窝用户直连、D2D收发端之间直连。注意这里RIS到D2D的信道维度是2*N_d也就是每个D2D簇的发射端和接收端各有一条RIS链路这是SINR计算的物理基础——RIS反射路径贡献在接收端必须区分收发两侧。def _generate_single_channel(self, dim): 生成单条瑞利衰落信道 # 随机距离(10-100m) d np.random.uniform(10, 100) # 路径损耗 PL 10**(-3.53 - self.params.alpha*np.log10(d/self.params.d0))/10 # 瑞利衰落 h np.sqrt(PL/2) * (np.random.randn(dim) 1j*np.random.randn(dim)) return h路径损耗公式里10**(-3.53)是1米参考距离下的基础损耗d01是参考距离。注意到代码中距离是每次随机生成的这意味着每次generate_channels()都会产生完全不同的拓扑。复现时如果想对比算法性能必须在相同信道下做多种算法的对比实验否则信道随机性会盖过算法差异。信道生成还有个容易忽略的细节——变量在类内被命名为H_br、H_ru、H_rd等但这些变量是类的属性generate_channels每次调用会覆盖旧的。如果你在训练循环里每帧重新生成信道需要重新调用BipartiteMatching来更新信道分配否则分配结果和信道对不上这也是很多人在循环训练时和速率曲线崩掉的原因之一。3. 二分图匹配做信道分配把干扰矩阵变成最大权重匹配3.1 为什么信道分配要用二分图而不是贪心D2D簇信道分配的本质是10个D2D对分配5个蜂窝信道每个信道可以复用于多个D2D簇NOMA叠加但目标是让整体干扰最小、速率最大。贪心算法在这里最大的问题是短视——先分配的第一对D2D可能占了最优信道导致后续D2D对只能选干扰更大的信道整体效果反而不如一个全局统筹的分配方案。二分图最大匹配把这个问题转换成了组合优化里的经典结构。一侧是D2D节点另一侧是蜂窝信道节点边的权重是干扰的倒数变换。通过nx.max_weight_matching(B, maxcardinalityTrue)一次性求解全局最优匹配这比逐对分配的贪心策略更接近论文里最大化系统吞吐量的目标。代码里有一个细节值得注意max_interf np.max(interference_matrix) for d in range(self.params.N_d): for c in range(self.params.N_c): weight max_interf - interference_matrix[d, c] B.add_edge(fD{d}, fC{c}, weightweight)NetworkX的max_weight_matching默认求最大权重而我们的目标是找最小干扰组合所以用max_interf - interference做权重翻转把最小化问题转为最大化问题。这是实现层面的一个小技巧不用自己写KM算法。3.2 干扰矩阵计算RIS路径上的干扰怎么表达calculate_interference里计算D2D发射端经过RIS反射到蜂窝用户的干扰用的公式是|H_ru_c * (theta * H_d2u)|^2。这里theta初始化为全1向量意味着初始相移是0RIS在这个阶段没有做波束成形增益计算出的干扰是RIS不参与优化时的基线干扰。def calculate_interference(self): interference_matrix np.zeros((self.params.N_d, self.params.N_c)) for d in range(self.params.N_d): for c in range(self.params.N_c): H_d2u self.channel_model.H_rd[2*d] # D2D发射端到RIS H_ru_c self.channel_model.H_ru[c] # RIS到蜂窝用户c theta np.ones(self.params.M) # 初始相移矩阵 interference np.abs(np.dot(H_ru_c, theta * H_d2u))**2 interference_matrix[d, c] interference return interference_matrix这条公式背后的物理含义是D2D发射功率经RIS反射面到蜂窝用户接收端的等效信道增益。功率还没进优化时这个矩阵代表的是信道分配阶段的干扰底数——哪个D2D对在哪个信道上对本簇蜂窝用户干扰最小预先算出来。注意这里只算了经RIS反射后的干扰路径D2D发射端到蜂窝用户的直射干扰路径没有加入干扰矩阵这算是一个模型简化假设。如果你的场景中D2D距离蜂窝用户很近直射路径不能忽略需要自己把直信道H_bu的贡献加进去。匹配结果matching_result是以字典形式返回的键是D2D索引值是分配到的蜂窝信道索引。但NOMA场景下通常一个信道可以复用给多个D2D对而当前perform_matching返回的是严格一一对应的匹配结果。这意味着默认实现里一个信道最多分给一个D2D对不是完整的NOMA-D2D复用。想恢复NOMA能力需要允许一个蜂窝信道绑定多个D2D簇通常做法是修改权重矩阵让每个信道节点允许连多条边或者在匹配后再用一个功率域NOMA配对步骤。后面要改的时候注意这个差异。4. DDPG动作设计功率与相移怎么映射、奖励函数怎么算4.1 网络结构与状态动作维度DDPGAgent里有两个核心设计Actor网络把状态映射到动作Critic网络评估状态动作对的Q值。Actor用了两层全连接256和128个神经元输出层用tanh激活把动作限制在[-1, 1]区间。状态维度是N_d 2*M即10个D2D功率值加上RIS相移的实部和虚部各32维总共74维。def _build_actor(self): inputs Input(shape(self.state_dim,)) x Dense(256, activationrelu)(inputs) x Dense(128, activationrelu)(x) outputs Dense(self.action_dim, activationtanh)(x) return tf.keras.Model(inputs, outputs)动作维度与状态维度相同是N_d 2*M。输出的前10维是D2D功率调整量范围在[-1,1]之间后64维是RIS相移的实部和虚部增量。这种调整量设计而不是绝对量设计对应DDPG的惯用做法——Actor输出增量叠加到当前状态上形成新状态这样训练初期不容易输出过大的动作导致系统状态剧烈波动。实操中要小心状态和动作的衔接。代码里的状态转移有这么一个模糊之处next_state state action * 0.1也就是把动作值乘以0.1再加到旧状态上这是很简单的线性转移假设。但在真实通信系统里功率的更新不应改变信道本身特性——RIS相移更新会影响信道增益功率更新影响发射端能量两者的物理更新步长和量纲完全不同。0.1这个系数对功率和相移统一生效实际训练中表现可能不太理想。我一般会改成分别设置power_step和phase_step两个系数比如功率步长1dBm、相移步长0.05π让两类动作的更新粒度彼此独立。4.2 动作映射从DDPG输出到物理世界参数DDPG输出的是一个连续向量需要映射到实际的发射功率和RIS相移。代码里powers 0.5 * (power_adjustments 1) * self.params.P_max # 映射到[0,P_max] theta phase_adjustments[:self.params.M] 1j * phase_adjustments[self.params.M:] theta theta / np.abs(theta) # 归一化为单位模功率映射用的线性变换从[-1,1]映射到[0, 23]dBm这个区间映射没问题但注意单位——P_max是23dBm而SINR计算里的噪声功率用的是线性单位瓦特中间的换算需要仔细检查。相移部分取实部虚部组成复数向量后归一化这一步确保了RIS反射系数的单位模约束物理含义是RIS单元无源反射不放大信号只改变相位。归一化是RIS优化里的另一个常见坑。theta / np.abs(theta)逐元素归一化结果没问题但如果Actor输出的实部虚部都是很小的值比如0.01归一化后相位依然正确但幅度信息全部丢失。更好的做法是直接从Actor输出32维相位角通过exp(1j*phase)构造反射系数这样既保证单位模约束又减少了一半的动作维度。维度降下来DDPG收敛难度会显著下降。4.3 奖励函数的关键问题calculate_reward计算的是所有D2D用户的和速率。遍历matching_result字典对每个D2D对计算SINR然后rate B * log2(1 SINR)累加。这段逻辑里有几个值得注意的细节sinr (powers[d_idx] * channel_gain) / (interference noise_power) rate self.params.B * np.log2(1 sinr) sum_rate rate噪声功率换算这里noise_power 10**(N0/10) * B * 1e6即把-174dBm/Hz的噪声谱密度先转成线性值再乘以带宽。B的单位是MHz乘以1e6转成Hz换算关系正确。但干扰计算部分有个明显简化干扰只算了同信道其他D2D用户对当前接收端的干扰蜂窝用户对D2D的干扰和RIS反射带来的额外干扰路径都没算。论文里说的三类干扰这里只实现了簇间干扰。如果你要完整复现论文需要把CU→D2D接收端的干扰项也加进来具体需要H_bu信道和蜂窝用户的发射功率作为输入。奖励函数是整个强化学习系统的核心反馈公式看起来合理但在实际训练里如果reward一直不涨先从匹配结果和信道生成查起——往往不是DDPG的问题而是底层输入本身就有逻辑矛盾。5. 训练循环与收敛判断完整可运行的骨架代码5.1 主训练循环的搭法main函数里先初始化参数和信道执行二分图匹配然后创建DDPG智能体。训练循环设置了1000个episode每个episode内最多100步。经验回放缓冲区大小10000批大小64折扣因子0.99目标网络软更新系数0.005。每100轮保存一次Actor和Critic模型权重。整体代码如下def main(): params SystemParameters() channel_model ChannelModel(params) channel_model.generate_channels() # 信道分配 bipartite_matching BipartiteMatching(params, channel_model) matching_result bipartite_matching.perform_matching() print(D2D信道分配结果:, matching_result) # DDPG训练 agent DDPGAgent(params, channel_model, matching_result) episodes 1000 max_steps 100 rewards_history [] for episode in range(episodes): state np.concatenate([ np.random.uniform(0, params.P_max, params.N_d), np.random.uniform(-1, 1, params.M), np.random.uniform(-1, 1, params.M) ]) episode_reward 0 for step in range(max_steps): action agent.get_action(state) next_state state action * 0.1 reward agent.calculate_reward(state, action) done (step max_steps - 1) agent.remember(state, action, reward, next_state, done) agent.learn() state next_state episode_reward reward rewards_history.append(episode_reward) if episode % 100 0: agent.actor.save(fddpg_actor_ep{episode}.h5) agent.critic.save(fddpg_critic_ep{episode}.h5)注意到这个循环里有两个普遍性的隐患。一是每个episode随机初始化状态这意味着整个训练过程中系统没有真正的目标状态概念DDPG的Critic很难学到稳定的价值评估。常见做法是设定一个基准状态比如初始功率为P_max/2相移为全1每个episode围绕基准加噪声扰动而不是完全随机。二是经验缓冲区在episode之间没有清理新经验持续覆盖旧经验。如果信道分配结果一直没变同一信道下训练问题不大但你如果做成每N个episode重生成信道、重新匹配、再继续训练的时变信道训练新信道下的经验会污染旧信道下学到的策略。这时候缓冲区要么清空重建要么加上信道特征向量让状态完整描述环境。5.2 观察哪些指标判断是否收敛训练中的episode_reward是首要观察项。典型收敛曲线是前面一二百轮缓慢上升中间可能出现一段平台期之后逐步稳定如果从头到尾reward在某个值附近剧烈震荡且没有上升趋势问题基本出在动作映射或奖励函数里。第二个观察项是Actor输出的功率值分布。训练稳定后向agent.actor喂入固定状态看输出的功率是否集中在某个合理区间。如果大量动作值是极端值全部1或全部-1说明Actor已经饱和很可能是奖励函数梯度方向一致导致策略坍缩。处理办法是减少Actor学习率或者给奖励加一个功率惩罚项让策略不要走极端。第三个判断点是最小速率约束是否满足。论文里有R_min1Mbps这个约束但代码里奖励函数只算了和速率没有对低于R_min的D2D对做惩罚。实际训练结果可能出现和速率很高但个别D2D对速率极低的情况。复现时建议在奖励函数里加入约束项penalty 0 for d_idx, rate in enumerate(rate_each_d2d): if rate self.params.R_min: penalty (self.params.R_min - rate) reward sum_rate - 10.0 * penalty惩罚系数10是经验值太小约束不起作用太大训练会只顾惩罚而忽略速率提升。可以先从5开始调观察违约D2D对数量和总速率之间的平衡。5.3 1000个episode的算力消耗估算这套模型维度是74维状态与74维动作Actor和Critic网络规模也不算大单个episode里有100步、每步训练一次。1000个episode下来约10万次梯度更新。普通CPU上TensorFlow 2.x的每步训练大约几十毫秒到几百毫秒不等全部跑完可能在几小时到十几小时量级。GPU能加速2到3倍但瓶颈主要在Python的数据处理和网络传输上提升空间有限。如果只想验证算法逻辑是否跑通建议先把episodes降到50、max_steps降到20跑通了再逐步加量。不要上来就1000个episode万一信道模型或奖励函数有bug一跑就是几小时白等。6. 避坑指南复现这套代码最容易翻车的五个位置6.1 训练不收敛reward曲线持续在低位震荡现象跑了几百个episode后episode_reward还在初始值附近波动没有上升趋势。原因最常见的是奖励函数里SINR分母的干扰项几乎没有变化——D2D功率变化对干扰的影响被噪声功率或干扰基线淹没导致奖励信号梯度太弱其次是动作噪声的sigma0.2持续扰动训练后期探索噪声没有衰减Actor学到的策略被噪声覆盖。解决分两步排查。第一步打印每个step的实际SINR和算出的reward确认奖励对不同动作有区分度第二步把OU噪声的sigma随episode线性衰减比如从0.5降到0.05让训练后期以利用为主。代码里OUNoise的sigma固定为0.2建议在DDPGAgent中加一个self.noise_decay参数每个episode结束后更新。6.2 功率输出全部贴到边界值现象训练结束后Actor输出的功率调整量大量逼近1或-1导致功率不是0就是P_max没有中间值。原因线性映射0.5*(power1)*P_max让Actor可以把动作推满而获得更高的奖励功率越大速率越高没有惩罚项约束。这在数学上可能确实是当前奖励函数的最优策略——无约束条件下功率当然应取最大——但这不是物理上可用的解因为干扰会随功率同步上升。解决给奖励函数加功率惩罚项形式可以是-beta * mean(powers)beta先从0.01试或者把功率映射改为对数映射让动作在小数值区域有更高的灵敏度大数值区域倾向于饱和让策略更新梯度更平稳。6.3 每次复现结果完全不一致现象同一份代码跑三次三次的和速率曲线和最终值差异巨大。原因信道生成用np.random.uniform随机了距离每个D2D对、每个蜂窝用户的位置和信道每次不同。三分频结果差异主要来自信道实现本身不全是算法波动。解决实验前设置np.random.seed()固定随机种子。但注意仅固定Python的seed不够TensorFlow有独立的随机种子——tf.random.set_seed()。在main函数开头同时设置这两个seed复现结果就能保持一致。6.4 匹配结果为空或者某些D2D簇没有分配到信道现象perform_matching返回的字典长度小于N_d部分D2D簇没有获得信道。原因nx.max_weight_matching在maxcardinalityTrue时会尽可能匹配更多边但当前建的二分图一侧有10个D2D节点、另一侧只有5个蜂窝信道节点最大匹配的边数上限就是5——不可能所有D2D对都分到独立信道这是模型的客观约束。解决如果想所有D2D簇都有信道可复用需要修改逻辑允许一个蜂窝信道分配给多个D2D对。常见做法是给每个蜂窝信道节点复制多个副本节点副本数量等于该信道可复用的D2D簇数量上限然后在一个增广二分图上做匹配。6.5 加载保存的模型后推理结果和训练时差距很大现象用agent.actor.save(...)保存权重后加载回去对新状态做推理输出动作不合理。原因模型保存/加载时如果网络结构定义有变化——比如输入维度对不上——推断结果自然不对。另一个可能是训练时状态分布和加载后输入的状态分布不一致训练中状态在合理范围内加载后你喂的state数值范围完全不在训练分布里Actor只能外推结果不可控。解决保存时同时保存状态的均值方差或归一化参数加载后第一件事是构造一个训练时典型状态喂给网络验证输出范围。我习惯把归一化层写进模型内部即Input - Normalization - Dense这样保存和加载都是自包含的不会出现推理时忘记归一化导致动作异常的翻车。7. 从骨架到完整方案三个可能让你少走弯路的进阶改法如果只按默认代码跑能复现的是信道分配DDPG联合优化的基本过程但和论文的完整设定还有距离。以下三个方向是我在实际复现中验证过、比较有效的升级路径。第一个方向是把状态转移从线性假设换成物理模型。默认next_state state action * 0.1对功率和相移一视同仁但真实场景下功率和相移的更新动力学完全不同。我一般会把它们拆开功率更新用对数域步进即new_power_dBm old_power_dBm power_action * step_dBRIS相移更新用角度步进即new_phase old_phase phase_action * step_angle。这样Actor输出的是归一化的增量方向而物理系统决定实际步长训练更稳。第二个方向是奖励函数里加最小速率约束和干扰惩罚。论文标题里明确提到干扰管理但默认reward只算和速率。要贴近论文设定需要给蜂窝用户的SINR也加上保护——比如reward sum_rate_d2d lambda1 * min_rate_cu - lambda2 * interference_penalty。这三个项的平衡关系就是论文里说的QoS感知机制。我第一次改的时候lambda10.5、lambda20.1起步逐步看效果微调比一次性拍板更容易调通。第三个方向是训练流程从静态场景升级到时变场景。通信系统最大的特点是环境持续变化——用户移动、信道随时间改变。虽然论文里没有明确做时变信道训练但LSTM增强、混合奖励这些摘要提到的方向都暗示这个系统最终要面对动态环境。一种相对简单的做法是每200个episode重新生成信道、重新做二分图匹配然后清空经验缓冲区继续训练。这样训练出来的策略对抗信道变化的鲁棒性会好很多代价是收敛时间大幅拉长。从那以后我每次复现这类深度强化学习无线通信优化的论文都会强制按先跑通默认骨架→检查动作映射和奖励设计的物理合理性→再叠加场景复杂度走一遍。很多报文代码看起来完整实际跑起来要么收敛慢要么结果不对问题多半出在中间映射环节的人为简化上。希望这些经验能帮你少折腾几个白天的调试时间。本文还有配套的精品资源点击获取