这篇paper于2021年发布在IEEE TRANSACTIONS ON COMMUNICATIONS 上,论文的具体细节可查看我的文章“No-Pain No-Gain: DRL Assisted Optimization in Energy-Constrained CR-NOMA Networks”论文学习笔记。作者已在GitHub上发布了源代码,下面将从源代码分析和添加实验验证两个方面展开介绍。
一、 源代码分析
作者设置了不同的PU个数、PU的坐标位置、PU和SU的信道状态等条件下的5个仿真实验,但我们只以其中最简单的一个实验为例来说明算法性能。
1. 基础参数设置
实验设定在二维平面上,具体参数设置如下:
- BS位于坐标原点,SU U 0 U_0 U0位于(1m,1m)处,考虑2个PU,其中 U 1 U_1 U1和 U 2 U_2 U2分别位于(0m,1m)和(0m,1000m)处;
- 所有的信道只考虑大尺度路径衰减;
- PU的发射功率 P n = 1 w P_n=1w Pn=1w,SU的最大发射功率为 P m a x = 0.1 w P_{max}=0.1w Pmax=0.1w,电池的最大容量为 E m a x = 0.1 J E_{max}=0.1J Emax=0.1J,每一帧持续时间为 T = 1 s T=1s T=1s
- 总共有400个episode,每个episode包含100个step;每个episode开始时,SU的电池能量被初始化为 E m a x E_{max} Emax;
- 在每个step中会往buffer中存一条experience,并且会更新一次actor和critic network参数值。
2. DDPG网络设置
源代码中搭建DDPG网络的代码如下:
def __init__(self, a_dim, s_dim, a_bound,):
self.memory = np.zeros((MEMORY_CAPACITY, s_dim * 2 + a_dim + 1), dtype=np.float32)
self.pointer = 0
self.sess = tf.Session()
self.a_dim, self.s_dim, self.a_bound = a_dim, s_dim, a_bound,
self.S = tf.placeholder(tf.float32, [None, s_dim], 's')
self.S_ = tf.placeholder(tf.float32, [None, s_dim], 's_')
self.R = tf.placeholder(tf.float32, [None, 1], 'r')
self.a = self._build_a(self.S,)
q = self._build_c(self.S, self.a, )
a_params = tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES, scope='Actor')
c_params = tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES, scope='Critic')
ema = tf.train.ExponentialMovingAverage(decay=1 - TAU)
def ema_getter(getter, name, *args, **kwargs):
return ema.average(getter(name, *args, **kwargs))
target_update = [ema.apply(a_params), ema.apply(c_params)]
a_ = self._build_a(self.S_, reuse=True, custom_getter=ema_getter)
q_ = self._build_c(self.S_, a_, reuse=True, custom_getter=ema_getter)
a_loss = - tf.reduce_mean(q)
self.atrain = tf.train.AdamOptimizer(LR_A).minimize(a_loss, var_list=a_params)
with tf.control_dependencies(target_update):
q_target = self.R + GAMMA * q_
td_error = tf.losses.mean_squared_error(labels=q_target, predictions=q)
self.ctrain = tf.train.AdamOptimizer(LR_C).minimize(td_error, var_list=c_params)
self.sess.run(tf.global_variables_initializer())
def _build_a(self, s, reuse=None, custom_getter=None):
trainable = True if reuse is None else False
with tf.variable_scope('Actor', reuse=reuse, custom_getter=custom_getter):
net = tf.layers.dense(s, 64, activation=tf.nn.relu, name='l1', trainable=trainable)
a2 = tf.layers.dense(net, 64, activation=tf.nn.tanh, name='l2', trainable=trainable)
a = tf.layers.dense(a2, self.a_dim, activation=tf.nn.tanh, name='a', trainable=trainable)
return tf.multiply(a, self.a_bound, name='scaled_a')
def _build_c(self, s, a, reuse=None, custom_getter=None):
trainable = True if reuse is None else False
with tf.variable_scope('Critic', reuse=reuse, custom_getter=custom_getter):
n_l1 = 64
w1_s = tf.get_variable('w1_s', [self.s_dim, n_l1], trainable=trainable)
w1_a = tf.get_variable('w1_a', [self.a_dim, n_l1], trainable=trainable)
b1 = tf.get_variable('b1', [1, n_l1], trainable=trainable)
net = tf.nn.relu(tf.matmul(s, w1_s) + tf.matmul(a, w1_a) + b1)
net2 = tf.layers.dense(net, 64, activation=tf.nn.relu, name='lx2', trainable=trainable)
#not sure about this part
return tf.layers.dense(net2, 1, trainable=trainable)
DDPG本来应该包含4个网络,但在源代码中只搭建了2个网络_build_a和_build_c。这两个网络不属于4个网络中的任何一个,而是通过不同的参数设置和更新来轮流充当update network和target network的角色。具体解释如下:
- actor network:_build_a方法中reuse=None(不复用),trainable=True(参数可更新),根据self.S计算出action值self.a (self.a = self._build_a(self.S,) )并取出要更新的网络参数( a_params = tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES, scope=‘Actor’)),然后通过 critic network的输出q计算损失函数 a_loss( a_loss = - tf.reduce_mean(q)),最后更新网络参数a_params(self.atrain = tf.train.AdamOptimizer(LR_A).minimize(a_loss, var_list=a_params) )。因为a_params是直接取出的,所以这个参数更新结果最终会反馈到_build_a方法中各层的参数值上。
- target actor network:_build_a方法中reuse=True(复用该网络结构),trainable=False(参数不更新),利用ema_getter方法(ema为指数移动平均,可对参数进行平滑更新,getter可对作用域中获取的网络参数添加自定义的处理逻辑ema,而不用重新搭建网络)对a_params进行软更新操作(计算图中操作,并不会影响网络参数值),然后根据self.S_计算出target action值a_ (a_ = self._build_a(self.S_, reuse=True, custom_getter=ema_getter))。
- target critic network:_build_c方法中reuse=True(复用该网络结构),trainable=False(参数不更新),利用ema_getter方法对c_params进行软更新操作,然后根据a_,self.S_计算出target Q值q_(q_ = self._build_c(self.S_, a_, reuse=True, custom_getter=ema_getter) )。
- critic network:_build_c方法中reuse=None(不复用),trainable=True(参数可更新),根据self.a,self.S计算出估计的Q值q(q = self._build_c(self.S, self.a, ))并取出要更新的网络参数(c_params = tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES, scope=‘Critic’)),然后根据q_和q计算损失函数td_error(q_target = self.R + GAMMA * q_; td_error = tf.losses.mean_squared_error(labels=q_target, predictions=q))并更新网络参数c_params(self.ctrain = tf.train.AdamOptimizer(LR_C).minimize(td_error, var_list=c_params))。
3. 实验理论结果
根据 U 0 U_0 U0和 U 1 U_1 U1、 U 2 U_2 U2之间的位置关系可知:
- 当 U 1 U_1 U1向BS发射数据时, U 1 U_1 U1会对 U 0 U_0 U0和BS之间的数据传输链路造成强干扰,所以 U 0 U_0 U0应该一直执行能量收集操作,即action=1,相应的 α n = 0 \alpha_n=0 αn=0和 P 0 , n = 0 P_{0,n}=0 P0,n=0;
- 当 U 2 U_2 U2向BS发射数据时,因为 U 2 U_2 U2离 U 0 U_0 U0过于遥远,所以 U 0 U_0 U0无法从 U 2 U_2 U2处收集能量,但相应地 U 2 U_2 U2也不会对 U 0 U_0 U0和BS之间的传输链路造成干扰,所以此时 U 0 U_0 U0应该一直执行数据发射操作,即action=0,相应的 α n = 1 \alpha_n=1 αn=1和 P 0 , n = 0.1 P_{0,n}=0.1 P0,n=0.1。
二、添加实验验证
原文中设置了两个benchmark算法:benchmark 1为SU使用所有可用能量发射数据以后才开始收集能量,相应的时间分配因子 α n = m i n { 1 , E n / T P m a x } \alpha_n=min\{1,E_n/TP_{max}\} αn=min{1,En/TPmax};benchmark 2为SU的发射功率固定为 P m a x P_{max} Pmax,时间分配因子 α n \alpha_n αn在0和 m i n { 1 , E n / T P m a x } min\{1,E_n/TP_{max}\} min{1,En/TPmax}之间均匀分布。很明显,两个benchmark算法不涉及任何的优化和学习方法,并不能作为很好的对比算法。
原文中使用辅助变量 E ˉ n \bar{E}_n Eˉn作为DDPG的action,而其它Energy Harvesting的论文一般使用 α n \alpha_n αn和 P 0 , n P_{0,n} P0,n作为action(功率一般论文都会考虑,时间分配因子不一定),所以添加对比实验: α n \alpha_n αn和 P 0 , n P_{0,n} P0,n设为DDPG的action。原文隐晦表明 E ˉ n \bar{E}_n Eˉn作为action要比 α n \alpha_n αn和 P 0 , n P_{0,n} P0,n作为action性能要更好,但并没有相应的实验验证。这里做一个对比实验,除了观察两种算法的性能优劣以外,还可验证优化算法在强化学习中所起的作用。
添加实验验证所得仿真图如下所示:
从图中可知, E ˉ n \bar{E}_n Eˉn作为action算法(DDPG_Enbar)的性能是优于 α n \alpha_n αn和 P 0 , n P_{0,n} P0,n作为action算法(DDPG_aP0n)的,具体分析如下:
- 当 α n \alpha_n αn和 P 0 , n P_{0,n} P0,n作为action时,正如原文所说,直接应用DDPG可能会存在训练不稳定的问题。当输出层选用tanh激活函数时,DDPG_aP0n算法会在训练初始或者中期出现action值为NaN的问题。这是因为tanh激活函数的输出范围为[-1,1],而两个action的取值范围分别在[0,1]和[0,0.1]之间。当利用replay buffer中的数据训练神经网络时,会迫使DDPG的输出层tanh输出[0,1]和[0,0.1]之间的值,而根据tanh激活函数图像可知,[0,0.1]之间值所对应的梯度很大,而[0,1]之间尤其是靠近1附近的值所对应的梯度很小,趋近于0,这会导致训练中梯度更新不一致,梯度爆炸或梯度消失的现象会使训练非常不稳定。除此以外,action取值范围差异也会导致采样偏差,即一些较大或者较小的action很少被采样到,从而使得网络无法学习到全局最优策略。当将输出层激活函数改为sigmoid时,采样偏差问题较为明显,action值基本上只能采到1和0.1,所以DDPG_aP0n算法性能肯定劣于DDPG_Enbar算法。
- 当 E ˉ n \bar{E}_n Eˉn作为action时,DDPG只有一个action,上面所说的问题全都不存在。DDPG_Enbar算法输出一个action值,再经过探索后得到新的action值,然后将其转换为 E ˉ n \bar{E}_n Eˉn值,最后根据优化算法求出的闭式解得到 α n \alpha_n αn和 P 0 , n P_{0,n} P0,n的值。优化算法先求 α n \alpha_n αn并且能保证 α n \alpha_n αn严格等于0或者1,然后再求 P 0 , n P_{0,n} P0,n得到0或者 − E ˉ n -\bar{E}_n −Eˉn,这是能使reward最大的解决方案。但对于DDPG_aP0n算法而言,DDPG输出action值,再经过探索输出新的action值,然后直接将该值用于reward计算。此时 α n \alpha_n αn可能是0和1之间的值, P 0 , n P_{0,n} P0,n可能是0和 − E ˉ n -\bar{E}_n −Eˉn之间的值,因为是随机探索而且并没有优化算法将它们修改成最理想的值,所以reward会小于DDPG_Enbar算法。
总的来说,当 E ˉ n \bar{E}_n Eˉn作为action时,DDPG更好训练,不会因为actions取值范围差异较大而出现梯度更新不一致、采样偏差等训练不稳定的问题。除此以外,DDPG_Enbar算法用优化算法消除了神经网络输出的随机性,即与reward有关的变量 α n \alpha_n αn和 P 0 , n P_{0,n} P0,n是通过优化算法求出的,不存在随机性且是能让reward最大的解决方案( α n = 0 / 1 \alpha_n=0/1 αn=0/1, P 0 , n = 0 / − E ˉ n P_{0,n}=0/-\bar{E}_n P0,n=0/−Eˉn),神经网络输出随机的 E ˉ n \bar{E}_n Eˉn用来求 α n \alpha_n αn和 P 0 , n P_{0,n} P0,n。