TensorFlow强化学习入门(0)——Q-Learning的查找表实现和神经网络实现

我们将学习如何处理OpenAI FrozenLake问题,当然我们的问题不像图片中那样逼真

在我这系列的强化学习教程中,我们将探索强化学习大家族中的Q-Learning算法,它和我们后面的教程(1-3)中基于策略的算法有一些差异。在本节中,我们先放下复杂而笨重的深度神经网络,首先在一个简单的查找表基础上实现第一个算法版本,随后我们再考虑如何使用TensorFlow将神经网络的形式集成进来。考虑到该节主要是回顾基础知识,所以我把它归为第0部分。对Q-Learning中发生的细节有所了解对于我们后面学习将策略梯度(policy gradient)和Q-Learning结合来构建先进的RL agent大有裨益。(如果你对策略网络更感兴趣或者已经掌握了Q-Learning相关知识,可以等译者后面的翻译或者查阅原文

与利用函数直接将当前观测转化为行动的策略梯度方法不同,Q-Learning尝试学习给定状态下的对应值并据此在给定状态下作出特定的行动。尽管两者作出行动的手段不同,但是都可以达到在给定场合下作出智能的行动的效果。你之前可能听说过深度Q-网络已经可以玩雅达利游戏了。这其实只是我们下面讨论的Q-Learning算法的更大更复杂的实现而已。

查找表实现

1
2
3
4
5
# FrozenLake 问题的规则
SFFF (S: 起始点, 安全)
FHFH (F: 冰层, 安全)
FFFH (H: 空洞, 跌落危险)
HFFG (G: 目的地, 飞盘所在地)

本教程会基于OpenAI gym尝试解决上述的FrozenLake问题。OpenAI gym给定了描述这个简单游戏的数组,人们可以让agent基于此进行学习。FrozenLake问题发生在一个4*4的网格区域上,其中包括起始区,安全冰层区,危险空洞区和目标地点,,在任意的时刻agent可以上下左右移动,我们的目标是让agent在不跌落至空洞的前提下到达目的地。这里有一个特殊的问题就是偶尔会有一阵风吹过,使agent被吹到并非它选择的区域。因此在这个问题中每一时刻都作出最优解是不可能的,但是避开空洞抵达目的地还是可行的。只有到达目的地才可以得到1分,除此之外都是0分。由此我们需要一个基于长期过程后的奖惩进行学习的算法,这正是Q-Learning设计的目的。

在最简单的解法中,Q-Learning就是一个表格,包含了问题中所有可能发生情况,表格中的值表征着我们在当前情况下应当作出什么行动。在FrozenLake问题中,有16个状态(每一个表格单元对应一个情况),4个可选行动,这产生了一个16*4的Q值表格。我们首先将表格初始化为全0,当有行动得分之后我们据此对表格进行更新。

我们使用贝尔曼方程对Q值表进行更新,贝尔曼方程可以将一系列行动带来的奖励值分配至当前的行动上。在这个方法中,我们不断根据未来决策得到的奖励值来更新当前的表格! 其数学表达式可以写作:

Eq 1. Q(s,a) = r + γ(max(Q(s’,a’))

这个方程中,当前状态(s)和行动(a)对应的Q值等于当前的奖励值(r)加上折算系数乘上当前行动后的状态和下一步行动可能产生的最大Q值。这个可变的折算值可以控制可能的未来奖励值和当前奖励值相比下的重要程度。通过这种方法,表格开始缓慢逼近得到预期奖励值所需的当前各行动的精确度量值。下面给出FrozenLake问题中Q表算法的Python实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# Q-Table Learning
import gym
import numpy as np
# 加载实验环境
env = gym.make('FrozenLake-v0')
# 集成Q表学习算法
# 初始表(全0)
Q = np.zeros([env.observation_space.n,env.action_space.n])
# 设定超参数
lr = .8
y = .95
num_episodes = 2000
# 创建episodes中包含当前奖励值和步骤的列表
rList = []
for i in range(num_episodes):
# 初始化环境,得到第一个状态观测值
s = env.reset()
rAll = 0
d = False
j = 0
# Q表学习算法
while j < 99:
j += 1
# 根据Q表和贪心算法(含噪)选定当前的动作
a = np.argmax(Q[s,:] + np.random.randn(1, env.action_space.n) * (1./(i+1)))
# 获取新的状态值和奖励值
s1, r, d, _ = env.step(a)
# 更新Q表
Q[s,a] = Q[s,a] + lr * (r + y*np.max(Q[s1,]) - Q[s,a])
rAll += r
s = s1
if d == True:
break
rList.append(rAll)
print("Score over time: " + str(sum(rList)/num_episodes))
print("Final Q-Table Values")
print(Q)

神经网络实现

在完成上面的例子的过程中,你可能已经意识到这一点:用表格的方式来实现固然不错,但是弹性太差了。上述的简单问题使用表格实现是很简单,但是有可能问题中的状态(s)和行动(a)会多到无法用表格来存储。不幸的是,大部分我们感兴趣的问题中可能的状态数和行动数都很多,无法使用上面的表格解法。这迫使我们寻找一种新的方式来描述状态,不再依赖Q表来决定下一步的行动:这正是神经网络擅长的地方。通过函数逼近的方法,我们可以将任意的状态表示为矢量形式并通过映射得到Q值。

在FrozenLake的例子中,我们使用单层网络来接受虚拟编码(One-hot encoding)后的当前状态(1x16),输出为包含4个Q值的矢量,每个Q值对应一个方向。这样一个简单的网络就可以充当上面的奖励值表格,网络中的权重值取代了之前的表格单元。更关键的一点是我们可以尝试增加层数,激活函数和不同的输入类型,这些在常规的表格中都是不可能实现的。除此之外,神经网络的更新方法也更胜一筹,和表格中直接更新值的做法不同,神经网络通过损失函数和反向传播的结合来实现权重更新。我们选取目标Q值和当前Q值差的平方和作为损失函数,“目标”值在计算之后其梯度会反馈于网络上。在理想的情况下,每一步之后的Q值应当都是不变的(当然如果一步一刮风的情况就不一定了~)

Eq2. Loss = ∑(Q-target - Q)²

下面给出我们的简易Q网络的TensorFlow集成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import gym
import numpy as np
import random
import tensorflow as tf
import matplotlib.pyplot as plt
%matplotlib inline
# 加载实验环境
env = gym.make('FrozenLake-v0')
# Q网络解法
tf.reset_default_graph()
# 建立用于选择行为的网络的前向传播部分
inputs1 = tf.placeholder(shape=[1,16], dtype=tf.float32)
W = tf.Variable(tf.random_uniform([16,4], 0, 0.01))
Qout = tf.matmul(inputs1, W)
predict = tf.argmax(Qout, 1)
# 计算预期Q值和目标Q值的差值平方和(损失值)
nextQ = tf.placeholder(shape=[1,4], dtype=tf.float32)
loss = tf.reduce_sum(tf.square(nextQ - Qout))
trainer = tf.train.GradientDescentOptimizer(learning_rate=0.1)
updateModel = trainer.minimize(loss)
# 训练网络
init = tf.initialize_all_variables()
# 设置超参数
y = .99
e = 0.1
num_episodes = 2000 # 为了快速设置为2000,实验调为20000时可以达到0.6的成功率
# 创建episodes中包含当前奖励值和步骤的列表
jList = []
rList = []
with tf.Session() as sess:
sess.run(init)
for i in range(num_episodes):
# 初始化环境,得到第一个状态观测值
s = env.reset()
rAll = 0
d = False
j = 0
# Q网络
while j < 99:
j += 1
# 根据Q网络和贪心算法(有随机行动的可能)选定当前的动作
a, allQ = sess.run([predict, Qout], feed_dict={inputs1:np.identity(16)[s:s+1]})
if np.random.rand(1) < e:
a[0] = env.action_space.sample()
# 获取新的状态值和奖励值
s1, r, d, _ = env.step(a[0])
# 通过将新的状态值传入网络获取Q'值
Q1 = sess.run(Qout, feed_dict={inputs1:np.identity(16)[s1:s1+1]})
# 获取最大的Q值并选定我们的动作
maxQ1 = np.max(Q1)
targetQ = allQ
targetQ[0, a[0]] = r + y*maxQ1
# 用目标Q值和预测Q值训练网络
_, W1 = sess.run([updateModel, W], feed_dict={inputs1:np.identity(16)[s:s+1], nextQ:targetQ})
rAll += r
s = s1
if d == True:
# 随着训练的进行不断减小随机行动的可能性
e = 1./((i/50) + 10)
break
jList.append(j)
rList.append(rAll)
print("Percent of succesful episodes: " + str(sum(rList)/num_episodes))
# 网络性能统计
plt.plot(rList)
plt.plot(jList)

虽然这个网络可以解决FrozenLake的问题,但是效率远远不及Q表。在Q-Learning中神经网络解法的灵活性是以牺牲稳定性的代价换来的。在我们上面简单的网络的基础上,我们有很多可供选择的扩展来提供更好的性能和更健壮的学习。这里特别强调两个方法:历程重现(Experience Replay)和目标网络冻结(Freezing Target Networks),这些调整和提升是深度Q网络能够玩转雅达利游戏的关键,这些部分我们在后面会一一提及。如果想进一步了解Q-Learning背后的理论基础,可以参考Tambet Matiisen的这篇文章。希望这篇文章可以帮助到对Q-Learning算法感兴趣的同学:)

译者计划翻译的系列文章:

  1. (0)Q-Learning的查找表实现和神经网络实现
  2. Part 1 — Two-Armed Bandit
  3. Part 1.5 — Contextual Bandits
  4. Part 2 — Policy-Based Agents
  5. Part 3 — Model-Based RL
  6. Part 4 — Deep Q-Networks and Beyond
  7. Part 5 — Visualizing an Agent’s Thoughts and Actions
  8. Part 6 — Partial Observability and Deep Recurrent Q-Networks
  9. Part 7 — Action-Selection Strategies for Exploration
  10. Part 8 — Asynchronous Actor-Critic Agents (A3C)
您的打赏是对我最大的鼓励!