目录
前言
本文由浅入深介绍如果使用强化学习玩cart pole游戏。
概念
在电子游戏世界(特指Atari 2600这一类的简单游戏。不包括推理解密类的游戏)中:
- 环境指的是游戏本身,包括其内部的各种逻辑;
- Agent指的是操作游戏的玩家,当然也可以是指操作游戏的AI算法; 状态就是指游戏在屏幕上展现的画面。游戏通过屏幕画面把状态信息传达给Agent。如果是棋类游戏,状态是离散的,状态的数量是有限的。但在动作类游戏(如打飞机)中,状态是画面中的每个物体(飞机,敌人,子弹等等)所处的位置和运动速度的组合。状态是连续的,而且数量几乎是无限的。
- 动作是指手柄的按键组合,包括方向键和按钮的组合,当然也包括什么都不按(不做任何动作)。
- 奖励是指游戏的得分,每击中一个敌人都可以得到一些得分的奖励。
- 策略是Agent脑子里从状态到动作的映射。也就是说,每当Agent看到一个游戏画面(状态),就应该知道该如何操纵手柄(动作)。
Reinforcement Learning算法的任务就是找到最佳的策略。策略的表示方法可以有很多。比如: 当状态的个数是有限的情况下,我们可以给每个状态指定一个最佳动作,以后只要看到某种状态出现,就去查询这个状态对应的动作就可以了。这种表示方法类似于查询字典。用这种方法表示最佳策略是没问题的,但是却不利于我们找出最佳策略,因为这种表示法是不可微分的(non-differentiable)。 我们还可以给每个状态下所有可能的动作估一个价值(Q-value),当我们看到某个状态的时候,我们去比较这个状态下所有动作的价值,找出价值最大的那一个。这种表示方法实际上可以用一个二维的表格来表示(Tabular Representation)。 当状态的个数是无限多时,我们已经不可能用表格来表示策略了。这时我们只能用一些函数拟合(Function Approximation)的方法把状态,动作,价值这三者的关系表示出来。神经网络目前是最佳的选择。
Cart Pole游戏介绍
游戏规则
cart pole即车杆游戏,游戏如下,很简单,游戏里面有一个小车,上有竖着一根杆子。小车需要左右移动来保持杆子竖直。如果杆子倾斜的角度大于15°,那么游戏结束。小车也不能移动出一个范围(中间到两边各2.4个单位长度)。
action
- 左移
- 右移
state variables
- position of the cart on the track
- angle of the pole with the vertical
- cart velocity
- rate of change of the angle
分别表示车的位置,杆子的角度,车速,角度变化率
游戏奖励
在gym的Cart Pole环境(env)里面,左移或者右移小车的action之后,env都会返回一个+1的reward。到达200个reward之后,游戏也会结束。
gym介绍
简单玩法
一直左移
# coding: utf8
import gym
import time
env = gym.make("CartPole-v0")
env.reset()
# 一直左移
action = 0
sum_reward = 0
for i in range(1000):
env.render()
time.sleep(1)
observation, reward, done, info = env.step(action)
sum_reward += reward
print(observation, reward, sum_reward, done, info)
if done:
break
最后输出: 注意每次输出不一定一样。
[ 0.01332186 -0.20951928 -0.01201124 0.27060169] 1.0 1.0 False {} 0
[ 0.00913147 -0.40446779 -0.0065992 0.55947214] 1.0 2.0 False {} 1
[ 0.00104212 -0.59949649 0.00459024 0.85006868] 1.0 3.0 False {} 2
[-0.01094781 -0.79468073 0.02159161 1.14419149] 1.0 4.0 False {} 3
[-0.02684143 -0.99007801 0.04447544 1.44356652] 1.0 5.0 False {} 4
[-0.04664299 -1.18571827 0.07334677 1.74980819] 1.0 6.0 False {} 5
[-0.07035735 -1.38159249 0.10834294 2.06437417] 1.0 7.0 False {} 6
[-0.0979892 -1.5776388 0.14963042 2.38850996] 1.0 8.0 False {} 7
[-0.12954198 -1.77372622 0.19740062 2.72318191] 1.0 9.0 False {} 8
[-0.1650165 -1.96963594 0.25186426 3.06899915] 1.0 10.0 True {} 9
根据杆子角度调整
如果杆子角度为负,则右移;否则左移。
# coding: utf8
import gym
import time
env = gym.make("CartPole-v0")
env.reset()
# 一直往左移
action = 0
sum_reward = 0
for i in range(1000):
env.render()
time.sleep(0.1)
observation, reward, done, info = env.step(action)
if observation[1] < 0:
action = 1
else:
action = 0
sum_reward += reward
print(observation, reward, sum_reward, done, info)
if done:
break
最后输出
[-0.01882535 -0.17022571 -0.03011459 0.27344505] 1.0 1.0 False {}
[-0.02222987 0.0253127 -0.02464569 -0.02858193] 1.0 2.0 False {}
[-0.02172361 -0.16944731 -0.02521733 0.25622426] 1.0 3.0 False {}
[-0.02511256 0.02602544 -0.02009284 -0.04430474] 1.0 4.0 False {}
[-0.02459205 -0.16880272 -0.02097894 0.2419716 ] 1.0 5.0 False {}
[-0.0279681 0.02661253 -0.0161395 -0.05725412] 1.0 6.0 False {}
[-0.02743585 -0.16827434 -0.01728459 0.2302933 ] 1.0 7.0 False {}
[-0.03080134 0.02709028 -0.01267872 -0.06779128] 1.0 8.0 False {}
[-0.03025953 -0.16784762 -0.01403455 0.22086463] 1.0 9.0 False {}
...
[-0.10049214 -0.18592199 0.11853794 0.62351417] 1.0 57.0 False {}
[-0.10421058 0.00736289 0.13100822 0.37038998] 1.0 58.0 False {}
[-0.10406332 -0.18935337 0.13841602 0.70134211] 1.0 59.0 False {}
[-0.10785039 0.00360618 0.15244286 0.45523554] 1.0 60.0 False {}
[-0.10777827 -0.19330536 0.16154758 0.79182232] 1.0 61.0 False {}
[-0.11164438 -0.00072631 0.17738402 0.55400362] 1.0 62.0 False {}
[-0.1116589 0.19151957 0.18846409 0.32203753] 1.0 63.0 False {}
[-0.10782851 -0.00571617 0.19490485 0.66773352] 1.0 64.0 False {}
[-0.10794283 0.18623823 0.20825952 0.44219351] 1.0 65.0 False {}
[-0.10421807 -0.01112806 0.21710339 0.79263264] 1.0 66.0 True {}
Random Guessing Algorithm
observation是一个四维向量,如果对这个向量求它的加权和,就可以得到一个值,那么就可以根据加权和的符号来决定action,同样可以用sigmoid函数当成二分类问题。这样就可以通过改变权重值来改变policy。
代码如下, 包含了hill climbing算法的代码。
# coding: utf8
import numpy as np
import gym
import time
def get_action(weights, observation):
wxb = np.dot(weights[:4], observation) + weights[4]
if wxb >= 0:
return 1
else:
return 0
def get_sum_reward_by_weights(env, weights):
observation = env.reset()
sum_reward = 0
for t in range(1000):
# time.sleep(0.01)
# env.render()
action = get_action(weights, observation)
observation, reward, done, info = env.step(action)
sum_reward += reward
# print(sum_reward, action, observation, reward, done, info)
if done:
break
return sum_reward
def get_weights_by_random_guess():
return np.random.rand(5)
def get_weights_by_hill_climbing(best_weights):
return best_weights + np.random.normal(0, 0.1, 5)
def get_best_result(algo="random_guess"):
env = gym.make("CartPole-v0")
np.random.seed(10)
best_reward = 0
best_weights = np.random.rand(5)
for iter in range(10000):
cur_weights = None
if algo == "hill_climbing":
# print(best_weights)
cur_weights = get_weights_by_hill_climbing(best_weights)
else:
cur_weights = get_weights_by_random_guess()
cur_sum_reward = get_sum_reward_by_weights(env, cur_weights)
# print(cur_sum_reward, cur_weights)
if cur_sum_reward > best_reward:
best_reward = cur_sum_reward
best_weights = cur_weights
if best_reward >= 200:
break
print(iter, best_reward, best_weights)
return best_reward, best_weights
print(get_best_result("hill_climbing"))
# env = gym.make("CartPole-v0")
# get_sum_reward_by_weights(env, [0.22479665, 0.19806286, 0.76053071, 0.16911084, 0.08833981])
Hill Climbing Algorithm
Hill Climbing Algorithm会给当前最好的权重加上一组随机值,如果加上这组值持续时间变长了那么就更新最好的权重,如果没有变的更好就不更新。
refer: https://www.fashici.com/tech/836.html
# -*- coding: utf-8 -*-
#载入库
import numpy as np
import tensorflow as tf
import gym
env = gym.make('CartPole-v0')
#创建CartPole问题的环境env
env.reset()
#初始化环境
random_episodes = 0
reward_sum = 0#奖励
while random_episodes < 10:
env.render()#将CartPole问题的图像渲染出来
observation, reward, done, _ = env.step(np.random.randint(0, 2))
#使用np.random.randint(0, 2)产生随机的Action
#然后使用env.step()执行随机的Action,并获取返回值
#如果done标记为True,则表示这次试验结束,即倾角超过15度或者偏离中心过远导致任务失败
reward_sum += reward
if done:#如果试验结束
random_episodes += 1
print("game over,Reward for this episode was:", reward_sum)
#输出这次试验累计的奖励
reward_sum = 0 #奖励重新置为0
env.reset()#重启环境
print "随机测试结束"
# 超参数
H = 50 # 隐含的节点数
batch_size = 25 #
learning_rate = 1e-1 # 学习率
gamma = 0.99 # Reward的discount比例设为0.99,该值必须小于1.
#防止Reward被无损耗地不断累加导致发散,这样也能区分当前Reward和未来的Reward的价值
#当前Action直接带来的Reward不需要discount,而未来的Reward因存在不确定性,所以需要discount
D = 4 # 环境信息observation的维度D为4
tf.reset_default_graph()
#策略网络的具体结构。
#该网络将接受observation作为信息输入,最后输出一个概率值用以选择Action
#这里只有两个Action,向左施加力或者向右施加力,因此可以通过一个概率值决定
observations = tf.placeholder(tf.float32, [None, D], name="input_x")
#创建输入信息observations的placeholder其维度为D
#使用tf.contrib.layers.xavier_initializer方法初始化隐含层的权重W1,其维度为[D,H]
W1 = tf.get_variable("W1", shape=[D, H],
initializer=tf.contrib.layers.xavier_initializer())
layer1 = tf.nn.relu(tf.matmul(observations, W1))
#接着使用tf.matmul将环境信息observations乘上W1再使用relu激活函数处理得到隐含层的输出layer1
#使用tf.contrib.layers.xavier_initializer方法初始化隐含层的权重W2,其维度为[H,1]
W2 = tf.get_variable("W2", shape=[H, 1],
initializer=tf.contrib.layers.xavier_initializer())
score = tf.matmul(layer1, W2)
probability = tf.nn.sigmoid(score)
#将隐含层输出layer1乘以W2后,使用Sigmoid激活函数处理得到最后的输出概率
# From here we define the parts of the network needed for learning a good policy.
tvars = tf.trainable_variables()#获取策略网络中全部可训练的参数tvars
input_y = tf.placeholder(tf.float32, [None, 1], name="input_y")
advantages = tf.placeholder(tf.float32, name="reward_signal")
#定义人工设置的虚拟label的占位符input_y
#以及每个Action的潜在价值的占位符
#当Action取值为1的概率为probability(即策略网络输出的概率)
#当Action取值为0的概率为1-probability
#label取值与Action相反,即label=1-Action。
#当Action为1时,label为0,此时loglik=tf.log(probability),Action取值为1的概率的对数
#当Action为0时,label为1,此时loglik=tf.log(1-probability),Action取值为0的概率的对数
#所以,loglik其实就是当前Action对应的概率的对数
loglik = tf.log(input_y * (input_y - probability) + (1 - input_y) * (input_y + probability))
loss = -tf.reduce_mean(loglik * advantages)
#将loglik与潜在价值advanages相乘,并取负数作为损失,即优化目标
newGrads = tf.gradients(loss, tvars)
#使用tf.gradients求解模型参数关于loss的梯度
# Once we have collected a series of gradients from multiple episodes, we apply them.
# We don't just apply gradeients after every episode in order to account for noise in the reward signal.
#模型的优化器使用Adam算法
adam = tf.train.AdamOptimizer(learning_rate=learning_rate) # Our optimizer
W1Grad = tf.placeholder(tf.float32, name="batch_grad1") # Placeholders to send the final gradients through when we update.
W2Grad = tf.placeholder(tf.float32, name="batch_grad2")
#我们分别设置两层神经网络参数的梯度的placeholder
batchGrad = [W1Grad, W2Grad]
updateGrads = adam.apply_gradients(zip(batchGrad, tvars))
#并使用adam.apply_gradients定义我们更新模型参数的操作updateGrads
#之后计算参数的梯度,当积累到一定样本量的梯度,就传入W1Grad和W2Grad,并执行updateGrads更新模型参数
#注意:
#深度强化学习的训练和其他神经网络一样,也使用batch training的方式。
#我们不逐个样本的更新参数,而是累计到一个batch_size的样本的梯度在更新参数
#防止单一样本随机扰动的噪声对模型带来不良影响。
#用来估算每一个Action对应的潜在价值discount_r
#因为CartPole问题中每次获得的Reward都和前面的Action有关,属于delayed reward
#因此需要比较精确地衡量每一个Action实际带来的价值,不能只看当前这一步的Reward,而要考虑后面的Delayed Reward
#哪些能让Pole长时间保持在空中竖直的Action应该拥有较大的值,而哪些最终导致pole倾倒的Action,应该拥有较小的期望价值。
#我们判断越靠后的Aciton的期望价值越小,因为它们更可能是导致Pole倾倒的原因,并且判断越考前的期望价值约大,因为它们
#长时间保持了Pole的竖直,和整倾倒的原因没那么大
#在CartPole问题中,除了最后结束时刻的Action为0,其余的均为1,。
def discount_rewards(r):
""" take 1D float array of rewards and compute discounted reward """
discounted_r = np.zeros_like(r)
running_add = 0
#定义每个Action除直接获得的Reward外的潜在价值running_add
#running_add是从后向前累计的,并且需要经过discount衰减。
#每一个Action的潜在价值,即为后一个Action的前在价值乘以衰减系数gamma,再加上它直接获得的reward
#即running_add * gamma + r[t]
#这样从最后一个Action不断向前累计计算,即可得到全部Action的潜在价值。
for t in reversed(range(r.size)):
running_add = running_add * gamma + r[t]
discounted_r[t] = running_add
return discounted_r
#xs为环境信息observation的列表
#ys为我们定义的label的列表
#drs为我们记录的每一个Action的Reward
xs, ys, drs = [], [], []
# running_reward = None
reward_sum = 0 #累计的Reward
episode_number = 1
total_episodes = 10000 #总试验次数
init = tf.global_variables_initializer()
# Launch the graph
with tf.Session() as sess:#创建sess
rendering = False #render的标志关闭,因为会带来较大的延迟
sess.run(init)#初始化全部参数
observation = env.reset() # Obtain an initial observation of the environment
#先初始化CartPole的环境并获得初始状态
# Reset the gradient placeholder. We will collect gradients in
# gradBuffer until we are ready to update our policy network.
gradBuffer = sess.run(tvars)
#获取所有模型参数,用来创建储存参数梯度的缓冲器gradBuffer
for ix, grad in enumerate(gradBuffer):
gradBuffer[ix] = grad * 0
#将gradBuffer全部初始化为0
#接下来,每次试验中,我们将收集参数的梯度存储到gradBuffer中,直到完成了一个batch_size的试验
#再将汇总的梯度更新到模型参数
while episode_number <= total_episodes:
# Rendering the environment slows things down,
# so let's only look at it once our agent is doing a good job.
# 当某个batch的平均Reward达到100以上时,即Agent表现良好。
if reward_sum / batch_size > 100 or rendering == True:
#调用env.render()对试验环境进行展示
env.render()
rendering = True
# Make sure the observation is in a shape the network can handle.
# 先使用tf.reshape将observation变形为策略网络的输入的格式
x = np.reshape(observation, [1, D])
# Run the policy network and get an action to take.
# 然后传入网络中
tfprob = sess.run(probability, feed_dict={observations: x})
#获得网络输出的概率tfprob,即Action取值为1的概率
action = 1 if np.random.uniform() < tfprob else 0
#接下来,我们在0到1之间随机抽样,若随机值小于tfprob,则令Action取值为1,
#否则令Action取值为0,
xs.append(x) # 将输入的环境信息observation添加到列表xs中
y = 1 if action == 0 else 0 # a "fake label" 虚拟的label
ys.append(y)#添加到列表ys中
# step the environment and get new measurements
observation, reward, done, info = env.step(action)
#使用env.step执行一次Action,获取observation,reward,done和info
reward_sum += reward
#将reward累加到reward_sum
drs.append(reward) # record reward (has to be done after we call step() to get reward for previous action)
#并将reward添加到列表drs中
if done:#为True时,表示一次试验结束
episode_number += 1
# stack together all inputs, hidden states, action gradients, and rewards for this episode
epx = np.vstack(xs)
epy = np.vstack(ys)
epr = np.vstack(drs)
#使用np.vstack将几个列表xs,ys,drs中的元素纵向堆叠起来
xs, ys, drs = [], [], [] # 清空,以备下次使用
discounted_epr = discount_rewards(epr)
discounted_epr -= np.mean(discounted_epr)
discounted_epr /= np.std(discounted_epr)
# 使用前面定义好的discount_rewards函数计算每一步Action的潜在价值
#并进行标准化,得到一个零均值,标准差为1的分布
#discount_reward会参与到模型损失的计算,分布稳定的discount_reward有利于训练的稳定
# Get the gradient for this episode, and save it in the gradBuffer
#将epx,epy,discounted_epr输入网络,并求解梯度
tGrad = sess.run(newGrads, feed_dict={observations: epx, input_y: epy, advantages: discounted_epr})
for ix, grad in enumerate(tGrad):
gradBuffer[ix] += grad
#将获得的梯度累加到gradBuffer中去
# If we have completed enough episodes, then update the policy network with our gradients.
if episode_number % batch_size == 0:
#当进行试验的次数达到batch_size的整数倍时,gradBuffer中就积累了足够多的梯度。
#因此使用updateGrads操作将gradBuffer中的梯度更新到策略网络的模型参数中去
sess.run(updateGrads, feed_dict={W1Grad: gradBuffer[0], W2Grad: gradBuffer[1]})
for ix, grad in enumerate(gradBuffer):
gradBuffer[ix] = grad * 0
#清空gradBuffer为计算下一个batch的梯度做准备
# Give a summary of how well our network is doing for each batch of episodes.
# running_reward = reward_sum if running_reward is None else running_reward * 0.99 + reward_sum * 0.01
print('Average reward for episode %d : %f.' % (episode_number, reward_sum / batch_size))
if reward_sum / batch_size > 200:
print("Task solved in", episode_number, 'episodes!')
break
reward_sum = 0
observation = env.reset()
#每次试验结束后,将任务环境env重置,方便下一次试验