☰
RIS辅助NOMA-D2D系统:基于DDPG的联合资源分配与干扰管理
2026/10/11 1:04:53 网站建设 项目流程

简介:面向无线通信与深度强化学习交叉领域的研究人员和技术人员,这份资源复现了智能超表面(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_c=5是蜂窝用户数量,N_d=10是D2D用户对数量,M=32是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米参考距离下的基础损耗,d0=1是参考距离。注意到代码中距离是每次随机生成的,这意味着每次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, maxcardinality=True)一次性求解全局最优匹配,这比逐对分配的贪心策略更接近论文里"最大化系统吞吐量"的目标。代码里有一个细节值得注意:

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(f"D{d}", f"C{c}", weight=weight)

NetworkX的max_weight_matching默认求最大权重,而我们的目标是找最小干扰组合,所以用max_interf - interference做权重翻转,把最小化问题转为最大化问题。这是实现层面的一个小技巧,不用自己写KM算法。

3.2 干扰矩阵计算:RIS路径上的干扰怎么表达

calculate_interference里计算D2D发射端经过RIS反射到蜂窝用户的干扰,用的公式是|H_ru_c * (theta * H_d2u)|^2。这里theta初始化为全1向量,意味着初始相移是0,RIS在这个阶段没有做波束成形增益,计算出的干扰是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, activation='relu')(inputs) x = Dense(128, activation='relu')(x) outputs = Dense(self.action_dim, activation='tanh')(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(f"ddpg_actor_ep{episode}.h5") agent.critic.save(f"ddpg_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_min=1Mbps这个约束,但代码里奖励函数只算了和速率,没有对低于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功率变化对干扰的影响被噪声功率或干扰基线淹没,导致奖励信号梯度太弱;其次是动作噪声的sigma=0.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*(power+1)*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在maxcardinality=True时会尽可能匹配更多边,但当前建的二分图一侧有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_dB;RIS相移更新用角度步进,即new_phase = old_phase + phase_action * step_angle。这样Actor输出的是归一化的增量方向,而物理系统决定实际步长,训练更稳。

第二个方向是奖励函数里加最小速率约束和干扰惩罚。论文标题里明确提到干扰管理,但默认reward只算和速率。要贴近论文设定,需要给蜂窝用户的SINR也加上保护——比如reward = sum_rate_d2d + lambda1 * min_rate_cu - lambda2 * interference_penalty。这三个项的平衡关系就是论文里说的QoS感知机制。我第一次改的时候lambda1=0.5、lambda2=0.1起步,逐步看效果微调,比一次性拍板更容易调通。

第三个方向是训练流程从静态场景升级到时变场景。通信系统最大的特点是环境持续变化——用户移动、信道随时间改变。虽然论文里没有明确做时变信道训练,但LSTM增强、混合奖励这些摘要提到的方向都暗示这个系统最终要面对动态环境。一种相对简单的做法是每200个episode重新生成信道、重新做二分图匹配,然后清空经验缓冲区继续训练。这样训练出来的策略对抗信道变化的鲁棒性会好很多,代价是收敛时间大幅拉长。

从那以后我每次复现这类"深度强化学习+无线通信优化"的论文,都会强制按"先跑通默认骨架→检查动作映射和奖励设计的物理合理性→再叠加场景复杂度"走一遍。很多报文代码看起来完整,实际跑起来要么收敛慢要么结果不对,问题多半出在中间映射环节的人为简化上。希望这些经验能帮你少折腾几个白天的调试时间。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询