最近在学习 RL ,不得不先接触一下“ 马尔可夫决策过程 ”,这里找到了 David Silver 的课程: UCL Course on RL
(http://www0.cs.ucl.ac.uk/staff/d.silver/web/Teaching.html),这里我将按课程 PPT 中的顺序讲述我的理解已经如何用代码实现相应的计算过程。
目录
一、马尔可夫过程(Markov Process)
(一)MDPs论述
(二)马尔科夫特性
(三)状态转移矩阵
(四)马尔可夫过程
(五)样例
二、马尔可夫报酬过程(Markov Reward Process)
(一)马尔可夫报酬过程
(二)回报
(三)值函数
(四)样例
(五)MRPs的Bellman等式
(六)Bellman等式
(七)Bellman等式的矩阵形式
三、马尔可夫决策过程(Markov Decision Process)
(一)马尔可夫决策过程
(二)策略
(三)值函数
(四)Bellman等式的矩阵形式
四、最优值函数(Optimal Value Function)
马尔可夫决策过程形式化地描述了强化学习的环境 ,这里的环境是完全可见的,例如,当前的状态完全描述了这个过程。几乎所有的 RL 问题都可以形式化地表示为 MDPs 。
“ The future is independent of the past given the present ”,也就是说对当前而言,未来与过去是毫无关系的。马尔可夫状态定义如下图1 :
上诉定义的意思是:当前状态的前提下,下个状态发生的概率 = 当前状态及其之前所有状态的前提下,下一个状态发生的概率。可以理解为,当前状态已经包含了历史所有信息。
定义了一个从当前马尔可夫状态 到后续状态 的 state transition probability (状态转移概率):
同时定义了一个从所有当前状态 到后续状态 的状态转移矩阵 :
这个很好理解,比如 表示从状态 到状态 的概率,注意可以存在从状态 转移到状态 的可能性,而且矩阵 的每一行的和为 1 。
马尔可夫过程是一个无记忆的随机过程,例如一个满足马尔可夫特性的随机状态序列 ,定义如下图2 :
马尔可夫过程(又叫马尔可夫链)是一个元组 , 是一个有限状态集, 是上诉状态转移概率矩阵。简单地理解就是给一个状态,再给一个转移概率,就会发生一个“ 过程 ” 。
David Silver 给出了一个研究生的例子,如图3 :
可以看出从 开始存在无数个马尔可夫链。
同时,从图3 中我们也可以得到状态转移概率矩阵 :
这里需要注意的一点是,作者认为 David Silver 的课程 PPT 这里存在一点问题,即 应该为 0 ,如果用 PPT 中的 1 ,在后续计算过程中存在十分明显的错误,而且从图3 看到,状态 到状态 之间并不存在转移情况。
截止到这里的内容比较容易理解,就不过多赘述了,下面开始详述马尔可夫报酬过程。
马尔可夫报酬过程是具有价值的马尔可夫链,定义如下图4 :
这里增加了报酬函数 ,表示当前状态 情况下,下一个阶段的报酬 的期望。折扣因子 。研究生马尔可夫报酬过程如下图5 所示:
这里我先用语言描述一下马尔可夫决策过程的回报。首先我们设想一下,现在我们处于“ 大学生 ”状态,之后可能的状态有“ 读研 ”和“ 找工作 ”,选择不同的“ 状态 ”将会有不同的“ 未来 ”,未来的“ 收入 ”当然也就有所区别了。现在我们假设选择了“ 读研 ”,以经济学中“ 折现 ”的概念来理解未来的“ 收入 ”,我们现在的选择其实已经一定程度上转换成了“ 一定折扣的未来收入 ”。这就是所谓的回报,用公式表示如下图6 :
这里的 return 表示从 步开始全部的折扣报酬,其中折扣因子 表示未来报酬的当前价值。 越小表示“ 目标短浅 ”只专注于眼前, 越大表示“ 远见 ”决策者更关注于对未来的展望。
就上诉回报内容,值函数 给出了状态 的长期价值,定义如下图7 :
表示确定的状态序列下的未来报酬的折扣,但是我们得清楚,在实际决策过程中,状态转移是依据状态转移概率矩阵 来转移的,所以从当前状态 开始的 MRP 期望就得用上述定义来表示了。
这里读者可能会想,每一个状态的马尔可夫链不是无数个吗,给定一个马尔可夫链求 容易,但是求值函数 却是几乎不可能的,这里借助于 Bellman 等式能够很好的解决这个问题。
计算过程如下,读者就自行理解吧!
然后给出了值函数 的计算公式:
为了理解这个公式,直接给出一张图8 :
这里需要注意一点,作者认为 David Silver 的课程 PPT 中 的位置存在一点瑕疵,容易误导读者,正确的位置应该是箭头所指方向。
解释一下吧,图8 中,状态 对应 ,状态 对应 , 表示当前状态 在下一阶段的报酬。我们再看看值函数 的计算公式,用白话解释就是:某个状态 的值函数 = 当前状态 在下一阶段的报酬 + 与当前状态 相接的后续状态 的值函数 相应的状态转移概率 的求和(后续状态可能有一个或多个,这里其实就是一个求期望的过程)的折扣。
下面给个例子来简单描述一下这个计算过程,如图9 :
这张图中描述了值函数 是如何计算出来的。
读者可能又有疑问,要计算状态 的值函数 ,得知道后续状态 的值函数 ,这不是又陷入了一个闭环了嘛!
这里给出两种解决方法:
迭代法就是我们令最初的值函数全部为 0 ,通过多次迭代,使得迭代次数达到指定值或者值函数趋于稳定,这里就直接放代码啦!
"""这是MRP迭代过程"""
# 迭代
def next_v(_lambda, r_list, old_v_list, weight_list):
new_v_list = []
for j in range(len(old_v_list)):
if j != len(old_v_list) - 1:
j_sum = .0
# 与当前状态相接的后续状态的值函数相应的状态转移概率的求和的折扣
for k in range(len(weight_list[j])):
j_sum += weight_list[j][k][0] * old_v_list[weight_list[j][k][1]]
# 当前状态在下一阶段的报酬
new_v_list.append(r_list[j] + _lambda * j_sum)
# Sleep状态无后续状态,故直接赋值0
new_v_list.append(0.0)
return new_v_list
if __name__ == '__main__':
# γ
my_lambda = 1
# 报酬顺序:Class 1、Class 2、Class 3、Pass、Pub、Facebook、Sleep,分别对应0, 1, 2, 3, 4, 5, 6
# 后续顺序皆与此相同
my_r_list = [-2., -2., -2., 10., 1., -1., 0.]
# 初始化值函数
my_old_v_list = [0, 0, 0, 0, 0, 0, 0]
# 状态转移概率(这里没有用概率矩阵,方法有点笨,读者可以用矩阵来表示)
# 这里以[[0.5, 1], [0.5, 5]]为例解释一下,该列表记录Class 1的状态:
# [0.5, 1]表示以0.5的概率转移到Class 2
# [0.5, 5]表示以0.5的概率转移到Facebook
my_weight_list = [[[0.5, 1], [0.5, 5]],
[[0.8, 2], [0.2, 6]],
[[0.6, 3], [0.4, 4]],
[[1, 6]],
[[0.2, 0], [0.4, 1], [0.4, 2]],
[[0.1, 0], [0.9, 5]],
[[0, 0]]]
my_new_v_list = []
# 指定迭代次数
for i in range(100):
my_new_v_list = next_v(my_lambda, my_r_list, my_old_v_list, my_weight_list)
# 用新生成的值函数列表替换旧的值函数列表
my_old_v_list = my_new_v_list
print(my_new_v_list)
运行结果与图9 近似,通过增加迭代次数可提高结果准确性:
[-12.418287702397645, 1.4703089405374907, 4.3371337110593915, 10.0, 0.8410368812854547, -22.318009521720207, 0.0]
四舍五入:
[-12, 1.5, 4.3, 10, 0.8, -22, 0]
将 Bellman 等式用矩阵形式表示出来,并进行线性变换:
我们可以看到值函数 的计算不再需要后续状态的值函数 ,只需要知道折扣因子 、状态概率转移矩阵 和报酬 。
给出代码:
import numpy as np
# γ
_lambda = 1
# 状态转移概率矩阵
p = [[0, 0.5, 0, 0, 0, 0.5, 0],
[0, 0, 0.8, 0, 0, 0, 0.2],
[0, 0, 0, 0.6, 0.4, 0, 0],
[0, 0, 0, 0, 0, 0, 1],
[0.2, 0.4, 0.4, 0, 0, 0, 0],
[0.1, 0, 0, 0, 0, 0.9, 0],
[0, 0, 0, 0, 0, 0, 0]]
# 报酬矩阵
r = [[-2],
[-2],
[-2],
[10],
[1],
[-1],
[0]]
# 单位矩阵
i = [[1, 0, 0, 0, 0, 0, 0],
[0, 1, 0, 0, 0, 0, 0],
[0, 0, 1, 0, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 0, 1, 0, 0],
[0, 0, 0, 0, 0, 1, 0],
[0, 0, 0, 0, 0, 0, 1]]
p_mat = np.matrix(p)
r_mat = np.matrix(r)
i_mat = np.matrix(i)
# Bellman等式的矩阵形式
v_mat = (i_mat - _lambda * p_mat).I * r_mat
# v_mat = np.dot(np.linalg.inv(i_mat - p_mat), r_mat)
print(v_mat)
通过这种方法计算得到的结果较第一种方法准确,结果如下:
[[-12.54320988]
[ 1.45679012]
[ 4.32098765]
[ 10. ]
[ 0.80246914]
[-22.54320988]
[ 0. ]]四舍五入:
[[-13 ]
[ 1.5]
[ 4.3]
[ 10 ]
[ 0.8]
[-23 ]
[ 0 ]]
马尔可夫决策过程是伴有决策的的马尔可夫报酬过程,给出定义如下图10 :
简单来说,马尔可夫决策过程就是比马尔可夫报酬过程多了一个有限动作集 ,这里所说的决策其实可以理解为决策者在某个状态 下执行某个动作 的概率分布。
这里给出研究生马尔可夫决策过程,如下图11 :
需要注意的一点是,作者认为如上图11 David Silver 的课程 PPT 中红框标注的部分,应该标为 Pass 。因为原先状态图中,旁边的状态为 Pass ,而且同样都是 Study 动作,报酬不同有点不合理,“课程通过 ”可能才配得上报酬 10 吧,哈哈!
这里结构上与图3 存在明显的不同就是取消了状态 Pub ,变成了现在的动作 Pub ,然后该动作可以将 Class 3 状态转移到 Class 1、 Class 2、 Class 3,这里这样修改是为了下面更具普遍性的说明,可以说 David Silver 也是别有用心呀。
policy 是给定状态下动作的分布,定义如下图12 :
理解起来也很简单, 就是在当前状态 前提下,执行动作 的概率。
我们再来看看状态转移概率 和 现在怎么计算:
我用白话解释一下:执行动作 Study,以 Class 1 状态到Class 2 状态为例计算 ,根据图11 ,我们可以发现 ,解释一下就是,从状态 Class 1 到状态 Class 2 只有执行动作 Study 才行,而且执行动作 Study 后,可以百分之百从状态 Class 1 转移到状态 Class 2 ,所以 。同时我们可以看到状态 Class 1 情况下,可执行的动作有两个 Study 和 Facebook ,所以这里的 。从状态 Class 3 执行动作 Pub 的情况比较特殊,读者需要注意,这里的 概率 ,剩下我的就靠读者自己思考啦!
现在就出现了两种值函数:状态值函数和动作值函数,定义如下图13 ,就不过多解释啦:
展示一个使用 Bellman 等式计算的样例,图14 :
这里直接给出 Bellman 等式的矩阵形式:
同样的需要状态转移概率矩阵 和 报酬 。
根据图11 ,可得到如下状态转移概率矩阵:
每个状态转移的平均报酬可如下计算:
可以计算出报酬 :
然后就可以计算值函数啦!这里分别给出使用 Bellman 等式和 Bellman 等式的矩阵表示的代码,建议读者使用 Bellman 等式的矩阵表示代码。
"""这是MDP迭代过程"""
def next_v(pi, r_list, old_v_list, weight_list):
new_v_list = []
for j in range(len(old_v_list)):
if j != len(old_v_list) - 1:
j_sum = .0
for k in range(len(weight_list[j])):
if type(weight_list[j][k][0]) is not list:
j_sum += pi * (r_list[weight_list[j][k][1]] + old_v_list[weight_list[j][k][0]])
else:
m_sum = .0
for m in range(len(weight_list[j][k][0])):
m_sum += old_v_list[weight_list[j][k][0][m][0]] * weight_list[j][k][0][m][1]
j_sum += pi * (r_list[weight_list[j][k][1]] + m_sum)
new_v_list.append(j_sum)
new_v_list.append(0.0)
return new_v_list
if __name__ == '__main__':
my_pi = 0.5
# 报酬顺序:study pass pub facebook quit sleep
my_r_list = [-2., 10., 1., -1., 0., 0.]
my_old_v_list = [0, 0, 0, 0, 0]
my_weight_list = [[[1, 0], [3, 3]],
[[2, 0], [4, 5]],
[[[[0, 0.2], [1, 0.4], [2, 0.4]], 2], [4, 1]],
[[0, 4], [3, 3]],
[]]
my_new_v_list = []
# my_new_v_list = next_v(my_pi, my_r_list, my_old_v_list, my_weight_list)
for i in range(100):
my_new_v_list = next_v(my_pi, my_r_list, my_old_v_list, my_weight_list)
my_old_v_list = my_new_v_list
print(my_new_v_list)
结果:
[-1.3076923099959044, 2.6923076920317515, 7.384615384159404, -2.3076923112229597, 0.0]
四舍五入:
[-1.3, 2.7, 7.4, -2.3, 0.0]
import numpy as np
# π、γ
_pi = 0.5
_lambda = 1
p = [[0, _pi, 0, _pi, 0],
[0, 0, _pi, 0, _pi],
[_pi*0.2, _pi*0.4, _pi*0.4, 0, _pi],
[_pi, 0, 0, _pi, 0],
[0, 0, 0, 0, 0]]
r = [[_pi*-2 + _pi*-1],
[_pi*-2 + _pi*0],
[_pi*1 + _pi*10],
[_pi*0 + _pi*-1],
[0]]
i = [[1, 0, 0, 0, 0],
[0, 1, 0, 0, 0],
[0, 0, 1, 0, 0],
[0, 0, 0, 1, 0],
[0, 0, 0, 0, 1]]
p_mat = np.matrix(p)
r_mat = np.matrix(r)
i_mat = np.matrix(i)
v_mat = (i_mat - _lambda * p_mat).I * r_mat
# v_mat = np.dot(np.linalg.inv(i_mat - p_mat), r_mat)
print(v_mat)
结果:
[[-1.30769231]
[ 2.69230769]
[ 7.38461538]
[-2.30769231]
[ 0. ]]四舍五入:
[[-1.3]
[ 2.7]
[ 7.4]
[-2.3]
[ 0. ]]
注意,这里求解的 MDP 状态值函数,每个状态的回报是根据转移概率计算的平均回报,这称为贝尔曼期望方程(Bellman Expectation Equation)。而在强化学习中,通常是选取使得价值最大的动作执行,这种解法称为最优值函数(Optimal Value Function),得到的解不再是期望值函数,而是理论最大值函数。此时相当于策略 π 已经改变,且非线性,线性方程组不再适用,需要通过数值计算的方式求出。通常的做法是设置状态值函数 和状态-动作值函数 迭代求解。最终得到:
这里给出计算 的计算代码:
"""这是OVF迭代过程"""
def next_v(pi, r_list, old_v_list, weight_list):
new_v_list = []
for j in range(len(old_v_list)):
if j != len(old_v_list) - 1:
max_list = []
for k in range(len(weight_list[j])):
if type(weight_list[j][k][0]) is not list:
max_list.append(pi * (r_list[weight_list[j][k][1]] + old_v_list[weight_list[j][k][0]]))
else:
m_sum = .0
for m in range(len(weight_list[j][k][0])):
m_sum += old_v_list[weight_list[j][k][0][m][0]] * weight_list[j][k][0][m][1]
max_list.append(pi * (r_list[weight_list[j][k][1]] + m_sum))
new_v_list.append(max(max_list))
new_v_list.append(0.0)
return new_v_list
if __name__ == '__main__':
my_pi = 1
# study pass pub facebook quit sleep
my_r_list = [-2., 10., 1., -1., 0., 0.]
my_old_v_list = [0, 0, 0, 0, 0]
my_weight_list = [[[1, 0], [3, 3]],
[[2, 0], [4, 5]],
[[[[0, 0.2], [1, 0.4], [2, 0.4]], 2], [4, 1]],
[[0, 4], [3, 3]],
[]]
my_new_v_list = []
# my_new_v_list = next_v(my_pi, my_r_list, my_old_v_list, my_weight_list)
for i in range(100):
my_new_v_list = next_v(my_pi, my_r_list, my_old_v_list, my_weight_list)
my_old_v_list = my_new_v_list
print(my_new_v_list)
结果:
[6.0, 8.0, 10.0, 6.0, 0.0]
结果如下图15 所示:
写到这里有点累了,具体公式读者就看 PPT 吧,这里讲一个简单的计算最优动作值函数 的方法,我们先看看下一张图16 吧:
最优值函数我们已经用代码算出来了,计算最优动作值函数 就变得简单多了:当前动作值函数 = 动作指向的值函数 + 当前动作的报酬(或者是期望)。
更多内容大家自己阅读 PPT 吧,内容以上传到我的博客。