【引言】上一篇文章介绍了价值函数和贝尔曼方程这两个工具对强化学习进行更加具体的过程性描述,并介绍最优价值函数和最优策略等概念。那么最优策略存在吗?是唯一的吗?如何找到最优策略?在回答这些问题之前,先考虑一下问题想细致一点。
- 首先,状态是智能体决策的依据,状态是连续的还是离散的?可不可以一一枚举出来?
- 其次,智能体的动作是连续的还是离散的?可不可以很方便地列出来?
假设状态和动作都是离散的并且能够方便地列出来,这样问题就变得好处理一些了,因为可以使用一张表格,将不同状态下的不同动作所对应的未来收益记录下来加以分析。因此,强化学习领域最早被人们研究的就是表格型(tabular)强化强化学习相关算法。
本文介绍表格型强化学习中的蒙特卡洛方法,并通过二十一点扑克牌游戏编程实现该方法。
中英文术语对照表
中文 | 英文 | 缩写或符号 |
---|---|---|
蒙特卡洛 | Monte Carlo | MC |
策略评估 | policy evaluation | - |
策略改进 | policy improvement | - |
1. 蒙特卡洛思想
“蒙特卡洛”泛指使用大量随机试验的方法来估计未知量的一类方法。
图1 使用蒙特卡洛方法估计圆周率
假设我们在一个正方形(边长 2 r 2r 2r)中嵌入一个最大的圆(半径 r r r),然后往正方体内均匀撒点,那么圆和正方形的面积比为 π r 2 / ( 2 r ) 2 = π / 4 {\pi r^2}/{(2r)^2}=\pi/4 πr2/(2r)2=π/4。而这一比值我们用圆内的点数量( n c n_c nc)除以总的点数量( n n n)进行估计,则 π \pi π的估计值 π ^ = n c n × 4 \hat\pi=\dfrac{n_c}{n}\times 4 π^=nnc×4。图中从左至右分别使用点的数量为1e2,1e3,1e4,1e5,估计得到的 π \pi π值分别为3.36,3.12,3.1352,3.14036。可以发现,随着试验点数增多,估计值与 π \pi π的真值越来越接近。
2. 使用蒙特卡洛方法解决强化学习问题
蒙特卡洛方法通过样本的平均回报来估计状价值函数,而不需要知道环境是如何转移的,只要要经验数据即可学习。
2.1 理论分析
2.1.1 动作价值函数评估
回顾一下动作价值函数的定义
q
π
(
s
,
a
)
=
E
[
G
t
∣
S
t
=
s
,
A
t
=
a
]
=
E
[
∑
k
=
0
T
−
1
γ
k
R
t
+
k
+
1
∣
S
t
=
s
,
A
t
=
a
]
q_\pi(s,a)=\mathbb E\left[G_t|S_t=s,A_t=a\right]=\mathbb E\left[\sum_{k=0}^{T-1}\gamma^k R_{t+k+1}\bigg|S_t=s,A_t=a\right]
qπ(s,a)=E[Gt∣St=s,At=a]=E[k=0∑T−1γkRt+k+1∣
∣St=s,At=a]
这个定义中包含求期望操作"
E
\mathbb E
E",接下来我们使用蒙特卡洛的方式代替它。假设我们使用策略
π
\pi
π获得了
N
N
N条起始状态-动作为
(
s
,
a
)
(s,a)
(s,a)的轨迹,用这
N
N
N条轨迹的累计奖励和的均值作为动作价值函数
q
π
(
s
,
a
)
q_\pi(s,a)
qπ(s,a)的一个估计,用
Q
(
s
,
a
)
Q(s,a)
Q(s,a)表示:
Q
(
s
,
a
)
=
1
N
∑
i
=
1
N
[
G
t
i
∣
S
t
=
s
,
A
t
=
a
]
Q(s,a)=\frac{1}{N}\sum_{i=1}^{N}\left[G^i_t\bigg|S_t=s,A_t=a\right]
Q(s,a)=N1i=1∑N[Gti∣
∣St=s,At=a]
其中
G
i
G^i
Gi表示第
i
i
i条轨迹的回报。根据大数定律,只要轨迹数量足够多,且完全覆盖所有可能的轨迹,那么
Q
(
s
,
a
)
Q(s,a)
Q(s,a)对
q
π
(
s
,
a
)
q_\pi(s,a)
qπ(s,a)估计是无偏的。
很显然,一个episode中状态-动作对 ( s , a ) (s,a) (s,a)可能出现多次,如果只考虑第一次出现的回报称为首次访问型(First-Visit)MC方法,如果每次出现都考虑的话则称为每次访问型(Every-Visit)MC方法。
使用首次访问型MC方法对 Q Q Q函数估计的算法如下。(有些人称这个过程为预测,predicattion,我觉得不是特别直白,这里不纠结术语了,别人写了能看懂就行)
算法1 MC预测算法:使用首次访问型MC方法估计动作价值函数
2.1.2 ε \varepsilon ε-greedy策略
通过蒙特卡洛方法得到
Q
Q
Q值表之后,直接使用贪婪策略。令
a
∗
=
arg
max
a
Q
(
s
,
a
)
a_*=\mathop {\arg \max }\limits_a Q(s,a)
a∗=aargmaxQ(s,a)
π
(
a
∣
s
)
=
{
1
−
ε
+
ε
∣
A
(
s
)
∣
if
a
=
a
∗
ε
∣
A
(
s
)
∣
otherwise
\pi(a|s)= \begin{cases} 1-\varepsilon+\dfrac{\varepsilon}{|\mathcal{A}(s)|}\quad\text{if } a=a_*\\ \dfrac{\varepsilon}{|\mathcal{A}(s)|}\quad\text{otherwise } \end{cases}
π(a∣s)=⎩
⎨
⎧1−ε+∣A(s)∣εif a=a∗∣A(s)∣εotherwise
ε
\varepsilon
ε-greedy策略在选择动作的之前先头骰子,即产生一个随机数。如果这个随机数比设定的探索率
ε
\varepsilon
ε(这里假设
ε
\varepsilon
ε=0.1)更小,则从所有可采取的动作中随机挑一个;否则,按照贪婪策略选择动作
a
∗
a_*
a∗。那选择
a
∗
a_*
a∗的概率有多大呢?贪婪的概率是
1
−
ε
1-\varepsilon
1−ε,并且在随机的时候还有
ε
/
∣
A
(
s
)
∣
\varepsilon/|\mathcal{A}(s)|
ε/∣A(s)∣的概率被选中,所以加起来就是
1
−
ε
+
ε
/
∣
A
(
s
)
1-\varepsilon+\varepsilon/|\mathcal{A}(s)
1−ε+ε/∣A(s)。
2.1.3 策略评估与策略改进
在2.1.1里面,我们知道了如何使用MC对给定的策略 π \pi π评估对应 Q Q Q值表。同时,在2.1.2里面,我们介绍了一种 ε \varepsilon ε-greedy策略。
我们试想一下,最开始我们有一张动作价值函数表 Q 0 Q_0 Q0, Q 0 Q_0 Q0里面的值全都为0。这时候我们使用策略 π 0 \pi_0 π0与环境交互,这个 π 0 \pi_0 π0,比如就像上面给出的算法1,取探索率 ε = 1 \varepsilon=1 ε=1,这时候 π 0 \pi_0 π0就是无论状态-动作对 ( s t , a t ) (s_t,a_t) (st,at)取何值,都随机选择动作与环境交互。经过大量的交互后,我们更新 Q 0 Q_0 Q0变成了 Q 1 Q_1 Q1。此后由于 Q Q Q值表变了,面对同一个状态 s s s,我们可能会采取不同的动作 a a a,比如采取 a = arg max a ′ ∈ A ( s ) Q ( s , a ′ ) a=\mathop {\arg \max }\limits_{a^\prime\in\mathcal{A}(s)} Q(s,a^\prime) a=a′∈A(s)argmaxQ(s,a′)。将新的策略记为 π 1 \pi_1 π1,我们用它又收集到了许多轨迹,然后再次更新 Q Q Q值表为 Q 2 Q_2 Q2, ⋯ \cdots ⋯以此循环往复,直到我们发现, Q Q Q值表两次更新之间的差异非常小,或者更新前后就是一模一样的,那么算法就收敛了。
Q Q Q值表的更新过程我们可以使用下面这张图来形象表示(注意这里面的每一个 Q Q Q都是一张表)。这样更新能收敛吗?根据泛函分析中的压缩映射理论表明,在一定条件下,上面的迭代过程一定会收敛。这一理论问分析过程先略过,初学者一头扎进去容易晕掉。
评估给定策略下的 Q Q Q值表称为策略评估(Policy evaluation),而根据 Q Q Q值表优化策略 π \pi π的过程称为策略改进(Policy improvement)。策略评估和策略改进之间的关系可以用下面的图形象表示:
2.2 探索黑杰克(二十一点)扑克牌游戏
下面通过玩二十一点游戏来介绍使用蒙特卡洛方法解决强化学习问题。
图2 二十一点游戏
2.2.1 游戏规则
游戏规则:
- 二十一点是一种流行于赌场的游戏,其目标是使得你的扑克牌点数之和在不超过21的情况下越大越好。
- 所有的人头牌(J、Q、K)的点数为 10, A 既可当作1也可当作11(怎么有利于赢牌就怎么算)。
- 假设每一个玩家都独立地与庄家进行比赛。游戏开始时会给各玩家与庄家发两张牌。庄家的牌一张正面朝上一张背面朝上。玩家直接获得21点(一张A与一 张10) 的情况称之为天和。此时玩家直接获胜,除非庄家也是天和,那就是平局。如果玩家不是天和,那么他可以一张一张地继续要牌,直到他主动停止(停牌)或者牌的点数和超过21点(爆牌)。如果玩家爆牌了就算输掉比赛。如果玩家选择停牌,就轮到庄家行动 。
- 庄家根据一个固定的策略进行游戏:他一直要牌,直到点数等于或超过17时停牌。如果庄家爆牌,那么玩家获胜,否则根据谁的点数更靠近21决定胜负或平局。
二十一点是一个典型的分幕式有限马尔可夫决策过程。可以将每一局看作一幕。胜、负、平局分别获得收益+1 、-1和0。 每局游戏进行中的收益都为0, 并且不打折扣(
γ
=
1
\gamma=1
γ=1); 所以最终的收益即为整个游戏的回报。玩家的动作为要牌(HIT)或停牌(STICK)。状态则取决于玩家的牌与庄家显示的牌。我们假设所有的牌来自无穷多的一组牌(即每次取出的牌都会再放回牌堆),因此就没有必要记下已经抽了哪些牌。如果玩家手上有一张 A,可以视作11且不爆牌,那么称这张A为可用的。此时这张牌总会被视作11, 因为如果视作1的话,点数总和必定小于等于11, 而这时玩家就无须进行选择,可以直接要牌,因为抽牌几乎一定会更优。所以,玩家做出的选择只会依赖于三个变量:他手牌的总和 (12~21), 庄家显示的牌 (A~10) ,以及他是否有可用的 A 。 这样共计就有 200 个状态。
2.2.2 与OpenAI中的BlacJack环境交互
先导入几个必要的Python包
import sys
import gym
import numpy as np
from collections import defaultdict # 一种字典,可以当索引不存在的时候可以用默认的函数创建元素
from plot_utils import plot_blackjack_values, plot_policy # 引入两个提前编写好的可视化函数
接下来,将创建一个 Blackjack扑克游戏环境。并查看环境的观测空间和动作空间。
env = gym.make('Blackjack-v1') # 创建一个二十一点游戏(环境)
print("观测空间:",env.observation_space)
print("动作空间:", env.action_space)
print("动作空间大小:", env.action_space.n)
观测空间: Tuple(Discrete(32), Discrete(11), Discrete(2))
动作空间: Discrete(2)
动作空间大小: 2
可以看到,每个状态是一个三元组:
- 玩家手牌的总点数 ∈ { 0 , 1 , … , 31 } \in \{0, 1, \ldots, 31\} ∈{0,1,…,31},
- 庄家正面朝上牌的点数 ∈ { 1 , … , 10 } \in \{1, \ldots, 10\} ∈{1,…,10}, 以及
- 玩家是否有可以当作11点的A (
no
= 0 =0 =0,yes
= 1 =1 =1).
智能体可以做的动作有两个:
- STICK = 0
- HIT = 1
下面这段代码讲的是如何使用随机策略与环境交互。这段代码目前设定的是玩3局,当然也可以设置玩更多局来感受一下与环境交互的过程。
for i_episode in range(3):
print('-'*30)
state = env.reset() # 每局开始的时候先重置游戏
while True:
print(state)
action = env.action_space.sample() # 从动作空间中随机采样一个动作
state, reward, done, info = env.step(action) # 让环境去执行吧
if done: # 当游戏结束的时候,将结果打印出来
print('游戏结束! 奖励: ', reward)
print('你赢了。') if reward > 0 else print('你输了。')
break
------------------------------
(17, 10, False)
游戏结束! 奖励: -1.0
你输了。
------------------------------
(12, 9, True)
(20, 9, True)
(21, 9, True)
游戏结束! 奖励: 1.0
你赢了。
------------------------------
(8, 9, False)
(15, 9, False)
游戏结束! 奖励: -1.0
你输了。
3 MC估计(MC Prediction)
接下来介绍如何使用MC估计动作价值函数。
先让智能体执行这样一个策略:当手牌点数大于18,以80%的概率"停牌(STICK)",否则以80%的概率“要牌(HIT)”。函数 generate_episod
使用这个策略与环境交互并生成轨迹.
函数输入 input:
bj_env
: OpenAI Gym中的 Blackjack游戏的实例.
函数返回值 output:
episode
: 包含一个或者多个 (state, action, reward)这样的元组的列表,其对应的轨迹为 ( S 0 , A 0 , R 1 , ⋯ , S T − 1 , A T − 1 , R T ) (S_0, A_0, R_1, \cdots, S_{T-1}, A_{T-1}, R_{T}) (S0,A0,R1,⋯,ST−1,AT−1,RT), 其中 T T T 为终止时间. 因此,对轨迹进行索引操作episode[i]
将返回 ( S i , A i , R i + 1 ) (S_i, A_i, R_{i+1}) (Si,Ai,Ri+1)这样的元组, 进一步使用episode[i][0]
,episode[i][1]
, andepisode[i][2]
进行索引将得到 S i S_i Si, A i A_i Ai,以及 R i + 1 R_{i+1} Ri+1.
3.1 使用随机策略与环境交互
先定义一个随机策略,就是不管状态是啥,一律随机选择一个动作与环境交互:
def generate_episode(bj_env):
episode = []
state = bj_env.reset()
while True:
probs = [0.8, 0.2] if state[0] > 18 else [0.2, 0.8]
action = np.random.choice(np.arange(2), p=probs)
next_state, reward, done, info = bj_env.step(action)
episode.append((state, action, reward))
state = next_state
if done:
break
return episode
这样的随机策略,与环境交互会产生什样的轨迹呢?执行下面的代码可以重复收集多条交互轨迹并打印出来。
for i in range(3):
print(generate_episode(env))
[((14, 4, False), 0, -1.0)]
[((21, 10, True), 0, 1.0)]
[((13, 10, False), 1, 0.0), ((16, 10, False), 0, -1.0)]
现在可以构建基于MC的估计价值函数的程序了。这里考虑每次访问型MC,即在一条轨迹中某个状态每次出现时都更新未来回报。
算法输入包含三个参数:
env
: 一个OpenAI Gym的 Blackjack扑克游戏环境实例.num_episodes
: 需要收集的轨迹数量.generate_episode
: 用于收集轨迹的函数.gamma
: 折扣因子 γ ∈ [ 0 , 1 ] \gamma\in[0,1] γ∈[0,1],这里默认值 γ = 1 \gamma=1 γ=1.
算法返回值:
Q
: 字典类型,Q[s][a]
表示动作价值函数 q π ( s , a ) q_\pi(s,a) qπ(s,a)的估计值。
3.2 估计随机策略下的价值函数
下面的代码,是完全按照2.1.1中的算法1实现的,用于估计给定策略下的Q值表,封装成一个函数便于调用。
def mc_prediction_q(env, num_episodes, generate_episode, gamma=1.0):
# 初始化空字典
returns_sum = defaultdict(lambda: np.zeros(env.action_space.n)) # returns_sum中的每个元都初始化为[0,0],(初次访问的时候初始化)
N = defaultdict(lambda: np.zeros(env.action_space.n)) # 用于记录状态-动作对出现的次数
Q = defaultdict(lambda: np.zeros(env.action_space.n)) # Q值表
# 对多条轨迹循环执行
for i_episode in range(1, num_episodes+1):
# monitor progress
if i_episode % 1000 == 0:
print("\r第 {}/{}局游戏.".format(i_episode, num_episodes), end="")
sys.stdout.flush()
# 收集一条轨迹
episode = generate_episode(env)
# 提取状态、动作、奖励信息
states, actions, rewards = zip(*episode)
# 计算折扣因子随时间的变化,用于计算折扣后的奖励
discounts = np.array([gamma**i for i in range(len(rewards))])
# 更新回报, 访问次数, 以及状态-动作对
# 估计一幕(一条轨迹)中每个状态-动作对的价值函数
for i, state in enumerate(states):
returns_sum[state][actions[i]] += sum(rewards[i:]*discounts[:len(states)-i]) # 回报
N[state][actions[i]] += 1.0 # 访问次数
Q[state][actions[i]] = returns_sum[state][actions[i]] / N[state][actions[i]] # 更新Q值表,这里的除法运算是逐元素的
return Q
3.3 结果可视化
下面一段代码用于调用 Q Q Q值表的估计函数,并将估计结果可视化显示出来。
# 获取动作价值函数
Q = mc_prediction_q(env, 50000, generate_episode)
# 获取对应的状态-动作价值函数
V_to_plot = dict((k,(k[0]>18)*(np.dot([0.8, 0.2],v)) + (k[0]<=18)*(np.dot([0.2, 0.8],v))) for k, v in Q.items())
# 可视化状态价值函数
plot_blackjack_values(V_to_plot)
第 50000/50000局游戏.
图3 蒙特卡洛估计的动作价值函数函数可视化结果(使用随机策略)
来分析一下这两张图。上面一张图是有可用的A(看作11点),下一张图无可用的A。两张图基本上反应了相同的趋势,那就是玩家的手牌点数(Player’s Current Sum)越大,状态价值函数越高,越有可能赢得游戏。这与二十一点游戏的特点是相符的。
3.4 MC估计的增量实现
在算法1中,对于状态-动作对 ( s , a ) (s,a) (s,a),我们把它在所有轨迹中的回报先加起来,然后除以出现的次数。这样一来,更新就必须等到所有的轨迹都收集好了才行。能不能收集一条轨迹就及时更新呢?可以的。
假设我们收集了
N
−
1
N-1
N−1条包含
(
s
,
a
)
(s,a)
(s,a)的轨迹,那么有
Q
(
s
,
a
)
=
1
N
−
1
∑
i
=
1
N
−
1
G
i
Q(s,a)=\dfrac{1}{N-1}\sum_{i=1}^{N-1}G^i
Q(s,a)=N−11∑i=1N−1Gi。当收集到第
N
N
N条包含
(
s
,
a
)
(s,a)
(s,a)的轨迹,并且该轨迹中
(
s
,
a
)
(s,a)
(s,a)的回报为
G
N
G^N
GN。于是我们重新计算
Q
(
s
,
a
)
Q(s,a)
Q(s,a):
Q
(
s
,
a
)
=
1
N
∑
i
=
1
N
G
i
=
1
N
[
∑
i
=
1
N
−
1
G
i
+
G
N
]
=
1
N
[
(
N
−
1
)
Q
(
s
,
a
)
+
G
N
]
=
Q
(
s
,
a
)
−
1
N
Q
(
s
,
a
)
+
1
N
G
N
=
Q
(
s
,
a
)
+
1
N
[
G
N
−
Q
(
s
,
a
)
]
\begin{aligned}Q(s,a)&=\dfrac{1}{N}\sum_{i=1}^{N}G^i\\ &=\dfrac{1}{N}\left[\sum_{i=1}^{N-1}G^i+G^N\right]\\ &=\dfrac{1}{N}\left[(N-1)Q(s,a)+G^N\right]\\ &=Q(s,a)-\dfrac{1}{N}Q(s,a)+\dfrac{1}{N}G^N\\ &=Q(s,a)+\dfrac{1}{N}\left[G^N-Q(s,a)\right]\end{aligned}
Q(s,a)=N1i=1∑NGi=N1[i=1∑N−1Gi+GN]=N1[(N−1)Q(s,a)+GN]=Q(s,a)−N1Q(s,a)+N1GN=Q(s,a)+N1[GN−Q(s,a)]
重新整理一下,将
G
N
G^N
GN记为
G
G
G,则使用一条新的轨迹更新动作价值函数
Q
Q
Q的公式为
Q
(
s
,
a
)
←
Q
(
s
,
a
)
+
1
N
[
G
−
Q
(
s
,
a
)
]
Q(s,a)\leftarrow Q(s,a)+\dfrac{1}{N}\left[G-Q(s,a)\right]
Q(s,a)←Q(s,a)+N1[G−Q(s,a)]
观察上面这个式子,
G
−
Q
(
s
,
a
)
G-Q(s,a)
G−Q(s,a)为新的回报与原来的动作价值
Q
(
s
,
a
)
Q(s,a)
Q(s,a)之间的增量。如果将该增量用
δ
t
\delta_t
δt表示,即
δ
t
=
G
−
Q
(
s
,
a
)
\delta_t=G-Q(s,a)
δt=G−Q(s,a)则它反映如下规律:
- 当 δ > 0 \delta>0 δ>0,新轨迹代表了更大的回报,因该增加 Q ( s , a ) Q(s,a) Q(s,a);
- 当 δ < 0 \delta<0 δ<0,新轨迹代表了更小的回报,因该减小 Q ( s , a ) Q(s,a) Q(s,a)。
而
1
N
\dfrac{1}{N}
N1可以视为更新步长,如果用一个更加一般的字母
α
\alpha
α表示,MC增量更新方式为
Q
(
s
,
a
)
←
Q
(
s
,
a
)
+
α
[
G
−
Q
(
s
,
a
)
]
Q(s,a)\leftarrow Q(s,a)+\alpha\left[G-Q(s,a)\right]
Q(s,a)←Q(s,a)+α[G−Q(s,a)]这里的
α
\alpha
α满足
α
∈
(
0
,
1
)
\alpha\in(0,1)
α∈(0,1)。这样就可以来一条轨迹就及时更新
Q
Q
Q值表了,称为MC的增量实现。
增量实现版本的MC预测算法伪代码如下:
算法2 MC预测算法(增量实现)
是不是感觉更简洁了?那下面赶紧动手实现它吧!
4 MC控制(MC Control)
4.1 ε \varepsilon ε-greedy策略下的价值函数
算法的输入:
env
: 一个OpenAI Gym的 Blackjack扑克游戏环境实例。num_episodes
: 需要收集的轨迹数量。alpha
: 更新步长 α \alpha α。gamma
: 折扣因子 γ ∈ [ 0 , 1 ] \gamma\in[0,1] γ∈[0,1],这里默认值 γ = 1 \gamma=1 γ=1。
输出:
Q
: 字典类型,Q[s][a]
表示动作价值函数 q π ( s , a ) q_\pi(s,a) qπ(s,a)的估计值。policy
: 字典类型,policy[s]
返回状态s
对应的动作.
4.2 使用 ε \varepsilon ε-greedy策略与环境交互
先定义一个 ε − g r e e d y \varepsilon-greedy ε−greedy策略:
def get_prob(Q_s, epsilon, nA):
""" 计算使用贪婪动作的概率 """
policy_s = np.ones(nA) * epsilon / nA
best_a = np.argmax(Q_s)
policy_s[best_a] = 1 - epsilon + (epsilon / nA)
return policy_s
然后写一个使用 ε − g r e e d y \varepsilon-greedy ε−greedy策略与环境进行交互,收集一条轨迹的函数,调用这个函数一次产生一条轨迹:
def generate_episode_from_Q(env, Q, epsilon, nA):
""" 使用epsilon-greedy策略生成一条轨迹"""
episode = []
state = env.reset()
while True:
action = np.random.choice(np.arange(nA), p=get_prob(Q[state], epsilon, nA)) \
if state in Q else env.action_space.sample()
next_state, reward, done, info = env.step(action)
episode.append((state, action, reward))
state = next_state
if done:
break
return episode
然后实现 Q Q Q值表的更新过程:
def update_Q(env, episode, Q, alpha, gamma):
""" 使用最新的轨迹更新Q值表 """
states, actions, rewards = zip(*episode)
# 计算折扣因子随时间的变化,用于计算折扣后的奖励
discounts = np.array([gamma**i for i in range(len(rewards))])
for i, state in enumerate(states):
old_Q = Q[state][actions[i]]
G = sum(rewards[i:]*discounts[:len(rewards)-i])
Q[state][actions[i]] = old_Q + alpha*(G- old_Q)
return Q
接下来是与环境的交互过程,包括轨迹数据的收集、探索率的逐渐减小等。
def mc_control(env, num_episodes, alpha, gamma=1.0, eps_start=1.0, eps_decay=.99999, eps_min=0.05):
nA = env.action_space.n
# 初始化空的Q值表
Q = defaultdict(lambda: np.zeros(nA))
epsilon = eps_start
# 玩多局游戏,获得更好的策略
for i_episode in range(1, num_episodes+1):
# 显示和刷新进度条
if i_episode % 1000 == 0:
print("\rEpisode {}/{}.".format(i_episode, num_episodes), end="")
sys.stdout.flush()
# 设置探索概率
epsilon = max(epsilon*eps_decay, eps_min) #探索率逐渐减小,但不会一直减小
# 使用epsilon-greedy生成一条轨迹
episode = generate_episode_from_Q(env, Q, epsilon, nA)
# 使用轨迹更新Q值表
Q = update_Q(env, episode, Q, alpha, gamma)
# 整理策略(无探索)
policy = dict((k,np.argmax(v)) for k, v in Q.items())
return policy, Q
4.3 结果可视化
下面的代码用于获得不同对局次数对应的,以及不同更新步长下,动作价值函数 Q Q Q和策略的最终更新结果。
policy, Q = mc_control(env, 50000, 0.02)
Episode 50000/50000.
接下来可视化动作价值函数 Q Q Q
# 获取状态价值函数
V = dict((k,np.max(v)) for k, v in Q.items())
# 可视化价值函数
plot_blackjack_values(V)
图4 蒙特卡洛估计的动作价值函数函数可视化结果(使用贪婪策略)
下面的代码用热力图显示估计出来的最优策略
# 可视化策略函数
plot_policy(policy)
图5 蒙特卡洛估计得到的最优策略可视化结果
对于前面描述的这一版本的二十一点游戏,真实的最优策略 π ∗ \pi_* π∗为教材的图5.2,这里已经将它显示在下面了。如果你发现你的估计结果和最优结果不太对那么尝试改变改变参数 ϵ \epsilon ϵ和 α \alpha α,或者进行更多局的游戏,看看能否获得更好的结果。
图6 真实的最优策略
5 小结
本文介绍如何使用蒙特卡洛对价值函数进行估计的方法,并使用贪婪策略实现了玩二十一点游戏。应重点掌握:
- 蒙特卡洛预测的基本原理
- 蒙特卡洛预测与控制相互循环的过程实现
至于价值函数和策略的可视化,对于初学者来说,可以不必太过于关注图示怎么画出来的,能够通过图来理解和解释价值与策略的含义即可。