莫烦Python代码实践(五)——REINFORCE基础算法工程化解析

提示:转载请注明出处,若文本无意侵犯到您的合法权益,请及时与作者联系。


声明

本文是作者学习莫烦Python的代码笔记总结,如想深入可移步莫烦Python的该课程


一、REINFORCE算法与DQN算法的区别

REINFORCE是基于Policy的RL算法中最经典、最基础的算法,可以说是学习基于基于Policy的RL算法的入门必备。

与作为基于Value的RL算法入门必备的DQN算法相比,两个算法的主要区别如下:

 

类别

 

DQN算法

 

REINFORCE

 

算法种类

 

value-based

 

Policy-based,

 

更新机制

 

单步更新

 

回合更新

 

函数拟合

 

拟合Q函数,输出在某个状态选择某个动作可以得到的预期价值

 

拟合策略函数,输出在某个状态选择某个动作的概率

 

损失函数

 

q_target和q_eval的均方误差

 

预期概率分布和目标概率分布的交叉熵损失

 

经验使用

 

用来采样计算q_target,以训练网络

 

用来计算各个状态的长期回报G等,以训练网络

注:可能一般意义上并不认为REINFORCE存在经验回放机制,这里我们将存储过去的状态信息的机制都一律称为经验池

关于网络的区别可以参考下图:

2、REINFORCE的算法流程

REINFORCE算法的主要流程如下:

同样上述流程相对抽象,我们使用基于面向对象思想详细说明下如上流程:

在将上述算法流程进行工程化的过程中,主要可以分为以下几个阶段:

  1. 算法的整体流程

  2. 经验池的初始化与清空

  3. 初始化策略网络

  4. 运行策略网络选择动作

  5. 学习并训练策略网络

接下来我们就依次描述这5个阶段,讲解如何实现上述算法的一些细节。

3、REINFORCE的代码实现

1、算法的整体流程

现在假设我们在Gym中的CartPole-v0环境中测试该算法代码,并且我们已经搭建了Agent_REINFORCE类,那么我们的主要流程初始化代码如下:

# 初始化环境
env = gym.make('CartPole-v0')
env = env.unwrapped
env.seed(1)
# 初始化agent
STATE_FEATURES = env.observation_space.shape[0]
ACTION_SPACE = env.action_space.n
agent = Agent_REINFORCE(
    n_features = STATE_FEATURES,
    n_actions = ACTION_SPACE,
    learning_rate=0.02,
    reward_decay=0.99,
    output_graph=False,
)
# 进行仿真训练
train(agent, num_episode=250)

train()函数中记录了整个算法的运行流程,这里并不复杂,对照上述算法流程即可理解:

def train(agent,num_episode=3000,is_Render = False):
    for i in range(num_episode):
        running_reward = 0 # 运行的回合奖励
        observation = env.reset() # 初始化环境状态
        while True:
            if is_Render: env.render() # 是否渲染环境,渲染会变慢
            action = agent.choose_action(observation) # 选择动作
            observation_, reward, done, info = env.step(action)
            agent.store_in_memory(observation, action, reward)
            observation = observation_ # 切换观察值
            if done:
                vt = agent.learn()  # 该训练回合结束时才开始学习
                running_reward = sum(agent.ep_rs)#获取本回合累计奖励
                # 打印信息
               print("episode:",i, "reward:",int(running_reward))
                break

接下来我们详细介绍上述每个关键细节的实现部分。

2、经验池的初始化与清空

在REINFORCE中的经验池每次存储的都是本回合各个step的(s,a,r),然后在每回合结束时,一下子将本回合的所有(s,a,r)输入策略网络进行learn,learn之后再清空经验池来存储下回合的信息,这种特点就是回合更新。这和我们之前实现DQN时每个step进行learn有很大区分。因为在learn时,我们是分别输入s,a,r,而经验池存在存入和清空两个操作,所以该算法中的经验池机制实现非常简单,使用以下三个列表即可实现:

def store_in_memory(self, s, a, r):
    self.ep_obs.append(s)
    self.ep_as.append(a)
    self.ep_rs.append(r)

def __clear_memory(self):
    self.ep_obs, self.ep_as, self.ep_rs = [], [], []

3、初始化策略网络

搭建策略网络是该算法中相对较难的一点。因为我们的RL任务比较简单,我们这里使用两层全连接网络搭建,为了让代码减少全连接层的细节影响,我们直接使用封装得tf.layer来搭建这个策略网络。

首先我们来看下这个网络的简单结构说明:

上图中的“?”表示这个数不确定,其是每回合训练的最大step数。

策略网络的输入是一个回合的所有状态序列(本质上就是一个shape=(?,n_features)的二维数组),输出是预测的动作的概率分布(本质上就是一个shape=(?,n_actions)的二维数组),为了训练这个网络,我们搭建的loss需要我们输入一个回合的所有动作序列(本质上就是一个shape=(?)的一维数组)和所有累计回报序列(本质上就是一个shape=(?)的一维数组)。

这里需要说明的是,假如我们输入一个状态值,这个状态值具备n_features=3个特征信息的,那么s=[0.5,0.6,0.7],这时训练较好的策略网络的输出应该是一个具备n_actions=3个动作个数的[0.2,0.1,0.7],这表示agent选择第1个动作的概率为0.2,第2个动作的概率为0.1,第三个动作的概率为0.7。

在训练时,我们并不会一个一个输入状态值,这样训练效率较低,我们一下子输入整个回合的所有状态值,所以此时的输入输出都变成了二维,本质没变。

现在我们先定义计算图的三个变量输入:

with tf.name_scope('inputs'):
    self.tf_obs = tf.placeholder(tf.float32, [None, self.n_features], name="observations") # 输入状态序列
    self.tf_acts = tf.placeholder(tf.int32, [None, ], name="actions") # 输入动作序列
    self.tf_vt = tf.placeholder(tf.float32, [None, ], name="reward") # 输入累计回报序列

接着搭建网络结构:

# fc1 生成一个全连接层
layer = tf.layers.dense(
    inputs=self.tf_obs, # 输入数据
    units=10, # 神经元数量
    activation=tf.nn.tanh,  # tanh 激活函数
    kernel_initializer=tf.random_normal_initializer(mean=0, stddev=0.3),# 权重矩阵的初始化器
    bias_initializer=tf.constant_initializer(0.1),# 偏置项的初始化器
    name='fc1'
)
# fc2 生成一个全连接层
all_act = tf.layers.dense(
    inputs=layer,
    units=self.n_actions,
    activation=None,
    kernel_initializer=tf.random_normal_initializer(mean=0, stddev=0.3),
    bias_initializer=tf.constant_initializer(0.1),
    name='fc2'
)
self.all_act_prob = tf.nn.softmax(all_act, name='act_prob')      

接着定义损失计算:

with tf.name_scope('loss'):
    neg_log_prob = tf.reduce_sum(-tf.log(self.all_act_prob)*tf.one_hot(self.tf_acts, self.n_actions), axis=1)# 求和    
    loss = tf.reduce_mean(neg_log_prob * self.tf_vt)  #求平均值
with tf.name_scope('train'):
    self.train_op = tf.train.AdamOptimizer(self.learning_rate).minimize(loss)

下面我们解释下上述损失函数的设计,我们这里使用的是交叉熵损失函数:

其中p(x)是我们的真实分布,q(x)是我们的预测分布,接下来我们再来看这行代码:

neg_log_prob = tf.reduce_sum(-tf.log(self.all_act_prob)*tf.one_hot(self.tf_acts, self.n_actions), axis=1)

在上述代码中-tf.log()容易理解,求对数的负数,也符合我们的公式,self.all_act_prob就是我们策略网路的输出,其shape=(?,n_actions),即网络预测分布。

self.tf_acts是我们输入的动作序列,其shape=(?),这样子的形式是无法直接使用的,所以我们使用tf.one_hot(self.tf_acts, self.n_actions)

将其变为one-hot编码,此时shape=(?,n_actions)。

tf.reduce_sum()是一个给高维数组制定axis进行求和的函数,我们使用axis=1指明我们仅对数组的第二维进行求和,因为我们输入的都是二维数组,第一维的数字表示有多个这样的序列而已。这样子就使用了上述的损失函数公式的代码实现。

需要注意到,我们之后又进行了loss = tf.reduce_mean(neg_log_prob * self.tf_vt),这是因为我们现在计算到的neg_log_prob仍然是一个二维数组,我们要先将其与self.tf_vt相乘,这样子就相当于给每个step的损失值加了一个权重,权重越大的地方越需要我们进行训练,tf.reduce_mean()求其平均值,降为了一维。

4、运行策略网络选择动作

这一部分比较简单,直接上代码:

def choose_action(self, observation):
    prob_weights = self.sess.run(self.all_act_prob, feed_dict={self.tf_obs: observation[np.newaxis, :]})
    action = np.random.choice(range(prob_weights.shape[1]), p=prob_weights.ravel())  
    return action

因为observation是一个一维数组,而我们的网络输入需要二维,所以我们使用observation[np.newaxis, :]来将其升为二维数组,其输入网络后得到shape=(1,n_actions)的prob_weights。

在上述代码中最关键的是一个np.random.choice(),它帮助我们实现按照概率分布进行输出,首先我们用prob_weights.shape[1]获取prob_weights第二维的个数,即n_actions,然后使用range()将其构造成0-n_actions-1的数组,prob_weights.ravel()来进行降维,直接降为一维,shape=(1,n_actions)变成了shape=(n_actions),按照这个概率从range()生成的数组中采样。

5、学习并训练策略网络

1、学习和训练网络

def learn(self):
    # 获取折扣后的长期回报
    discounted_ep_rs_norm = self._discount_and_norm_rewards()
    # 训练策略网络
    self.sess.run(self.train_op, feed_dict={
        self.tf_obs: np.vstack(self.ep_obs), # 给计算图输入观察值
        self.tf_acts: np.array(self.ep_as),  # 给计算图输入动作
        self.tf_vt: discounted_ep_rs_norm,  # 给计算图输入折扣回报
    })
    self.__clear_memory() # 清空本回合的数据
    return 

2、计算折扣回报

首先计算折扣回报

# 折扣的回合奖励(使用了一个技巧)
discounted_ep_rs = np.zeros_like(self.ep_rs)
running_add = 0
for t in reversed(range(0, len(self.ep_rs))):
    running_add = running_add * self.gamma + self.ep_rs[t]
    discounted_ep_rs[t] = running_add

在我们按照公式公式来计算各个step的R,这里我们使用一种将技巧来进行这种计算处理:

self.ep_rs的存储是,T表示self.ep_rs的个数

 

t的取值

 

discounted_ep_rs数组存储数值

 

T-1

 

 

T-2

 

 

.......

 

.......

 

1

 

 

0

 

然后将折扣回报进行均值方差归一化来使效果更好:

# 将折扣回报全部均值方差归一化,为了使训练效果更好
discounted_ep_rs -= np.mean(discounted_ep_rs) # 求出平均值
discounted_ep_rs /= np.std(discounted_ep_rs) # 除以标准差

这种归一化操作是数据处理的基本知识,类似的我们还有:

import numpy as np
nums = [99,86,87,88,111,86,103,87,94,78,77,85,86]  
# 均值方差归一化
x = (nums-np.mean(nums))/np.std(nums) 
# 最值归一化
x =(nums-np.min(nums))/(np.max(nums)-np.min(nums))  

 

  • 0
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

魔法攻城狮MRL

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值