【强化学习】A3C代码注释版本

本文介绍了A3C(Asynchronous Advantage Actor-Critic)算法的改进版,旨在解决AC算法收敛问题。代码实现了一个包含全局和局部网络的框架,通过多线程并行执行多个工作者,每个工作者有自己的局部网络并定期同步到全局网络。算法中包含了策略梯度和价值函数的损失计算,以及熵项以鼓励探索。在CartPole-v0环境中进行训练,并展示了训练过程中累计奖励的变化情况。
摘要由CSDN通过智能技术生成
##########################################
# A3C做出的改进:
# 解决AC难以收敛的问题
# 不一样的地方:
#

import threading
# import tensorflow as tf
import tensorflow.compat.v1 as tf

tf.compat.v1.disable_eager_execution()
import numpy as np
import gym
import os
import shutil
import matplotlib.pyplot as plt

GAME = 'CartPole-v0'  # 游戏名称
OUTPUT_GRAPH = True
LOG_DIR = '../log'  # 一个文件名
N_WORKERS = 3  # 有3个并行的worker
MAX_GLOBAL_EP = 3000  # 最大执行回合数
GLOBAL_NET_SCOPE = 'Global_Net'  # object名称
UPDATE_GLOBAL_ITER = 100  # 每UPDATE_GLOBAL_ITER 步 或者回合完了,进行sync操作
GAMMA = 0.9  # 折扣因子
ENTROPY_BETA = 0.001
LR_A = 0.001  # learning rate for actor  Actor的学习率
LR_C = 0.001  # learning rate for critic   Actor的学习率
GLOBAL_RUNNING_R = []  # 游戏整体的这个游戏里面一共进行了多少步,这个数组长度就是多少
GLOBAL_EP = 0  # 整体进行的回合数
STEP = 3000  # Step limitation in an episode   TODO 这里应该改成200,因为200步游戏就达到终止条件了
TEST = 10  # 实验次数每100回合测试一次 TODO 这里应该是100吧?

env = gym.make(GAME)
N_S = env.observation_space.shape[0]  # 状态维度(?,4)
N_A = env.action_space.n  # 动作维度(2)


# 这个 class 可以被调用生成一个 global net.
# 也能被调用生成一个 worker 的 net, 因为他们的结构是一样的,
# 所以这个 class 可以被重复利用.
class ACNet(object):
    def __init__(self, scope, globalAC=None):  # 这里的scope是网络名称
        # 当创建worker网络的时候,我们传入之前创建的globalAC给这个worker
        # 判断当下建立的网络是 local 还是 global
        if scope == GLOBAL_NET_SCOPE:  # 如果建立的网络是Global网络
            # 关于tf.name_scope和tf.variable_scope
            with tf.variable_scope(scope):
                self.s = tf.placeholder(tf.float32, [None, N_S], 'S')  # 定义一组状态
                # 这里的a_params具体代表求导时的那个分母,也就是对什么求导
                self.a_params, self.c_params = self._build_net(scope)[-2:]  # 取这个数组从第一个到倒数第三个,最后两个数被丢弃了
        else:  # local net, calculate losses
            with tf.variable_scope(scope):
                # 接着计算 critic loss 和 actor loss
                # 用这两个 loss 计算要推送的 gradients
                self.s = tf.placeholder(tf.float32, [None, N_S], 'S')  # 定义一组状态
                self.a_his = tf.placeholder(tf.int32, [None, ], 'A')  # 定义了一组动作
                self.v_target = tf.placeholder(tf.float32, [None, 1], 'Vtarget')  # 目标价值

                self.a_prob, self.v, self.a_params, self.c_params = self._build_net(scope)

                # 接着计算 critic loss 和 actor loss
                # 用这两个 loss 计算要推送的 gradients

                # 张量减法运算:tf.subtract函数是一个算术运算符, 用于表示减法, 返回x - y的元素
                td = tf.subtract(self.v_target, self.v, name='TD_error')  # td_error = v_target - v
                with tf.name_scope('c_loss'):  # c网络loss函数
                    self.c_loss = tf.reduce_mean(tf.square(td))  # td_error先求平方然后取平均

                with tf.name_scope('a_loss'):  # 下面这些操作都封装到一个叫a_loss的作用域中,用来计算a_loss,方便可视化
                    # 下面这些应该是在实现a_loss的那个公式
                    log_prob = tf.reduce_sum(tf.log(self.a_prob + 1e-5) * tf.one_hot(self.a_his, N_A, dtype=tf.float32),
                                             axis=1, keep_dims=True)
                    # tf.stop_gradient: 停止梯度运算,当在一个图中执行时, 这个op按原样输出它的输入张量。
                    # 当构建ops来计算梯度时,该op会阻止将其输入贡献考虑在内。
                    exp_v = log_prob * tf.stop_gradient(td)
                    entropy = -tf.reduce_sum(self.a_prob * tf.log(self.a_prob + 1e-5),
                                             axis=1, keep_dims=True)  # encourage exploration
                    self.exp_v = ENTROPY_BETA * entropy + exp_v  # expect value
                    self.a_loss = tf.reduce_mean(-self.exp_v)  # actor_loss

                with tf.name_scope('local_grad'):
                    # tf.gradients 实现a_loss对a_params求导  梯度
                    self.a_grads = tf.gradients(self.a_loss, self.a_params)
                    self.c_grads = tf.gradients(self.c_loss, self.c_params)

            with tf.name_scope('sync'):  # 同步
                with tf.name_scope('pull'):  # 调用pull,这个worker就会从global_net中获取到最新的参数
                    # assign相当于连线,一般是将一个变量的值不间断地赋值给另一个变量,就像把这两个变量连在一起,所以习惯性的当做连线用,比如把一个模块的输出给另一个模块当输入。
                    self.pull_a_params_op = [l_p.assign(g_p) for l_p, g_p in zip(self.a_params, globalAC.a_params)]
                    self.pull_c_params_op = [l_p.assign(g_p) for l_p, g_p in zip(self.c_params, globalAC.c_params)]
                with tf.name_scope('push'):  # 调用push,这个worker就会将自己的个人更新推送去global_net
                    self.update_a_op = OPT_A.apply_gradients(zip(self.a_grads, globalAC.a_params))
                    self.update_c_op = OPT_C.apply_gradients(zip(self.c_grads, globalAC.c_params))

    def _build_net(self, scope):  # 这里搭建Actor和Critic网络
        w_init = tf.random_normal_initializer(0., .1)  # 生成一组符合标准正态分布的 tensor 对象,初始化张量
        with tf.variable_scope('actor'):  # 搭建一个actor网络
            """
            这个actor网络有两层,第一层输入是状态,输出一个维度为200的东西,第一层的输出l_a是第二层的输入,第一层的激活函数和第二层不一样
            """
            # https://blog.csdn.net/yangfengling1023/article/details/81774580/
            # 全连接层,曾加了一个层,全连接层执行操作 outputs = activation(inputs.kernel+bias) 如果执行结果不想进行激活操作,则设置activation=None
            # self.s:输入该网络层的数据  200:输出的维度大小,改变inputs的最后一维    tf.nn.relu6:激活函数,即神经网络的非线性变化
            # kernel_initializer=w_init:卷积核的初始化器    name:层的名字
            l_a = tf.layers.dense(self.s, 200, tf.nn.relu6, kernel_initializer=w_init, name='la')

            a_prob = tf.layers.dense(l_a, N_A, tf.nn.softmax, kernel_initializer=w_init, name='ap')
        with tf.variable_scope('critic'):  # 搭建critic网络
            """
            这个网络也是有两层,第一层有100个输出,第二层只有一个输出,
            """
            l_c = tf.layers.dense(self.s, 100, tf.nn.relu6, kernel_initializer=w_init, name='lc')
            v = tf.layers.dense(l_c, 1, kernel_initializer=w_init, name='v')  # state value

        # tf.get_collection 用来获取一个名称是‘key’的集合中的所有元素,返回的是一个列表,列表的顺序是按照变量放入集合中的先后;
        # scope参数可选,表示的是名称空间(名称域),如果指定,就返回名称域中所有放入‘key’的变量的列表,不指定则返回所有变量。
        # TODO 这个含义看不懂。。。TRAINABLE_VARIABLES:将由优化程序训练的Variable对象的子集
        a_params = tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES, scope=scope + '/actor')
        c_params = tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES, scope=scope + '/critic')
        return a_prob, v, a_params, c_params  # 返回均值、方差,v:state_value TODO a_params, c_params 这两个参数什么意思啊

    def update_global(self, feed_dict):  # run by a local  进行push操作
        SESS.run([self.update_a_op, self.update_c_op], feed_dict)  # 将本地

    def pull_global(self):  # run by a local     #进行pull操作
        SESS.run([self.pull_a_params_op, self.pull_c_params_op])

    def choose_action(self, s):  # run by a local    根据s选择动作
        prob_weights = SESS.run(self.a_prob, feed_dict={self.s: s[np.newaxis, :]})
        action = np.random.choice(range(prob_weights.shape[1]),
                                  p=prob_weights.ravel())  # select action w.r.t the actions prob   根据概率选择动作
        return action


class Worker(object):
    def __init__(self, name, globalAC):
        self.env = gym.make(GAME).unwrapped  # 创建自己的环境
        self.name = name  # 自己的名字
        self.AC = ACNet(name, globalAC)  # 自己的local net,并绑定上globalAC

    def work(self):  # worker网络的主体部分
        # s,a,r 的缓存,用于n_steps更新
        global GLOBAL_RUNNING_R, GLOBAL_EP
        total_step = 1  # 初始化步数
        buffer_s, buffer_a, buffer_r = [], [], []    # state,action,和reward的缓存
        # 当 COORD不需要停止的时候,或者当GLOBAL_EP比MAX_GLOBAL_EP小的时候
        while not COORD.should_stop() and GLOBAL_EP < MAX_GLOBAL_EP:  # MAX_GLOBAL_EP最大训练EP
            s = self.env.reset()  # 重置环境
            ep_r = 0  # 统计ep的总reward
            while True:
                # if self.name == 'W_0':
                #     self.env.render()
                a = self.AC.choose_action(s)  # 选择动作
                s_, r, done, info = self.env.step(a)  # 与环境互动
                if done: r = -5  # 把reward-100的时候,改为-5
                ep_r += r  # 保存数据   ep_r 总的reward
                buffer_s.append(s)  # 添加各种缓存
                buffer_a.append(a)
                buffer_r.append(r)

                # 每UPDATE_GLOBAL_ITER 步 或者回合完了,进行sync操作,也就是学习的步骤
                if total_step % UPDATE_GLOBAL_ITER == 0 or done:  # update global and assign to local net

                    # 获得用于计算TD_error 的下一state的value
                    # 下面对应价值函数的那个公式,如果是terminal state,则对未来的期望等于0
                    if done:
                        v_s_ = 0  # terminal
                    else:
                        v_s_ = SESS.run(self.AC.v, {self.AC.s: s_[np.newaxis, :]})[0, 0]
                    buffer_v_target = []  # 下 state value的缓存,用于计算TD

                    # 计算每个state的V(s')
                    # print(a[::-1]) ### 取从后向前(相反)的元素[1 2 3 4 5]-->[ 5 4 3 2 1 ]
                    # (莫烦说)对MDP的一个反向的计算,对未来的reward的一个递解的步骤
                    for r in buffer_r[::-1]:
                        v_s_ = r + GAMMA * v_s_
                        buffer_v_target.append(v_s_)
                    buffer_v_target.reverse()  # 先反转填充再转回来

                    # np.vstack:按垂直方向(行顺序)堆叠数组构成一个新的数组
                    buffer_s, buffer_a, buffer_v_target = np.vstack(buffer_s), np.array(buffer_a), np.vstack(
                        buffer_v_target)
                    # feed_dict的作用是给使用placeholder创建出来的tensor赋值
                    feed_dict = {
                        self.AC.s: buffer_s,
                        self.AC.a_his: buffer_a,
                        self.AC.v_target: buffer_v_target,
                    }
                    self.AC.update_global(feed_dict)  # 推送更新去globalAC

                    buffer_s, buffer_a, buffer_r = [], [], []  # 清空缓存
                    self.AC.pull_global()  # 获取globalAC的最新参数

                s = s_  # 更新状态
                total_step += 1  # 整体部署+1
                if done:
                    # 达到游戏终止条件,更新reward
                    if len(GLOBAL_RUNNING_R) == 0:  # 如果是第一回合
                        GLOBAL_RUNNING_R.append(ep_r)
                    else:  # 不是第一回合
                        GLOBAL_RUNNING_R.append(0.99 * GLOBAL_RUNNING_R[-1] + 0.01 * ep_r)
                    print(
                        self.name,
                        "Ep:", GLOBAL_EP,
                        "| Ep_r: %i" % GLOBAL_RUNNING_R[-1],
                    )
                    GLOBAL_EP += 1  # 加一回合
                    break  # 结束这回合


if __name__ == '__main__':
    SESS = tf.Session()  # 创建一个会话
    # 下面是真正的重点!!worker并行计算
    # 使用 tf.device() 指定模型运行的具体设备,可以指定运行在GPU还是CPU上,以及哪块GPU上。
    with tf.device("/cpu:0"):  # 以下部分,都在CPU0完成
        # tf.train.RMSPropOptimizerSHI是一种优化算法,有很多种优化算法,具体见下面这个文档,有空好好学习下
        # https://www.cnblogs.com/bigcome/p/10084220.html
        OPT_A = tf.train.RMSPropOptimizer(LR_A, name='RMPropA')  # 定义了一个actor优化器
        OPT_C = tf.train.RMSPropOptimizer(LR_C, name='RMPropC')  # 定义了一个Critic优化器
        GLOBAL_AC = ACNet(GLOBAL_NET_SCOPE)  # we only need its params 定义了一个总的AC网络
        workers = []  # 定义workers
        for i in range(N_WORKERS):  # 创建worker,之后再并行
            i_name = 'W_%i' % i  # worker的名字
            workers.append(Worker(i_name, GLOBAL_AC))  # 每个worker都有共享这个global AC

    # 调用 tf.train.Coordinator() 来创建一个线程协调器,用来管理之后在Session中启动的所有线程;
    COORD = tf.train.Coordinator()  # Tensorflow用于并行的工具
    SESS.run(tf.global_variables_initializer())

    # 执行到这儿的意思是,OUTPUT_GRAPH这个参量只出现了一次,而且是true,也就是说,删除path这个路径里面的LOG_DIR文件,然后保存一张新的进去,也就是咱们收敛情况的图
    if OUTPUT_GRAPH:
        # os.path.exists()就是判断括号里的文件是否存在的意思,path代表路径,括号内的可以是文件名。
        if os.path.exists(LOG_DIR):  # 这句是说,如果LOG_DIR这个文件存在,就将它删除
            # 在python文件中,使用代码删除文件夹以及里面的文件,可以使用shutil.rmtree,递归地删除文件夹以及里面的文件。
            shutil.rmtree(LOG_DIR)
        # tf.summary.FileWriter 指定一个文件用来保存图 ,指定LOG_DIR这个文件来保存SESS.graph这个图
        tf.summary.FileWriter(LOG_DIR, SESS.graph)

    # 开启tf线程
    worker_threads = []
    for worker in workers:  # 执行每一个worker
        job = lambda: worker.work()  # 有一个工人(worker),有一个方法(work),这句是说让这个工人worker去执行work这个方法
        t = threading.Thread(target=job)  # 添加一个工作线程
        t.start()  # 开始这个线程
        worker_threads.append(t)  # 把这个线程添加到worker_threads中
    COORD.join(worker_threads)  # 当所有的worker都运行完了才会进行下面的步骤,如果没有这一句,那么每一个worker运行完就会进行下面的步骤

    testWorker = Worker("test", GLOBAL_AC)  # 创建一个测试worker   TODO 不知道这个worker有什么作用
    testWorker.AC.pull_global()  # 对testworker执行pull操作

    # 下面应该是测试部分
    total_reward = 0
    for i in range(TEST):
        state = env.reset()  # 初始化状态
        for j in range(STEP):
            env.render()  # env.render()函数用于渲染出当前的智能体以及环境的状态
            action = testWorker.AC.choose_action(state)  # 选择一个测试动作
            state, reward, done, _ = env.step(action)
            total_reward += reward  # reward叠加
            if done:  # 达到终止条件就跳出循环
                break
    ave_reward = total_reward / TEST
    # 打印内容:整个过程一共进行了多少回合,平均奖励是多少
    print('episode: ', GLOBAL_EP, 'Evaluation Average Reward:', ave_reward)

    plt.plot(np.arange(len(GLOBAL_RUNNING_R)), GLOBAL_RUNNING_R)  # TODO GLOBAL_RUNNING_R这个参量再研究一下
    plt.xlabel('step')
    plt.ylabel('Total moving reward')
    plt.show()

  • 2
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值