##########################################
# 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()
【强化学习】A3C代码注释版本
最新推荐文章于 2024-03-13 10:36:03 发布
本文介绍了A3C(Asynchronous Advantage Actor-Critic)算法的改进版,旨在解决AC算法收敛问题。代码实现了一个包含全局和局部网络的框架,通过多线程并行执行多个工作者,每个工作者有自己的局部网络并定期同步到全局网络。算法中包含了策略梯度和价值函数的损失计算,以及熵项以鼓励探索。在CartPole-v0环境中进行训练,并展示了训练过程中累计奖励的变化情况。
摘要由CSDN通过智能技术生成