Xiaowu/20220413 (#723)

* update folder name

* add

* Create Code-1.png

* add

* Update 矩阵法.md

* Update 安全驾驶问题.md

* Update 安全驾驶问题.md

* update

* add

* update

* fix

* update

* up

* update

* update

* update

* update

* update
This commit is contained in:
xiaowuhu
2022-04-22 09:33:42 +08:00
committed by GitHub
parent 942d8bf573
commit dc2a91170a
52 changed files with 1771 additions and 623 deletions
@@ -290,5 +290,5 @@ if __name__ == "__main__":
1. 强化学习中,基于模型的理论部分,也是以概率论为基础的,可以先热复习一下热身。
2. 图 2 中,其实是由“策略-动作-状态”组成的,这与强化学习的理论基本一致。
3. 使用代码模拟,也是一种“聪明的笨办法”,利用计算机快速模拟实际环境的交互,这也是强化学习的重要方法。
3. 使用代码模拟,也是一种“聪明的笨办法”,利用计算机快速模拟实际环境的交互,这也是强化学习的重要方法,在理论上被称为蒙特卡洛方法
4. 对于没有受过训练的可以直接阅读并理解公式推导的读者来说,用代码理解公式及理论知识,是一种重要手段。
@@ -340,7 +340,7 @@ $$
#### 可以反向走,但是有失败的限制
为了理解上一小节的理论推导,可以把图 3 中的 n 设置为 250,即,反向到达 200 步的地方,就认为是回家失败(醉汉被野兽吃掉了)。
为了理解上一小节的理论推导,可以把图 3 中的 n 设置为 250(包含正向回家的 50 步),即,反向到达 200 步的地方,就认为是回家失败(醉汉被野兽吃掉了)。
```
步数 : 9653 位置 : 50 平均步数 : -2.3523257018543458
@@ -357,6 +357,11 @@ $$
运行 10 次试验后,可以看到其中有 3 次都失败了,即位置达到了 -200 的地方。
### 思考与练习
1. 如果醉汉向前后向后的概率都是 0.5,这个问题的试验结果有变化吗?
2. 如果醉汉向前(回家方向)的概率是 0.6,反向回酒馆的概率是 0.4,这个问题的试验结果会有变化吗?
### 参考资料
- David Silver RL
@@ -30,12 +30,12 @@
图 1 安全驾驶问题的状态转移概率图
</center>
状态说明:
状态的缩写字母及其说明:
- 出发
- S出发
- 0.9 的概率心情较好,进入正常行驶状态;
- 0.1 的概率可能有急事而超速行驶。
- 正常行驶
- N正常行驶
正常行驶是标准状态,但是由于各种路况,可能会转移到其它几个状态:
- 0.1 的概率由于路况好或与他人斗气飙车而超速行驶;
- 0.1 的概率开车时心不在焉,在路口闯灯;
@@ -43,31 +43,31 @@
- 0.1 的概率开到闹市时减速行驶;
- 0.1 的概率遇到急事而拨打手机;
- 0.4 的概率进入目的地区域后减速行驶。
- 拨打手机
- M拨打手机
指的是开车过程中拨打手机,属于危险行为。
- 0.6 的概率结束通话返回正常行驶;
- 0.4 的概率发生事故。
- 礼让行人
- P礼让行人
礼让后以 1.0 的概率回到正常行驶状态。
- 小区减速
- L小区减速
减速后以 1.0 的概率安全抵达终点。
- 安全抵达
- G安全抵达
安全抵达后进入结束状态。
- 闹市减速
- D闹市减速
在闹市低速行驶时:
- 0.3 的概率遇到行人较多,礼让;
- 0.7 的概率回到正常行驶状态。
- 超速行驶
属于危险行为
- X超速行驶
属于危险行为。“十次事故九次快”的含义是,统计发生事故的原因,90% 是因为超速行驶,而不是说“超速行驶会有 90% 的概率发生事故”。
- 0.2 的概率回到正常行驶;
- 0.3 的概率闯红灯;
- 0.5 的概率发生事故。
- 路口闯灯
- R路口闯灯
- 0.9 的概率会出事故;
- 0.1 的概率遇到警察心情好,侥幸回到正常行驶。
- 发生事故
- C发生事故
1.0 的概率结束,不能再达到目的地。
- 结束
- E结束
进入此状态后将不再进行转移,或者是说以 100% 的概率转移到自己,叫做结束状态或者吸收状态。
### 3 分幕和采样
@@ -160,15 +160,17 @@
......
- 到达终点 $T$ 时,会得到$R_{T}$的奖励。
这些状态的下标值,只表示前后发生的顺序,即**时刻**,而与状态的序号无关。比如一个状态集中有 4 个状态 $[S_a,S_b,S_c,S_T]$,马尔可夫链的顺序有可能是 $S_a,S_b,S_a,S_c,S_T$,那么有:$S_1=S_a,\ S_2=S_b,\ S_3=S_a,\ S_4=S_c,\ S_5=S_T$。
这些状态的下标值,只表示前后发生的顺序,即**时刻**,而与状态的序号无关。比如一个状态集中有 4 个状态 $[s_a,s_b,s_c,s_T]$,马尔可夫链的顺序有可能是 $s_a,s_b,s_a,s_c,s_T$,那么有:$S_1=s_a,\ S_2=s_b,\ S_3=s_a,\ S_4=s_c,\ S_5=s_T$。
*注:在本书中使用这种定义方式:整个序列的序号是 $S_1,R_2,S_2,R_3,\cdots,S_t,R_{t+1},\cdots$ 的过程。而在有些资料中,用这种定义方式:$S_1,R_1,S_2,R_2,\cdots,S_t,R_{t},\cdots$,需要读者事先注意。*
*注:在本书中使用大写的 $S$ 表示某个时刻的状态,如 $S_1,S_t$,分别表示时刻 1 和时刻 t 的状态。用小写的 $s$ 表示具体问题的状态,如 $s_S,s_N$,表示安全驾驶问题中的 **出发** 状态和 **正常行驶** 状态。那么 $S_3=s_N$* 的含义就是在时刻 t=3 时处于状态 $s_N$。
*注:在本书中使用这种定义方式:整个序列的序号是 $S_1,R_2,S_2,R_3,\cdots,S_t,R_{t+1},\cdots$ 的过程,即 $R$ 的序号是 $S$ 的序号+1。而在有些资料中,用这种定义方式:$S_1,R_1,S_2,R_2,\cdots,S_t,R_{t},\cdots$,需要读者事先注意。*
#### 回报
如何定义“长期的累积收益”呢?那就是**回报**。
在英文中用 Return (收获)来表示,但是在公式中一般用 G 来表示,所以我们不妨理解为 Gain 这个单词。而且 Return 的首字母与奖励(Reward)的首字母相同,容易混淆。
在英文中用 Return(收获)来表示,但是在公式中一般用 G 来表示,所以我们不妨理解为 Gain 这个单词。而且 Return 的首字母与奖励(Reward)的首字母相同,容易混淆。
式 1 就是回报的定义:
@@ -185,7 +187,7 @@ $$
图 4 奖励与回报
</center>
图 4 中展示了 $G_1$ 和 $G_t$ 的计算方式,表示了 $S_1$ 和 $S_t$ 的回报,同时也告诉读者,在一个完整的序列中,我们可以从任意时刻 $t$ 开始计算 $S_t$ 状态的回报,而忽略前面的数据。
图 4 中展示了 $G_1$ 和 $G_t$ 的计算方式,表示了在同一个马尔可夫过程中的状态 $S_1$ 和 $S_t$ 的回报,同时也告诉读者,在一个完整的序列中,我们可以从任意时刻 $t$ 开始计算 $S_t$ 状态的回报,而忽略前面的数据。
有的读者可能会有疑问:我目前处于 $S_t$ 的位置,根本得不到将来的状态和奖励,我如何计算总的回报呢?
@@ -201,7 +203,7 @@ $$
马尔可夫奖励过程 = 马尔可夫链 + 奖励函数
$$
根据上面学习的知识,再结合上面学生学习的状态转移图 1,我们给每个状态定义一个奖励,如图 5 所示。
根据上面学习的知识,再结合上面的状态转移图 1,我们给每个状态定义一个奖励,如图 5 所示。
<center>
<img src="./img/Drive-2.png" width="600">
@@ -209,7 +211,7 @@ $$
图 5 安全驾驶问题的马尔可夫奖励过程
</center>
显然,我们使用了图 2 中的第一种方式(**注重结果**)来定义奖励,举例来说,无论状态“发生事故”是通过什么路径到达的(可以通过“拨打手机”到达,也可以通过“路口闯灯”到达),都可以得到 -12 的奖励(实际上是惩罚)。
显然,这里使用了图 2 中的第一种方式(**注重结果**)来定义奖励,举例来说,无论状态“发生事故”是通过什么路径到达的(可以通过“拨打手机”到达,也可以通过“路口闯灯”到达),都可以得到 -12 的奖励(实际上是惩罚)。
奖励函数(值)的设计一般是人工设定的,是通过分析目标问题的实际科学意义或者人文意义来决定的。比如,在图 5 中,通过交通规则的学习,给出制定奖励的过程如下:
@@ -218,7 +220,7 @@ $$
3. 礼让行人在交规上不加分,但是在强化学习系统中可以加 1 分,以鼓励自动驾驶的智能体强化此状态,保证安全。
4. 正常行驶是一个常见状态,得 0 分;
5. 闹市减速、小区内减速,和礼让行人一样,都给 1 分奖励。
6. 安全抵达给 5 分奖励。
6. 安全抵达给 5 分奖励。之所以给的奖励值很高,是要强化安全驾驶行为/状态,让智能体更倾向于安全驾驶的习惯。
7. 出发和结束都是 0 分。
根据状态转移可以得到一些完整的分幕采样,从而可以计算出每个采样的回报值,列在表 1 中。
@@ -227,10 +229,10 @@ $$
||分幕采样序列|回报值计算|
|-|-|-|
|1|Start - N - L - G - End|$G_{S}=0+0+1+5+0=6$|
|2|Start - N - P - N - L - G - End|$G_{S}=0+0+1+0+1+5+0=7$|
|3|Start - N - R - C - End|$G_{S}=0+0-6-1+0=-7$|
|4|Start - X - R - N - R - C - End|$G_{S}=0-3-6+0-6-1+0=-16$|
|1|S - N - L - G - E|$G_{S}=0+0+1+5+0=6$|
|2|S - N - P - N - L - G - E|$G_{S}=0+0+1+0+1+5+0=7$|
|3|S - N - R - C - E|$G_{S}=0+0-6-1+0=-7$|
|4|S - X - R - N - R - C - E|$G_{S}=0-3-6+0-6-1+0=-16$|
读者可能会产生怀疑:为什么表 1 中都是同样计算 $G_{S}$ 的回报值,但是有不同的结果?这是因为采样不同,路径不同,造成的回报值不同,这种情况是正常的。
@@ -75,7 +75,7 @@
$$
\begin{aligned}
V_t(s) &= \mathbb E [G_t | S_t = s]
V(s) &= \mathbb E [G_t | S_t = s]
\\\\
&=\mathbb E [ R_{t+1}+\gamma R_{t+2}+\gamma^2 R_{t+3}+ \gamma^3 R_{t+4}+ \cdots]
\end{aligned}
@@ -88,9 +88,9 @@ $$
简单地回忆一下**数学期望**的概念。
一个正常的六面的骰子,投出去后可以得到 [1,2,3,4,5,6] 六种结果,而且概率相等,那么这个骰子的期望值是 $(1+2+3+4+5+6)/6=3.5$。哈哈,读者可能会发现 3.5 这个数子,骰子无法投出来,所以它只是一种定义。
一个正常的六面的骰子,投出去后可以得到 $[1,2,3,4,5,6]$ 六种结果,而且概率相等,那么这个骰子的期望值是 $(1+2+3+4+5+6)/6=3.5$。哈哈,读者可能会发现 3.5 这个数子,骰子无法投出来,所以它只是一种定义。
但是,一个不正常的骰子,比如 [4,5,6] 出现的概率 $p$ 都是 $\frac{1}{5}$,而 [1,2,3] 出现的概率 $p$ 都是 $\frac{2}{15}$,那么它的数学期望是:
但是,一个不正常的骰子,比如 $[4,5,6]$ 出现的概率 $p$ 都是 $\frac{1}{5}$,而 $[1,2,3]$ 出现的概率 $p$ 都是 $\frac{2}{15}$,那么它的数学期望是:
$$
\begin{aligned}
@@ -221,6 +221,17 @@ S3 = -0.15136664866974026
我们在上面举的例子是一个一维的积分问题,积分结果是曲线下的面积;当然可以再增加一维随机采样,变成二维积分问题,结果为曲面下的体积,比一维积分更有实际作用。
蒙特卡洛在强化学习中应用的核心主要包含以下几点:
- MC方法是直接从经验轨迹当中直接进行学习。
- MC方法是一种model-free方法,即没有MDP的转移概率以及奖励的先验知识。
- MC方法从完整的经验轨迹中学习,不使用bootstrapping方法。
- MC方法简单得使用这个思想:价值=平均回报。
- 缺点在于只能应用于一定有终结点的按幕分的MDP过程。
### 参考资料
https://new.qq.com/omn/20220314/20220314A09IY600.html
@@ -1,6 +1,14 @@
### 7 如何确定采样的次数
###
直接采样
每次访问
首次访问
重要性采样
接受拒绝
### 如何确定采样的次数
<center>
<img src="./img/Sampling-RMSE.png" width="500">
@@ -56,9 +56,9 @@ $$
  $T \Leftarrow (S,R)$
  幕内循环直到终止状态:
    从 $S$ 根据状态转移概率得到 $S',R'$ 以及终止标志
    $T \Leftarrow S',R'$
    $T \Leftarrow (S',R')$
    $S \leftarrow S'$
  对 $T$ 从后向前遍历, $t=T-1,T-2,...,0$
  对 $T$ 从后向前遍历, $t=\tau-1,\tau-2,...,0$
    从 $T$ 中取出 $S_t,R_t$
    $G \leftarrow \gamma G+R_t$
    $G_{value}[S_t] \leftarrow G_{value}[S_t]+G$
@@ -0,0 +1,95 @@
## 代码生命周期问题 - 贝尔曼方程
### 1 提出问题
在解决安全驾驶问题的过程中,我们学习了马尔可夫奖励过程、状态价值函数、蒙特卡洛采样法等强化学习的重要概念。蒙特卡洛法虽然是一种科学的方法,但是需要大量的采样才能得到**比较理想的结果**,并不能说是**准确的结果**。
蒙特卡洛法是针对**无模型**强化学习问题的,对于**有模型**的问题,我们有什么更好的方法可以解决吗?
在本节中,我们以代码的生命周期问题为例,通过描述开发过程(编码、测试、审查、合并等)的状态/过程,来寻找用于分析解决有模型问题的**更好的办法**,而不是一味地使用蒙特卡洛方法依靠大数定理来计算状态价值函数。
### 2 建立模型
图 1 是一个有关代码生命周期的马尔可夫链,也可以叫做状态转移图,并且给每个状态都赋予了一个即时奖励值。在这里使用了**注重结果**的奖励方式,即只给状态定义奖励,在图 1 中用红色文字表示。
奖励值的确定方案有很多,在此我们采用了面向代码质量的指导思想给各个状态定义不同的值,对代码质量有损害的为负值,反之为正值。
<center>
<img src="./img/Code-1.png">
图 1 代码生命周期问题的状态转移概率图
</center>
状态的说明已经奖励函数的定义:
- C:编码 Coding
开发人员写代码的状态,全神贯注,废寝忘食。在此状态下无法确定代码质量,所以 $R=0$。
- 有 0.6 的概率产生 bug,需要修复。
- 另外 0.4 的概率进入审查状态。
- B:缺陷 Bug
在单元测试过程中发现 Bug,如果不妥善解决会对代码质量有很大影响,所以 $R=-3$。
- 有 0.7 的概率越改越糟糕。
- 有 0.3 的概率很快就改好了。
- T:测试 Testing
这里的测试是指 CI pipeline 测试,代码 check in 后自动执行,通过后方可进入下一阶段,严格把关,所以 $R=+1$。
- 有 0.9 的概率进入代码复审状态。
- 有 0.1 的概率,写了100行代码,测试出200多个错误,代码质量根本不行,直接扔掉,进入结束状态。
- R:审查 Review
由小组中其他人做代码审查,可以帮助提高代码质量,所以 $R=+3$。
- 有 0.8 的概率通过了审查,进入签入状态。
- 有 0.2 的概率决定重构代码。
- F:重构 Refactor
在审查过程中发现代码设计上的一些缺陷,所以需要在现有的基础上重构代码,对将来的可维护性、可扩展性等等有很大帮助,所以 $R=+2$。
- 有 0.2 的概率需要补充新代码。
- 有 0.5 的概率进入测试状态。
- 有 0.3 的概率进入代码复审状态。
- M:合并 Merge
代码合并到 main branch,然后结束。合并的过程实际上是有风险的,它有可能给集成测试带来新的问题,所以 $R=-1$。
- E:结束 End
结束本次代码的生命周期。进入此状态后将不再进行转移,或者是说以 100% 的概率转移到自己,叫做结束状态或者吸收状态,$R=0$,吸收状态总是把奖励值设置为 0。
表 1 中列出了状态转移矩阵,与租车问题中的矩阵形式相同。
表 1 状态转移矩阵
|P: 从$\rightarrow$到|缺陷|编码|测试|审查|重构|合并|结束|
|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
|**缺陷**|0.7|0.3|-|-|-|-|-|
|**编码**|0.6|-|0.4|-|-|-|-|
|**测试**|-|-|-|0.9|-|-|0.1|
|**审查**|-|-|-|-|0.2|0.8|-|
|**重构**|-|0.2|0.5|0.3|-|-|-|
|**合并**|-|-|-|-|-|-|1.0|
|**结束**|-|-|-|-|-|-|1.0|
有的读者可能有个疑问:在修复代码缺陷的状态,刚开始时有可能是越改越遭,有 0.7 的概率转回到自身,但是后来应该会大概率向好的方向转移,不应该只有 0.3 的概率。
这是一个简单的平稳环境的马尔科夫链,如果考虑更复杂的情况,可以在该状态下增加一个计数器:
- 如果在缺陷状态超过 1 天,则有 0.5 的概率回到正常编码状态;
- 如果没超过 1 天,则有 0.7 的概率回到自身。
需要再次说明的是,在图 1 中,我们使用了**注重结果**的奖励定义方式,直接给每个状态赋值一个奖励,意味只要达到这个状态,就可以立刻获得标注出的奖励值,而不管是从哪条路径达到的。
@@ -0,0 +1,224 @@
## 贝尔曼方程 Bellman Equation
### 对状态价值函数的进一步分解
前面已经学习了状态价值函数的定义,其公式为:
$$
\begin{aligned}
V(s) &= \mathbb E [G_t | S_t = s]
\\
&=\mathbb E [ R_{t+1}+\gamma R_{t+2}+\gamma^2 R_{t+3}+ \gamma^3 R_{t+4}+ \cdots]
\end{aligned}
\tag{1}
$$
由于
$$
\begin{aligned}
G_t &= R_{t+1}+\gamma R_{t+2}+ \gamma^2 R_{t+3} + \gamma^3 R_{t+4} + \cdots
\\
G_{t+1} &= R_{t+2}+ \gamma R_{t+3}+ \gamma^2 R_{t+4} + \cdots
\end{aligned}
\tag{2}
$$
结合式 1,2,可以做进一步的分解和推导:
$$
\begin{aligned}
V(s)&=\mathbb E [G_t|S_t=s]
\\
&=\mathbb E[R_{t+1}+\gamma R_{t+2}+\gamma^2 R_{t+3}+ \gamma^3 R_{t+4}+\cdots|S_t=s]
\\
&=\mathbb E[R_{t+1}+\gamma (R_{t+2}+\gamma R_{t+3}+ \gamma^2 R_{t+4}+\cdots)|S_t=s]
\\
&=\mathbb E[(R_{t+1}+\gamma G_{t+1})|S_t=s]
\\
&= \underbrace{ \mathbb E[R_{t+1}|S_t=s]}_A + \gamma \underbrace{\mathbb E[G_{t+1}|S_t=s]}_B
\end{aligned}
\tag{3}
$$
到了这一步,把 $V(s)$ 分成了 $A,B$ 两部分,下面分析一下这两部分如何分别得到。
### $A$ 部分:关于 $R_{t+1}$ 的数学期望
<center>
<img src="./img/Bellman-Rs.png">
图 2 关于 $R_{t+1}$ 的数学期望
</center>
#### 状态集定义
大写的 $S_t,S_{t+1}$,表示当前状态 $t$ 和下一个状态 $t+1$ 的**状态变量**,在不同的马尔可夫过程中会有不同的实例,所以只是一种通用表达方式,并不知道具体是哪个状态。
小写的 $s,s'$,表示具体的**状态实例**,具体到图 2 中:
$$
s = [s_a], \quad s' = [s_b,s_c]
$$
#### 转移概率定义
$P_{ss'}$ 是状态转移概率的集合,即转移矩阵。在图 2 中从 $s_a$ 出发有两个转移方向,分别是 $s_b,s_c$,对应的概率是 $p_1,p_2$,并且 $p_1+p_2=1$。
$$P_{ss'}=[p_1,p_2]$$
#### 奖励函数定义
在图 2 中,$R_{t+1}$ 指的是到达状态 $s_a$ 后可以得到的奖励 $R(s_a)$,可以看作是 $s_a$ 的函数。
在使用**注重过程**的奖励函数定义方式时,从$s_a$ 转移到 $s_b$ **或** $s_c$,分别可以得到 $r_1,r_2$ 的奖励,但不能同时得到。
$$
R_{ss'}=[r_1,r_2]
$$
图 2 中从 $s_a$ 出发并不能确定具体转移到哪个状态,所以也无法确定得到的奖励 $R_{t+1}$ 是 $r_1$ 还是 $r_2$。但是幸好有转移概率 $P_{ss'}$ 存在,给了我们一个机会,可以通过数学期望(带权重的平均值)来定义 $R(s_a)$:
$$R(s_a)=\mathbb E[R_{t+1}|S_t=s_a] = \underbrace {p_1 \cdot r_1}_{S_{t+1}=s_b} + \underbrace{p_2 \cdot r_2}_{S_{t+1}=s_c} = \sum P_{ss'} R_{ss'} \tag{4}$$
*注:式 4 实际上统一了**注重过程**的奖励方式和**注重结果**的奖励方式,把过程的期望看作是结果。这一点很重要,希望读者牢记,避免在以后的学习中遇到概念上的疑问。*
#### 结论
式 3 的 $A$ 部分可以定义为 $R(s)$
$$
\begin{aligned}
R(s) & = \mathbb E[R_{t+1}|S_t=s]
\\
({\footnotesize 根据图2实例化}\ s \to s_a) &= \mathbb E[R_{t+1}|S_t=s_a]
\\
({\footnotesize 式4})&= p_1 \cdot r_1+p_2 \cdot r_2
\\
({\footnotesize 抽象化\ p,r}) &=\sum_{s'} P_{ss'} \cdot R_{ss'}
\end{aligned}
\tag{5}
$$
### $B$ 部分:关于 $G_{t+1}$ 的数学期望
<center>
<img src="./img/Bellman-G.png">
图 3 关于 $G_{t+1}$ 的数学期望
</center>
#### 回报的定义
具体到图 3 中,回报 $G$ 在不同时刻的取值为:
$$
G_t=[G_a],\quad G_{t+1}=[G_b,G_c]
$$
和 $A$ 部分中遇到的问题一样:当 $S_t=s$ 时,即在 $s_a$ 状态下,只能确定 $G_{t}=G_a$,不能确定$G_{t+1}$ 是 $G_b,G_c$ 的哪一个,因为不知道下一步会转移到哪个状态,是 $s_b$ 还是 $s_c$
但是,由于 $P_{ss'}$ 的存在,我们仍然可以用数学期望的方式
$$
G_{t+1}=\mathbb E\big[[G_b,G_c]|S_{t+1}=[s_b,s_c]\big]= \underbrace{p_1 \cdot G_b}_{S_{t+1}=s_b} + \underbrace {p_2 \cdot G_c}_{S_{t+1}=s_c} \tag{6}
$$
#### 价值函数定义
首先要注意的一个问题是,$B$ 部分不等于 $V(s')$,因为按式 1 的定义:
$$
V(s) = \mathbb E [G_t | S_t = s]
$$
则有:
$$
V(s') = \mathbb E [G_{t+1} | S_t = s']\ne \mathbb E[G_{t+1}|S_t=s]
$$
具体到图 3 中,状态 $s_a, s_b, s_c$ 的价值函数的实例化表示:
$$
\begin{aligned}
V(s_a)&=\mathbb E [G_a|S_t=s_a]
\\
V(s_b)&=\mathbb E [G_b|S_{t+1}=s_b]
\\
V(s_c)&=\mathbb E [G_c|S_{t+1}=s_c]
\end{aligned}
\tag{7}
$$
不同时刻状态的价值函数的取值范围是:
$$
V(s)=[V(s_a)], \quad V(s')=[V(s_b),V(s_c)]
$$
#### 结论
$$
\begin{aligned}
B&=\mathbb E\big[G_{t+1}|S_t=s \big ]
\\
({\footnotesize 实例化})&=\mathbb E\big[G_{t+1}|S_t=s_a \big ]
\\
({\footnotesize 带入式\ 6})&= \mathbb E\big[(p_1 \cdot G_{b}|S_{t+1}=s_b) + (p_2\cdot G_{c}|S_{t+1}=s_c)\big|S_t=s_a]
\\
({\footnotesize 期望加法变换})&=\mathbb E\big[p_1\cdot G_{b}|S_{t+1}=s_b,S_t=s_a]+\mathbb E[p_2\cdot G_{c}|S_{t+1}=s_c,S_t=s_a\big]
\\
({\footnotesize 马尔可夫性质})&=\mathbb E\big[p_1\cdot G_{b}|S_{t+1}=s_b]+\mathbb E[p_2\cdot G_{c}|S_{t+1}=s_c\big]
\\
({\footnotesize 提出常数}\ p_1,p_2)&=p_1 \cdot \mathbb E[G_{b}|S_{t+1}=s_b]+ p_2 \cdot \mathbb E[G_{c}|S_{t+1}=s_c]
\\
({\footnotesize 带入式\ 7})&= p_1 \cdot V(s_b) + p_2 \cdot V(s_c)
\\
({\footnotesize 抽象化\ p,V})&= \sum_{s'} P_{ss'}V(s')
\end{aligned}
\tag{8}
$$
### 价值函数
所以式 3 最终为:
$$
\begin{aligned}
V(s) &= \mathbb E[R_{t+1}|S_t=s] + \gamma \mathbb E[G_{t+1}|S_t=s]
\\
{\footnotesize (带入式\ 4,8)}&=\sum_{s'} P_{ss'} R_{ss}+ \gamma \sum_{s'} P_{ss'}V(s') & (9.1)
\\
&= R(s)+ \gamma \sum_{s'} P_{ss'}V(s') &(9.2)
\\
&=\sum_{s'} P_{ss'} [R_{ss'}+\gamma V(s')] &(9.3)
\end{aligned}
\tag{9}
$$
- 在**针对状态定义奖励函数**的问题中,使用式 9.2 比较方便,因为 $R(s)$ 是直接定义在状态 $s$ 上。这是 David Silver 课件中的写法。
- 在**针对过程定义奖励函数**的问题中,使用式 9.3 比较方便,因为 $R(s,s')$ 是定义在从 $s\to s'$ 的转移过程上。这是 Richard S. Sutton and Andrew G. Barto 书中的写法,但不完全相同,我们后面再讲。
如果针对图 1,状态 $s_a$ 的价值函数实例计算公式为:
$$
\begin{aligned}
V(s_a)&=(p_1 \cdot r_1 + p_2 \cdot r_2) + \gamma[p_1 \cdot V(s_b) + p_2 \cdot V(s_c)]
\\
&=R(s)+\gamma[p_1 \cdot V(s_b) + p_2 \cdot V(s_c)]
\end{aligned}
$$
也就是说,一个状态 $s$ 的价值函数 $V(s)$ 由它的下游状态(可能不止一个)$s'$ 的价值函数 $V(s')$ 和转移概率 $P_{ss'}$ 以及转移过程中的奖励 $R_{ss'}$ 构成。
@@ -0,0 +1,146 @@
## 手工验算
下面我们来一起解决一下代码生命周期问题。
<center>
<img src="./img/Code-2.png">
图 2
</center>
图 2 中,每个状态下方都用括号表示了该状态的序号,比如 编码 (1) 表示状态 1,价值函数值用 $v_1$ 表示。
以状态 复审状态 (3) 为例,根据式 9.3,可以得到其价值函数为:
$$
\begin{aligned}
v_3&=R(s_3)+\gamma[P_{s_3,s_4} \cdot V(s_4) + P_{s_3,s_5} \cdot V(s_5)]
\\
&=3+ (0.2 v_4 + 0.8 v_5),
\end{aligned}
$$
为简化起见,令 $\gamma=1$,不影响对计算过程的说明。
同理可以得到其它所有状态的价值函数表达式,列出方程组如下:
$$
\begin{cases}
v_0=(-3)+0.7v_0+0.3v_1 & (10.1)
\\
v_1=(0)+0.6v_0+0.4v_2 & (10.2)
\\
v_2=(+1)+0.9v_3+0.1v_6 & (10.3)
\\
v_3=(+3)+0.2v_4+0.8v_5 & (10.4)
\\
v_4=(+2)+0.2v_1+0.5v_2+0.3v_3 & (10.6)
\\
v_5=(-1)+v_6 & (10.5)
\\
v_6=0 & (10.7)
\end{cases}
\tag{10}
$$
这是一个七元一次方程组,肯定有解。先简化式 10 中的各项,得到新的表达式:
$$
\begin{cases}
v_0=v_1-10 & (11.1)
\\
v_1=0.6v_0+0.4v_2 & (11.2)
\\
v_2=0.9v_3+1 & (11.3)
\\
v_3=0.2v_4+2.2 & (11.4)
\\
v_4=0.2v_1+0.5v_2+0.3v_3+2 & (11.6)
\\
v_5=-1 & (11.5)
\\
v_6=0 & (11.7)
\end{cases}
\tag{11}
$$
将 $(11.1)(11.2)(11.3)(11.4)$ 都变成 $v_3$ 的表达式,带入$(11.6)$ 的两侧,可以得到:
$$
5v_3-11=0.2(0.9v_3-14)+0.5(0.9v_3+1)+0.3v_3+2
$$
得到:$v_3 \approx 2.629$
所以,最终的结果为:
$$
\begin{cases}
v_0 \approx -22.634
\\
v_1 \approx -11.634
\\
v_2 \approx 3.366
\\
v_3 \approx 2.629
\\
v_4 \approx 2.145
\\
v_5=-1
\\
v_6=0
\end{cases}
\tag{12}
$$
读者可以用式 12 的结果验证式 10 中的任意等式。
<center>
<img src="./img/Code-3.png">
图 3 价值函数计算结果
</center>
### 给过程定义奖励
在图 x 中,是通过给状态定义奖励来得到 $R(s)$ 值的,直接使用贝尔曼方程的第 2 种形式即可。
前面我们学习过,有的问题是注重状态,有的问题是注重过程。如果遇到的问题是给过程定义奖励,应该如何解决?很简单,使用贝尔曼方程的第 1 种形式即可。
如图 3 所示
<center>
<img src="./img/Code-4.png">
图 2
</center>
举例,编码状态 C 的下游状态是 缺陷 B 和 测试 T,转移概率 $p_{ss'}$ 分别是 0.6, 0.4
$$
\begin{aligned}
R(C) &= p_{C,T} r_{C,T} + p_{C,B} r_{C,B}
\\
&=0.4 \times 0.6 + 0.6 \times (-0.4)
\\
&=0
\end{aligned}
$$
$$
\begin{aligned}
R(F) &= p_{F,C} r_{F,C} + p_{F,T} r_{F,T} + p_{F,R} r_{F,R}
\\
&=0.2 \times 0.35 + 0.5 \times 2 + 0.3 \times 1
\\
&=2
\end{aligned}
$$
然后再列方程组即可的解。
@@ -0,0 +1,238 @@
### 矩阵法
观察式 10 方程组,可以把它变形为:
$$
\begin{bmatrix}
v_0
\\
v_1
\\
v_2
\\
v_3
\\
v_4
\\
v_5
\\
v_6
\end{bmatrix}
= \
\begin{bmatrix}
-3
\\
0
\\
+1
\\
+3
\\
+2
\\
-1
\\
0
\end{bmatrix}
+\gamma
\begin{bmatrix}
0.7v_0+0.3v_1
\\
0.6v_0+0.4v_2
\\
0.9v_3+0.1v_6
\\
0.2v_4+0.8v_5
\\
0.2v_1+0.5v_2+0.3v_3
\\
v_6
\\
0
\end{bmatrix}
\tag{13}
$$
关于式 13
- 等式左侧的部分,就是状态值的向量,可以写成 $V_s$。
- 等式右侧的第一项,就是状态上的奖励值组成的向量,可以写成 $R_s$。
- 等式右侧的第二个矩阵,又可以写成两个矩阵的乘积:
$$
\begin{bmatrix}
0.7 & 0.3 & 0 & 0 & 0 & 0 & 0
\\
0.6 & 0 & 0.4 & 0 & 0 & 0 & 0
\\
0 & 0 & 0 & 0.9 & 0 & 0 & 0.1
\\
0 & 0 & 0 & 0 & 0.2 & 0.8 & 0
\\
0 & 0.2 & 0.5 & 0.3 & 0 & 0 & 0
\\
0 & 0 & 0 & 0 & 0 & 0 & 1.0
\\
0 & 0 & 0 & 0 & 0 & 0 & 1.0
\end{bmatrix}
\begin{bmatrix}
v_0
\\
v_1
\\
v_2
\\
v_3
\\
v_4
\\
v_5
\\
v_6
\end{bmatrix}
\tag{14}
$$
其中,第一个矩阵就是该问题的状态转移矩阵 $P_{ss'}$,第二个矩阵是状态值向量 $V_s$,于是,式 9 可以变成:
$$
V_s = R_s+ \gamma P_{ss'}V_s \tag{15}
$$
从式 9 直接看过来,式 15 等式右侧的 $V_s$ 应该是 $V_{s'}$ 才对,即 $V_s = R_s+\gamma P_{ss'}V_{s'}$。但是经过上述的实例化推导,希望读者可以理解到:
- 所谓的 $s'$ 是在时间维度上的定义,表示下一步的状态 $s'$ 的状态值;
- 而在空间上,由于状态值一旦确定就不会变化,并没有 $V_{s'}$ 的概念。
比如:
- 式 10.4$v_3=3+0.2v_4+0.8v_5$ 中,$V_s=v_3,V_{s'}=\{v_4,v_5\}$$v_4$ 是 $v_3$ 的后续状态,即 $s=v_3,s'=v_4$。
- 式 10.6$v_4=2+0.2v_1+0.5v_2+0.3v_3$ 中,$V_s=v_4,V_{s'}=\{v_1,v_2,v_3\}$$v_3$ 是 $v_4$ 的后续状态,即 $s=v_4,s'=v_3$。
两者在不同的马尔可夫过程中互为后续状态,但是 $v_3,v_4$ 这两个值不论在式 15 的等式左侧还是右侧,都是前后一致的。
对于式 13 的一个泛化的形式是式 16:
$$
\begin{bmatrix}
V_1
\\
V_2
\\
\vdots
\\
V_n
\end{bmatrix}
=\
\begin{bmatrix}
R_1
\\
R_2
\\
\vdots
\\
R_n
\end{bmatrix}
+\gamma
\begin{bmatrix}
P_{11} & P_{12} & \cdots & P_{1n}
\\
P_{21} & P_{22} & \cdots & P_{2n}
\\
\vdots & \vdots & \ddots & \vdots
\\
P_{n1} & P_{n2} & \cdots & P_{nn}
\end{bmatrix}
\begin{bmatrix}
V_1
\\
V_2
\\
\vdots
\\
V_n
\end{bmatrix}
\tag{16}
$$
式 15 可以变形,并最终解出 $V_s$:
$$
\begin{aligned}
V_s &= R_s+ \gamma P_{ss'}V_s
\\
V_s - \gamma P_{ss'}V_s &= R_s
\\
(I-\gamma P_{ss'})V_s&=R_s, &(I \ {\footnotesize 是对角矩阵})
\\
V_s&=(I-\gamma P_{ss'})^{-1}R_s
\end{aligned}
\tag{17}
$$
式 16 中,等式右侧的值都是已知的,所以可以解出 $V_s$ 的数学解析解。
定义状态转移矩阵:
```python
# 状态转移概率
P = np.array(
[ # B C T R F M E
[0.7, 0.3, 0.0, 0.0, 0.0, 0.0, 0.0], # Bug
[0.6, 0.0, 0.4, 0.0, 0.0, 0.0, 0.0], # Coding
[0.0, 0.0, 0.0, 0.9, 0.0, 0.0, 0.1], # Test (CI)
[0.0, 0.0, 0.0, 0.0, 0.2, 0.8, 0.0], # Review
[0.0, 0.2, 0.5, 0.3, 0.0, 0.0, 0.0], # reFactor
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0], # Merge
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] # End
]
)
```
定义奖励函数值向量:
```python
# 奖励向量 缺陷 编码 测试 审查 重构 合并 结束
Rewards = [-3, 0, +1, +3, +2, -1, 0]
```
代码
```python
def SolveMatrix(dataModel, gamma):
# 在对角矩阵上增加一个微小的值来解决奇异矩阵不可求逆的问题
I = np.eye(dataModel.N) * (1+1e-7)
# I = np.eye(dataModel.N) # 非奇异矩阵时使用此行代码以提高计算精度
factor = I - gamma * dataModel.P
inv_factor = np.linalg.inv(factor)
vs = np.dot(inv_factor, dataModel.R)
return vs
```
在定义状态转移矩阵时,右下角的值,即从 $S_{End} \to S_{End}$ 的转移概率,即可以写成 0.0,也可以写成 1.0,从强化学习的概念角度出发,都没有错。
与代码处理逻辑有关。
写成 0.0 时,np.random.choice(p=) 函数由于概率之和不为 1,所以函数调用出错。
写成 1.0 时,由于矩阵的行列式为 0,是个奇异矩阵,不可求逆。此时可以在对角矩阵上增加一个微小的值来解决奇异矩阵不可求逆的问题,但是最终结果会有 1e-7 的误差,可以接受。
```
[-21.63390663 -11.63390663 3.36609337 2.62899263 2.14496314
-1. 0. ]
Bug: -21.634
Coding: -11.634
Test: 3.366
Review: 2.629
Refactor: 2.145
Merge: -1.0
End: 0.0
```
可能有读者好奇:用矩阵法得到的结果,与式 12 相比,哪一个更准确?
答案是:式 12 更准确。原因是用代码求矩阵的逆时,由于具体实现的问题有一些误差,否则的话两者应该完全相等。
@@ -0,0 +1,497 @@
### 迭代法
- 用手工列方程组的办法肯定可以得到解,但是当状态数量到几十个时,虽然都是一次线性方程组,手工计算量已经变得很大,非常容易出错。
- 矩阵法虽然可以得到数学解析解,但是,计算逆矩阵时非常耗时,时间复杂度为 $O(n^3)$,$n$ 是状态的数量。当状态数量增加到千级、万级时,时间开销将变得不可接受。
式 $x=f(x)$ 的形式让我们想起了迭代法,比如式 1 所示的方程:
$$
x = 10 + \log_{10}(\frac{x}{2}) \tag{1}
$$
用迭代法是这样解,首先假定等式左侧的 $x$ 为 $x_{[k+1]}$,而右侧的为 $x_{[k]}$,如式 2
$$
x_{[k+1]} = 10 + \log_{10}(\frac{x_{[k]}}{2}) \tag{2}
$$
任意给定 $x_k$ 的初始值(在本问题中需要 $x>0$,否则 $\log_{10}(\frac{x_k}{2})$ 无法计算),计算出 $x_{k+1}$,如此重复,然后检查 $|x_{k+1}-x_k|$ 是否小于一个给定的误差,即可判定收敛。
写成代码也很简单:
【代码位置:0_Simple_Iteration.py】
```Python
import math
def f(x):
y = 10 + math.log10(x/2)
return y
if __name__=="__main__":
x = 100
delta = 100
count = 0
while delta > 1e-5: # 判断是否收敛
count += 1 # 计数器
y = f(x) # 迭代计算
delta = abs(x - y) # 检查误差
print(str.format("{0}: x={1},相对误差={2}", count, y, delta))
x = y # 为下一次迭代做准备
```
在给定 x 初值为 100 的情况下,指定误差 delta 小于 1e-5 即可结束迭代,试验结果如下:
```
1: x=11.698970004336019,相对误差=88.30102999566398
2: x=10.767117631798069,相对误差=0.93185237253795
3: x=10.73106946212392,相对误差=0.03604816967414948
4: x=10.72961301039834,相对误差=0.0014564517255788445
5: x=10.72955406269718,相对误差=5.894770116121606e-05
6: x=10.729551676708828,相对误差=2.3859883508947632e-06
```
共迭代了 6 次,误差达到 2.38e-6,结果为 10.73。读者可自行带入式 1 中验证。
### 原始迭代法
根据上面的思路,我们尝试一下最原始的手工迭代,以帮助读者理解迭代过程。
根据式
$$
V(s) = R(s)+ \gamma \sum_{s'} P_{ss'}V(s')
\tag{3}
$$
可以定义 $V_{[k+1]},V_{[k]}$ 的迭代关系如式 3
$$
V_{[k+1]}(s) = R(s)+ \gamma \sum_{s'} P_{ss'}V_{[k]}(s')
\tag{4}
$$
可以给 $V_{[0]}$ 的初始值为全 0 ,下标 $[0]$ 表示迭代次数(下同);然后根据式 4 进行第一次迭代,得到各个状态的奖励值 $V_{[1]}$(因为除了 $R(s)$ 以外,$V(s')$ 的值全为 0,所以第一次迭代的结果就是状态奖励值):
$$
V_{[0]}=
\begin{cases}
v_0=0
\\
v_1=0
\\
v_2=0
\\
v_3=0
\\
v_4=0
\\
v_5=0
\\
v_6=0
\end{cases}
, \qquad V_{[1]}=
\begin{cases}
v_0=-3
\\
v_1=0
\\
v_2=1
\\
v_3=3
\\
v_4=2
\\
v_5=-1
\\
v_6=0
\end{cases}
$$
第二次迭代,根据式 3 可以写出方程组:
$$
V_{[2]}=
\begin{cases}
v_0=-3+0.7v_0+0.3v_1 =(-3)+0.7\cdot(-3)+0.3\cdot0=-5.1
\\
v_1=0+0.6v_0+0.4v_2 =0+0.6\cdot(-3)+0.4\cdot1=-1.4
\\
v_2=1+0.9v_3+0.1v_6=1+0.9\cdot3+0.1\cdot0=3.7
\\
v_3=3+0.2v_4+0.8v_5=3+0.2\cdot2+0.8\cdot(-1)=2.6
\\
v_4=2+0.2v_1+0.5v_2+0.3v_3=2+0.2\cdot0+0.5\cdot1+0.3\cdot3=3.4
\\
v_5=-1+v_6=-1+0=-1
\\
v_6=0
\end{cases}
$$
把 $V_{[2]}$ 的结果带入式 3 做第三次迭代:
$$
V_{[3]}=
\begin{cases}
v_0=-3+0.7\cdot(-5.1)+0.3\cdot(-1.4) =-6.99
\\
v_1=0+0.6\cdot(-5.1)+0.4\cdot3.7=-1.58
\\
v_2=1+0.9\cdot2.6+0.1\cdot0=3.34
\\
v_3=3+0.2\cdot3.4+0.8\cdot(-1)=2.88
\\
v_4=2+0.2\cdot(-1.4)+0.5\cdot3.7+0.3\cdot2.6=4.35
\\
v_5=-1
\\
v_6=0
\end{cases}
$$
依此类推,读者可以比较 $V_{[0]},V_{[1]},V_{[2]},V_{[3]}$ 的数值,是不是一步步地向着上一小节中用矩阵法得到的结果迈进。
做为一名伟大的程序员,我们当然要用代码来解决上述的繁复手工计算过程:
【代码位置:1_Linear_Equations_Iteration.py】
```Python
# 线性方程组原始迭代法
def linear_equations_iteration(dataModel, gamma):
print("---原始迭代法---")
V_next = np.zeros(dataModel.N) # 初始化为全 0
count = 0
while (count < 1000): # 1000 是随意指定的一个比较大的数,避免不收敛而导致while无限
V = V_next.copy() # 准备一个备份,用于比较,检查是否收敛
count += 1 # 计数器+1
# 列方程组, 更新 V_next 的值
V_next[0] = dataModel.R[0] + gamma*(0.7 * V[0] + 0.3 * V[1])
V_next[1] = dataModel.R[1] + gamma*(0.6 * V[0] + 0.4 * V[2])
V_next[2] = dataModel.R[2] + gamma*(0.9 * V[3] + 0.1 * V[6])
V_next[3] = dataModel.R[3] + gamma*(0.2 * V[4] + 0.8 * V[5])
V_next[4] = dataModel.R[4] + gamma*(0.2 * V[1] + 0.5 * V[2] + 0.3 * V[3])
V_next[5] = dataModel.R[5] + gamma*V[6]
V_next[6] = dataModel.R[6]
if np.allclose(V_next, V): # 检查是否收敛
break
print("迭代次数 :", count)
return V
```
输出结果如下:
```
---原始迭代法---
迭代次数 : 98
[-21.63275089 -11.6331189 3.36614097 2.62904047 2.14517952 -1. 0.]
Bug: -21.633
Coding: -11.633
Test: 3.366
Review: 2.629
Refactor: 2.145
Merge: -1.0
End: 0.0
```
从结果上看,与矩阵法的结果非常接近,是可以接受的解。
### 矩阵迭代法
更一般地,求解线性方程组有一套成熟的做法,算法如下:
---
1. 定义迭代式:$x=Ax+B$
2. 给定任意初始值$x_{[0]}$,一般可以设置为 0 或其它随机数
3. 迭代求解 $x_{[k]}=Ax_{[k-1]}+B$
4. 得到:$x_{[0]},x_{[1]},\cdots,x_{[k]}$,直到序列收敛于某个值
---
对于式 4 来说:$A = \gamma P_{ss'}, x=V(s), B=R(s)$。代码实现如下:
【代码位置:2_Matrix_Iteration.py】
```Python
# 矩阵迭代法
def matrix_iteration(dataModel, gamma):
print("---矩阵迭代法---")
V_next = np.zeros(dataModel.N)
count = 0 # 迭代计数器
while (count < 1000): # 1000 是随意指定的一个比较大的数,避免不收敛而导致while无限
count += 1 # 计数器+1
V = V_next.copy() # 准备一个备份,用于比较,检查是否收敛
V_next = dataModel.R + gamma * np.dot(dataModel.P, V) # 式 3
if np.allclose(V_next, V): # 检查收敛性
break
print("迭代次数 :", count)
return V
```
其结果与上面的原始迭代法完全一致:
```
---矩阵迭代法---
迭代次数 : 98
[-21.63275089 -11.6331189 3.36614097 2.62904047 2.14517952 -1. 0.]
Bug: -21.633
Coding: -11.633
Test: 3.366
Review: 2.629
Refactor: 2.145
Merge: -1.0
End: 0.0
```
矩阵迭代法为什么可以收敛?下面就来做一下理论推导。
#### 收敛定理
对于线性方程组,用矩阵迭代方式可以表示为:
$$
x_{[k]}=Ax_{[k-1]}+B \tag{5}
$$
假设 $x_{[*]}$ 是最终的收敛值,则当 $k \to \infty$ 时有:
$$
x_{[*]}=Ax_{[*]}+B \tag{6}
$$
定义第 $k$ 次迭代的误差为:
$$
\begin{aligned}
\varepsilon_{[k]} &= x_{[k]}-x_{[*]}
\\
({\footnotesize 带入式4,5})&=Ax_{[k-1]}+B - (Ax_{[*]}+B)
\\
&=A(x_{[k-1]}-x_{[*]})
\\
&=A \varepsilon_{[k-1]}
\\
({\footnotesize 迭代带入 \varepsilon_{[k-1]}=A \varepsilon_{[k-2]}})&=A^2 \varepsilon_{[k-2]}
\\
&\cdots
\\
&=A^k \varepsilon_{[0]}=A^k(x_{[0]}-x_{[*]})
\end{aligned}
\tag{7}
$$
如果
$$
\underset{k \to +\infty}{\lim} A^k =0 \tag{8}
$$
则式 7 表示的误差 $\varepsilon_{[k]}$ 也趋近于 0,即,$x_{[k]}$ 可以收敛到 $x_{[*]}$。所以,我们来检查一下 $A^k$ 是否趋近于 0 就可以了。
【代码位置:2_Matrix_Iteration.py】
```Python
def check_convergence(dataModel):
print("迭代100次,检查状态转移矩阵是否趋近于 0:")
P_new = dataModel.P.copy()
for i in range(100):
P_new = np.dot(dataModel.P, P_new)
print(np.around(P_new, 3))
```
式 5 中的 $A$ 实际就是本问题中的状态转移矩阵 $P$,结果为:
```
迭代100次,检查状态转移矩阵是否趋近于 0:
[[0. 0. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 0. 0.]]
```
可以看到 100 次迭代后,式 8 满足条件,所以证明了矩阵迭代法是可以收敛的。
- 理论上,当 $A$ 的谱半径小于 1 时迭代法收敛。谱半径就是特征值绝对值集合的上确界,一般若为方阵 A 的谱半径则写作 ρ(A)。
- 实际上,一般只要 $A$ 可逆,迭代就能收敛。
### 双数组迭代 vs. 单数组原地更新
观察原始迭代法中的代码,读者会发现我们“聪明地”使用了两个数组,一个是 $V$ 来存储上一轮迭代的状态数值,另一个是 $V_{next}$ 来计算本轮迭代的状态数值,这样可以保证所有的 $V_{next}$ 都是从上一次迭代的 $V$ 计算得到,很“干净”。
但是,真的需要这么“干净”吗?如果把式 4 改成式 9:
$$
V_{[k]}(s) = R(s)+ \gamma \sum_{s'} P_{ss'}V_{[k]}(s')
\tag{9}
$$
会如何呢?
一个不怎么伟大的程序员也可以立刻写出如下代码:
【代码位置:3_Linear_Equations_Iteration_SingleArray.py】
```Python
# 单数组原始迭代法
def linear_equations_iteration_single_array(dataModel, gamma):
print("---单数组原始迭代法---")
V = np.zeros(dataModel.N) # 初始化为全 0
count = 0 # 迭代计数器
while (count < 1000): # 1000 是随意指定的一个比较大的数,避免不收敛而导致while无限
count += 1 # 计数器+1
V_old = V.copy() # 备份上一次的迭代值用于检查收敛性
# 线性方程组
V[0] = dataModel.R[0] + gamma*(0.7 * V[0] + 0.3 * V[1])
V[1] = dataModel.R[1] + gamma*(0.6 * V[0] + 0.4 * V[2])
V[2] = dataModel.R[2] + gamma*(0.9 * V[3] + 0.1 * V[6])
V[3] = dataModel.R[3] + gamma*(0.2 * V[4] + 0.8 * V[5])
V[4] = dataModel.R[4] + gamma*(0.2 * V[1] + 0.5 * V[2] + 0.3 * V[3])
V[5] = dataModel.R[5] + gamma*(1.0 * V[6])
V[6] = dataModel.R[6]
if np.allclose(V_old, V): # 检查收敛
break
print("迭代次数 :", count)
return V
```
与双数组方法相比,就是把线性方程组等式前面的 V_next 改成 V 了。运行结果如下:
```
---单数组原始迭代法---
迭代次数 : 81
[-21.63293714 -11.63330975 3.36612696 2.62902565 2.14510923 -1. 0.]
Bug: -21.633
Coding: -11.633
Test: 3.366
Review: 2.629
Refactor: 2.145
Merge: -1.0
End: 0.0
```
读者会惊奇地发现,迭代次数从以前的 98 次变成了 81 次即达到收敛状态。这是为什么呢?
原因是这样的:观察线性方程组中的代码,当第一行计算完 V[0] 后(这里的方括号表示状态数组),第二行在计算 V[1] 时立刻就用到了新的 V[0]......,计算 V[4] 时就已经用到了最新的 V[1],V[2],V[3]。所以,收敛的速度变快了。在处理动态规划问题时,一般都使用这种原地更新法(in place update)。
有兴趣的读者可以修改一下 V[0]~V[6] 的计算顺序,看看是否还可以提高一点儿迭代效率。如果前后依赖比较强的话,更改遍历计算的顺序会提高性能;但如果是交叉依赖,就不那么明显了。
### 通用的迭代实现
前面的原始迭代法其实就是矩阵迭代法,只是把矩阵运算的过程变成实例化代码了,但是只能针对本案例有效,换一个问题时,就需要重新书写代码。而矩阵迭代法只需要修改数据结构中的概率转移矩阵 dataModel.P 的具体内容就可以了适应任何场景了。
但是,有时候由于状态成千上万,没有可能写出状态转移矩阵来,对于人类来说,手工维护一个 $20 \times 20$ 的矩阵,已经是极限了,需要非常小心才能不出错。所以,最佳设计是只需要得到在某个状态下转移到可能达到的下游状态的列表,而不是转移到所有状态(包括不能达到的状态即概率为 0)的矩阵。
举例来说,在本问题中,状态转移矩阵是这样定义的:
【代码位置:CodeLifeCycle_DataModel_P.py】
```Python
# 状态转移概率
P = np.array(
[ # B C T R F M E
[0.7, 0.3, 0.0, 0.0, 0.0, 0.0, 0.0], # Bug
[0.6, 0.0, 0.4, 0.0, 0.0, 0.0, 0.0], # Coding
[0.0, 0.0, 0.0, 0.9, 0.0, 0.0, 0.1], # Test (CI)
[0.0, 0.0, 0.0, 0.0, 0.2, 0.8, 0.0], # Review
[0.0, 0.2, 0.5, 0.3, 0.0, 0.0, 0.0], # reFactor
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0], # Merge
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] # End
]
)
```
其中第二行表示从 状态 Coding 到其它所有 7 个状态的转移概率,但其中有些状态是达不到的,所以只有两个值大于 0,是一个稀疏矩阵:
```
[0.6, 0.0, 0.4, 0.0, 0.0, 0.0, 0.0], # Coding
```
对于这种情况,可以定义另外一种数据结构,来解决稀疏问题:
```Python
# 用字典代替状态转移矩阵
D = {
States.Bug: [(States.Bug, 0.7), (States.Coding, 0.3)],
States.Coding: [(States.Bug, 0.6), (States.Test, 0.4)],
States.Test: [(States.Review, 0.8), (States.End, 0.1)],
States.Review: [(States.Refactor, 0.2),(States.Merge, 0.8)],
States.Refactor:[(States.Coding, 0.2), (States.Test, 0.5),(States.Review, 0.3)],
States.Merge: [(States.End, 1.0)],
States.End: [(States.End, 1.0)]
}
```
用第一行数据举例,它表示:在 Bug 状态下,可以以 0.7 的概率转移到 Bug,以 0.3 的概率转移到 Coding。
相应地,需要改动数据模型代码:
【代码位置:CodeLifeCycle_DataModel_D.py】
```Python
class DataModel(object):
def __init__(self):
self.D = D # 状态转移字典
self.R = Rewards # 奖励
self.S = States # 状态集
self.N = len(self.S) # 状态数量
self.E = [self.S.End] # 终止状态集
def get_next(self, curr_s):
list_state_prob = self.D[curr_s] # 根据当前状态返回可用的下游状态及其概率
return list_state_prob
```
因此,必须改进原始迭代法中的代码,让它可以适应通用场景。
#### 单数组就地更新算法
---
定义误差 $error$
任意初始化 $V(s)$,其中 $V(s_{End})=0$
循环:
  $V_{old}(s) \leftarrow V(s)$
  对每一个 $s \in S$
    $V(s) \leftarrow R_{ss'}+\gamma \sum P_{ss'}V(s')$
  检查收敛性 $|V_{old} - V| < error$
  如收敛则退出循环
---
代码如下:
【代码位置:4_Bellman_Equation_Iteration_SingleArray.py】
```Python
# 贝尔曼方程单数组就地更新
def Bellman_iteration_single_array(dataModel, gamma):
print("---单数组就地更新法---")
V = np.zeros(dataModel.N)
count = 0
while True:
count += 1
V_old = V.copy() # 复制备份用于检查收敛性
# 遍历每一个 state 作为 curr_state
for curr_state in dataModel.S:
# 得到转移概率
list_state_prob = dataModel.get_next(curr_state)
# 计算 \sum(P·V)
v_sum = 0
for next_state, next_prob in list_state_prob:
v_sum += next_prob * V[next_state.value]
# 计算 V = R + gamma * \sum(P·V)
V[curr_state.value] = dataModel.R[curr_state.value] + gamma * v_sum
# 检查收敛性
if np.allclose(V, V_old):
break
print("迭代次数 :", count)
return V
```
运行结果
```
迭代次数 : 81
[-21.63293714 -11.63330975 3.36612696 2.62902565 2.14510923 -1. 0.]
Bug: -21.633
Coding: -11.633
Test: 3.366
Review: 2.629
Refactor: 2.145
Merge: -1.0
End: 0.0
```
结果与线性方程组单数组的实现一样,都是 81 次迭代。但是,这一段代码可以适应各种应用场景,只要定义好 dataModel 中的状态转移字典即可,而不需要每次都改动算法代码。
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

@@ -0,0 +1,17 @@
import math
def f(x):
y = 10 + math.log10(x/2)
return y
if __name__=="__main__":
x = 100
delta = 100
count = 0
while delta > 1e-5: # 判断是否收敛
count += 1 # 计数器
y = f(x) # 迭代计算
delta = abs(x - y) # 检查误差
print(str.format("{0}: x={1},相对误差={2}", count, y, delta))
x = y # 为下一次迭代做准备
@@ -0,0 +1,37 @@
import numpy as np
import CodeLifeCycle_DataModel_P as dm
# 线性方程组原始迭代法
def linear_equations_iteration(dataModel, gamma):
print("---原始迭代法---")
V_next = np.zeros(dataModel.N) # 初始化为全 0
count = 0
while (count < 1000): # 1000 是随意指定的一个比较大的数,避免不收敛而导致while无限
V = V_next.copy() # 准备一个备份,用于比较,检查是否收敛
count += 1 # 计数器+1
# 列方程组, 更新 V_next 的值
V_next[0] = dataModel.R[0] + gamma*(0.7 * V[0] + 0.3 * V[1])
V_next[1] = dataModel.R[1] + gamma*(0.6 * V[0] + 0.4 * V[2])
V_next[2] = dataModel.R[2] + gamma*(0.9 * V[3] + 0.1 * V[6])
V_next[3] = dataModel.R[3] + gamma*(0.2 * V[4] + 0.8 * V[5])
V_next[4] = dataModel.R[4] + gamma*(0.2 * V[1] + 0.5 * V[2] + 0.3 * V[3])
V_next[5] = dataModel.R[5] + gamma*V[6]
V_next[6] = dataModel.R[6]
if np.allclose(V_next, V): # 检查是否收敛
break
print("迭代次数 :", count)
return V
def print_V(V):
print(V)
vv = np.around(V,3)
for s in dataModel.S:
print(str.format("{0}:\t{1}", s.name, vv[s.value]))
if __name__=="__main__":
dataModel = dm.DataModel()
gamma = 1
V0 = linear_equations_iteration(dataModel, gamma)
print_V(V0)
@@ -0,0 +1,38 @@
import numpy as np
import CodeLifeCycle_DataModel_P as dm
# 矩阵迭代法
def matrix_iteration(dataModel, gamma):
print("---矩阵迭代法---")
V_next = np.zeros(dataModel.N)
count = 0 # 迭代计数器
while (count < 1000): # 1000 是随意指定的一个比较大的数,避免不收敛而导致while无限
count += 1 # 计数器+1
V = V_next.copy() # 准备一个备份,用于比较,检查是否收敛
V_next = dataModel.R + gamma * np.dot(dataModel.P, V) # 式 3
if np.allclose(V_next, V): # 检查收敛性
break
print("迭代次数 :", count)
return V
def print_V(V):
print(V)
vv = np.around(V,3)
for s in dataModel.S:
print(str.format("{0}:\t{1}", s.name, vv[s.value]))
def check_convergence(dataModel):
print("迭代100次, 检查状态转移矩阵是否趋近于 0: ")
P_new = dataModel.P.copy()
for i in range(100):
P_new = np.dot(dataModel.P, P_new)
print(np.around(P_new, 3))
if __name__=="__main__":
dataModel = dm.DataModel()
gamma = 1
V = matrix_iteration(dataModel, gamma)
print_V(V)
check_convergence(dataModel)
@@ -0,0 +1,36 @@
import numpy as np
import CodeLifeCycle_DataModel_P as dm
# 单数组原始迭代法
def linear_equations_iteration_single_array(dataModel, gamma):
print("---单数组原始迭代法---")
V = np.zeros(dataModel.N) # 初始化为全 0
count = 0 # 迭代计数器
while (count < 1000): # 1000 是随意指定的一个比较大的数,避免不收敛而导致while无限
count += 1 # 计数器+1
V_old = V.copy() # 备份上一次的迭代值用于检查收敛性
# 线性方程组
V[0] = dataModel.R[0] + gamma*(0.7 * V[0] + 0.3 * V[1])
V[1] = dataModel.R[1] + gamma*(0.6 * V[0] + 0.4 * V[2])
V[2] = dataModel.R[2] + gamma*(0.9 * V[3] + 0.1 * V[6])
V[3] = dataModel.R[3] + gamma*(0.2 * V[4] + 0.8 * V[5])
V[4] = dataModel.R[4] + gamma*(0.2 * V[1] + 0.5 * V[2] + 0.3 * V[3])
V[5] = dataModel.R[5] + gamma*(1.0 * V[6])
V[6] = dataModel.R[6]
if np.allclose(V_old, V): # 检查收敛
break
print("迭代次数 :", count)
return V
def print_V(V):
print(V)
vv = np.around(V,3)
for s in dataModel.S:
print(str.format("{0}:\t{1}", s.name, vv[s.value]))
if __name__=="__main__":
dataModel = dm.DataModel()
gamma = 1
V = linear_equations_iteration_single_array(dataModel, gamma)
print_V(V)
@@ -0,0 +1,40 @@
import numpy as np
import CodeLifeCycle_DataModel_D as dm
# 贝尔曼方程单数组就地更新
def Bellman_iteration_single_array(dataModel, gamma):
print("---单数组就地更新法---")
V = np.zeros(dataModel.N)
count = 0
while (count < 1000): # 1000 是随意指定的一个比较大的数,避免不收敛而导致while无限
count += 1
V_old = V.copy()
# 遍历每一个 state 作为 curr_state
for curr_state in dataModel.S:
# 得到转移概率
list_state_prob = dataModel.get_next(curr_state)
# 计算 \sum(P·V)
v_sum = 0
for next_state, next_prob in list_state_prob:
v_sum += next_prob * V[next_state.value]
# 计算 V = R + gamma * \sum(P·V)
V[curr_state.value] = dataModel.R[curr_state.value] + gamma * v_sum
# 检查收敛性
if np.allclose(V, V_old):
break
print("迭代次数 :", count)
return V
def print_V(V):
print(V)
vv = np.around(V,3)
for s in dataModel.S:
print(str.format("{0}:\t{1}", s.name, vv[s.value]))
if __name__=="__main__":
dataModel = dm.DataModel()
gamma = 1
V = Bellman_iteration_single_array(dataModel, gamma)
print_V(V)
@@ -0,0 +1,42 @@
from sre_parse import State
import numpy as np
from enum import Enum
# 状态
class States(Enum):
Bug = 0
Coding = 1
Test = 2
Review = 3
Refactor = 4
Merge = 5
End = 6
# 奖励向量,顺序与States定义一致
#Rewards = [-3, -2, -1, 0, 5, -4, 0]
# 奖励向量 缺陷 编码 测试 审查 重构 合并 结束
Rewards = [-3, 0, +1, +3, +2, -1, 0]
# 用字典代替状态转移矩阵
D = {
States.Bug: [(States.Bug, 0.7),(States.Coding, 0.3)],
States.Coding: [(States.Bug, 0.6),(States.Test, 0.4)],
States.Test: [(States.Review, 0.9),(States.End, 0.1)],
States.Review: [(States.Refactor, 0.2),(States.Merge, 0.8)],
States.Refactor: [(States.Coding, 0.2),(States.Test, 0.5),(States.Review, 0.3)],
States.Merge: [(States.End, 1.0)],
States.End: [(States.End, 1.0)]
}
class DataModel(object):
def __init__(self):
self.D = D # 状态转移字典
self.R = Rewards # 奖励
self.S = States # 状态集
self.N = len(self.S) # 状态数量
self.E = [self.S.End] # 终止状态集
def get_next(self, curr_s):
list_state_prob = self.D[curr_s] # 根据当前状态返回可用的下游状态及其概率
return list_state_prob
@@ -0,0 +1,56 @@
import numpy as np
from enum import Enum
# 状态
class States(Enum):
Bug = 0
Coding = 1
Test = 2
Review = 3
Refactor = 4
Merge = 5
End = 6
# 奖励向量,顺序与States定义一致
#Rewards = [-3, -2, -1, 0, 5, -4, 0]
# 奖励向量 缺陷 编码 测试 审查 重构 合并 结束
Rewards = [-3, 0, +1, +3, +2, -1, 0]
# 状态转移概率
P = np.array(
[ # B C T R F M E
[0.7, 0.3, 0.0, 0.0, 0.0, 0.0, 0.0], # Bug
[0.6, 0.0, 0.4, 0.0, 0.0, 0.0, 0.0], # Coding
[0.0, 0.0, 0.0, 0.9, 0.0, 0.0, 0.1], # Test (CI)
[0.0, 0.0, 0.0, 0.0, 0.2, 0.8, 0.0], # Review
[0.0, 0.2, 0.5, 0.3, 0.0, 0.0, 0.0], # reFactor
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0], # Merge
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] # End
]
)
class DataModel(object):
def __init__(self):
self.P = P # 状态转移矩阵
self.R = Rewards # 奖励
self.S = States # 状态集
self.N = len(self.S) # 状态数量
self.end_states = [self.S.End] # 终止状态集
def SolveMatrix(dataModel, gamma):
# 在对角矩阵上增加一个微小的值来解决奇异矩阵不可求逆的问题
#I = np.eye(dataModel.N) * (1+1e-7)
I = np.eye(dataModel.N)
factor = I - gamma * dataModel.P
inv_factor = np.linalg.inv(factor)
vs = np.dot(inv_factor, dataModel.R)
return vs
if __name__=="__main__":
dataModel = DataModel()
v = SolveMatrix(dataModel, 1.0)
print(v)
vv = np.around(v,3)
for s in dataModel.S:
print(str.format("{0}:\t{1}", s.name, vv[s.value]))
Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

@@ -1,81 +0,0 @@
import numpy as np
from enum import Enum
# 状态
class States(Enum):
Game = 0
Class1 = 1
Class2 = 2
Class3 = 3
Pass = 4
Rest = 5
End = 6
# 奖励向量
# [Game, Class1, Class2, Class3, Pass, Rest, End]
Rewards = [-1, -2, -2, -2, 10, 1, 0]
#Rewards = [-2, 0.5, 1, 1.5, 10, -2, 0]
# 状态转移概率
P = np.array(
[ #Game Cl1 Cl2 Cl3 Pass Rest End
[0.9, 0.1, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.5, 0.0, 0.5, 0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 0.8, 0.0, 0.0, 0.2],
[0.0, 0.0, 0.0, 0.0, 0.6, 0.4, 0.0],
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0],
[0.0, 0.2, 0.4, 0.4, 0.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
]
)
ground_truth = [-22.543, -12.543, 1.457, 4.321, 10.00, 0.803, 0.00]
a = [-22.5, -12.5, 1.5, 4.3, 10, 0.8, 0.00]
def RMSE(a,b):
err = np.sqrt(np.sum(np.square(a - b))/a.shape[0])
return err
class DataModel(object):
def __init__(self):
self.P = P # 状态转移矩阵
self.R = Rewards # 奖励
self.S = States # 状态集
self.N = len(self.S) # 状态数量
self.end_states = [self.S.End] # 终止状态集
self.Y = SolveMatrix(self, 1)
# 判断给定状态是否为终止状态
def is_end(self, s):
if (s in self.end_states):
return True
return False
# 获得即时奖励,保留此函数可以为将来更复杂的奖励函数做准备
def get_reward(self, s):
return self.R[s.value]
# 根据转移概率前进一步,返回(下一个状态、即时奖励、是否为终止)
def step(self, curr_s):
next_s = np.random.choice(self.S, p=self.P[curr_s.value])
return next_s, self.get_reward(next_s), self.is_end(next_s)
def SolveMatrix(dataModel, gamma):
# 在对角矩阵上增加一个微小的值来解决奇异矩阵不可求逆的问题
#I = np.eye(dataModel.N) * (1+1e-7)
I = np.eye(dataModel.N)
factor = I - gamma * dataModel.P
inv_factor = np.linalg.inv(factor)
vs = np.dot(inv_factor, dataModel.R)
return vs
if __name__=="__main__":
dataModel = DataModel()
v = SolveMatrix(dataModel, 1.0)
print(v)
vv = np.around(v,3)
for s in dataModel.S:
print(str.format("{0}:\t{1}", s.name, vv[s.value]))
print(RMSE(np.array(a), dataModel.Y))
print(RMSE(np.array(ground_truth), dataModel.Y))
@@ -1,509 +0,0 @@
## 学生学习问题 - 贝尔曼方程
### 1 提出问题
在解决安全驾驶问题的过程中,我们学习了马尔可夫奖励过程、状态价值函数、蒙特卡洛采样法等强化学习的重要概念。蒙特卡洛法虽然是一种科学的方法,但是需要大量的采样才能得到**比较理想的结果**,并不能说是**准确的结果**。
蒙特卡洛法是针对**无模型**强化学习问题的,对于**有模型**的问题,我们有什么更好的方法可以解决吗?
在本节中,我们以在校大学生为例,描述学生学习(上课、复习、考试)的过程,来分析解决上述问题。
假设一门课只需要上三次课就可以结束,然后就可以通过考试而结课拿学分。当然,这其中也不是那么顺利的,学生可能会遇到各种挑战:
- 上课不专心听讲而去打手机游戏;
- 学到一半的时候觉得这门课索然无味,中途退课;
- 觉得离考试还远,不着急复习巩固知识,而是去休息;
......
### 2 建立模型
图 1 是一个有关学生的学习、考试等一些列状态的马尔可夫链,也可以叫做状态转移图,并且给每个状态都赋予了一个即时奖励值。
<center>
<img src="./img/Student-2.png" width="500">
图 1 学习问题的状态转移概率图
</center>
- Class 1,2,3
上课/学习/复习状态,假设一门课是需要三次课的学习就可以结束。
- 在上课 1 中,有 0.5 的概率跑到 Game 状态,在课堂上用手机偷偷摸摸打游戏,另外 0.5 的概率到上课 2 状态。
- 在上课 2 中,有 0.2 的概率直接退课,另外 0.8 的概率到上课 3 状态。
- 在上课 3 中,有 0.6 的概率去考试,另外 0.4 的概率以为考试还远,不着急准备呢,就去休息了。
- Pass
考试通过状态。考试结束后,以 100% 概率到结课状态。
- Rest
休息状态。休息结束后,发现学的内容都忘得差不多了,遂分别以不同的概率回到三次课的上课/学习/复习状态。
在这里我们不区分上课和复习,如果是第一次到达 C1 状态,就认为是上课,否则就认为是复习。
- Game
娱乐/打游戏状态。在游戏状态中很大可能不能自拔,以 0.9 概率继续打游戏,只有 0.1 的概率幡然悔悟回到学习状态。
- End
结课/退课状态。进入此状态后将不再进行转移,或者是说以 100% 的概率转移到自己,叫做结束状态或者吸收状态。
表 1 中列出了状态转移矩阵,与租车问题中的矩阵形式相同。
表 1 状态转移矩阵
|P: 从$\rightarrow$到|Game|Class1|Class2|Class3|Pass|Rest|End|
|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
|**Game**|0.9|0.1||||||
|**Class1**|0.5||0.5|||||
|**Class2**||||0.8|||0.2|
|**Class3**|||||0.6|0.4||
|**Pass**|||||||1.0|
|**Rest**||0.2|0.4|0.4||||
|**End**|||||||1.0|
有的读者可能有个疑问:打游戏上瘾,从 Game 到 Game 有 0.9 的高概率,那么当该学生打游戏 2 小时后良心发现,转到学习状态的概率会不会大于 0.1 呢?
这是一个简单的平稳环境的马尔科夫链,如果考虑更复杂的情况,可以在 Game 状态下增加一个计数器:
- 如果打游戏超过 1 小时了,则有 0.5 的概率回到学习状态;
- 如果没超过 1 小时,则有 0.1 的概率回到学习状态。
在学生学习的模型中有很多状态,如何确定某个状态比另一个状态好呢?或者说如何比较两个状态的好坏呢?因为从直觉上讲,学生在 C1,C2,C3 的状态明显要比 Game 状态好,但是如何能用数值的大小来体现这种好坏关系呢?
在上一节中,已经有了分幕、奖励、回报的概念,这一节中,将会利用这些基础概念来定义每个**状态价值函数**,从而可以比较状态之间的好坏。
需要再次说明的是,在图 1 中,我们使用了**注重结果**的奖励定义方式,直接给每个状态赋值一个奖励,意味只要达到这个状态,就可以立刻获得标注出的奖励值,而不管是从哪条路径达到的。
### 马尔可夫奖励过程中的贝尔曼方程
<center>
<img src="./img/Bellman.png">
图 1 贝尔曼公式推导
</center>
- 左图:在 $s_a$ 状态得到 $R(s)$ 的表达式。
在使用**注重过程**的奖励函数定义方式时,从$s_a$ 转移到 $s_b,s_c$ 的过程中,分别可以得到 $r_1,r_2$ 的奖励,则 $s_a$ 的奖励函数定义为一种期望:$R(s_a)=\mathbb E[R_{Sa}|S_t=s_a] = p_1 \cdot r_1+p_2 \cdot r_2$。
- 右图:在 $s_a$ 状态得到 $G_{t+1}$ 的表达式。
当 $S_t=s$ 时,即在 $s_a$状态下,只能确定 $G_{t}=G_a$,不能确定$G_{t+1}$,因为不知道下一步会转移到哪个状态,是 $s_b$ 还是 $s_c$
所以,在 $s_a$ 状态时,$G_{t+1}$ 只能用转移概率(即 $p_1,p_2$)与下层状态 $s_b,s_c$ 的 $G$ 值(即 $G_a,G_b$)的乘积来表示,相当于在 $S_t=s_a$ 时,对 $G_{t+1}$ 求一次期望(带权重的平均值):$G_{t+1}=\mathbb E[G_{b,c}|S_t=s_a]=(p_1 \cdot G_b|S_{t+1}=s_b)+(p_2 \cdot G_c|S_{t+1}=s_c)$。一旦确定到达 $s_b,s_c$ 状态后,$S_t=s_a$ 的条件就可以去掉了,分别用 $S_{t+1}=s_b,S_{t+1}=s_c$ 代替。
做实例化推导之前,针对图 1,先给出一些必要的定义。
状态集定义:
$$
s_a \in s, \ (s_b,s_c) \in s'
$$
其中:$s$ 等同于 $S_t$$s'$ 等同于 $S_{t+1}$。
由价值函数的定义:
$$
V(s)=\mathbb E [G_t|S_t=s] \tag{1}
$$
可以得到图 1 中状态 $S_a, S_b, S_c$ 的价值函数的实例化表示:
$$
\begin{aligned}
V(s_a)&=\mathbb E [G_a|S_t=s_a] & (2.1)
\\
V(s_b)&=\mathbb E [G_b|S_{t+1}=s_b] & (2.2)
\\
V(s_c)&=\mathbb E [G_c|S_{t+1}=s_c] & (2.3)
\end{aligned}
\tag{2}
$$
在本例中,如果 $V(s)=V(s_a)$,则 $V(s_b),V(s_c) \in V(s')$。
图 1 中状态转移概率的实例化表示:
$$
\begin{aligned}
p_1 &= p(s_b|s_a)=P(s'|s), \ (s=s_a,s'=s_b) &(3.1)
\\
p_2 &= p(s_c|s_a)=P(s'|s), \ (s=s_a,s'=s_c) &(3.2)
\end{aligned}
\tag{3}
$$
图 1 中奖励函数的实例化表示:
$$
\begin{aligned}
r_1 &= r(s_a,s_b)=R(s,s'), \ (s=s_a,s'=s_b) &(4.1)
\\
r_2 &= r(s_a,s_c)=R(s,s'), \ (s=s_a,s'=s_c) &(4.2)
\end{aligned}
\tag{4}
$$
该奖励函数的定义属于**注重过程**的定义方式,即定义在状态转移过程中。而此时状态 $S_a$ 的奖励为:
$$
R_{t+1}=p_1 \cdot r_1+p_2 \cdot r_2 \tag{5}
$$
推导
$$
\begin{aligned}
V(s)&=\mathbb E [G_t|S_t=s]
\\
&=\mathbb E[R_{t+1}+\gamma R_{t+2}+\gamma^2 R_{t+3}+\cdots|S_t=s]
\\
&=\mathbb E[R_{t+1}+\gamma (R_{t+2}+\gamma R_{t+3}+\cdots)|S_t=s]
\\
&=\mathbb E[R_{t+1}+\gamma G_{t+1}|S_t=s]
\\
&= \underbrace{ \mathbb E[R_{t+1}|S_t=s]}_A + \gamma \underbrace{\mathbb E[G_{t+1}|S_t=s]}_B
\end{aligned}
\tag{6}
$$
式 6 的 $A$ 部分:
$$
\begin{aligned}
A & = \mathbb E[R_{t+1}|S_t=s]
\\
({\footnotesize 实例化}\to) &= \mathbb E[R_{Sa}|S_t=s_a]
\\
({\footnotesize 图 1 左图} \to )&= p_1 \cdot r_1+p_2 \cdot r_2
\\
({\footnotesize 式3,4} \to) &=p(s_b|s_a) \cdot r(s_a,s_b)+p(s_c|s_a) \cdot r(s_a,s_c)
\\
({\footnotesize 抽象化} \to) &=\sum_{s'} P(s'|s) \cdot R(s,s') \to R(s)
\end{aligned}
\tag{7}
$$
式 6 的 $B$ 部分:
首先要注意的一个问题是,B 不等于 $V(s')$,因为按式 6 的第一行,$V(s')=\mathbb E[G_{t+1}|S_{t+1}=s'] \ne \mathbb E[G_{t+1}|S_t=s]$。
$$
\begin{aligned}
B&=\mathbb E\big[G_{t+1}|S_t=s \big ]
\\
({\footnotesize 实例化} \to)&=\mathbb E \big[\mathbb E[G_{b,c}|S_t=s_a] \big ]
\\
({\footnotesize 图1右图} \to)&= \mathbb E\big[(p_1 \cdot G_{b}|S_{t+1}=s_b) + (p_2\cdot G_{c}|S_{t+1}=s_c)\big]
\\
({\footnotesize 期望加法变换} \to)&=\mathbb E\big[p_1\cdot G_{b}|S_{t+1}=s_b]+\mathbb E[p_2\cdot G_{c}|S_{t+1}=s_c\big]
\\
({\footnotesize 提出常数} p_1,p_2\to)&=p_1 \cdot \mathbb E[G_{b}|S_{t+1}=s_b]+ p_2 \cdot \mathbb E[G_{c}|S_{t+1}=s_c]
\\
({\footnotesize 式2} \to)&= p(s_b|s_a) \cdot V(s_b) + p(s_c|s_a) \cdot V(s_c)
\\
({\footnotesize 抽象化} \to)&= \sum_{s'} P(s'|s)V(s')
\end{aligned}
\tag{8}
$$
所以式 6 最终为:
$$
\begin{aligned}
V(s) &= \mathbb E[R_{t+1}|S_t=s] + \gamma \mathbb E[G_{t+1}|S_t=s]
\\
&=\sum_{s'} P(s'|s) R(s,s')+ \gamma \sum_{s'} P(s'|s)V(s') & (9.1)
\\
&=\sum_{s'} P(s'|s)[R(s,s')+\gamma V(s')] &(9.2)
\\
&= R(s)+ \gamma \sum_{s'} P(s'|s)V(s')=R_s+ \gamma \sum_{s'} P_{ss'}V(s') &(9.3)
\end{aligned}
\tag{9}
$$
- 在**针对过程定义奖励函数**的问题中,使用式 9.2 比较方便,因为 $R(s,s')$ 是定义在从 $s\to s'$ 的转移过程上。这是 Richard S. Sutton and Andrew G. Barto 书中的写法。
- 在**针对状态定义奖励函数**的问题中,使用式 9.3 比较方便,因为 $R(s)$ 是直接定义在状态 $s$ 上。这是 David Silver 课件中的写法。
如果针对图 1,状态 $s_a$ 的价值函数实例计算公式为:
$$
\begin{aligned}
V(s_a)&=(p_1 \cdot r_1 + p_2 \cdot r_2) + \gamma[p_1 \cdot V(s_b) + p_2 \cdot V(s_c)]
\\
&=R(s)+\gamma[p_1 \cdot V(s_b) + p_2 \cdot V(s_c)]
\end{aligned}
$$
也就是说,一个状态 $s$ 的价值函数 $V(s)$ 由它的下游状态 $s'$ 的价值函数 $V(s')$ 和转移概率 $P(s,s')$ 以及转移过程中的奖励 $R(s,s')$ 构成。
Bellman Equation for MRP
<center>
<img src="./img/student-3.png" width="500">
图 2
</center>
图 2 中,每个状态下方都用括号表示了该状态的序号,比如 C1(1) 表示 $v_1$。以状态 C3 为例,根据式 9.3,可以得到其价值函数为:
$$
\begin{aligned}
v_3&=R(C3)+\gamma[P_{C3,Pass} \cdot V(Pass) + P_{C3,Rest} \cdot V(Rest)]
\\
&=-2+ (0.6 v_4 + 0.4 v_5), &(\gamma=1)
\end{aligned}
$$
同理可以得到其它所有状态的价值函数表达式,列出方程组如下:
$$
\begin{cases}
v_0=-1+0.9v_0+0.1v_1 & (10.1)
\\
v_1=-2+0.5v_0+0.5v_2 & (10.2)
\\
v_2=-2+0.8v_3+0.2v_6 & (10.3)
\\
v_3=-2+0.6v_4+0.4v_5 & (10.4)
\\
v_4=10+v_6 & (10.5)
\\
v_5=1+0.2v_1+0.4v_2+0.4v_3 & (10.6)
\\
v_6=0 & (10.7)
\end{cases}
\tag{10}
$$
这是一个七元一次方程组,肯定有解。先简化式 10 中的各项,得到新的表达式:
$$
\begin{cases}
v_0=v_1-10 & (11.1)
\\
v_1=-2+0.5v_0+0.5v_2 & (11.2)
\\
v_2=0.8v_3-2 & (11.3)
\\
v_3=0.4v_5+4 & (11.4)
\\
v_4=10 & (11.5)
\\
v_5=1+0.2v_1+0.4v_2+0.4v_3 & (11.6)
\\
v_6=0 & (11.7)
\end{cases}
\tag{11}
$$
将 $(11.1)(11.2)(11.3)(11.4)$ 都变成 $v_3$ 的表达式,带入$(11.6)$ 的两侧,可以得到:
$$
2.5v_3-10=1+0.2(0.8v_3-16)+0.4(0.8v_3-2)+0.4v_3
$$
得到:$v_3=4.321$
所以,最终的结果为:
$$
\begin{cases}
v_0=-22.543 \approx -22.5
\\
v_1=-12.543 \approx -12.5
\\
v_2=1.457 \approx 1.5
\\
v_3=4.321 \approx 4.3
\\
v_4=10
\\
v_5=0.803 \approx 0.8
\\
v_6=0
\end{cases}
\tag{12}
$$
读者可以用式 12 的结果验证式 10 中的任意等式。
### 矩阵法
观察式 10 方程组,可以把它变形为:
$$
\begin{bmatrix}
v_0
\\
v_1
\\
v_2
\\
v_3
\\
v_4
\\
v_5
\\
v_6
\end{bmatrix}
= \
\begin{bmatrix}
-1
\\
-2
\\
-2
\\
-2
\\
10
\\
1
\\
0
\end{bmatrix}
+\gamma
\begin{bmatrix}
0.9v_0+0.1v_1
\\
0.5v_0+0.5v_2
\\
0.8v_3+0.2v_6
\\
0.6v_4+0.4v_5
\\
v_6
\\
0.2v_1+0.4v_2+0.4v_3
\\
0
\end{bmatrix}
\tag{13}
$$
关于式 13
- 等式左侧的部分,就是状态值的向量,可以写成 $V_s$。
- 等式右侧的第一项,就是状态上的奖励值组成的向量,可以写成 $R_s$。
- 等式右侧的第二个矩阵,又可以写成两个矩阵的乘积:
$$
\begin{bmatrix}
0.9 & 0.1 & 0 & 0 & 0 & 0 & 0
\\
0.5 & 0 & 0.5 & 0 & 0 & 0 & 0
\\
0 & 0 & 0 & 0.8 & 0 & 0 & 0.2
\\
0 & 0 & 0 & 0 & 0.6 & 0.4 & 0
\\
0 & 0 & 0 & 0 & 0 & 0 & 1.0
\\
0 & 0.2 & 0.4 & 0.4 & 0 & 0 & 0
\\
0 & 0 & 0 & 0 & 0 & 0 & 1.0
\end{bmatrix}
\begin{bmatrix}
v_0
\\
v_1
\\
v_2
\\
v_3
\\
v_4
\\
v_5
\\
v_6
\end{bmatrix}
\tag{14}
$$
其中,第一个矩阵就是该问题的状态转移矩阵 $P_{ss'}$,第二个矩阵是状态值向量 $V_s$,于是,式 9 可以变成:
$$
V_s = R_s+ \gamma P_{ss'}V_s \tag{15}
$$
从式 9 直接看过来,式 15 等式右侧的 $V_s$ 应该是 $V_{s'}$ 才对,即 $V_s = R_s+\gamma P_{ss'}V_{s'}$。但是经过上述的实例化推导,读者可以理解所谓的 $V_{s'}$ 是在时间维度上的定义,表示下一步的状态;而在空间上,由于状态值一旦确定就不会变化,并没有 $s'$ 的概念。
比如:
- 式 10.4$v_3=-2+ (0.6 v_4 + 0.4 v_5)$ 中,$V_s=v_3,V_{s'}=\{v_4,v_5\}$$v_5$ 是 $v_3$ 的后续状态。
- 式 10.6$v_5=1+0.2v_1+0.4v_2+0.4v_3$ 中,$V_s=v_5,V_{s'}=\{v_1,v_2,v_3\}$$v_3$ 是 $v_5$ 的后续状态。
两者在不同的马尔可夫过程中互为后续状态,所以实际上并没有 $V_{s'}$ 的概念,或者说 $V_s$ 和 $V_{s'}$ 对于具体的状态实例有区别,对于状态向量组没有区别。
式 15 可以变形,并最终解出 $V_s$:
$$
\begin{aligned}
V_s &= R_s+ \gamma P_{ss'}V_s
\\
V_s - \gamma P_{ss'}V_s &= R_s
\\
(I-\gamma P_{ss'})V_s&=R_s, &(I \ {\footnotesize 是对角矩阵})
\\
V_s&=(I-\gamma P_{ss'})^{-1}R_s
\end{aligned}
\tag{16}
$$
式 16 中,等式右侧的值都是已知的,所以可以解出 $V_s$ 的数学解析解。
定义状态转移矩阵:
```python
# 状态转移概率
P = np.array(
[ #Game Cl1 Cl2 Cl3 Pass Rest End
[0.9, 0.1, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.5, 0.0, 0.5, 0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 0.8, 0.0, 0.0, 0.2],
[0.0, 0.0, 0.0, 0.0, 0.6, 0.4, 0.0],
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0],
[0.0, 0.2, 0.4, 0.4, 0.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0]
]
)
```
定义奖励函数值向量:
```python
# 奖励向量
# [Game, Class1, Class2, Class3, Pass, Rest, End]
Rewards = [-1, -2, -2, -2, 10, 1, 0]
```
代码
```python
def SolveMatrix(dataModel, gamma):
# 在对角矩阵上增加一个微小的值来解决奇异矩阵不可求逆的问题
I = np.eye(dataModel.N) * (1+1e-7)
# I = np.eye(dataModel.N) # 非奇异矩阵时使用此行代码以提高计算精度
factor = I - gamma * dataModel.P
inv_factor = np.linalg.inv(factor)
vs = np.dot(inv_factor, dataModel.R)
return vs
```
在定义状态转移矩阵时,右下角的值,即从 $S_{End} \to S_{End}$ 的转移概率,即可以写成 0.0,也可以写成 1.0,从强化学习的概念角度出发,都没有错。
与代码处理逻辑有关。
写成 0.0 时,np.random.choice(p=) 函数由于概率之和不为 1,所以函数调用出错。
写成 1.0 时,由于矩阵的行列式为 0,是个奇异矩阵,不可求逆。此时可以在对角矩阵上增加一个微小的值来解决奇异矩阵不可求逆的问题,但是最终结果会有 1e-7 的误差,可以接受。
```
[-22.54320988 -12.54320988 1.45679012 4.32098765 10. 0.80246914 0. ]
Game: -22.543
Class1: -12.543
Class2: 1.457
Class3: 4.321
Pass: 10.0
Rest: 0.802
End: 0.0
```
可能有读者好奇:用矩阵法得到的结果,与式 12 相比,哪一个更准确?
答案是:式 12 更准确。原因是用代码求矩阵的逆时,由于具体实现的问题有一些误差,否则的话两者应该完全相等。
### 迭代法(动态规划)
@@ -0,0 +1,155 @@
### 动态规划(迭代法)
**Those who cannot remember the past are condemned to repeat it.**
**那些不记得过去的人注定要重蹈覆辙。** 这是动态规划的经典名句。下面我们先回忆一下动态规划的基本思想。
动态规划(Dynamic Programming,简称DP)动态规划常常适用于有重叠子问题和最优子结构性质的问题,动态规划方法所耗时间往往远少于朴素解法。
动态规划背后的基本思想非常简单。大致上,若要解一个给定问题,我们需要解其不同部分(即子问题),再合并子问题的解以得出原问题的解。 通常许多子问题非常相似,为此动态规划法试图仅仅解决每个子问题一次,从而减少计算量: 一旦某个给定子问题的解已经算出,则将其记忆化存储,以便下次需要同一个子问题解之时直接查表。 这种做法在重复子问题的数目关于输入的规模呈指数增长时特别有用。虽然抽象后进行求解的思路并不复杂,但具体的形式千差万别,找出问题的子结构以及通过子结构重新构造最优解的过程很难统一,为了解决动态规划问题,只能靠多练习、多思考了。
### 问题一
妈妈问:“$1+2+3+4=?$”
弟弟(一年级小学生,花了30秒在纸上验算加法然后)答:“等于10。”
爸爸问:“很好!那么 $7+4+3+2+1=?$”
姐姐(五年级小学生花了2秒思考然后立刻)答:“等于17。”
弟弟把刚拿起来的铅笔放下,无限崇拜地看着姐姐:“你是怎么这么快算出来的?”
姐姐说:“在你的答案上直接加 7 就行了。”
爸爸说:“不错,已经知道**利用历史经验**了!”
### 问题二
有一个10级的台阶,弟弟每天放学回家都要经过。他有时候一级一级地上台阶,有时候跳两级,有时候一级两级随意上跳。问:他一共有多少种跳法?这次需要输出所有数列(1级时输出1,2级时输出2,3级时输出3,4级时输出5......)。
其实这是一个斐波那契数列的变形题目,可以抽象为:
$$
f(n)=f(n-1)+f(n-2), \qquad \qquad (f(1)=1, f(2)=2)
$$
其解题思路在于把问题逐级分解:
- 想跳到第 10 级台阶,先要跳到第 9 级或第 8 级台阶;
又分成两种情况:
- 想跳到第 9 级台阶,先要跳到第 8 级或第 7 级台阶;
双分成两种情况:
- 想跳到第 8 级台阶,先要跳到第 7 级或第 6 级台阶;
叒分成两种情况:
- 想跳到第7级台阶,先要跳到第 6 级或第 5 级台阶;
- 想跳到第6级台阶,先要跳到第 5 级或第 4 级台阶;
......
- 想跳到第 8 级台阶,先要跳到第 7 级或第 6 级台阶;
......
所以有:
$$
\begin{aligned}
f(10)&=f(9)+f(8) & {\footnotesize (到第10级台阶的方法有 9 \to 10 和 8 \to 10两种组合)}
\\
f(9)&=f(8)+f(7)
\\
\cdots
\\
f(3)&=f(2)+f(1)
\\
f(2)&=2 & {\footnotesize (两级台阶有2种跳法)}
\\
f(1)&=1 & {\footnotesize (一级台阶只有1种跳法)}
\end{aligned}
$$
下面用三种方法来解决这个问题:
- 递归法
- 迭代法
- 备忘录法
【代码位置:】
```python
# 递归法
def f1(n):
if n==1 or n==2:
return n
else:
fn = f1(n-1) + f1(n-2) # 递归调用函数本身两次
return fn
# 迭代法
def f2(n):
fn_2 , fn_1 = 1, 2
for i in range(n-2):
fn = fn_2 + fn_1 # 计算
fn_2 = fn_1 # 迭代更换上一次的数值便于下次计算
fn_1 = fn
return fn
# 备忘录法
def f3(n):
results = {1:1, 2:2} # 当n=1和n=2时的结果是1和2
for i in range(3, n+1):
fi = results[i-1] + results[i-2] # 从字典中直接取出结果
results[i] = fi # 在字典中添加n=i时的结果,i=3,4,5...
return results
```
输出结果:
```
递归法结果 = 89
递归法耗时 = 0.0009970664978027344
迭代法结果 = 89
迭代法耗时 = 0.0010001659393310547
备忘录法结果 = {1: 1, 2: 2, 3: 3, 4: 5, 5: 8, 6: 13, 7: 21, 8: 34, 9: 55, 10: 89}
备忘录法耗时 = 0.0
```
所以 10 级台阶一共有 89 种跳法。其中,备忘录法可以记录所有的计算结果,当然需要耗费 $O(n)$ 空间。
当计算 $f(40)$ 时,递归法需要花费成指数级增加的时间,在笔者的电脑上约17秒,复杂度为 $O(2^n)$。而迭代法和备忘录法仍然是瞬时完成,复杂度 $O(n)$。
```
递归法结果 = 165580141
递归法耗时 = 16.985862016677856
迭代法结果 = 165580141
迭代法耗时 = 0.0010018348693847656
备忘录法结果 = {1: 1, 2: 2, 3: 3, 4: 5, ......, 38: 63245986, 39: 102334155, 40: 165580141}
备忘录法耗时 = 0.0010018348693847656
```
**递归是朴素的思想,迭代是有效的方法**,两者都要掌握。
### 总结
动态规划问题满足三大重要性质:
- 最优子结构性质
如果问题的最优解所包含的子问题的解也是最优的,我们就称该问题具有最优子结构性质(即满足最优化原理)。最优子结构性质为动态规划算法解决问题提供了重要线索。
- 子问题重叠性质
在用递归算法自顶向下对问题进行求解时,每次产生的子问题并不总是新问题,有些子问题会被重复计算多次。动态规划算法正是利用了这种子问题的重叠性质,对每一个子问题只计算一次,然后将其计算结果保存在一个表格中(称作 DP 表格),当再次需要计算已经计算过的子问题时,只是在表格中简单地查看一下结果,从而获得较高的效率。
- 无后效性
将各阶段按照一定的次序排列好之后,对于某个给定的阶段状态,它以前各阶段的状态无法直接影响它未来的决策,而只能通过当前的这个状态。换句话说,每个状态都是过去历史的一个完整总结。这就是无后向性,又称为无后效性。
动态规划最核心的思想,就在于拆分子问题,记住过往,减少重复计算。所以,**动态规划**,简单地说就是**记住求过解来节省后续的计算时间**。
在动态规划问题的解题思路中,重点是**定义 DP 表格的含义 + 状态转移方程**,已经有很多相关文献记载,请读者自行学习。在稍后要学习的贝尔曼方程
在本书中不再赘述
### 参考资料
https://cloud.tencent.com/developer/article/1817113
https://zhuanlan.zhihu.com/p/70689256
https://blog.csdn.net/weixin_41082481/article/details/115922389
@@ -0,0 +1,44 @@
import time
# 递归法
def f1(n):
if n==1 or n==2:
return n
else:
fn = f1(n-1) + f1(n-2) # 递归调用函数本身两次
return fn
# 迭代法
def f2(n):
fn_2 , fn_1 = 1, 2
for i in range(n-2):
fn = fn_2 + fn_1 # 计算
fn_2 = fn_1 # 迭代更换上一次的数值便于下次计算
fn_1 = fn
return fn
# 备忘录法
def f3(n):
results = {1:1, 2:2} # 当n=1和n=2时的结果是1和2
for i in range(3, n+1):
fi = results[i-1] + results[i-2] # 从字典中直接取出结果
results[i] = fi # 在字典中添加n=i时的结果,i=3,4,5...
return results
if __name__=="__main__":
n = 41
start = time.time()
print("递归法结果 =", f1(n))
end1 = time.time()
print("递归法耗时 =", end1-start)
print("迭代法结果 = ", f2(n))
end2 = time.time()
print("迭代法耗时 =", end2-end1)
print("备忘录法结果 =", f3(n))
end3 = time.time()
print("备忘录法耗时 =", end3-end2)
@@ -0,0 +1,24 @@
import numpy as np
# C:背包容量,N:物品数量,V:物品价值,S:物品体积
def f(Capacity, Number, Value, Size):
dp_Value = np.zeros((Number+1, Capacity+1), dtype=np.int32)
for item in range(1, Number+1): # 物品
for capacity in range(1, Capacity+1): # 容量
if capacity < Size[item]: # 容量不够装下这个物品
dp_Value[item, capacity] = dp_Value[item-1, capacity] # 仍然使用上一次的结果
else: # 容量足够, 计算新value
dp_Value[item, capacity] = \
max(dp_Value[item-1, capacity],
dp_Value[item-1, capacity - Size[item]] + Value[item])
return dp_Value
if __name__=="__main__":
Capacity = 10
Number = 4
Value = [0,2,4,3,7]
Size = [0,2,3,5,5]
result = f(Capacity,Number,Value,Size)
print(result)
+24 -1
View File
@@ -1,3 +1,11 @@
《用代码学习算法系列之强化学习》
- 提高编程能力
- 帮助算法理解
- 学习理论知识
- 解决实际问题
draft里面将会以日记形式存放独立的小“故事”,每个“故事”会解决一个强化学习的问题。
首先会提出问题,然后提出理论知识,解决问题。
@@ -18,9 +26,24 @@ draft里面将会以日记形式存放独立的小“故事”,每个“故事
网格世界(A->A', B-B', 5x5)
悬崖问题
FrozenLake
Car
GYM Car
BlackJack
肥皂泡
有风的世界
迷宫
Gym里面的more
计算V*的时候,手工计算 最大值 方程组
从统计到概率
从静态概率到概率转移
从简单转移到过程形成
从简单过程到奖励过程
从单次奖励到价值形成
从粗暴原始的价值估算到科学合理的价值估算
从估算状态价值到计算状态价值(贝尔曼方程)
从状态价值到动作价值
从动作价值到最优动作价值