Xiaowuhu/20220606 (#740)
* update * Create car.py * add files * update * Update 1-赌博机研究平台.md * Update 0-第2章.md * Update 1-赌博机研究平台.md * update * update * update * update * update * update * update * update * up * Update 6-置信上界算法.md * up * up * Update 6-置信上界算法.md * update * Create 6-置信上界算法 copy.md * Update 6-置信上界算法 copy.md * Update 6-置信上界算法.md * up * update * up * update * jhjkhjk * hhl * klhkj * update * update * update
|
@ -185,33 +185,31 @@ $$
|
|||
$$
|
||||
\frac{\partial loss}{\partial a_1}=- \frac{y_1}{a_1} \tag{13}
|
||||
$$
|
||||
|
||||
$$
|
||||
\frac{\partial loss}{\partial a_2}=- \frac{y_2}{a_2} \tag{14}
|
||||
$$
|
||||
|
||||
$$
|
||||
\frac{\partial loss}{\partial a_3}=- \frac{y_3}{a_3} \tag{15}
|
||||
\frac{\partial loss}{\partial a_3}=- \frac{y_3}{a_3}
|
||||
\tag{15}
|
||||
$$
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
\frac{\partial a_1}{\partial z_1}&=(\frac{\partial e^{z_1}}{\partial z_1} E -\frac{\partial E}{\partial z_1}e^{z_1})/E^2 \\\\
|
||||
&=\frac{e^{z_1}E - e^{z_1}e^{z_1}}{E^2}=a_1(1-a_1)
|
||||
\end{aligned}
|
||||
\frac{\partial a_1}{\partial z_1}=(\frac{\partial e^{z_1}}{\partial z_1} E -\frac{\partial E}{\partial z_1}e^{z_1})/E^2 =\frac{e^{z_1}E - e^{z_1}e^{z_1}}{E^2}=a_1(1-a_1)
|
||||
\tag{16}
|
||||
$$
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
\frac{\partial a_2}{\partial z_1}&=(\frac{\partial e^{z_2}}{\partial z_1} E -\frac{\partial E}{\partial z_1}e^{z_2})/E^2 \\\\
|
||||
&=\frac{0 - e^{z_1}e^{z_2}}{E^2}=-a_1 a_2
|
||||
\frac{\partial a_2}{\partial z_1}&=(\frac{\partial e^{z_2}}{\partial z_1} E -\frac{\partial E}{\partial z_1}e^{z_2})/E^2 =\frac{0 - e^{z_1}e^{z_2}}{E^2}=-a_1 a_2
|
||||
\end{aligned}
|
||||
\tag{17}
|
||||
$$
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
\frac{\partial a_3}{\partial z_1}&=(\frac{\partial e^{z_3}}{\partial z_1} E -\frac{\partial E}{\partial z_1}e^{z_3})/E^2 \\\\
|
||||
&=\frac{0 - e^{z_1}e^{z_3}}{E^2}=-a_1 a_3
|
||||
\frac{\partial a_3}{\partial z_1}&=(\frac{\partial e^{z_3}}{\partial z_1} E -\frac{\partial E}{\partial z_1}e^{z_3})/E^2 =\frac{0 - e^{z_1}e^{z_3}}{E^2}=-a_1 a_3
|
||||
\end{aligned}
|
||||
\tag{18}
|
||||
$$
|
||||
|
@ -274,21 +272,20 @@ $$
|
|||
所以:
|
||||
$$
|
||||
\begin{aligned}
|
||||
\frac{\partial{a_j}}{\partial{z_i}}&=\frac{-(E)'e^{z_j}}{(E)^2}=-\frac{e^{z_j}e^{z_i}}{{(E)^2}} \\\\
|
||||
&=-\frac{e^{z_j}}{{E}}\frac{e^{z_j}}{{E}}=-a_{i}a_{j}
|
||||
\frac{\partial{a_j}}{\partial{z_i}}&=\frac{-(E)'e^{z_j}}{(E)^2}=-\frac{e^{z_j}e^{z_i}}{{(E)^2}} =-\frac{e^{z_i}}{{E}}\frac{e^{z_j}}{{E}}=-a_{i}a_{j}
|
||||
\end{aligned}
|
||||
\tag{22}
|
||||
$$
|
||||
|
||||
2. 结合损失函数的整体反向传播公式
|
||||
|
||||
看上图,我们要求Loss值对Z1的偏导数。和以前的Logistic函数不同,那个函数是一个z对应一个a,所以反向关系也是一对一。而在这里,a1的计算是有z1,z2,z3参与的,a2的计算也是有z1,z2,z3参与的,即所有a的计算都与前一层的z有关,所以考虑反向时也会比较复杂。
|
||||
看上图,我们要求Loss值对Z1的偏导数。和以前的Logistic函数不同,那个函数是一个 $z$ 对应一个 $a$,所以反向关系也是一对一。而在这里,$a_1$ 的计算是有 $z_1,z_2,z_3$ 参与的,$a_2$ 的计算也是有 $z_1,z_2,z_3$ 参与的,即所有 $a$ 的计算都与前一层的 $z$ 有关,所以考虑反向时也会比较复杂。
|
||||
|
||||
先从Loss的公式看,$loss=-(y_1lna_1+y_2lna_2+y_3lna_3)$,a1肯定与z1有关,那么a2,a3是否与z1有关呢?
|
||||
先从Loss的公式看,$loss=-(y_1lna_1+y_2lna_2+y_3lna_3)$,$a_1$ 肯定与 $z_1$ 有关,那么 $a_2,a_3$ 是否与 $z_1$ 有关呢?
|
||||
|
||||
再从Softmax函数的形式来看:
|
||||
|
||||
无论是a1,a2,a3,都是与z1相关的,而不是一对一的关系,所以,想求Loss对Z1的偏导,必须把Loss->A1->Z1, Loss->A2->Z1,Loss->A3->Z1,这三条路的结果加起来。于是有了如下公式:
|
||||
无论是 $a_1,a_2,a_3$,都是与 $z_1$ 相关的,而不是一对一的关系,所以,想求Loss对Z1的偏导,必须把Loss->A1->Z1, Loss->A2->Z1,Loss->A3->Z1,这三条路的结果加起来。于是有了如下公式:
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
# 第 2 章 从概率计算到模拟验证
|
||||
|
||||
|
||||
|
||||
|
||||
### 参考资料
|
||||
|
||||
- https://godfiregame.com/5-helpful-tips-to-playing-slot-machines/
|
||||
- https://medium.com/emergent-future/simple-reinforcement-learning-with-tensorflow-part-1-5-contextual-bandits-bff01d1aad9c
|
||||
- https://arxiv.org/pdf/1904.07272v7.pdf
|
||||
- https://cloud.tencent.com/developer/article/1521732
|
|
@ -0,0 +1,195 @@
|
|||
## 2.1 赌博机研究平台
|
||||
|
||||
### 2.1.1 原始的赌博机
|
||||
|
||||
注意,赌博在中国是违法行为!在本章中的“赌博机”并非真正地研究赌博问题,而是把它抽象成为数学问题和强化学习问题,得到结论后用来解决类似的问题。把数学变成赌博工具是人类的发明,正如同科学可以救人也可以杀人一样。
|
||||
|
||||
赌博机,即“老虎机”,英语为 Slot Machine,或者 one-arm bandit(单臂强盗)。
|
||||
|
||||
- 为什么叫老虎机?
|
||||
|
||||
- 因为最初的设计中,内部有三个大卷轴,印有很多图案;
|
||||
- 赌徒投入一个投硬币,搬动摇杆,卷轴开始各自转动;
|
||||
- 最终停止转动时,如果三个卷轴上面显示的都是老虎图案,赌徒会获取丰厚的奖励。
|
||||
|
||||
- 为什么叫单臂/多臂强盗?
|
||||
|
||||
- 因为这机器回报率低,但是容易上瘾,让玩家输个精光,所以叫一条胳膊的强盗。很多现代的老虎机仍然保持一个传统的操作摇臂,可与按钮并行使用。
|
||||
|
||||
- 有的赌徒有策略的操作多台老虎机,以实现最高的可能获利,这样的赌徒就称作“多臂强盗”(K-Armed Bandits)。
|
||||
|
||||
- 某些翻译软件把 K-Armed Bandits 翻译成“武装匪徒”,因为 Armed 有“武装”的意思,读者自行理解吧。
|
||||
|
||||
### 2.1.2 如何在老虎机上赢钱
|
||||
|
||||
这是一个国外友人总结的如何在赌场中的老虎机上赢钱的几条经验。请读者一起阅读一下,以便对我们后续的算法研究提供思路。
|
||||
|
||||
1. Choose Slot Machines with High Price per Spin
|
||||
|
||||
选择每次摇臂操作赌注高的老虎机。
|
||||
|
||||
虽然这一提示似乎违反直觉,但下注的赌注越高的老虎机回报的频率越高,金额越大。这就是老虎机的设计方式,吐币的百分比与赌注大小成正比。此外,最大赌注可以帮助您解锁特殊功能,如免费旋转或头奖。
|
||||
|
||||
2. Choose the Least Complicated Slot Machines
|
||||
|
||||
选择最简单的老虎机。
|
||||
|
||||
在赌场使用这条普遍的经验法则:如果游戏复杂,它的回报就会少。在复杂的老虎机游戏中,赌场试图通过特殊的乘数、奖金或额外的生命来吸引你的注意力。虽然您可能喜欢这些特殊功能,但它们只会使游戏复杂化,并降低您获胜的几率。
|
||||
|
||||
3. Test the Machines Before you Play
|
||||
|
||||
在玩之前测试机器。
|
||||
|
||||
大多数赌场在其许多老虎机上提供免费试用,这意味着你可以先做测试五到十圈,让你更容易制定出一个行动计划,以便在使用真金白银的时候采取行动。
|
||||
|
||||
4. Use Cash Instead of Card
|
||||
|
||||
以现金代替信用卡。
|
||||
|
||||
赌场可以欺骗客户的一个窍门是,他们可以兑换零钱,这就是为什么赌场使用筹码而不是现金,玩家的借记卡/信用卡就像筹码一样。如果使用卡,玩家可能会忘记自己花了多少钱。只带一定数量的现金到赌场,购买一定数量的筹码,一旦损失殆尽后就离开。
|
||||
|
||||
5. Know When to Leave the Casino
|
||||
|
||||
知道什么时候离开赌场。
|
||||
|
||||
赌场里的老虎机注定会让人上瘾。赢钱时,你可能会想赢更多的钱;输钱时,你可能会想着把输的钱都赚回来。就和股票一样,如果没有止盈和止损目标,最后的结果差不多都是输光。
|
||||
|
||||
我们可以从中得到的指导思想是:
|
||||
|
||||
1. 先探索(exploration),看看哪个摇臂带来的回报最高。
|
||||
2. 后利用(exploitation),确定好第一条后,就可以“贪婪地”获得回报。
|
||||
3. 设置一个限制值来比较各自算法的优略,比如,比较 100 轮内哪个算法表现最好;扩大到 200 轮内又会有何变化?
|
||||
4. 不同的应用场景使用不同的算法。
|
||||
5. 设置合理的奖励值,便于比较算法性能。
|
||||
|
||||
|
||||
### 2.1.3 多臂赌博机研究平台
|
||||
|
||||
因为单臂的赌博机应用场景比较简单,所以在算法研究领域都是多臂赌博机平台,有以下几类:
|
||||
|
||||
- 随机多臂赌博机
|
||||
- 贝叶斯多臂赌博机
|
||||
- 上下文赌博机
|
||||
- 其它
|
||||
|
||||
注意,多臂赌博机不是单臂赌博机的简单集合,而是在内部有一定的关系。比如:两个单臂赌博机的得奖概率都是 0.2,那么一个双臂赌博机的两个拉杆臂,有可能其中一个设置为 0.1 的得奖概率,另外一个设置为 0.3。
|
||||
|
||||
下面我们来一一介绍。
|
||||
|
||||
#### 固定收益的多臂赌博机
|
||||
|
||||
如图 2.1.1 所示,假设是一个三臂赌博机,玩家投一次币(假设是一元钱),选择一个序号,拉动一次拉杆臂(或者按一次按钮)。
|
||||
|
||||
<center>
|
||||
|
||||
<img src='./img/mab-1.png'/>
|
||||
|
||||
图 2.1.1 固定收益的多臂赌博机
|
||||
</center>
|
||||
|
||||
假设玩家选择 1 号,则该臂机会以 $p_1=0.3$ 的概率回吐一元钱做为奖励,即 10 次中可能有 3 次回吐一元钱,其它 7 次没有奖励。另外两臂的回吐概率是 $p_2=0.5,p_3=0.8$,但是玩家在开始时并不知道这个信息,需要摸索。
|
||||
|
||||
这个应用场景相对简单,可以总结为:
|
||||
|
||||
- 收益固定为离散值 0/1,即伯努利分布。
|
||||
- 收益概率可以灵活设置。
|
||||
- 算法简单,运行速度快,见效快。
|
||||
- 初选者容易理解。
|
||||
- 有预设的上限可以参考:比如以图 2.1.1 的设置为例,玩 1000 次的最大收益应该是 800 元。
|
||||
- 衡量指标有限,不能体现复杂算法的优势。
|
||||
- 能体现客观世界(赌场)的真实情况,但是对算法研究没有帮助,所以不能应用于其它场景。
|
||||
|
||||
这种场景下,先玩 10 次,基本上就能知道 3 号臂是收益最高的,以后总拉动 3 号臂即可。
|
||||
|
||||
#### 分布收益的多臂赌博机
|
||||
|
||||
第二类研究平台如图 2.1.2 所示,每个臂的回报有正有负,而且不固定,也不是整数,而是有一个正态分布的。
|
||||
|
||||
<center>
|
||||
|
||||
<img src='./img/mab-2.png'/>
|
||||
|
||||
图 2.1.2 收益按概率分布的多臂赌博机
|
||||
</center>
|
||||
|
||||
依然假设只有三臂,每个拉杆臂的收益都是一个正态分布,方差为 1,均值有可能是 0,-1,+1。
|
||||
|
||||
举例来说,假设用户选择 1 号臂,进行 200 轮游戏的话,会看到如图 2.1.3 的奖励分布。
|
||||
|
||||
【代码位置】bandit_21_Armit.py
|
||||
|
||||
<center>
|
||||
|
||||
<img src='./img/mab-2-sample.png'/>
|
||||
|
||||
图 2.1.3 收益按概率分布的多臂赌博机的 1 号臂
|
||||
</center>
|
||||
|
||||
1 号臂的情况是一个正态分布:
|
||||
|
||||
- 奖励(即收益)不是正整数,而是浮点数,有正有负;
|
||||
- 奖励在 0 附近的次数最多,占据了 70% 左右;
|
||||
- 最大值,有机会得到 +2 的奖励或更大,次数极少;
|
||||
- 最小值,有机会得到 -2 的奖励或更小,次数极少。
|
||||
|
||||
2 号和 3 号臂就是 1 号臂的分布情况分别向左或右移动一个单位的正态分布。
|
||||
|
||||
|
||||
玩家事先肯定不知道图 2.1.4 所示的概率分布,现在我们假设:
|
||||
|
||||
- 玩家第一次玩,选择了 2 号臂(蓝色虚线),得到了 2 的奖励;
|
||||
- 第二次选择了 1 号臂(红色虚线),得到了 1 的奖励;
|
||||
- 第三次选择了 3 号臂(绿色虚线),得到了 0 的奖励;
|
||||
|
||||
那么该玩家如何选择后面几轮的玩法?
|
||||
|
||||
【代码位置】bandit_21_Armit.py
|
||||
|
||||
<center>
|
||||
|
||||
<img src='./img/mab-2-sample2.png'/>
|
||||
|
||||
图 2.1.4 收益按概率分布的多臂赌博机的 1 号臂
|
||||
</center>
|
||||
|
||||
我们从上帝视角看,显然 3 号臂是最好的,但是在前三轮,很不巧,玩家遇到了相反的情况。如果玩家就此认定 2 号臂最好,那么最后将会输的很惨。所以,如何尽快地确定哪个臂最好,就是这一类多臂赌博机要解决的问题。
|
||||
|
||||
**在本章中,这类赌博机是我们研究的目标。**
|
||||
|
||||
|
||||
#### 上下文赌博机(Contextual Bandits)
|
||||
|
||||
在上面介绍的普通多臂赌博机中,玩家面对的是一个“面部毫无感情而且内心似铁的冷冰冰的家伙”,也就是说:
|
||||
|
||||
1. 内置的的奖励机制预先设置不会变化;
|
||||
2. 没有任何外在的特征可以供玩家识别。
|
||||
|
||||
上下文赌博机(Contextual Bandits)也是多臂赌博机的一种,但是,上下文赌博机提供了一种外在的状态,比如:赌博机上有一个灯,平时不亮,当它亮起时,表示赌博机内部运行了不一样的奖励机制,鼓励玩家多下注赢大奖,或者是选择一个特殊的拉杆臂。
|
||||
|
||||
图 2.1.5 从概念上展示了上下文(多臂)赌博机与多臂赌博机的不同。
|
||||
|
||||
<center>
|
||||
|
||||
<img src='./img/mab-cmab.png'/>
|
||||
|
||||
图 2.1.5 普通多臂赌博机与上下文赌博机
|
||||
(左图:普通多臂赌博机;右图:上下文赌博机)
|
||||
</center>
|
||||
|
||||
可以理解为上下文赌博机有“表情”了,即图 2.1.5 中的“状态”,玩家可以通过观察外在状态来决定选择哪个臂,因为状态也和奖励有关联,相当于是给了玩家一个线索。但是,玩家的动作并不会改变赌博机的状态,状态完全由赌博机内部决定并展示出来,不受外界影响。
|
||||
|
||||
#### 其它变种
|
||||
|
||||
多臂赌博机研究平台的其它一些变种有:
|
||||
|
||||
- Adversarial bandit,对抗式赌博机,赌博机会有一些智能设计来针对你的习惯做一下改变,让玩家不能沿用以前的经验继续获利。
|
||||
- Infinite-armed bandit,无限臂赌博机,玩家可以选择一个非离散值的臂,比如 1.5,2.1 等等,类似于在一个连续函数上随机任意选择一个自变量。
|
||||
- Lipschitz bandit,利普希茨赌博机,就是具有利普希茨奖励属性的赌博机平台。
|
||||
- Non-stationary bandit,非平稳状态的赌博机,奖励并不会按照预先设计的分布来执行,而是在一定范围内有一些波动。
|
||||
- Dueling bandit,决斗式赌博机,玩家一次可以拉动两个臂,得到的信息是哪一个臂获得的奖励更多。
|
||||
- Collaborative bandit,协作式赌博机,是上下文赌博机的一种,相关文献介绍的不多。
|
||||
- Combinatorial bandit,组合式赌博机,每次玩家必须拉动几个拉杆臂,而不是单独的一个臂。
|
||||
|
||||
上述这些变种的多臂赌博机,与强化学习的关系已经不是很大了,所以我们在本书中不会去研究它们的细节。
|
||||
|
||||
**即使学习玩了本章的内容,也不要去赌博,因为赌场都是骗人的。**
|
|
@ -0,0 +1,187 @@
|
|||
## 2.2 多臂赌博机建模
|
||||
|
||||
### 2.2.1
|
||||
|
||||
下面再描述一下我们将要研究的多臂赌博机平台的细节。
|
||||
|
||||
首先看单臂的情况,如图 2.2.1 所示。
|
||||
|
||||
<center>
|
||||
|
||||
<img src='./img/One-Arm.png'/>
|
||||
|
||||
图 2.2.1 多臂赌博机的一个臂的奖励情况
|
||||
(左图:奖励及其分布次数;右图:用小提琴图表示的)
|
||||
</center>
|
||||
|
||||
- 每个臂的奖励都是一个正态分布,均值为 0,方差为 1,样本数量为 2000。
|
||||
- 最小值和最大值都是采样的结果,基本是在 $\pm3$ 左右。
|
||||
- 均值在 0 左右,偏差很小。
|
||||
- 小提琴蓝色区域的宽度表示样本的密度。
|
||||
- 上下四分位和置信区间在这个问题中暂时用不上。
|
||||
|
||||
生成原始数据的代码如下:
|
||||
|
||||
【代码位置】bandit_22_ArmBandits.py
|
||||
|
||||
```python
|
||||
# 生成原始数据
|
||||
num_arm = 10
|
||||
num_data = 2000
|
||||
np.random.seed(5)
|
||||
k_reward_dist = np.random.randn(num_data, num_arm) # num_arm 放在后面是为了可以做加法
|
||||
print("原始均值=", np.round(np.mean(k_reward_dist, axis=0),3))
|
||||
draw_one_arm(k_reward_dist[:,0])
|
||||
```
|
||||
|
||||
得到原始数据的均值为:
|
||||
|
||||
```
|
||||
原始均值= [ 0.018 0.019 0.004 -0.02 -0.005 0.004 -0.011 -0.005 0.027 -0.015]
|
||||
```
|
||||
|
||||
可以看到基本接近于 0。如果 10 个臂返回的奖励值都类似,就没有可比性了,所以我们要人为地定义一个正态分布的均值:
|
||||
|
||||
```python
|
||||
# 生成期望均值
|
||||
reward_mu = np.random.randn(num_arm)
|
||||
print("期望平均回报=", np.round(reward_mu,3))
|
||||
draw_mu(reward_mu)
|
||||
```
|
||||
|
||||
它的期望平均回报为:
|
||||
|
||||
```
|
||||
期望平均回报= [-0.418 0.379 0.792 -0.951 -0.536 1.202 -1. -0.306 -0.69 0.231]
|
||||
```
|
||||
|
||||
绘制在图 2.2.2 中。
|
||||
|
||||
<center>
|
||||
|
||||
<img src='./img/K-arm-expection.png'/>
|
||||
|
||||
图 2.2.2 多臂赌博机期望的奖励均值分布
|
||||
|
||||
</center>
|
||||
|
||||
|
||||
然后把期望均值叠加在原始数据上,生成奖励的期望数据:
|
||||
|
||||
【代码位置】bandit_22_ArmBandits.py
|
||||
|
||||
```python
|
||||
# 生成期望数据(=原始数据+期望均值)
|
||||
k_reward_dist_mu = reward_mu + k_reward_dist
|
||||
print("实际均值=", np.round(np.mean(k_reward_dist_mu, axis=0),3))
|
||||
```
|
||||
|
||||
实际均值如下:
|
||||
|
||||
```
|
||||
实际均值= [-0.4 0.398 0.797 -0.971 -0.541 1.206 -1.011 -0.311 -0.663 0.216]
|
||||
```
|
||||
|
||||
$$
|
||||
实际均值 = 原始均值 + 期望均值
|
||||
$$
|
||||
|
||||
|
||||
从图 2.2.2 可以看到,期望均值是随机的,由于随机种子可能不同,我们不能保证最好的选择一定是 5 号臂,所以,我们给这 10 组数据按照期望均值重新排个序,这样在出统计图时就能很容易地知道谁大谁小。
|
||||
|
||||
【代码位置】bandit_22_ArmBandits.py
|
||||
|
||||
```python
|
||||
# 按均值排序
|
||||
reward_mu_sort_arg = np.argsort(reward_mu) # 对期望均值排序(并不实际排序,而是返回序号)
|
||||
k_reward_dist_mu_sort = np.zeros_like(k_reward_dist_mu)
|
||||
for i in range(10):
|
||||
idx = reward_mu_sort_arg[i] # 第i个臂对应的新序号是idx
|
||||
k_reward_dist_mu_sort[:,i] = k_reward_dist_mu[:,idx] # 重新排序
|
||||
draw_k_arm(k_reward_dist_mu, k_reward_dist_mu_sort)
|
||||
```
|
||||
|
||||
绘制出图 2.2.3。
|
||||
|
||||
<center>
|
||||
|
||||
<img src='./img/K-arm-bandits.png'/>
|
||||
|
||||
图 2.2.3 多臂赌博机奖励分布
|
||||
(上图:10臂赌博机期望数据分布;下图:10臂赌博机按均值排序分布)
|
||||
</center>
|
||||
|
||||
图 2.2.3 的上图,是按期望均值的原始顺序排列的 10 臂赌博机的奖励分布情况,下图是排序后的分布情况,比如:上图中的 1 号臂,在下图中排在 5 号位置,这样一来,10 个臂的回报情况是从左到右依次变好。当然,我们在做算法的时候,要假装不知道 10 号臂是最佳选择,而是盲猜。
|
||||
|
||||
### 2.2.2 如何定义最好的动作
|
||||
|
||||
我们假设只有三个动作,而且只玩 10 轮,记录的结果如表 2.2.1 所示。
|
||||
|
||||
表 2.2.1 一个三臂赌博机的 10 轮的模拟结果
|
||||
|
||||
|轮数 $t\to$|1|2|3|4|5|6|7|8|9|10|
|
||||
|-|-|-|-|-|-|-|-|-|-|-|
|
||||
|动作 $A_t$|$a_1$|$a_1$|$a_2$|$a_2$|$a_2$|$a_3$|$a_1$|$a_1$|$a_2$|$a_3$|
|
||||
|收益 $R_t$|1.0|1.2|0.9|1.1|0.8|1.3|1.2|1.1|0.9|1.0|
|
||||
|
||||
表 2.2.1 中,第二行表示玩家(算法)选择了哪个臂(动作)$a_i, \ i=1,2,3$;第三行表示得到的奖励(只是一些模拟数据)。很自然地,可以通过奖励的平均值来衡量三个动作的优略:
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
q(a_1)&=\frac{1.0+1.2+1.2+1.1}{4}=1.125
|
||||
\\
|
||||
q(a_2)&=\frac{0.9+1.1+0.8+0.9}{4}=0.924
|
||||
\\
|
||||
q(a_3)&=\frac{1.3+1.0}{2}=1.15
|
||||
\end{aligned}
|
||||
\tag{2.2.1}
|
||||
$$
|
||||
|
||||
式(2.2.1)告诉我们,在只玩 10 轮的经验下,$q(a_1)$ 的值最大,$a_1$ 就是最好的动作。
|
||||
|
||||
理论上,可以定义某个动作的价值为 $q_*$,则有:
|
||||
|
||||
$$
|
||||
q_*(a) \doteq \mathbb E [R_t|A_t=a] \tag{2.2.2}
|
||||
$$
|
||||
|
||||
意思是在任意时刻 $t$,动作 $a$ 的价值是执行该动作获得的收益的期望值。而期望的计算方式如式(2.2.1)所示,可以泛化为:
|
||||
|
||||
$$
|
||||
Q_n(a)=\frac{动作 a 的收益总和}{执行动作 a 的次数 n} \tag{2.2.3}
|
||||
$$
|
||||
|
||||
$n$ 表示动作被选择的次数,当 $n$ 足够大时,可以保证每个动作都被采样到足够多的次数,然后求平均值,称为**采样平均**,这样计算出来的 $Q_n(a)$ 将会收敛到 $q_*(a)$。大写的 $Q$ 表示泛化,小写的 $q$ 表示实例化到某个具体动作。
|
||||
|
||||
按照式(2.2.3)的规则,式(2.2.1)的三个 $q$ 值可以分别写成 $q_4(a_1),q_4(a_2),q_2(a_3)$。
|
||||
|
||||
在式(2.2.1)中,当前状态对于 $a_3$ 来说是 $n=2$。假设 $t=11$ 时选择了 $a_3$,则 $n=3$,$q(a_1),q(a_2)$ 的值不会变化,$q(a_3)$ 的值会被重新计算,由$q_2(a_3)$ 变成 $q_3(a_3)$。介绍一个小技巧,计算 $q_3(a_3)$ 时可以这样做:
|
||||
|
||||
$$
|
||||
q_{3}(a_3)= \frac{1}{3}[2q_{2}(a_3) + R_{3}]=\frac{1}{3}[3q_{2}(a_3)-q_{2}+R_{3}]=q_{2}(a_3)+\frac{1}{3}[R_{3}-q_{2}(a_3)]
|
||||
\tag{2.2.4}
|
||||
$$
|
||||
|
||||
在式(2.2.4)中:
|
||||
- $q_2$ 表示当前的 $a_3$ 动作价值;
|
||||
- $q_3$ 表示第三次选择 $a_3$ 后的动作价值;
|
||||
- $R_3$ 表示第三次执行 $a_3$ 后的收益。
|
||||
|
||||
如果泛化到一般情况,针对某个动作如果第 $n$ 次被选到,有:
|
||||
|
||||
$$
|
||||
Q_{n}=Q_{n-1}+\frac{1}{n}[R_n - Q_{n-1}] \tag{2.2.5}
|
||||
$$
|
||||
|
||||
式(2.2.5)的好处是当历史数据很多时,我们只需要记录 $n, Q_{n-1}$ 两个变量,得到 $R_n$ 后即可计算出 $Q_n$ 来。另外,式(2.2.5)的一个泛化形式是:
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
Q_{n}&=Q_{n-1}+\alpha [R_n - Q_{n-1}]
|
||||
\end{aligned}
|
||||
\tag{2.2.6}
|
||||
$$
|
||||
|
||||
步长参数 $\alpha \in (0,1]$ 是一个常数,而不是像式(2.2.5)的 $\frac{1}{n}$ 那样随着采样次数的增加而降低。在解决一个非平稳问题的时候,即无法用 $\frac{1}{n}$ 来做平均值计算的时候,可以使用式(2.2.6)。
|
||||
|
||||
在 2.4 节贪心法中,会使用式(2.2.5);在 2.5 节梯度上升法中,会使用式(2.2.6)。
|
|
@ -0,0 +1,204 @@
|
|||
## 2.3 多臂赌博机基类实现
|
||||
|
||||
### 2.3.1 基类实现
|
||||
|
||||
定义 K 臂赌博机的基类。
|
||||
|
||||
【代码位置】bandit_23_Base.py
|
||||
|
||||
```python
|
||||
class KArmBandit(object):
|
||||
def __init__(self, k_arms=10, mu=0, sigma=1):
|
||||
self.k_arms = k_arms # 臂数
|
||||
self.mu = mu # 奖励均值
|
||||
self.sigma = sigma # 奖励方差
|
||||
# 初始化 k 个 arm 的期望收益,并排序,但算法不要依赖这个排序
|
||||
self.E = np.sort(self.sigma * np.random.randn(self.k_arms) + self.mu)
|
||||
```
|
||||
|
||||
清零操作,用于反复执行算法,算法开始时调用此方法一次。
|
||||
|
||||
```python
|
||||
def reset(self):
|
||||
# 初始化 k 个 arm 的动作估值 Q_n 为 0
|
||||
self.Q = np.zeros(self.k_arms)
|
||||
# 保存每个 arm 被选择的次数 n
|
||||
self.action_count = np.zeros(self.k_arms, dtype=int)
|
||||
self.step = 0 # 总步数,用于统计
|
||||
```
|
||||
|
||||
算法要重载这个方法,用于选择动作。
|
||||
|
||||
```python
|
||||
# 得到下一步的动作(下一步要使用哪个arm,由算法决定)
|
||||
def select_action(self):
|
||||
pass
|
||||
```
|
||||
|
||||
执行动作,获得奖励,在算法中的每一步需要调用此方法。
|
||||
|
||||
```python
|
||||
# 执行指定的动作,并返回此次的奖励
|
||||
def pull_arm(self, action):
|
||||
reward = np.random.randn() + self.__expection[action]
|
||||
return reward
|
||||
```
|
||||
|
||||
更新动作价值,需要在 pull_arm() 之后立刻调用。
|
||||
|
||||
```python
|
||||
# 更新 q_n
|
||||
def update_Q(self, action, reward):
|
||||
# 总次数(time)
|
||||
self.step += 1
|
||||
# 动作次数(action_count)
|
||||
self.action_count[action] += 1
|
||||
# 计算动作价值,采样平均
|
||||
self.Q[action] += (reward - self.Q[action]) / self.action_count[action]
|
||||
```
|
||||
|
||||
### 2.3.2 用随机算法做基本测试
|
||||
|
||||
【算法 2.3.1】随机算法。
|
||||
|
||||
---
|
||||
|
||||
初始化赌博机 k_arms=10
|
||||
初始化奖励分布
|
||||
$r \leftarrow 0$,循环 2000 次:
|
||||
动作集 $A$ 的价值 $Q(A)=0$
|
||||
每个动作被执行的次数计数器 $n \leftarrow 0$
|
||||
$t \leftarrow 0$,迭代 1000 轮:
|
||||
随机选择动作 $a$
|
||||
执行 $a$ 得到奖励 $r$
|
||||
$N(a) \leftarrow N(a)+1$
|
||||
更新动作价值 $Q(a) \leftarrow Q(a)+\frac{1}{N(a)}[r - Q(a)]$
|
||||
$t \leftarrow t+1$
|
||||
$r \leftarrow r+1$
|
||||
|
||||
---
|
||||
|
||||
代码实现片段如下:
|
||||
|
||||
```python
|
||||
bandit = KArmBandit(k_arms)
|
||||
# 运行 2000 次取平均值
|
||||
for r in trange(runs):
|
||||
# 每次run都清零计算 q 用的统计数据
|
||||
bandit.reset()
|
||||
# 训练 1000 轮
|
||||
for t in range(steps):
|
||||
action = np.random.randint(k_arms)
|
||||
reward = bandit.pull_arm(action)
|
||||
bandit.update_Q(action, reward)
|
||||
```
|
||||
|
||||
得到基本的统计结果如图 2.3.1。
|
||||
|
||||
<center>
|
||||
<img src='./img/10-mab-testing-wrong.png'/>
|
||||
|
||||
图 2.3.1 10 臂赌博机随机动作选择测试结果
|
||||
(左图:2000 次的平均奖励;右图:10 个动作的被选择次数)
|
||||
</center>
|
||||
|
||||
从图 2.3.1 右图中可以看到,10 个动作(从 0 到 9)的被选择次数是非常接近的,都是 100 次左右(1000/10=100);而左图中可以看到同一个 10 臂赌博机运行 2000 次(每次 1000 轮)后的平均奖励值,结果居然不是 0 或者接近 0,而是 0.23 附近,这是为什么呢?
|
||||
|
||||
我们不妨打印出 self.E 来观察:
|
||||
|
||||
```
|
||||
10 臂的奖励均值:[-0.909 -0.592 -0.331 -0.330 -0.252 0.109 0.188 0.441 1.582 2.431]
|
||||
奖励均值的均值:0.2337991556306549
|
||||
```
|
||||
|
||||
这 10 个值看上去分布还比较均匀,但是它们的均值是 0.23,而不是 0,也就是说在正态分布中没有取到中点上,而是偏右了,相当于产生了偏差,这是不满足多臂赌博机测试条件的。
|
||||
|
||||
那么如何得到没有偏差的数据呢?用 np.random.randn(self.k_arms) 函数,当 self.k_arms=10 的时候,是没有办法得到的,只能手工调整数据。
|
||||
|
||||
等一等!我们还有一个办法:由于需要运行 2000 次求平均,所以如果每次运行我们都初始化一个不同赌博机,则这 2000 个赌博机的均值就可以接近于 0 了。所以,我们需要改一下前面两段代码:
|
||||
|
||||
```python
|
||||
class KArmBandit(object):
|
||||
def __init__(self, k_arms=10, mu=0, sigma=1): # 臂数,奖励分布均值,奖励分布方差
|
||||
self.k_arms = k_arms # 臂数
|
||||
self.mu = mu # 奖励均值
|
||||
self.sigma = sigma # 奖励方差
|
||||
|
||||
def reset(self):
|
||||
# 初始化 k 个 arm 的期望收益,并排序,但算法不要依赖这个排序
|
||||
self.E = np.sort(self.sigma * np.random.randn(self.k_arms) + self.mu)
|
||||
......
|
||||
```
|
||||
|
||||
把生成 self.__expection 的生成代码放到 reset() 函数中,就可以达到上面的目的。
|
||||
|
||||
相应地,算法 2.3.1 也要有所调整:
|
||||
|
||||
【算法 2.3.2】正确初始化的随机算法。
|
||||
|
||||
---
|
||||
|
||||
初始化 KArmBandit(k_arms=10)
|
||||
$r \leftarrow 0$,循环 2000 次:
|
||||
初始化奖励分布(注意这里与算法 2.3.1 不同)
|
||||
动作集 $A$ 的价值 $Q(A)=0$
|
||||
每个动作被执行的次数计数器 $n \leftarrow 0$(一共有10个计数器)
|
||||
$t \leftarrow 0$,迭代 1000 步:
|
||||
随机选择动作 $a$
|
||||
第 $n$ 次执行 $a$ 得到奖励 $r_n$
|
||||
针对动作 $a$ 的计数器 $n \leftarrow n+1$
|
||||
更新动作价值 $q_{n}(a)=q_{n-1}(a)+\frac{1}{n}[r_n - q_{n-1}(a)]$
|
||||
$t \leftarrow t+1$
|
||||
$r \leftarrow r+1$
|
||||
|
||||
---
|
||||
|
||||
再试一遍:
|
||||
|
||||
<center>
|
||||
<img src='./img/10-mab-testing-correct.png'/>
|
||||
|
||||
图 2.3.2 正确设置的 10 臂赌博机随机动作选择测试结果
|
||||
(左图:2000 次的平均奖励;右图:10 个动作的被选择次数)
|
||||
</center>
|
||||
|
||||
这次可以看到图 2.3.2 左图中均值接近于 0 的奖励曲线了。
|
||||
|
||||
还有几个部分要简单说明
|
||||
|
||||
类函数 simulate(),执行【算法 2.3.2】:
|
||||
|
||||
```python
|
||||
def simulate(self, runs, steps):
|
||||
# 记录历史 reward,便于后面统计,每一轮每一步
|
||||
rewards = np.zeros(shape=(runs, steps))
|
||||
for r in trange(runs):
|
||||
# 每次run都清零计算 q 用的统计数据,并重新初始化奖励均值
|
||||
self.reset()
|
||||
# 测试 time 次
|
||||
for s in range(steps):
|
||||
action = self.select_action()
|
||||
reward = self.pull_arm(action)
|
||||
self.update_Q(action, reward)
|
||||
rewards[r, s] = reward
|
||||
return rewards
|
||||
```
|
||||
|
||||
最后返回 rewards 数组即可。但是由于需要多方面统计,所以在实际的代码中还增加了关于动作的两个统计数据数组。
|
||||
|
||||
函数 mp_simulate(),开多进程执行上面的 simulate(),每个赌博机占用一个进程,可以让多个不同参数的赌博机同时训练。
|
||||
|
||||
```python
|
||||
# 输入参数:赌博机列表,臂数,运行次数,训练轮数,图例文字,图标题
|
||||
def mp_simulate(bandits, k_arms, runs, steps, labels, title):
|
||||
......
|
||||
pool = mp.Pool(processes=4)
|
||||
results = []
|
||||
for i, bandit in enumerate(bandits):
|
||||
results.append(pool.apply_async(bandit.simulate, args=(runs,steps,)))
|
||||
pool.close()
|
||||
pool.join()
|
||||
......
|
||||
```
|
||||
|
||||
用户可以根据自己的机器的 CPU 个数来调整 processes 的值。由于我们每次只运行 4 个参数的比较,所以设置为大于 4 的值没有意义。
|
|
@ -0,0 +1,239 @@
|
|||
|
||||
## 4.4 两种贪心算法
|
||||
|
||||
### 4.4.1 先试探后贪心
|
||||
|
||||
由于玩家不知道 10 个臂的收益情况,所以先要在每个臂上试探几次,找到最佳的臂,然后持续拉动它就可以了。
|
||||
|
||||
#### 算法描述
|
||||
|
||||
【算法 4.4.1】贪心算法
|
||||
|
||||
---
|
||||
初始化:$T \leftarrow$ 试探次数
|
||||
$r \leftarrow 0$,循环 2000 次:
|
||||
初始化奖励分布和计数器,动作集 $A$ 的价值 $Q(A)=0$
|
||||
$t \leftarrow 0$,迭代 1000 步:
|
||||
如果 $t < T$,随机选择动作 $a = random\{A\}$;否则 $a = \argmax_a \ Q(A)$
|
||||
执行 $a$ 得到奖励 $r$
|
||||
$N(a) \leftarrow N(a)+1$
|
||||
更新动作价值 $Q(a) \leftarrow Q(a)+\frac{1}{N(a)}[r-Q(a)]$
|
||||
$t \leftarrow t+1$
|
||||
$r \leftarrow r+1$
|
||||
|
||||
---
|
||||
|
||||
在本例中动作集空间为 10,如果只试探 10 次,一是可能没有选到所有的动作进行试探,二是即使选到了,由于概率分布,该动作的收益在当时的表现不佳,都有可能错过最佳动作的确定。
|
||||
|
||||
如果试探次数足够多,就大概率可以找到最佳动作。找到合适的试探次数参数,是我们的目标。
|
||||
|
||||
#### 算法实现
|
||||
|
||||
【代码位置】bandit_24_Greedy.py
|
||||
|
||||
```python
|
||||
class KAB_Greedy(kab_base.KArmBandit):
|
||||
def __init__(self, k_arms=10, try_steps=10):
|
||||
super().__init__(k_arms=k_arms)
|
||||
self.try_steps = try_steps # 试探次数
|
||||
|
||||
def select_action(self):
|
||||
if (self.step < self.try_steps):
|
||||
action = np.random.randint(self.k_arms) # 随机选择动作
|
||||
else:
|
||||
action = np.argmax(self.Q) # 贪心选择目前最好的动作
|
||||
return action
|
||||
```
|
||||
|
||||
- 试探次数 $T$ 定义为 self.try_steps;
|
||||
- 时间步 $t$ 定义为 self.steps;
|
||||
- $t \leftarrow t+1,r \leftarrow r+1$ 的操作在基类 KArmBandit 中实现。
|
||||
|
||||
#### 参数设置
|
||||
|
||||
设置 try_steps 分别为 10、20、40、80 来做四组试验:
|
||||
|
||||
```python
|
||||
if __name__ == "__main__":
|
||||
runs = 2000
|
||||
steps = 1000
|
||||
k_arms = 10
|
||||
|
||||
bandits:kab_base.KArmBandit = []
|
||||
bandits.append(KAB_Greedy(k_arms, try_steps=10))
|
||||
bandits.append(KAB_Greedy(k_arms, try_steps=20))
|
||||
bandits.append(KAB_Greedy(k_arms, try_steps=40))
|
||||
bandits.append(KAB_Greedy(k_arms, try_steps=80))
|
||||
|
||||
labels = [
|
||||
'Greedy(10)',
|
||||
'Greedy(20)',
|
||||
'Greedy(40)',
|
||||
'Greedy(80)'
|
||||
]
|
||||
title = "Greedy"
|
||||
kab_base.mp_simulate(bandits, k_arms, runs, steps, labels, title)
|
||||
```
|
||||
|
||||
#### 结果分析
|
||||
|
||||
10 个动作,每次迭代 1000 步,运行 2000 次取平均。注意,是纵向平均,即 2000 次内有 2000 个第 1 步,2000 个第 2 步,......,然后对每一步取平均,分母为 2000。
|
||||
|
||||
<center>
|
||||
<img src='./img/algo-greedy.png'/>
|
||||
|
||||
图 2.4.1 贪心算法
|
||||
</center>
|
||||
|
||||
读者第一次看到图 2.4.1 这张图,在本章中全是相同模板的图,所以有必要详细说明一下。
|
||||
|
||||
- 第一部分:0-100 步内的平均收益。
|
||||
|
||||
这一部分衡量算法的**探索效率**。
|
||||
|
||||
比如红色线条,参数为 Greedy(80),表示 try_steps=80,这个参数设置可以保证后期取得最好的效果,但是前期 80 步内几乎没有收益,而在有些实际应用中,希望马上能看到算法效果,这个参数就不合适了,20 或 40 可能比较合适。
|
||||
|
||||
这一部分的图例是四种参数的总平均收益,其中 Greedy(40) 最好,为 1.409。
|
||||
|
||||
- 第二部分:300-500 步内的平均收益。
|
||||
|
||||
这一部分衡量算法的**收敛效率**。
|
||||
|
||||
所谓收敛,就是看这一部分的曲线的趋势,是继续向上攀升呢,还是一直横向振动。如果是横向振动,说明收敛的速度还不错;如果是向上攀升,说明还没有收敛,有继续提高的空间。
|
||||
|
||||
- 第三部分:700-900 步内的平均收益。
|
||||
|
||||
这一部分衡量算法的**利用能力**。
|
||||
|
||||
如果持续在高位横盘,就是有很好的利用能力;如果持续在低位横盘,说明算法能力有限或者参数设置不当,导致前期的最佳动作判断不准确。
|
||||
|
||||
- 第四部分:0-1000 步内的最优动作选择比例。
|
||||
|
||||
这一部分从另外一个角度衡量算法的**性能**和**稳定性**。
|
||||
|
||||
在前三部分中都是用平均收益来衡量,这一部分是来考察最佳动作的选择比例,其它 9 个动作忽略。值越高越好,最大值为 1。它是在 0-1000 区间内的平均数,以红色曲线为例,在该参数设置下,在大概 400 步后,其实已经非常稳定地选择了最佳动作。
|
||||
|
||||
在这一部分中,
|
||||
|
||||
|
||||
- 第五部分:所有动作被选择的次数。
|
||||
|
||||
这一部分衡量算法在每个动作上的**探索与利用**次数。
|
||||
|
||||
展示了算法的 4 个不同的参数设置下的每个动作的执行次数,10 个柱子上的数字相加应该等于 1000。由于我们在 KArmBandit() 类中“偷偷地”对十个动作的收益期望值进行了由小到大的排序,所以,四个柱状图都是梯形的左低右高分布,过渡过程越陡峭越好,靠右端的柱子越高越好。所以红色和绿色较好。
|
||||
|
||||
|
||||
#### 关于探索与利用(EE - Exploration and Exploitation)
|
||||
|
||||
**探索与利用**,在强化学习中是一个永久的话题,请读者牢记这一点。
|
||||
|
||||
首先,前期探索是为了后期利用,尤其是对于贪婪算法来说,前期探索的结果正确的话,后期利用时就可以最大限度地获利。
|
||||
|
||||
以图 2.4.1 中的蓝色曲线来说:
|
||||
|
||||
- 它在前期探索的步数非常少(只有10步),就立刻开始利用,实际上它还没有找到最佳动作;
|
||||
- 从第一部分来看,它可以很快地获利;
|
||||
- 但是从第二部分和第三部分来看,明显后劲不足;
|
||||
- 第四部分显示了它的最佳动作选择率只有 52% 左右;
|
||||
- 第五部分也印证了它在前 9 个非最佳动作上浪费了很多时间。
|
||||
|
||||
在互联网应用中,敏捷开发是大家所推崇的做法,但是每一个好的功能都是经过前期的长时间用户调研后才开始开发应用的,否则就是浪费开发资源。一旦这些功能部署出去,开发者就可以很舒服地获利了。相反,一些急躁的决策者,闭门造车地臆想出了一些功能,快速开发上线,发现用户不买账,然后又不断地修改,造成开发资源的持续浪费,却始终没有获利。
|
||||
|
||||
|
||||
其次,探索也要适可而止,不要花费太多的资源,尤其是在资源有限的情况下。
|
||||
|
||||
- 比如参数 Greedy(80)(红色),在第四部分中比其它三个参数好很多,第五部分也是如此,但是在第一部分尝试次数太多,导致了它在 1000 步内的平均收益为 1.379;
|
||||
- 再看参数 Greedy(40)(蓝色),首先看它的平均收益为 1.409,在四个参数中最好。虽然它只探索了 40 步就开始了利用,但是在 1000 步这个限定的步长内,它还是超过了看起来比它好的 Greedy(80)。
|
||||
- Greedy(10) 和 Greedy(20) 这两个参数,匆忙结束了探索,就想立刻获得回报,“心急吃不上热豆腐”,平均收益只有 1.268,1.361。
|
||||
|
||||
|
||||
### 4.4.2 $\epsilon$-贪心算法
|
||||
|
||||
不同于上面的“先探索后利用”的方式,$\epsilon$-贪心算法在任何时候都保留着探索的机会,这个机会用 $\epsilon$ 的值来控制。
|
||||
|
||||
#### 算法描述
|
||||
|
||||
【算法 4.4.2】$\epsilon$-贪心算法
|
||||
|
||||
---
|
||||
初始化:$\epsilon \leftarrow$ 探索概率 $\in [0,1]$
|
||||
$r \leftarrow 0$,循环 2000 次:
|
||||
初始化奖励分布和计数器,动作集 $A$ 的价值 $Q(A)=0$
|
||||
$t \leftarrow 0$,迭代 1000 步:
|
||||
得到一个随机数 $m \in [0,1]$
|
||||
如果 $m < \epsilon$,随机选择动作 $a = random\{A\}$;否则 $a = \argmax_a \ Q(A)$
|
||||
执行 $a$ 得到奖励 $r$
|
||||
$N(a) \leftarrow N(a)+1$
|
||||
更新动作价值 $Q(a) \leftarrow Q(a)+\frac{1}{N(a)}[r-Q(a)]$
|
||||
$t \leftarrow t+1$
|
||||
$r \leftarrow r+1$
|
||||
|
||||
---
|
||||
|
||||
与算法 2.4.1 不同的地方在于动作选择策略取决于输入参数 $\epsilon$,通常把它设置为 0.1 或更小。每次迭代都先取一个随机数:
|
||||
|
||||
- 如果它大于 $\epsilon$,则贪心选择目前 Q 值最大的动作——利用;
|
||||
- 如果它小于 $\epsilon$,则随机选择动作——探索。
|
||||
|
||||
所以,可以预想到,$\epsilon$ 越大,探索的机会越多,利用的机会就越少。找到合适的探索概率参数,是我们的目标。
|
||||
|
||||
#### 算法实现
|
||||
|
||||
【代码位置】bandit_24_E_Greedy.py
|
||||
|
||||
```python
|
||||
class KAB_E_Greedy(kab_base.KArmBandit):
|
||||
def __init__(self, k_arms=10, epsilon=0.1):
|
||||
super().__init__(k_arms=k_arms)
|
||||
self.epsilon = epsilon # 探索概率
|
||||
|
||||
def select_action(self):
|
||||
if (np.random.random_sample() < self.epsilon):
|
||||
action = np.random.randint(self.k_arms) # 随机选择动作进行探索
|
||||
else:
|
||||
action = np.argmax(self.Q) # 贪心选择目前最好的动作进行利用
|
||||
return action
|
||||
```
|
||||
#### 参数设置
|
||||
|
||||
设置 $\epsilon$ 分别为 0.01、0.05、0.10、0.20 来做四组试验:
|
||||
|
||||
```python
|
||||
......
|
||||
bandits.append(KAB_E_Greedy(k_arms, epsilon=0.01))
|
||||
bandits.append(KAB_E_Greedy(k_arms, epsilon=0.05))
|
||||
bandits.append(KAB_E_Greedy(k_arms, epsilon=0.10))
|
||||
bandits.append(KAB_E_Greedy(k_arms, epsilon=0.20))
|
||||
......
|
||||
```
|
||||
|
||||
有的读者可能会有疑问,为什么选择这四个参数而不是 0.3、0.4 等等?因为笔者已经事先做了一些试验,得到了一个最佳范围。读者也可以自行试验,间距从大到小,一步步来得到最佳范围,最后要保证第一个和第四个参数的结果比第二个和第三个参数的结果要差,就说明已经达到目的了。
|
||||
|
||||
#### 结果分析
|
||||
|
||||
同样是 10 个动作,每次迭代 1000 步,运行 2000 次取平均。
|
||||
|
||||
<center>
|
||||
<img src='./img/algo-E-greedy.png'/>
|
||||
|
||||
图 2.4.2 $\epsilon$-贪心算法
|
||||
</center>
|
||||
|
||||
从图 2.4.2 来看,
|
||||
|
||||
- 参数 $\epsilon=0.01$
|
||||
|
||||
探索的机会太少,所以一直到了 1000 步时,平均收益还处于上升状态,意味着还在继续强化对于最佳动作的判断。
|
||||
|
||||
- 参数 $\epsilon=0.20$
|
||||
|
||||
探索的机会太多,在已经得到了最佳动作后,还在继续探索。看第四部分的红色柱图,前 8 个非最佳动作的选择次数比绿色柱图要多,浪费了很多利用的机会。
|
||||
|
||||
- 参数 $\epsilon=0.10$
|
||||
|
||||
最佳参数,平均收益可以达到 1.282,但是比算法 4.4.1 的最大值 1.409 要小,原因是 1000x0.1=100,即花了 100 步做探索,而算法 4.4.1 中的最佳参数是 40 步。
|
||||
|
||||
- 参数 $\epsilon=0.05$
|
||||
|
||||
这个参数在 700-900 步中的表现很好,从第四部分看,也属于后来居上的,但是在 1000 步的限制下,会比第三个参数要差一些。读者可以延长步数进行更多的验证。
|
||||
|
|
@ -0,0 +1,286 @@
|
|||
## 2.5 梯度上升法
|
||||
|
||||
|
||||
### 2.5.1 Softmax 分布
|
||||
|
||||
在 2.4 节中,贪心法使用了 argmax() 操作,非黑即白地选出了一个最大值来执行动作,然后又不得不引入 $\epsilon$ 来提供探索的机会。那么有没有一种操作可以同时兼顾探索与利用呢?我们知道 np.random.choice(n, p=) 可以通过指定概率 p 来从 n 个项目中做出选择,所以把动作价值转换成概率就可以实现这个目标。
|
||||
|
||||
假设有 3 个动作 $a_1,a_2,a_3$,它们到目前为止的动作价值 Q 分别为 1,2,3,按照贪心法,动作 $a_3$ 肯是下一个备选。转换成概率的话,有几个方法。
|
||||
|
||||
#### 方法一
|
||||
|
||||
$$
|
||||
p_{a_1}=\frac{1}{1+2+3}=\frac{1}{6}, \quad p_{a_2}=\frac{2}{6}, \quad p_{a_3}=\frac{3}{6}
|
||||
$$
|
||||
|
||||
满足 $p_{a_1}+p_{a_2}+p_{a_3}=1$ 的条件。但是当其中有一个负数的时候,就不能这样做了。
|
||||
|
||||
#### 方法二
|
||||
|
||||
$$
|
||||
p_{a_1}=\frac{1^2}{1^2+2^2+3^2}=\frac{1}{14}, \quad p_{a_2}=\frac{4}{14}, \quad p_{a_3}=\frac{9}{14}
|
||||
$$
|
||||
|
||||
也满足 $p_{a_1}+p_{a_2}+p_{a_3}=1$ 的条件。但是当其中有一个值为 0 时,就不能这样做了。
|
||||
|
||||
#### 方法三
|
||||
|
||||
$$
|
||||
p_{a_1}=\frac{e^1}{e^1+e^2+e^3} \approx 0.09, \quad p_{a_2} \approx 0.24, \quad p_{a_3} \approx 0.67
|
||||
$$
|
||||
|
||||
这种方法可以克服前两种方法的缺陷,并满足 $p_{a_1}+p_{a_2}+p_{a_3}=1$ 的条件,被称作 Softmax,在神经网络中做分类时广泛采用。它的泛化形式是:
|
||||
|
||||
$$
|
||||
softmax (a) =\frac{e^{a}}{\sum_{x \in A} e^x} \tag{2.5.1}
|
||||
$$
|
||||
|
||||
其中,$a$ 表示一个具体的动作,$A$ 是动作集合,$x$ 是 $A$ 中的每个动作,$a$ 也是 $A$ 中的某个动作。
|
||||
|
||||
#### 策略 $\pi$
|
||||
|
||||
在将几个备选动作的价值转换为概率后,就可以用 np.random.choice(n, p=) 轻松地从中选择一个动作了,动作值大的概率也大,反之亦然,兼顾了探索与利用。那么在每一次选择动作时的策略 $\pi$,就可以认为是使用 np.random.choice() 函数实现的,每个动作被选择的概率就是式(2.5.1)。
|
||||
|
||||
因此,策略可以定义为动作价值 $Q(x)$ 的函数:
|
||||
|
||||
$$
|
||||
\pi(a) \doteq \mathbb P[A_t=a] =\frac{e^{Q(a)}}{\sum_{x \in A} e^{Q(x)}} \tag{2.5.2}
|
||||
$$
|
||||
|
||||
$\pi(x)$ 对 $Q(a)$ 的偏导数为:
|
||||
|
||||
$$
|
||||
\frac{\partial \pi(x)}{\partial Q(a)}=
|
||||
\begin{cases}
|
||||
\pi(a)(1-\pi(a)) & x=a
|
||||
\\
|
||||
-\pi(a)\pi(x) & x \ne a
|
||||
\end{cases}
|
||||
\tag{2.5.3}
|
||||
$$
|
||||
|
||||
由于策略 $\pi$ 是一个概率形式,在该策略下每个动作都有被选择的可能,进而得到动作对应的奖励,那么最终的奖励只能是一个期望:
|
||||
|
||||
$$
|
||||
\mathbb E [R_t]= \sum_{x \in A} \pi(x) R_x \tag{2.5.4}
|
||||
$$
|
||||
|
||||
其中,$R_x$ 是执行动作 $x$ 后得到的收益 $R$。
|
||||
|
||||
### 2.5.2 梯度上升(Gradient Ascent)
|
||||
|
||||
在赌博机问题中,收益越大越好。在开始阶段,算法需要探索,逐步找到最佳动作后,平均收益会上升,但是不是无限上升,而是在逐步逼近一个最大值。这就满足用梯度上升法解决问题的条件。
|
||||
|
||||
<center>
|
||||
<img src="./img/GradientAscent.png"/>
|
||||
|
||||
图 2.5.1 梯度上升
|
||||
</center>
|
||||
|
||||
在图 2.5.1 中,假设一个函数 $y=f(x)$ 的曲线是一个类似抛物线的形状,有最大值点 $p_*$。目前我们处于 $p_0$ 点,在该点处求出导数,即梯度 $\nabla f(x_0)$,乘以一个步进值 $\alpha$,再加上 $x_0$,得到 $x_1$,如式(2.5.5)所示:
|
||||
|
||||
$$
|
||||
x_1 = x_0 + \alpha \cdot \nabla f(x_0) \tag{2.5.5}
|
||||
$$
|
||||
|
||||
如果 $\alpha$ 值足够小的话,经过几轮这样的计算,就会最终达到 $x_*$,从而得到最大值。
|
||||
|
||||
具体到赌博机问题上:
|
||||
- $x$ 就相当于动作价值 $Q(x)$;
|
||||
- $\alpha$ 是式(2.2.6)中所述的非平稳状态下的步长值;
|
||||
- $f(x)$ 是式(2.5.3)中的 $\mathbb E[R_t]$;
|
||||
- $\nabla f(x)$ 就是对 $f(x)$ 求关于被选中的动作 $a$ 的价值函数 $Q(a)$ 的偏导。
|
||||
|
||||
$$
|
||||
\nabla f(a) = \frac{\partial \mathbb E[R_t]}{\partial Q(a)}=\frac{\partial [\sum \pi(x)R_x]}{\partial Q(a)}
|
||||
\tag{2.5.6}
|
||||
$$
|
||||
|
||||
为了简化问题,下面我们进行实例化推导,即:假设只有 3 个动作 $a,b,c$,对应的动作价值为 $Q(a),Q(b),Q(c)$,那么式(2.5.6)可以表示为:
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
\nabla f(a) &= \frac{\partial [\pi(a)R_a + \pi(b)R_b + \pi (c) R_c]}{\partial Q(a)}
|
||||
\\
|
||||
&= \frac{\partial \pi(a)}{\partial Q(a)}R_a + \frac{\partial \pi(b)}{\partial Q(a)}R_b+ \frac{\partial \pi (c)}{\partial Q(a)}R_c
|
||||
\\
|
||||
&=\pi(a)[1-\pi(a)]R_a-\pi(a)\pi(b)R_b-\pi(a)\pi(c)R_c
|
||||
\\
|
||||
&=\pi(a)\big[R_a - [\pi(a)R_a+\pi(b)R_b+\pi(c)R_c]\big]=\pi(a)(R_a-\sum_x \pi_x R_x)\\
|
||||
&=\pi(a)(R_a-\mathbb E[R_t])
|
||||
\end{aligned}
|
||||
\tag{2.5.7}
|
||||
$$
|
||||
|
||||
由于是假设执行了动作 $a$,所以 $R_a$ 就是当前动作的收益 $R_t$;$\mathbb E[R_t]$ 可以用到当前为止历史收益的均值来表示,简写为 $\bar{R_t}$。则式(2.5.7)进一步表示为 $\pi(a)(R_t-\bar{R_t})$,那么最终结果是:
|
||||
|
||||
$$
|
||||
Q_{t+1}(a) = Q_t(a) + \alpha (R_t - \bar {R_t})\pi_t(a) \tag{2.5.8}
|
||||
$$
|
||||
|
||||
式(2.5.8)都加上了下标 $t$,表示当前迭代次数,也暗示了 $Q(a),\pi(a)$ 都是随着迭代次数而变化的。
|
||||
|
||||
在 Sutton 的书中,经过一系列的假设和推导,得到的梯度更新公式为:
|
||||
|
||||
$$
|
||||
\begin{cases}
|
||||
Q_t(a) \doteq Q_{t-1} (a) + \alpha (R_t - \bar{R})(1-\pi_t(a)), & 被选动作 a=x
|
||||
\\
|
||||
Q_t(x) \doteq Q_{t-1} (x) + \alpha (R_t - \bar{R})(-\pi_t(x)), & 其它动作 a \ne x
|
||||
\end{cases}
|
||||
\tag{2.5.9}
|
||||
$$
|
||||
|
||||
由读者自己阅读理解,看看哪一个更合理,可以接受。
|
||||
|
||||
### 2.5.3 算法与实现
|
||||
|
||||
#### 算法描述
|
||||
|
||||
【算法 2.5.1】
|
||||
|
||||
---
|
||||
初始化:$\alpha \leftarrow$ 步长值 $\in (0,1]$
|
||||
$r \leftarrow 0$,循环 2000 次:
|
||||
初始化奖励分布和计数器,动作集 $A$ 的价值 $Q(A)=0$,$\bar{R} \leftarrow 0$
|
||||
$t \leftarrow 0$,迭代 1000 步:
|
||||
从动作价值计算备选概率:$\pi(x)=\frac{e^{Q(x)}}{\sum_{y \in A} e^{Q(y)}}$
|
||||
根据概率选择动作:$a=random.choice(p=\pi(x))$
|
||||
执行 $a$ 得到奖励 $r$
|
||||
$N(a) \leftarrow N(a)+1$
|
||||
$\bar{R} \leftarrow \bar{R} + (r-\bar{R})/t$
|
||||
更新 $a$ 的动作价值函数 $q(a) \leftarrow q(a)+\alpha(r-\bar{R})\pi(a)$
|
||||
$t \leftarrow t+1$
|
||||
$r \leftarrow r+1$
|
||||
|
||||
---
|
||||
|
||||
找到合适的 $\alpha$ 参数,是我们的目标。
|
||||
|
||||
#### 从动作价值计算备选概率
|
||||
|
||||
实现式(2.5.1)有一个小技巧,以避免计算指数时值过大而溢出:
|
||||
|
||||
假设一共有 a,b,c 三个值,a 最大,则计算 $p_b$ 时有:
|
||||
|
||||
$$
|
||||
p_b=\frac{e^b}{e^a+e^b+e^c}=\frac{e^b/e^a}{e^a/e^a+e^b/e^a+e^c/e^a}=\frac{e^{b-a}}{e^{a-a}+e^{b-a}+e^{c-a}}
|
||||
\tag{2.5.10}
|
||||
$$
|
||||
|
||||
这样一来,所有的指数值都是小于等于 0 的,肯定不会溢出。实现如下:
|
||||
|
||||
【代码位置】bandit_25_Softmax.py
|
||||
|
||||
```python
|
||||
def select_action(self):
|
||||
q_exp = np.exp(self.Q - np.max(self.Q)) # 所有的值都减去最大值
|
||||
self.P = q_exp / np.sum(q_exp) # softmax 实现
|
||||
action = np.random.choice(self.k_arms, p=self.P) # 按概率选择动作
|
||||
return action
|
||||
```
|
||||
|
||||
#### 更新动作价值
|
||||
|
||||
由于本算法需要特殊的更新动作价值的方法,所以需要重载基类中的 update_Q() 函数,以实现式(2.5.8)。
|
||||
|
||||
```python
|
||||
def update_Q(self, action, reward):
|
||||
self.steps += 1 # 迭代次数
|
||||
self.action_count[action] += 1 # 动作次数(action_count)
|
||||
self.average_reward += (reward - self.average_reward) / self.steps
|
||||
self.Q[action] += self.alpha * (reward - self.average_reward) * self.P[action]
|
||||
```
|
||||
|
||||
#### 参数设置
|
||||
|
||||
```python
|
||||
bandits.append(KAB_Softmax(k_arms, alpha=0.5))
|
||||
bandits.append(KAB_Softmax(k_arms, alpha=0.6))
|
||||
bandits.append(KAB_Softmax(k_arms, alpha=0.7))
|
||||
bandits.append(KAB_Softmax(k_arms, alpha=0.8))
|
||||
```
|
||||
|
||||
设置 4 个不同的 $\alpha$ 参数。
|
||||
|
||||
#### 运行结果
|
||||
|
||||
<center>
|
||||
<img src='./img/algo-Gradient.png'/>
|
||||
|
||||
图 2.5.1 梯度上升法结果
|
||||
</center>
|
||||
|
||||
从图 2.5.1 来看,4 组参数的效果差不多。
|
||||
|
||||
- $\alpha=0.5$
|
||||
|
||||
因为步长太小,探索阶段的“热身”过于缓慢,但是在最佳动作利用率方面后来居上。相信如果迭代次数多时,会有更好的效果。
|
||||
|
||||
- $\alpha=0.8$
|
||||
|
||||
在探索阶段最“猛”,快速上升到本身的最佳值,但是后期的后劲不足,一直处于末位。因为步长过大,不容易稳定地寻找到最佳动作。
|
||||
|
||||
这就给了我们一个启示:能不能在开始阶段用 $\alpha=0.8$,后期用 $\alpha=0.5$ 呢?读者可以自行试验。
|
||||
|
||||
### 2.6.4 深入理解
|
||||
|
||||
我们还可以加入一些特殊的代码,来观察算法中动作价值的变化,包括动作价值(self.Q)的变化以及备选概率(self.P)的变化。
|
||||
|
||||
根据图 2.5.1,可以看到迭代 200 步后,算法就可比较稳定了,所以我们可以用下面的代码来得到这 200 步内的一些关键数据:
|
||||
|
||||
【代码位置】bandit_25_softmax_test.py
|
||||
|
||||
创建一个测试类,从 KAB_Softmax 继承。
|
||||
|
||||
```python
|
||||
class KAB_Softmax_test(KAB_Softmax):
|
||||
def __init__(self, k_arms=10, alpha:float=0.1):
|
||||
super().__init__(k_arms=k_arms, alpha=alpha)
|
||||
self.Ps = [] # 记录运行过程中每一步的动作备选概率
|
||||
self.Qs = [] # 记录运行过程中每一步的动作价值
|
||||
# 重载
|
||||
def select_action(self):
|
||||
q_exp = np.exp(self.Q - np.max(self.Q)) # 所有的值都减去最大值
|
||||
self.P = q_exp / np.sum(q_exp) # softmax 实现
|
||||
action = np.random.choice(self.k_arms, p=self.P) # 按概率选择动作
|
||||
self.Ps.append(self.P) # 记录概率
|
||||
self.Qs.append(self.Q.copy()) # 记录价值
|
||||
return action
|
||||
```
|
||||
|
||||
主函数:
|
||||
|
||||
```python
|
||||
if __name__ == "__main__":
|
||||
runs = 1 # 只运行一次
|
||||
steps = 200 # 迭代200步
|
||||
k_arms = 3 # 3个动作
|
||||
bandit = KAB_Softmax_test(k_arms, alpha=0.15) # 步长参数0.15
|
||||
bandit.simulate(runs, steps)
|
||||
# 绘图
|
||||
......
|
||||
```
|
||||
|
||||
为了节省篇幅,还有一些绘图代码省略了。代码中只设置了 3 个动作(动作太多的话挤在一起看不清楚),运行 200 步,而且只运行一次(而不是多次求平均),这样的话,如果多次运行会出现不同的结果,但是大致趋势应该相同。绘出图 2.5.2。
|
||||
|
||||
<center>
|
||||
<img src='./img/algo-Gradient-P.png'/>
|
||||
|
||||
图 2.5.2 迭代 200 步内的备选动作的变化
|
||||
(左图:动作价值的变化;右图:备选概率的变化)
|
||||
</center>
|
||||
|
||||
从左图看,3 个动作的初始价值都是 0,随着算法的进行而分裂,第 2 个动作(从 0 开始数)最后可以到达 10,其它 2 个动作按照它们的收益均值依次排列,1 在 0 的上面,因为在基类中我们“偷偷”地做了排序,序号越大的动作收益均值也越大,这样便于直观比较。
|
||||
|
||||
右图是 Softmax 函数计算的概率,大概在第 25 步时,动作 2 就已经脱颖而出了,然后一路飙升。在任意时刻,所有的动作的概率之和为 1,这是由 softmax 的算法决定的。
|
||||
|
||||
打印输出最终的动作价值和备选概率,可以看到,基本上有很少的几率可以选到 0 号和 1 号动作,99.79% 都会选择 2 号动作。
|
||||
|
||||
```
|
||||
最终的动作价值 = [-0.13 -0.36 6.64]
|
||||
最终的备选概率 = [1.14896740e-03 9.13158833e-04 9.97937874e-01]
|
||||
备选概率的和 = 1.0000000000000002
|
||||
```
|
||||
|
||||
但是如果 k_arms=10 的话,0.15 这个参数值就不合适了,读者可以自行试验。
|
|
@ -0,0 +1,328 @@
|
|||
## 2.6 置信上界法(UCB)
|
||||
|
||||
UCB - Upper Confidence Bound,置信上界。要理解此算法,需要先了解置信度、置信区间等基本知识。
|
||||
|
||||
### 2.6.1 置信度和置信区间
|
||||
|
||||
#### 点估计与区间估计
|
||||
|
||||
一个中学有 2000 名学生,体育老师想知道男生女生的平均身高。
|
||||
|
||||
- 如果只随机测量一个学生的身高,不具有代表性,误差会很大;
|
||||
- 如果所有学生都测量一遍,工作量太大。
|
||||
- 所以体育老师就只在每个年级中抽测一个班的学生的身高,用于估计全校学生的平均身高,称作点估计。
|
||||
|
||||
在抽测之前,几个体育老师先给出了自己的“盲猜”:
|
||||
|
||||
- 老师甲说:我估计男生的平均身高是 1.75 米,女生是 1.65 米。
|
||||
- 老师乙说:我估计男生的平均身高在 1.73 米到 1.77 米,女生是 1.62 米到 1.66 米之间。
|
||||
- 老师丙说:我估计男生在 1 米到 2 米之间,女生也一样......其它老师一阵哄笑。
|
||||
|
||||
#### 置信度(置信水平)与置信区间
|
||||
|
||||
置信度(confidence coefficient)
|
||||
|
||||
- 老师甲的估计数据过于精确,但是不容易正确。
|
||||
- 老师丙说的虽然是个笑话,但是它的正确程度基本上可以达到 100%。
|
||||
- 老师乙估计得更“靠谱”一些,也就是说有 95% 的把握是正确的,这个 95% 就是置信度,[1.73,1.77] 以及 [1.62,1.66] 就是置信区间。
|
||||
|
||||
如果让老师乙进一步缩小范围的话,那么只能这样说:我有 90% 的把握确定男生的身高在 [1.74, 1.76] 之间。范围虽然小了(更精确了),但是把握(置信度)也降低了。
|
||||
|
||||
在图 2.6.1 中,有三个分布,虽然都是正态分布,但是方差和均值各不相同。
|
||||
|
||||
<center>
|
||||
<img src='./img/UCB-1.png'/>
|
||||
|
||||
图 2.6.1 置信度与置信上界
|
||||
</center>
|
||||
|
||||
- 绿色的分布方差最大,90% 置信区间的范围较宽。
|
||||
- 蓝色的分布方差最小,90% 置信区间的范围最窄。
|
||||
- 红色的分布方差居中,90% 置信区间的范围也中等。
|
||||
|
||||
三者的置信上界如各自的箭头所示。
|
||||
|
||||
#### 多臂赌博机问题
|
||||
|
||||
回到多臂赌博机问题上,根据设定,玩家并不知道哪个臂的真实均值最大,而是需要去探索。假设只有三个臂,那么玩家在前三次只是拉动每个臂一次,以获得初步的估计均值。如图 2.6.2 所示。
|
||||
|
||||
<center>
|
||||
<img src='./img/UCB-2.png'/>
|
||||
|
||||
图 2.6.2 多臂赌博机问题的置信上界
|
||||
</center>
|
||||
|
||||
- 左图是最开始的情况,由于没有任何历史数据依赖,所以三个动作的估计均值和上界是相同的。
|
||||
- 右图是三个臂各拉动一次后的结果:
|
||||
- 可以看到绿色的曲线均值 $\hat{\mu}_0$ 向左偏移了(因为得到的收益值是负数),而且置信区间也缩窄了。
|
||||
- 红色的曲线似乎原地未动,但是置信区间也缩窄了。
|
||||
- 蓝色的曲线向右移动了一些,是因为得到的收益值比较高,$\hat{\mu}_2$ 值会变大。
|
||||
|
||||
当迭代次数越来越多时,三个动作的 $\hat{\mu}$ 肯定会估计得越准确,而 $\hat{\mu}$ 到 $\epsilon$ 的距离也会越来越近。
|
||||
|
||||
### 2.6.2 霍夫丁不等式
|
||||
|
||||
下面我们要解决图 2.6.2 中的 $\epsilon$ 的计算问题。
|
||||
|
||||
若 $X=[x_1,\cdots,x_n]$ 为 [0,1] 之间的随机变量,其样本均值为 $\hat{\mu} = \frac{1}{n}(x_1+\cdots+x_n)$,则根据霍夫丁不等式有:
|
||||
|
||||
$$
|
||||
\mathbb P\big[\mu > \hat{\mu} + \epsilon \big ] \le e^{-2n\epsilon^2}
|
||||
\tag{2.6.1}
|
||||
$$
|
||||
|
||||
意为 X 的平均数 $\hat{\mu}$ 与真实均值 $\mu$ 的差大于任意值 $\epsilon$ (一般定义为误差)的概率为 $e^{-2n\epsilon^2}$。当 $n$ 很大的时候,不等式后面的值会很小,也就是说 X 的平均数很接近于真实均值。
|
||||
|
||||
可以与真实应用场景对接:
|
||||
|
||||
- $X$,某个臂的一系列收益值;
|
||||
- $\mu$,样本期望值,可以看作某个动作的真实价值 $Q(a)$;
|
||||
- $\hat{\mu}$,样本均值,可以看作根据历史收益记录估算出来的某个动作的价值 $\hat{Q}(a)$;
|
||||
- $n$,样本数量,可以看作选择某个动作的次数 $N(a)$;
|
||||
- $\epsilon$,任何正数,一般表示误差,可以看作某个动作价值的置信上界 $U(a)$。
|
||||
|
||||
式(2.6.1)可以改写为:
|
||||
|
||||
$$
|
||||
\mathbb P[ Q(a) > \hat{Q}(a) + U(a)] \le e^{-2N(a)U(a)^2} \tag{2.6.2}
|
||||
$$
|
||||
|
||||
从数学符号层面令 $p=e^{-2N(a)U(a)^2}$,两边取自然对数运算,得到关于上界 $U(a)$ 的表达式:
|
||||
|
||||
$$
|
||||
U(a) = \sqrt{\frac{-\ln p}{2N(a)}}
|
||||
$$
|
||||
|
||||
因为式(2.6.1)中的 $\epsilon$ 可以是任何正数,所以 $p$ 也可以是任何整数,接下来我们赋予 $p$ 一个真实的含义,指定 $p = t^{-2}$,$t$ 为赌博机迭代次数,一般都很大,$p$ 就会很小,则:
|
||||
|
||||
$$
|
||||
U(a) = \sqrt{\frac{-\ln p}{2N(a)}}=\sqrt{\frac{-\ln t^{-2}}{N(a)}}=\sqrt{\frac{\ln t}{N(a)}}
|
||||
\tag{2.6.3}
|
||||
$$
|
||||
|
||||
在有些文献中会令 $p=t^{-4}$,则式(2.6.3)根号中的分子会变成 $2\ln t$,就是所谓的 UCB1 算法。由于式(2.6.4)中还会增加一个参数 $c$,所以这个 $2\ln t$ 与 $\ln t$ 没有区别,可以借 $c$ 的调整来弥补,就会形成下文要介绍的 UCB 算法。
|
||||
|
||||
霍夫丁不等式告诉我们,虽然在有限的样本数量内,我们估计的 $\hat{\mu}$ 不准,但是我们有把握真实的均值就在 $\hat{\mu} \pm \epsilon$ 的区间内,如果一定要给一个把握的程度的话,是 90%。
|
||||
|
||||
### 2.6.3 UCB 算法
|
||||
|
||||
赌博机问题中,搬动每个臂的结果收益也属于一个分布,其动作价值的真实均值取决于数据设置,而实际价值取决于收益。一方面,我们利用每个臂的多次动作获得的实际收益来计算 $Q(a)$,另一方面,我们会根据算法的迭代次数和每个动作的被选择次数来计算出该动作的上界。两者相加为:
|
||||
|
||||
$$
|
||||
q(a) = \Big [Q_t(a) + \sqrt{\frac{\ln t}{N_t(a)}} \Big ]
|
||||
\tag{2.6.5}
|
||||
$$
|
||||
|
||||
由于一共有 10 个动作,所以一共有 10 个 $q(a)$,而且每轮迭代都会发生变化:被选中的动作的 UCB 会变小,其它动作的 UCB 值会变大(因为分母部分的 $N(a)$ 没变,但是分子 $t$ 增加了)。我们会选择 10 个 $q(a)$ 中最大值做为下一个被选动作。这样就会有式(2.6.4):
|
||||
|
||||
$$
|
||||
A_t = \argmax_a \Big [Q_t(a) + c\sqrt{\frac{\ln t}{N_t(a)}} \Big ]
|
||||
\tag{2.6.4}
|
||||
$$
|
||||
|
||||
由于操作符是 $\argmax_a$,所以后面的表达式中的 $a$ 都是指的动作集中的每个动作都做一次计算,然后取出最大值。
|
||||
|
||||
- $A_t$:最终选出的最佳动作。
|
||||
- $Q_t(a)$:每个动作的价值估算。
|
||||
- $c$:参数。
|
||||
- $\ln t$:$t$ 为迭代次数,求其自然对数值。
|
||||
- $N_t(a)$:每个动作被选择的次数,小于迭代次数 $t$。
|
||||
|
||||
我们需要平衡利用与探索二者的关系,在式(2.6.4)中,参数 $c$ 就可以达到这个目的:
|
||||
- 当 $c$ 值较小时,表达式主要依赖 $Q_t(a)$ 的值,,即相信前面的采样结果,偏利用;
|
||||
- 当 $c$ 值较大时,表达式会更加依赖后面的部分的计算结果,给被选择次数少的动作以更多的机会,偏探索。
|
||||
|
||||
根据式(2.6.4),再看图 2.6.2,下一轮的动作选择肯定是蓝色的 2 号臂。所以,UCB 算法告诉我们:因为真实的 $\mu$ 值落到 $\hat{\mu} \pm \epsilon$ 的区间内的概率很大,我们应该乐观地选择 $\hat{\mu} + \epsilon$ 值最大的那个动作。
|
||||
|
||||
|
||||
### 2.6.4 算法与实现
|
||||
|
||||
#### 算法描述
|
||||
|
||||
【算法 2.6.1】
|
||||
|
||||
---
|
||||
初始化:$c$
|
||||
$r \leftarrow 0$,循环 2000 次:
|
||||
初始化奖励分布和计数器,动作集 $A$ 的价值 $Q(A)=0$
|
||||
$t \leftarrow 0$,迭代 1000 步:
|
||||
计算所有动作的置信上界 $UCB = c \sqrt{\frac{\ln t}{N(A)}}$
|
||||
选择最大值的动作:$a=\argmax_{a \in A} [ Q(a) + UCB]$
|
||||
执行 $a$ 得到奖励 $r$
|
||||
$N(a) \leftarrow N(a)+1$
|
||||
更新动作价值 $Q(a) \leftarrow Q(a)+\frac{1}{N(a)}[r-Q(a)]$
|
||||
$t \leftarrow t+1$
|
||||
$r \leftarrow r+1$
|
||||
|
||||
---
|
||||
|
||||
找到合适的 $c$ 是我们的目标。
|
||||
|
||||
#### 代码实现
|
||||
|
||||
【代码位置】bandit_26_UCB.py
|
||||
|
||||
```python
|
||||
def select_action(self):
|
||||
# 式(2.6.4)
|
||||
ucb = self.C * np.sqrt(math.log(self.steps + 1) / (self.action_count + 1e-2))
|
||||
estimation = self.Q + ucb
|
||||
action = np.argmax(estimation)
|
||||
return action
|
||||
```
|
||||
|
||||
读者可以看到代码实现和式(2.6.4)不完全相同:
|
||||
|
||||
- $\ln t$ 实现为 math.log(slef.steps+1),这是因为对数计算的输入值必须大于 0。但是在第一步时 self.steps = 0,所以要 +1,避免计算错误。由于这个 +1 是对所有动作都做的,所以不会对算法结果有影响。
|
||||
- $N(A)$ 实现为 self.action_count + 1e-2,因为最初阶段,self.action_count 有很多 0 的单元,直到所有动作至少被选择一次。如果是 0,则在 +1e-2 后,UCB 值会很大,则算法就会选择此动作。
|
||||
|
||||
#### 参数设置
|
||||
|
||||
四组测试的参数 c 分别设置为:[0.5, 0.7, 1.0, 1.2]:
|
||||
|
||||
```python
|
||||
bandits:kab_base.KArmBandit = []
|
||||
bandits.append(KAB_UCB(k_arms, c=0.5))
|
||||
bandits.append(KAB_UCB(k_arms, c=0.7))
|
||||
bandits.append(KAB_UCB(k_arms, c=1))
|
||||
bandits.append(KAB_UCB(k_arms, c=1.2))
|
||||
```
|
||||
|
||||
#### 运行结果
|
||||
|
||||
得到图 2.6.3 的结果。
|
||||
|
||||
<center>
|
||||
<img src='./img/algo-UCB.png'/>
|
||||
|
||||
图 2.6.3 置信上界法结果
|
||||
</center>
|
||||
|
||||
其中,c=0.7 和 c=1.0 时的效果最好,在多次测试中,它们两个的平均收益基本相等。
|
||||
|
||||
图 2.6.3 右侧的动作统计比较有趣,低价值动作的选择次数非常少,只有当 c=1.2 时,探索的次数会多一些,但是由于最佳动作选择准确,所以这种探索没有带了什么更多的好处。
|
||||
|
||||
### 2.6.5 深入理解
|
||||
|
||||
在代码中加入一些统计逻辑,可以帮助读者更深入地理解算法的运行过程。
|
||||
|
||||
在此,我们只设置了 3 个臂的赌博机,迭代 100 次,借以观察几个关键值的变化:
|
||||
|
||||
- 动作价值 Q;
|
||||
- 上界值 UCB;
|
||||
- c 值固定为 1,计算 Q+UCB,由此得出最大值;
|
||||
- 被选择的动作,以及得到的收益。
|
||||
|
||||
第一次运行结果如下:
|
||||
|
||||
【代码位置】bandit_26_ucb_test.py
|
||||
|
||||
```
|
||||
step= 0, Q=[0. 0. 0.], UCB=[0. 0. 0.], Q+UCB=[0. 0. 0.], a=0, r=-0.26
|
||||
step= 1, Q=[-0.26 0. 0. ], UCB=[0.83 8.33 8.33], Q+UCB=[0.57 8.33 8.33], a=1, r=1.30
|
||||
step= 2, Q=[-0.26 1.3 0. ], UCB=[1.04 1.04 10.48],Q+UCB=[0.78 2.34 10.48],a=2, r=1.29
|
||||
step= 3, Q=[-0.26 1.3 1.29], UCB=[1.17 1.17 1.17], Q+UCB=[0.91 2.47 2.46], a=1, r=1.31
|
||||
step= 4, Q=[-0.26 1.3 1.29], UCB=[1.26 0.89 1.26], Q+UCB=[1. 2.2 2.55], a=2, r=1.61
|
||||
step= 5, Q=[-0.26 1.3 1.45], UCB=[1.33 0.94 0.94], Q+UCB=[1.07 2.25 2.39], a=2, r=2.23
|
||||
......
|
||||
```
|
||||
|
||||
其中:
|
||||
- Q=[x y z],分别是 3 个动作的动作价值(估计的均值);
|
||||
- UCB=[x y z],分别 3 个动作的上界值;
|
||||
- Q+UCB=[x y z],分别 3 个动作的均值+上界值;
|
||||
|
||||
按算法运行过程分析:
|
||||
|
||||
- step 0
|
||||
|
||||
- 初始值,所有数值都为 0。
|
||||
- 由于 step=0,step+1=1,所以计算 UCB 时 $\ln(step+1)=0$;
|
||||
- Q+UCB 的值都是 0,此时按顺序选择了 a=0,即第 0 个动作;
|
||||
- 得到 r=-0.26 的收益,这个收益会计算到动作 0 的价值上。
|
||||
|
||||
- step 1
|
||||
|
||||
- 上一次动作 0 收益 -0.26,所以 Q[0]=-0.26, 其它两个动作值不变;
|
||||
- 计算 UCB,动作 0 的次数为 1,所以分母为 1:$\sqrt{\frac{\ln 2}{1+1e-2}}=0.83$,动作 1,2 的次数为 0,所以分母是 1e-2:$\sqrt{\frac{\ln 2}{0+1e-2}}=8.33$;
|
||||
- 与 Q 值相加,得到 [-0.57, 8.33, 8.33];
|
||||
- 按顺序取最大值 8.33,执行动作 1;
|
||||
- 得到 r=1.30 的收益。
|
||||
|
||||
- step 2
|
||||
|
||||
- 上一次动作 1 收益 1.30,所以 Q[1]=1.3, 其它两个动作值不变;
|
||||
- 计算 UCB,动作 0,1 的次数为 1,所以分母为 1:$\sqrt{\frac{\ln 3}{1+1e-2}}=1.04$,动作 2 的次数为 0,所以分母是 1e-2:$\sqrt{\frac{\ln 3}{0+1e-2}}=10.48$;
|
||||
- 与 Q 值相加,得到 [0.78, 2.34, 10.48];
|
||||
- 取最大值 10.48,执行动作 2;
|
||||
- 得到 r=1.29 的收益。
|
||||
|
||||
- step 3
|
||||
|
||||
计算过程与前几步相同,由于动作 2 刚被执行过一次,所以其 UCB 值立刻降低,三者都是 1.71,就要比拼 Q 值了。
|
||||
动作 1 比动作 2 略好,执行动作 1,得到收益 r=1.31。
|
||||
|
||||
- step 4
|
||||
|
||||
尽管动作 1 的 Q 值没有降低,但是由于多被执行了一次,所以 UCB 值降低了,所以本轮选择动作 2。
|
||||
|
||||
- step 5
|
||||
|
||||
这一步仍然会选动作 2。后面一直到 100 步都选择了动作 2。
|
||||
|
||||
图 2.6.4 解释了前 5 步的运算过程。三种颜色代表三个动作,柱形图的底部表示 $\hat{\mu}$ 值,柱形图的高度表示 UCB 值。这样的话,只要比较三个柱子的顶部值,谁最大就选择谁。
|
||||
|
||||
<center>
|
||||
<img src='./img/UCB-3.png'/>
|
||||
|
||||
图 2.6.4 置信上界法前 5 步的图形化解释
|
||||
</center>
|
||||
|
||||
每张子图的标题是当前状态下选择的动作 $a$ 的序号,以及执行 $a$ 后获得的收益,会影响到下一张子图的柱形图的形状。
|
||||
|
||||
有趣的是动作 0 的 UCB 值,虽然在后几步中没有选择到它,但是它的 UCB 值在不断地增加,这是因为式(2.6.4)中 $t$ 的增加。其它两个动作,只要被执行了,它的 UCB 值就会缩短,这样就给其它动作留出更多的被选机会。
|
||||
|
||||
我们再看后面的过程,也是比较曲折的:
|
||||
|
||||
```
|
||||
.....
|
||||
step= 6, Q=[-0.26 1.3 1.71], UCB=[1.39 0.98 0.8 ], Q+UCB=[1.13 2.29 2.52], a=2, r=-0.29
|
||||
step= 7, Q=[-0.26 1.3 1.21], UCB=[1.43 1.02 0.72], Q+UCB=[1.17 2.32 1.93], a=1, r=-0.83
|
||||
step= 8, Q=[-0.26 0.59 1.21], UCB=[1.47 0.85 0.74], Q+UCB=[1.21 1.45 1.95], a=2, r=-0.51
|
||||
......
|
||||
step=11, Q=[-0.26 0.59 0.88], UCB=[1.57 0.91 0.6 ], Q+UCB=[1.31 1.5 1.48], a=1, r=0.87
|
||||
step=12, Q=[-0.26 0.66 0.88], UCB=[1.59 0.8 0.6 ], Q+UCB=[1.33 1.46 1.49], a=2, r=1.07
|
||||
step=13, Q=[-0.26 0.66 0.9 ], UCB=[1.62 0.81 0.57], Q+UCB=[1.36 1.47 1.48], a=2, r=0.88
|
||||
step=14, Q=[-0.26 0.66 0.9 ], UCB=[1.64 0.82 0.55], Q+UCB=[1.38 1.48 1.45], a=1, r=2.11
|
||||
......
|
||||
step=17, Q=[-0.26 0.75 0.9 ], UCB=[1.69 0.64 0.57], Q+UCB=[1.43 1.39 1.47], a=2, r=-0.23
|
||||
step=18, Q=[-0.26 0.75 0.79], UCB=[1.71 0.65 0.54], Q+UCB=[1.45 1.4 1.33], a=0, r=0.48
|
||||
step=19, Q=[0.11 0.75 0.79], UCB=[1.22 0.65 0.55], Q+UCB=[1.33 1.4 1.34], a=1, r=0.91
|
||||
step=20, Q=[0.11 0.77 0.79], UCB=[1.23 0.62 0.55], Q+UCB=[1.34 1.38 1.34], a=1, r=-1.13
|
||||
step=21, Q=[0.11 0.56 0.79], UCB=[1.24 0.59 0.56], Q+UCB=[1.35 1.14 1.34], a=0, r=-0.86
|
||||
step=22, Q=[-0.21 0.56 0.79], UCB=[1.02 0.59 0.56], Q+UCB=[0.81 1.15 1.35], a=2, r=0.99
|
||||
......
|
||||
step=25, Q=[-0.21 0.56 0.65], UCB=[1.04 0.6 0.5 ], Q+UCB=[0.83 1.16 1.15], a=1, r=1.35
|
||||
step=26, Q=[-0.21 0.64 0.65], UCB=[1.05 0.57 0.5 ], Q+UCB=[0.83 1.21 1.15], a=1, r=-1.66
|
||||
step=27, Q=[-0.21 0.43 0.65], UCB=[1.05 0.55 0.51], Q+UCB=[0.84 0.98 1.16], a=2, r=1.24
|
||||
......
|
||||
step=70, Q=[-0.21 0.43 0.77], UCB=[1.19 0.62 0.28], Q+UCB=[0.98 1.05 1.05], a=1, r=0.50
|
||||
step=71, Q=[-0.21 0.43 0.77], UCB=[1.19 0.6 0.28], Q+UCB=[0.98 1.03 1.05], a=2, r=-0.10
|
||||
step=72, Q=[-0.21 0.43 0.75], UCB=[1.19 0.6 0.27], Q+UCB=[0.98 1.03 1.03], a=1, r=0.42
|
||||
step=73, Q=[-0.21 0.43 0.75], UCB=[1.2 0.58 0.27], Q+UCB=[0.98 1.01 1.03], a=2, r=0.03
|
||||
......
|
||||
step=77, Q=[-0.21 0.43 0.74], UCB=[1.2 0.58 0.27], Q+UCB=[0.99 1.01 1.01], a=1, r=0.77
|
||||
step=78, Q=[-0.21 0.46 0.74], UCB=[1.2 0.56 0.27], Q+UCB=[0.99 1.02 1.01], a=1, r=-1.23
|
||||
step=79, Q=[-0.21 0.35 0.74], UCB=[1.21 0.54 0.27], Q+UCB=[0.99 0.89 1.01], a=2, r=-0.01
|
||||
step=80, Q=[-0.21 0.35 0.73], UCB=[1.21 0.54 0.27], Q+UCB=[0.99 0.89 0.99], a=0, r=0.02
|
||||
step=81, Q=[-0.16 0.35 0.73], UCB=[1.05 0.54 0.27], Q+UCB=[0.89 0.89 0.99], a=2, r=1.52
|
||||
......
|
||||
step=99, Q=[-0.16 0.35 0.71], UCB=[1.07 0.55 0.24], Q+UCB=[0.92 0.9 0.95], a=2, r=-0.02
|
||||
```
|
||||
|
||||
- 前 26 步,一直在三个动作之间来回轮换;
|
||||
- 从第 27 步开始,动作 2 占据霸主地位;
|
||||
- 一直到第 70 步,虽然动作 2 的 Q 值持续增加,但是随着被选择次数的增加,上界也不断变小,终于被动作 1 等到了机会。
|
||||
|
||||
这是由于 $t$ 的值一直在增大,而前两个动作的 $N(a)$ 没有变化,所以总体值会增大;而动作 2 的 $N(a)$ 虽然也在线性增大,但是分子是对数趋势增加,所以总体值会减小。
|
||||
- 第 77 步,三个动作又展开了争夺,最终动作 2 靠着持续的高收益而笑到最后。
|
|
@ -0,0 +1,230 @@
|
|||
## 2.7 后验采样法
|
||||
|
||||
后验采样法,又叫做汤普森采样(Thompson Sampling),因为其认为赌博机的每个臂的收益有一个先验的伯努利分布,其后验分布为 Beta 分布,然后我们可以在 Beta 分布上采样,所以叫做后验采样。
|
||||
|
||||
### 2.7.1 伯努利分布与二项分布
|
||||
|
||||
在日常生活和工程实践中,我们会经常遇到非此即彼的情景,比如:
|
||||
- 一个集体活动,参加还是居家;
|
||||
- 一个项目上线后,成功还是失败;
|
||||
- 自己做了一个菜,好吃还是不好吃;
|
||||
- 给用户展示的广告,用户点击还是忽略;
|
||||
- 一个乒乓球运动员,在比赛中是输了还是赢了;
|
||||
......
|
||||
|
||||
如果把对自己有利的结果叫做 win,不利的结果叫做 loss,只有两种结果,这就是一个伯努利分布。可以定义成功率为:
|
||||
|
||||
$$
|
||||
P(x=1)=\frac{win}{win+loss}=\frac{\alpha}{\alpha+\beta} \tag{2.7.1}
|
||||
$$
|
||||
|
||||
而失败率为:$P(x=0)=\frac{loss}{win+loss}=\frac{\beta}{\alpha+\beta}=1-P(x=1)$,$x$ 代表一次事件。
|
||||
|
||||
如果上述事件发生了很多次,每次都会得到 win 或 loss 的结果,就叫做二项分布。
|
||||
|
||||
### 2.7.2 Beta 分布
|
||||
|
||||
当式(2.7.1)所描述的事件发生很多次以后,可以用于预测未来的结果。但是为了提供决策依据,我们通常会给出一个成功或失败的概率,而不是简单地说会成功或者会失败,那么这个概率会形成一种分布,就是 Beta 分布。Beta 分布是二项分布的共轭先验分布。
|
||||
|
||||
|
||||
$$
|
||||
f(x)=\frac{x^{\alpha-1}(1-x)^{\beta-1}}{B(\alpha,\beta)} \tag{2.7.2}
|
||||
$$
|
||||
|
||||
$B(\alpha,\beta)=\frac{\Gamma(\alpha)\Gamma(\beta)}{\Gamma(\alpha+\beta)}$,而 $\Gamma$ 为伽马函数。
|
||||
|
||||
图 2.7.1 是不同参数 $(\alpha,\beta)$ 组合的 Beta 概率密度函数的示意图。
|
||||
|
||||
【代码位置】bandit_27_BetaDist.py
|
||||
|
||||
<center>
|
||||
<img src='./img/Beta.png'/>
|
||||
|
||||
图 2.7.1 Beta分布
|
||||
</center>
|
||||
|
||||
- 红色线:$\alpha=1,\beta=1$
|
||||
|
||||
当成功和失败的次数都各只有 1 次时,无法给出一个有倾向性预判,所以是一条直线。
|
||||
|
||||
- 紫色线:$\alpha=10,\beta=10$
|
||||
|
||||
成功和失败的次数都有 10 次,预判为一个正态分布,成功和失败的概率相同。
|
||||
|
||||
- 蓝色线:$\alpha=50,\beta=50$
|
||||
|
||||
成功和失败的次数都有 50 次,仍预判为一个正态分布,成功和失败的概率相同,但是它的方差比橙色线的要小很多,峰值突出,范围缩小。
|
||||
|
||||
- 橙色线:$\alpha=10,\beta=50$
|
||||
|
||||
成功 10 次,失败 50 次,整个分布会向左(成功概率较低的方向)移动。在此分布下进行 10 次采样(橙色的点),都处于该曲线的主要分布范围内。其峰值位于 10/(10+50)=0.17 附近。
|
||||
|
||||
- 绿色线:$\alpha=105.3,\beta=32.5$
|
||||
|
||||
$\alpha,\beta$ 参数也也可以是浮点数,由于代表成功的 105.3 比 代表失败的 32.5 大很多,整个分布会向右(成功概率较高的方向)移动。在此分布下进行 10 次采样(绿色的点),都处于该曲线的主要分布范围内。其峰值位于 105.3/(105.3+32.5)=0.76 附近。
|
||||
|
||||
### 2.7.3 汤普森采样法(Thompson Sampling)
|
||||
|
||||
上面的理论知识给了我们一些启示:假设图 2.7.1 中的红色、绿色、蓝色代表三个动作的话,我们只需要从三个分布中获得一个采样,然后比较它们的采样值,谁更大(更接近右侧的 1)就采用谁。
|
||||
|
||||
这样做面临的一个问题是:如何判断什么是成功什么是失败呢?
|
||||
|
||||
在选择动作后,唯一能够得到的反馈就是收益值,我们只能根据收益值的大小来确定成功与失败。方法可以是:
|
||||
|
||||
- 与当前时刻所有收益的均值做比较,大于均值认为是成功;
|
||||
- 与心目中的一个期望值做比较,该值可以是 0, 0.5, 1, 1.5 等等,但不可能太大而超出了赌博机能给出收益的一般范围,太小的话又会起不到作用。
|
||||
|
||||
还有一种方法是与当前时刻执行某个动作的所有收益的均值做比较,大于均值认为是成功。这种方法的效果很糟糕,读者可以自行试验。
|
||||
|
||||
在 $\alpha,\beta$ 的计数方法上,也可以有两种方法:
|
||||
- 使用动作计数(正整数),成功时 win += 1,失败时 loss += 1;
|
||||
- 使用收益值计数(浮点数),成功时 win += abs(reward),失败时 loss += abs(reward)。
|
||||
|
||||
我们首先确认一下计数方法,经过多次试验验证,使用收益方式比动作计数更有效,因为 reward 数值更能反映出“成功与失败”的关系来:
|
||||
- 如果用动作计数,每次增加一个固定值 1;
|
||||
- 如果用收益值计数,reward=1.2 就会比 reward=0.8 要好一些,可以更细粒度地表现“成功的程度”。
|
||||
|
||||
读者可以自行测试。
|
||||
|
||||
### 2.7.4 算法实现
|
||||
|
||||
#### 算法描述
|
||||
|
||||
理论比较复杂,但是实现比较简单。
|
||||
|
||||
|
||||
【算法 2.7.1】
|
||||
|
||||
---
|
||||
初始化:$m$,当 $m=-1$ 时,与整体均值比较;$m\ge0$ 时是指定的比较值
|
||||
$r \leftarrow 0$,循环 2000 次:
|
||||
初始化奖励分布和计数器,动作集 $A$ 的价值 $Q(A)=0$
|
||||
设置初始Beta分布参数 $\alpha[A] \leftarrow 1, \beta[A] \leftarrow 1$
|
||||
整体均值 $\bar{R} \leftarrow 0$
|
||||
$t \leftarrow 0$,迭代 1000 步:
|
||||
概率 $p[A] \leftarrow$ 所有动作的 $Beta(\alpha,\beta)$ 采样
|
||||
选择最大值的动作:$a=\argmax_{a \in A} p[a]$
|
||||
执行 $a$ 得到奖励 $r$
|
||||
$N(a) \leftarrow N(a)+1$
|
||||
$\bar{R} \leftarrow \bar{R}+(r-\bar{R})/t$
|
||||
在 $a$ 上更新参数 $\alpha,\beta$
|
||||
win = False
|
||||
$m=-1$ 时,$r > \bar{R}$, win = True
|
||||
$m\ge0$ 时,$r > m$, win = True
|
||||
if win == True,则 $\alpha \leftarrow \alpha + |r|$,否则 $\beta \leftarrow \beta + |r|$
|
||||
$t \leftarrow t+1$
|
||||
$r \leftarrow r+1$
|
||||
|
||||
---
|
||||
|
||||
#### 初始化
|
||||
|
||||
【代码位置】bandit_27_Thompson.py
|
||||
|
||||
```python
|
||||
def reset(self):
|
||||
super().reset()
|
||||
self.total_average = 0
|
||||
self.alpha = np.ones(self.k_arms) # 初始化为 1
|
||||
self.beta = np.ones(self.k_arms) # 初始化为 1
|
||||
```
|
||||
|
||||
把 $\alpha,\beta$ 两个数值都初始化为 1,是为了避免最开始时计算 B 函数失败,因为要求两个参数都要大于 0。
|
||||
|
||||
|
||||
#### 动作选择
|
||||
|
||||
```python
|
||||
def select_action(self):
|
||||
p_beta = np.random.beta(self.alhpa, self.beta) # 从Beta分布中采样
|
||||
action = np.argmax(p_beta) # 取最大值动作
|
||||
return action
|
||||
```
|
||||
|
||||
从 10 个动作的 Beta 分布中采样,获得 10 个概率值,从中选择最大者。
|
||||
|
||||
#### 更新价值函数
|
||||
|
||||
```python
|
||||
def update_Q(self, action, reward):
|
||||
super().update_Q(action, reward)
|
||||
# 更新整体均值
|
||||
self.total_average += (reward - self.total_average) / self.steps
|
||||
|
||||
is_win = False
|
||||
if (self.method == -1): # 与整体均值比较
|
||||
if (reward >= self.total_average):
|
||||
is_win = True
|
||||
else: # 与输入的期望值比较
|
||||
if (reward >= self.method):
|
||||
is_win = True
|
||||
# 用reward计数
|
||||
if is_win:
|
||||
self.alpha[action] += abs(reward)
|
||||
else:
|
||||
self.beta[action] += abs(reward)
|
||||
|
||||
```
|
||||
|
||||
其实这一部分没有必要更新价值函数,只需要更新 $\alpha,\beta$,用收益值 reward 的绝对值作为更新值。
|
||||
|
||||
#### 运行结果
|
||||
|
||||
<center>
|
||||
<img src='./img/algo-Thompson.png'/>
|
||||
|
||||
图 2.7.2 Beta分布
|
||||
</center>
|
||||
|
||||
### 2.7.5 深入理解
|
||||
|
||||
下面我们仍然在上面的算法代码基础上注入一些辅助代码,来帮助读者理解算法运行过程。
|
||||
|
||||
在图 2.7.3 中,15 副子图展示了三个动作前 15 步的算法过程。其中,蓝色为动作 0,橙色为动作 1,绿色为动作 2,它们依次从坏到好。
|
||||
|
||||
【代码位置】bandit_27_thompson_test.py
|
||||
|
||||
<center>
|
||||
<img src='./img/algo-Thompson-winloss.png'/>
|
||||
|
||||
图 2.7.3 三个动作的前 15 步的运行结果
|
||||
</center>
|
||||
|
||||
- step 0
|
||||
|
||||
最开始时三个动作的 $\alpha,\beta$ 初始值相同,所以三条曲线重合了,只能看到绿色曲线。对三个分布分布采样,得到蓝、橙、绿三个点,由于具有随机性,蓝色点的概率最大(最靠近 1),所以采用了动作 a=0,得到 r=-1.93。
|
||||
|
||||
- step 1
|
||||
|
||||
step 0 的收益并不好,于是动作 0 的 $\beta = \beta + 1.93$,其它两个动作不变,形成子图 2,蓝色曲线向左偏移了。再次采用,蓝色点显然就比其它两个点靠近 0,这次选择绿色点代表的 2 号动作,得到 r=2.43,相当给力。
|
||||
|
||||
- step 2
|
||||
|
||||
由于上一轮 2 号动作的出色表现,绿色曲线向右偏移了。再次采用,橙色点稍微大一点,于是选择了 1 号动作。
|
||||
|
||||
......
|
||||
|
||||
后面的过程就不赘述了,读者可以结合下面的打印输出的具体数值自行理解。
|
||||
|
||||
```
|
||||
step:0 win:loss=[2. 2. 2.]:[2. 2. 2.] beta=[0.86 0.61 0.83] a=0 r=-1.93
|
||||
step:1 win:loss=[2. 2. 2.]:[3. 2. 2.] beta=[0.34 0.47 0.54] a=2 r=2.43
|
||||
step:2 win:loss=[2. 2. 3.]:[3. 2. 2.] beta=[0.32 0.48 0.37] a=1 r=0.7
|
||||
step:3 win:loss=[2. 3. 3.]:[3. 2. 2.] beta=[0.4 0.44 0.58] a=2 r=1.12
|
||||
step:4 win:loss=[2. 3. 4.]:[3. 2. 2.] beta=[0.15 0.53 0.67] a=2 r=2.83
|
||||
step:5 win:loss=[2. 3. 5.]:[3. 2. 2.] beta=[0.59 0.53 0.55] a=0 r=-0.53
|
||||
step:6 win:loss=[2. 3. 5.]:[4. 2. 2.] beta=[0.43 0.56 0.69] a=2 r=1.82
|
||||
step:7 win:loss=[2. 3. 6.]:[4. 2. 2.] beta=[0.13 0.59 0.61] a=2 r=2.6
|
||||
step:8 win:loss=[2. 3. 7.]:[4. 2. 2.] beta=[0.27 0.8 0.48] a=1 r=2.28
|
||||
step:9 win:loss=[2. 4. 7.]:[4. 2. 2.] beta=[0.28 0.75 0.75] a=1 r=-1.81
|
||||
step:10 win:loss=[2. 4. 7.]:[4. 3. 2.] beta=[0.25 0.49 0.77] a=2 r=4.2
|
||||
step:11 win:loss=[2. 4. 8.]:[4. 3. 2.] beta=[0.59 0.58 0.92] a=2 r=1.37
|
||||
step:12 win:loss=[2. 4. 9.]:[4. 3. 2.] beta=[0.38 0.7 0.8 ] a=2 r=2.32
|
||||
step:13 win:loss=[2. 4. 10.]:[4. 3. 2.] beta=[0.31 0.43 0.93] a=2 r=2.9
|
||||
step:14 win:loss=[2. 4. 11.]:[4. 3. 2.] beta=[0.66 0.63 0.84] a=2 r=3.48
|
||||
step:15 win:loss=[2. 4. 12.]:[4. 3. 2.] beta=[0.24 0.32 0.85] a=2 r=1.82
|
||||
step:16 win:loss=[2. 4. 13.]:[4. 3. 2.] beta=[0.07 0.87 0.9 ] a=2 r=2.2
|
||||
step:17 win:loss=[2. 4. 14.]:[4. 3. 2.] beta=[0.08 0.69 0.88] a=2 r=3.25
|
||||
step:18 win:loss=[2. 4. 15.]:[4. 3. 2.] beta=[0.32 0.31 0.95] a=2 r=1.77
|
||||
step:19 win:loss=[2. 4. 16.]:[4. 3. 2.] beta=[0.25 0.63 0.88] a=2 r=1.74
|
||||
```
|
|
@ -0,0 +1,217 @@
|
|||
|
||||
## 2.8 算法性能比较
|
||||
|
||||
### 2.8.1 根据平均收益值比较
|
||||
|
||||
在前面的几个小节中,一共介绍了 4 种算法:
|
||||
|
||||
- 贪心法
|
||||
- 梯度上升法
|
||||
- 置信上界法
|
||||
- 后验采样法
|
||||
|
||||
读者肯定会问:这些算法中哪一种最好呢?这要看问题的设置和参数选择。一种最简单的比较方式,就是在指定的有限迭代步数内,谁的平均收益最高,谁就最好。
|
||||
|
||||
【代码位置】bandit_28_Compare.py
|
||||
|
||||
```python
|
||||
if __name__ == "__main__":
|
||||
runs = 200
|
||||
steps = 1000
|
||||
k_arms = 10
|
||||
# 算法类名称
|
||||
algo_names = [KAB_Greedy, KAB_E_Greedy, KAB_Softmax, KAB_UCB, KAB_Thompson]
|
||||
algo_params = { # 算法参数
|
||||
KAB_Greedy: [10, 20, 30, 40, 50, 60, 70, 80, 90, 100],
|
||||
KAB_E_Greedy: [0.01, 0.02, 0.03, 0.04, 0.05, 0.08, 0.10, 0.15, 0.20, 0.25],
|
||||
KAB_Softmax: [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0],
|
||||
KAB_UCB: [0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.2],
|
||||
KAB_Thompson: [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]
|
||||
}
|
||||
```
|
||||
|
||||
每个算法给出 10 个不同参数(由小到大排序),运行 200 轮 x 1000 步后,获得总体的平均收益值,然后把 10 个值用一条平滑曲线连接起来(拟合),形成一个趋势图。如图 2.8.1 所示。
|
||||
|
||||
<center>
|
||||
<img src='./img/algo-compare2.png'/>
|
||||
|
||||
图 2.8.1 五种算法的平均收益比较
|
||||
</center>
|
||||
|
||||
可以看到:
|
||||
|
||||
- 红色曲线,UCB 算法似乎最好,每个参数上的收益值都普遍高于其它几个算法。
|
||||
- 橙色曲线,$\epsilon$-贪心算法最差,它的第 5 个或第 6 个参数最好,也就是 $\epsilon$ 为 0.1 左右时。
|
||||
- 蓝色曲线,普通的贪心算法虽然简单,但是针对这个问题还是比较有效的。
|
||||
- 紫色曲线,后验采样法大概处于第二位。
|
||||
- 绿色曲线,只比 $\epsilon$-贪心算法好一点儿。如果采用 Sutton 的算法,读者可以自己进行比较。
|
||||
|
||||
### 2.8.2 根据综合指标比较
|
||||
|
||||
#### 可视化比较
|
||||
|
||||
其中贪心法包括两种,从上面的比较中,我们已经得知 $\epsilon$-贪心算法最差,所以去掉它以后,我们再进行一次多指标比较。
|
||||
|
||||
【代码位置】bandit_28_Segment.py
|
||||
|
||||
```python
|
||||
bandits.append(KAB_Greedy(k_arms, 40))
|
||||
bandits.append(KAB_Softmax(k_arms, 1.0))
|
||||
bandits.append(KAB_UCB(k_arms, 1.0))
|
||||
bandits.append(KAB_Thompson(k_arms, 0.7))
|
||||
```
|
||||
|
||||
<center>
|
||||
<img src='./img/algo-compare.png'/>
|
||||
|
||||
图 2.8.2 四种算法的多指标比较
|
||||
</center>
|
||||
|
||||
从各方面看,梯度上升(Softmax)法远远落后于其它三种算法,其原因有二:
|
||||
|
||||
1. 在 0-100 步内,迟迟没有确定最佳动作,试探次数太多(见图 2.8.2 左上角子图);
|
||||
2. 在后续的步骤内,虽然 9 号动作概率较大,但是 6,7,8 三个动作还是分走了不少流量,造成对 9 号动作的利用率不够(见图 2.8.2 右侧橙色柱图)。
|
||||
|
||||
综合指标比较:
|
||||
|
||||
- 探索效率
|
||||
|
||||
比较 0-100 步内的平均收益,即“探索效率”。蓝色的贪心算法最好,绿色的 UCB 置信上界法其次,很快地就达到很高的位置。
|
||||
|
||||
- 收敛效率
|
||||
|
||||
比较 300-500 步内的平均收益,即“收敛效率”。蓝、绿、橙三色的算法都已经收敛了,只有红色的后验采样法还处于上升阶段。
|
||||
|
||||
- 利用能力
|
||||
|
||||
比较 700-900 步内的平均收益,即“利用能力”。这一部分中,只要曲线不出现很大的抖动就可以。蓝、绿两线都可以保持高位运行。
|
||||
|
||||
- 稳定性
|
||||
|
||||
比较 0-1000 步内最佳动作的利用率,这也直接决定了收益均值。绿色和红色曲线最好。从这一部分看,如果迭代次数不是 1000 次而是 2000 次,那么红色线所代表的后验采样法的平均收益将会高于贪心法。
|
||||
|
||||
- 探索利用比例
|
||||
|
||||
比较 10 个动作的选择次数。第 9 个动作的数值越高越好,绿色和红色最好,橙色最差。
|
||||
|
||||
#### 量化比较
|
||||
|
||||
我们可以采样优劣距离法(TOPSIS - Technique for Order Preference by Similarity to Ideal Solution),通过比较算法的各个子部分的数值,来得到综合排名。
|
||||
|
||||
【代码位置】bandit_28_TOPSIS.py
|
||||
|
||||
这个方法的步骤是:
|
||||
|
||||
1. 设计并提取特征值
|
||||
|
||||
针对赌博机问题的特点,我们一共设计了 8 个特征值,如表 2.8.1 所示。
|
||||
|
||||
表 2.8.1 原始特征值
|
||||
|
||||
|算法|0-100步<br>平均收益|300-500步<br>平均收益|500-900步<br>平均收益|1000步<br>平均收益|0-100步<br>平均收益|300-500步<br>平均收益|500-900步<br>平均收益|1000步<br>平均收益|
|
||||
|-|-|-|-|-|-|-|-|-|
|
||||
|贪心法| 0.867|1.485|1.483|1.421|0.460|0.766|0.775|0.737|
|
||||
|梯度上升法|0.817|1.383|1.382|1.328|0.365|0.6|0.6|0.576|
|
||||
|置信上界法|1.185|1.491|1.503|1.460|0.565|0.857|0.903|0.838|
|
||||
|后验采样法|1.080|1.512|1.532|1.468|0.514|0.862|0.912|0.836|
|
||||
|
||||
2. 特征值归一化:把要比较的数值按列进行归一化。
|
||||
|
||||
$$y=\frac{x-x_{min}}{x_{max}-x_{min}} \tag{2.8.1}$$
|
||||
|
||||
比如在表 2.8.1 中,0-100步平均收益这一列,使用式(2.8.1)计算,$\frac{0.867-0.817}{1.185-0.817}=0.137$。这一整列变成了下面的 [0.137, 0, 1, 0.714],其中,最小的值变成 0,最大的值变成 1。
|
||||
|
||||
```
|
||||
[[0.137 0.791 0.674 0.664 0.476 0.634 0.562 0.615]
|
||||
[0. 0. 0. 0. 0. 0. 0. 0. ]
|
||||
[1. 0.839 0.807 0.94 1. 0.979 0.97 1. ]
|
||||
[0.714 1. 1. 1. 0.745 1. 1. 0.995]]
|
||||
```
|
||||
|
||||
3. 计算加权规范矩阵
|
||||
|
||||
每一列的数值的平方和再开根号做分母,每一列的每个数值做分子:
|
||||
|
||||
$$
|
||||
z_{ij} = \frac{y_{ij}}{\sqrt{\sum_{i=1}^n y^2_{ij}}} \tag{2.8.2}
|
||||
$$
|
||||
|
||||
比如,在上面的输出中的第一列数值,$\frac{0.137}{\sqrt{0.137^2+0^2+1^2+0.714^2}}=0.111$。
|
||||
|
||||
得到如下结果:
|
||||
|
||||
```
|
||||
[[0.111 0.518 0.464 0.436 0.357 0.413 0.374 0.399]
|
||||
[0. 0. 0. 0. 0. 0. 0. 0. ]
|
||||
[0.809 0.550 0.556 0.617 0.749 0.637 0.646 0.650]
|
||||
[0.578 0.655 0.689 0.656 0.558 0.651 0.666 0.647]]
|
||||
```
|
||||
|
||||
4. 确定最优(上界)和最劣(下界)向量
|
||||
|
||||
取八个特征值(列)中的最大值和最小值,组成最优向量和最劣向量:
|
||||
|
||||
$$
|
||||
Z^+=[\max(z_{11},\cdots,z_{n1}),\cdots,\max(z_{1m},\cdots,z_{nm})]=[Z_1^+,\cdots,Z_m^+]
|
||||
\\
|
||||
Z^-=[\min(z_{11},\cdots,z_{n1}),\cdots,\min(z_{1m},\cdots,z_{nm})]=[Z_1^-,\cdots,Z_m^-]
|
||||
\tag{2.8.3}
|
||||
$$
|
||||
|
||||
```
|
||||
Z_max = [0.809 0.655 0.689 0.656 0.749 0.651 0.666 0.650]
|
||||
Z_min = [0. 0. 0. 0. 0. 0. 0. 0.]
|
||||
```
|
||||
|
||||
5. 计算样本到上下界的距离
|
||||
|
||||
$$
|
||||
D_i^+=\sqrt{\sum_{j=1}^m (Z_j^+ - z_{ij})^2}
|
||||
\\
|
||||
D_i^-=\sqrt{\sum_{j=1}^m (Z_j^- - z_{ij})^2}
|
||||
\tag{2.8.4}
|
||||
$$
|
||||
|
||||
```
|
||||
d_plus.shape= (4,)
|
||||
[0.981 1.959 0.176 0.300]
|
||||
d_minus.shape= (4,)
|
||||
[1.133 0.000 1.858 1.807]
|
||||
```
|
||||
|
||||
6. 计算总距离值
|
||||
|
||||
$$
|
||||
D_i = \frac{D_i^-}{D_i^- + D_i^+} \tag{2.8.5}
|
||||
$$
|
||||
|
||||
比如上面的输出中,$\frac{1.133}{1.133+0.981}=0.536$,得到下面的输出:
|
||||
|
||||
```
|
||||
D = [0.536 0.000 0.913 0.858]
|
||||
```
|
||||
|
||||
<center>
|
||||
<img src='./img/TOPSIS.png'/>
|
||||
|
||||
图 2.8.3 TOPSIS 算法中计算总距离的含义
|
||||
</center>
|
||||
|
||||
图 2.8.3 中所示,距离 $Z^-$ 越近的话(即 $D_i^-$ 值越小),$D_i$ 值就越小。
|
||||
|
||||
|
||||
7. 排序并得到最优方法
|
||||
|
||||
$$
|
||||
best = \argmax(D_i) \tag{2.8.6}
|
||||
$$
|
||||
|
||||
```
|
||||
序号:[2, 3, 0, 1]
|
||||
0 = KAB_UCB
|
||||
1 = KAB_Thompson
|
||||
2 = KAB_Greedy
|
||||
3 = KAB_Softmax
|
||||
```
|
||||
|
||||
最后可知,UCB 置信上界算法最好,Thompson 后验采样算法其次,Greedy 贪心法第三,Softmax 梯度上升法最差。这与图 2.8.1 所示的结果相同。
|
После Ширина: | Высота: | Размер: 88 KiB |
После Ширина: | Высота: | Размер: 59 KiB |
После Ширина: | Высота: | Размер: 54 KiB |
После Ширина: | Высота: | Размер: 11 KiB |
После Ширина: | Высота: | Размер: 63 KiB |
После Ширина: | Высота: | Размер: 23 KiB |
После Ширина: | Высота: | Размер: 37 KiB |
После Ширина: | Высота: | Размер: 16 KiB |
После Ширина: | Высота: | Размер: 34 KiB |
После Ширина: | Высота: | Размер: 58 KiB |
После Ширина: | Высота: | Размер: 20 KiB |
После Ширина: | Высота: | Размер: 213 KiB |
После Ширина: | Высота: | Размер: 59 KiB |
После Ширина: | Высота: | Размер: 224 KiB |
После Ширина: | Высота: | Размер: 219 KiB |
После Ширина: | Высота: | Размер: 243 KiB |
После Ширина: | Высота: | Размер: 224 KiB |
После Ширина: | Высота: | Размер: 246 KiB |
После Ширина: | Высота: | Размер: 88 KiB |
После Ширина: | Высота: | Размер: 195 KiB |
После Ширина: | Высота: | Размер: 62 KiB |
После Ширина: | Высота: | Размер: 22 KiB |
После Ширина: | Высота: | Размер: 43 KiB |
После Ширина: | Высота: | Размер: 66 KiB |
После Ширина: | Высота: | Размер: 17 KiB |
|
@ -0,0 +1,50 @@
|
|||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
import matplotlib as mpl
|
||||
|
||||
num_arm = 3
|
||||
num_data = 10
|
||||
|
||||
mpl.rcParams['font.sans-serif'] = ['SimHei']
|
||||
mpl.rcParams['axes.unicode_minus']=False
|
||||
|
||||
|
||||
#正态分布的概率密度函数
|
||||
def normpdf(x,mu,sigma):
|
||||
pdf=np.exp(-(x-mu)**2/(2*sigma**2))/(sigma * np.sqrt(2 * np.pi))
|
||||
return pdf
|
||||
|
||||
def draw(x, y, color, y_label, title, label=None):
|
||||
#概率分布曲线
|
||||
plt.plot(x,y,color,linewidth=2,label=label)
|
||||
plt.title(title)
|
||||
#plt.xticks ([mu-2*sigma,mu-sigma,mu,mu+sigma,mu+2*sigma],['-2','-1','0','1','2'])
|
||||
plt.xlabel(u"奖励")
|
||||
plt.ylabel(y_label)
|
||||
|
||||
|
||||
if __name__=="__main__":
|
||||
sigma = 1
|
||||
mu = 0
|
||||
bins = 30
|
||||
n = 200
|
||||
np.random.seed(5)
|
||||
a = sigma * np.random.randn(n) + mu
|
||||
plt.hist(a, bins=bins)
|
||||
|
||||
x= np.arange(mu-4*sigma,mu+4*sigma,0.01) #生成数据,步长越小,曲线越平滑
|
||||
y=normpdf(x,mu,sigma) * (1/0.2*n/bins)
|
||||
title = '$\mu = {:.2f}, \sigma={:.2f}$'.format(mu,sigma)
|
||||
draw(x, y, 'r--', u"次数", title)
|
||||
plt.grid()
|
||||
plt.show()
|
||||
|
||||
for mu, color, label in zip([-1,0,1],['b--','r--','g--'],['2','1','3']):
|
||||
x= np.arange(mu-3*sigma,mu+3*sigma,0.01) #生成数据,步长越小,曲线越平滑
|
||||
y=normpdf(x,mu,sigma)
|
||||
title = '$\mu = [-1,0,1], \sigma=1$'
|
||||
draw(x, y, color, u"概率密度", title, label)
|
||||
plt.legend()
|
||||
plt.grid()
|
||||
plt.show()
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
import matplotlib as mpl
|
||||
|
||||
mpl.rcParams['font.sans-serif'] = ['SimHei']
|
||||
mpl.rcParams['axes.unicode_minus']=False
|
||||
|
||||
|
||||
def draw_one_arm(reward_dist):
|
||||
fig, axes = plt.subplots(nrows=1, ncols=2)
|
||||
ax = axes[0]
|
||||
ax.grid()
|
||||
ax.hist(reward_dist, bins=21)
|
||||
|
||||
ax = axes[1]
|
||||
ax.grid()
|
||||
ax.violinplot(reward_dist, showmeans=True, quantiles=[0,0.025,0.25,0.75,0.925])
|
||||
|
||||
plt.show()
|
||||
|
||||
def draw_mu(reward_mu):
|
||||
plt.plot(reward_mu, 'ro--')
|
||||
plt.xlabel(u"10臂")
|
||||
plt.ylabel(u"期望均值")
|
||||
plt.show()
|
||||
|
||||
def draw_k_arm(k_reward_dist_mu, k_reward_dist_mu_sort):
|
||||
fig, axes = plt.subplots(nrows=2, ncols=1)
|
||||
ax = axes[0]
|
||||
ax.grid()
|
||||
ax.violinplot(k_reward_dist_mu, showmeans=True)
|
||||
mean = np.round(np.mean(k_reward_dist_mu, axis=0), 3)
|
||||
for i in range(10):
|
||||
ax.text(i+1+0.2,mean[i]-0.1,str(mean[i]))
|
||||
|
||||
ax = axes[1]
|
||||
ax.grid()
|
||||
ax.violinplot(k_reward_dist_mu_sort, showmeans=True)
|
||||
mean = np.round(np.mean(k_reward_dist_mu_sort, axis=0), 3)
|
||||
for i in range(10):
|
||||
ax.text(i+1+0.2,mean[i]-0.1,str(mean[i]))
|
||||
|
||||
plt.show()
|
||||
|
||||
if __name__=="__main__":
|
||||
# 生成原始数据
|
||||
num_arm = 10
|
||||
num_data = 2000
|
||||
np.random.seed(5)
|
||||
k_reward_dist = np.random.randn(num_data, num_arm)
|
||||
print("原始均值=", np.round(np.mean(k_reward_dist, axis=0),3))
|
||||
draw_one_arm(k_reward_dist[:,0])
|
||||
# 生成期望均值
|
||||
reward_mu = np.random.randn(num_arm)
|
||||
print("期望平均回报=", np.round(reward_mu,3))
|
||||
draw_mu(reward_mu)
|
||||
# 生成期望数据(=原始数据+期望均值)
|
||||
k_reward_dist_mu = reward_mu + k_reward_dist
|
||||
print("实际均值=", np.round(np.mean(k_reward_dist_mu, axis=0),3))
|
||||
# 按均值排序
|
||||
reward_mu_sort_arg = np.argsort(reward_mu) # 对期望均值排序(并不实际排序,而是返回序号)
|
||||
k_reward_dist_mu_sort = np.zeros_like(k_reward_dist_mu)
|
||||
for i in range(10):
|
||||
idx = reward_mu_sort_arg[i] # 第i个臂对应的新序号是idx
|
||||
k_reward_dist_mu_sort[:,i] = k_reward_dist_mu[:,idx] # 重新排序
|
||||
draw_k_arm(k_reward_dist_mu, k_reward_dist_mu_sort)
|
|
@ -0,0 +1,195 @@
|
|||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
import scipy.signal as ss
|
||||
from tqdm import trange
|
||||
import matplotlib as mpl
|
||||
|
||||
mpl.rcParams['font.sans-serif'] = ['SimHei']
|
||||
mpl.rcParams['axes.unicode_minus']=False
|
||||
|
||||
class KArmBandit(object):
|
||||
def __init__(self, k_arms=10, mu=0, sigma=1): # 臂数,奖励分布均值,奖励分布方差
|
||||
self.k_arms = k_arms # 臂数
|
||||
self.mu = mu # 奖励均值
|
||||
self.sigma = sigma # 奖励方差
|
||||
self.__best_arm = self.k_arms - 1 # 对算法透明,用于统计
|
||||
|
||||
def reset(self):
|
||||
# 初始化 k 个 arm 的期望收益,并排序,但算法不要依赖这个排序
|
||||
self.E = np.sort(self.sigma * np.random.randn(self.k_arms) + self.mu)
|
||||
# 初始化 k 个 arm 的动作估值 Q_n 为 0
|
||||
self.Q = np.zeros(self.k_arms)
|
||||
# 保存每个 arm 被选择的次数 n
|
||||
self.action_count = np.zeros(self.k_arms, dtype=int)
|
||||
self.steps = 0 # 总步数,用于统计
|
||||
|
||||
# 得到下一步的动作(下一步要使用哪个arm,由算法决定)
|
||||
def select_action(self):
|
||||
pass
|
||||
|
||||
# 执行指定的动作,并返回此次的奖励
|
||||
def pull_arm(self, action):
|
||||
reward = np.random.randn() + self.E[action]
|
||||
return reward
|
||||
|
||||
# 更新 q_n
|
||||
def update_Q(self, action, reward):
|
||||
# 总次数(time)
|
||||
self.steps += 1
|
||||
# 动作次数(action_count)
|
||||
self.action_count[action] += 1
|
||||
# 计算动作价值,采样平均
|
||||
self.Q[action] += (reward - self.Q[action]) / self.action_count[action]
|
||||
|
||||
# 模拟运行
|
||||
def simulate(self, runs, steps):
|
||||
# 记录历史 reward,便于后面统计
|
||||
rewards = np.zeros(shape=(runs, steps))
|
||||
num_actions_per_arm = np.zeros(self.k_arms, dtype=int)
|
||||
is_best_action = np.zeros(shape=(runs, steps), dtype=int)
|
||||
for r in trange(runs):
|
||||
# 每次run都清零计算 q 用的统计数据,并重新初始化奖励均值
|
||||
self.reset()
|
||||
# 测试 time 次
|
||||
for s in range(steps):
|
||||
action = self.select_action()
|
||||
reward = self.pull_arm(action)
|
||||
self.update_Q(action, reward)
|
||||
rewards[r, s] = reward
|
||||
if (action == self.__best_arm): # 是否为最佳动作
|
||||
is_best_action[r, s] = 1
|
||||
# end for t
|
||||
num_actions_per_arm += self.action_count # 每个动作的选择次数
|
||||
# end for r
|
||||
return rewards, is_best_action, num_actions_per_arm
|
||||
#end class
|
||||
|
||||
import multiprocessing as mp
|
||||
|
||||
# 多进程运行 simulate() 方法
|
||||
def mp_simulate(bandits, k_arms, runs, steps, labels, title):
|
||||
all_rewards = []
|
||||
all_best = []
|
||||
all_actions = []
|
||||
print(labels)
|
||||
# 多进程执行
|
||||
pool = mp.Pool(processes=4)
|
||||
results = []
|
||||
for i, bandit in enumerate(bandits):
|
||||
results.append(pool.apply_async(bandit.simulate, args=(runs,steps,)))
|
||||
pool.close()
|
||||
pool.join()
|
||||
# 收集结果
|
||||
for i in range(len(results)):
|
||||
rewards, best_action, actions = results[i].get()
|
||||
all_rewards.append(rewards)
|
||||
all_best.append(best_action)
|
||||
all_actions.append(actions)
|
||||
# 计算统计数据
|
||||
all_best_actions = np.array(all_best).mean(axis=1)
|
||||
all_mean_rewards = np.array(all_rewards).mean(axis=1)
|
||||
all_done_actions = np.array(all_actions)
|
||||
# 最优动作选择的频率
|
||||
best_action_per_bandit = all_done_actions[:,k_arms-1]/all_done_actions.sum(axis=1)
|
||||
# 平均奖励值
|
||||
mean_reward_per_bandit = all_mean_rewards.sum(axis=1) / steps
|
||||
|
||||
# 绘图
|
||||
# 四行三列
|
||||
grid = plt.GridSpec(nrows=4, ncols=3)
|
||||
plt.figure(figsize=(15, 10))
|
||||
# 绘制average reward[0:100]
|
||||
plt.subplot(grid[0:2, 0])
|
||||
for i, mean_rewards in enumerate(all_mean_rewards):
|
||||
tmp = ss.savgol_filter(mean_rewards[0:100], 10, 3)
|
||||
plt.plot(tmp, label=labels[i] + str.format("={0:0.3f}", mean_reward_per_bandit[i]))
|
||||
plt.ylabel('平均收益(0~100)', fontsize=14)
|
||||
plt.legend(fontsize=14)
|
||||
plt.grid()
|
||||
# 绘制average reward[300:500]
|
||||
plt.subplot(grid[0:1, 1])
|
||||
for i, mean_rewards in enumerate(all_mean_rewards):
|
||||
tmp = ss.savgol_filter(mean_rewards[300:500], 20, 3)
|
||||
plt.plot(tmp)
|
||||
ticks = [0,50,100,150,200]
|
||||
tlabels = [300,350,400,450,500]
|
||||
plt.xticks(ticks, tlabels)
|
||||
plt.ylabel('平均收益(300~500)', fontsize=14)
|
||||
plt.grid()
|
||||
# 绘制average reward[700:900]
|
||||
plt.subplot(grid[1:2, 1])
|
||||
for i, mean_rewards in enumerate(all_mean_rewards):
|
||||
tmp = ss.savgol_filter(mean_rewards[700:900], 20, 3)
|
||||
plt.plot(tmp)
|
||||
ticks = [0,50,100,150,200]
|
||||
tlabels = [700,750,800,850,900]
|
||||
plt.xticks(ticks, tlabels)
|
||||
plt.ylabel('平均收益(700~900)', fontsize=14)
|
||||
plt.grid()
|
||||
# 绘制正确的最优动作选择频率
|
||||
plt.subplot(grid[2:4, 0:2])
|
||||
for i, counts in enumerate(all_best_actions):
|
||||
tmp = ss.savgol_filter(counts, 20, 3)
|
||||
plt.plot(tmp, label=labels[i] + str.format("={0:0.3f}", best_action_per_bandit[i]))
|
||||
plt.xlabel('迭代步数', fontsize=14)
|
||||
plt.ylabel('最佳动作采用比例', fontsize=14)
|
||||
plt.legend(fontsize=14)
|
||||
plt.grid()
|
||||
# 绘制所有动作的执行次数
|
||||
all_done_actions = (all_done_actions/runs + 0.5).astype(int)
|
||||
X = ["0","1","2","3","4","5","6","7","8","9"]
|
||||
colors = ['tab:blue', 'tab:orange', 'tab:green', 'tab:red']
|
||||
for i in range(4):
|
||||
ax = plt.subplot(grid[i, 2])
|
||||
Y = all_done_actions[i].tolist()
|
||||
ax.bar(X, Y, label=labels[i], color=colors[i])
|
||||
for x, y in zip(X, Y): # 在bar上方标出动作执行次数
|
||||
ax.text(x, y, str(y), ha='center')
|
||||
ax.legend(fontsize=14)
|
||||
plt.show()
|
||||
|
||||
return
|
||||
|
||||
if __name__=="__main__":
|
||||
# 模拟运行
|
||||
runs = 2000
|
||||
steps = 1000
|
||||
k_arms = 10
|
||||
|
||||
# 记录历史 reward,便于后面统计
|
||||
rewards = np.zeros(shape=(runs, steps))
|
||||
num_actions_per_arm = np.zeros(k_arms, dtype=int)
|
||||
is_best_action = np.zeros(shape=(runs, steps), dtype=int)
|
||||
np.random.seed(5)
|
||||
bandit = KArmBandit(k_arms)
|
||||
# 运行 200 次取平均值
|
||||
for r in trange(runs):
|
||||
# 每次run都清空统计数据,但是使用相同的初始化参数
|
||||
bandit.reset()
|
||||
# 玩 1000 轮
|
||||
for t in range(steps):
|
||||
action = np.random.randint(k_arms)
|
||||
reward = bandit.pull_arm(action)
|
||||
bandit.update_Q(action, reward)
|
||||
rewards[r, t] = reward
|
||||
if (action == 9):
|
||||
is_best_action[r, t] = 1
|
||||
# end for t
|
||||
num_actions_per_arm += bandit.action_count
|
||||
# end for r
|
||||
# 平均收益
|
||||
r_m = rewards.mean(axis=0)
|
||||
smooth = ss.savgol_filter(r_m, 100, 3)
|
||||
plt.plot(smooth)
|
||||
plt.xlabel(u'训练步数')
|
||||
plt.ylabel(u'平均奖励')
|
||||
plt.grid()
|
||||
plt.show()
|
||||
# 动作选择次数
|
||||
X = ["0","1","2","3","4","5","6","7","8","9"]
|
||||
a_m = num_actions_per_arm / runs
|
||||
Y = a_m.tolist()
|
||||
plt.bar(X, Y)
|
||||
plt.xlabel(u'动作序号')
|
||||
plt.ylabel(u'动作选择次数')
|
||||
plt.show()
|
|
@ -0,0 +1,35 @@
|
|||
import numpy as np
|
||||
import bandit_23_Base as kab_base
|
||||
|
||||
class KAB_E_Greedy(kab_base.KArmBandit):
|
||||
def __init__(self, k_arms=10, epsilon=0.1):
|
||||
super().__init__(k_arms=k_arms)
|
||||
self.epsilon = epsilon # 非贪心概率
|
||||
|
||||
def select_action(self):
|
||||
if (np.random.random_sample() < self.epsilon):
|
||||
action = np.random.randint(self.k_arms) # 随机选择动作进行探索
|
||||
else:
|
||||
action = np.argmax(self.Q) # 贪心选择目前最好的动作进行利用
|
||||
return action
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
runs = 2000
|
||||
steps = 1000
|
||||
k_arms = 10
|
||||
|
||||
bandits:kab_base.KArmBandit = []
|
||||
bandits.append(KAB_E_Greedy(k_arms, epsilon=0.01))
|
||||
bandits.append(KAB_E_Greedy(k_arms, epsilon=0.05))
|
||||
bandits.append(KAB_E_Greedy(k_arms, epsilon=0.10))
|
||||
bandits.append(KAB_E_Greedy(k_arms, epsilon=0.20))
|
||||
|
||||
labels = [
|
||||
'E-Greedy(0.01)',
|
||||
'E-Greedy(0.05)',
|
||||
'E-Greedy(0.10)',
|
||||
'E-Greedy(0.20)',
|
||||
]
|
||||
title = "E-Greedy"
|
||||
kab_base.mp_simulate(bandits, k_arms, runs, steps, labels, title)
|
|
@ -0,0 +1,35 @@
|
|||
import numpy as np
|
||||
import bandit_23_Base as kab_base
|
||||
|
||||
class KAB_Greedy(kab_base.KArmBandit):
|
||||
def __init__(self, k_arms=10, try_steps=10):
|
||||
super().__init__(k_arms=k_arms)
|
||||
self.try_steps = try_steps # 试探次数
|
||||
|
||||
def select_action(self):
|
||||
if (self.steps < self.try_steps):
|
||||
action = np.random.randint(self.k_arms) # 随机选择动作
|
||||
else:
|
||||
action = np.argmax(self.Q) # 贪心选择目前最好的动作
|
||||
return action
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
runs = 2000
|
||||
steps = 1000
|
||||
k_arms = 10
|
||||
|
||||
bandits:kab_base.KArmBandit = []
|
||||
bandits.append(KAB_Greedy(k_arms, try_steps=10))
|
||||
bandits.append(KAB_Greedy(k_arms, try_steps=20))
|
||||
bandits.append(KAB_Greedy(k_arms, try_steps=40))
|
||||
bandits.append(KAB_Greedy(k_arms, try_steps=80))
|
||||
|
||||
labels = [
|
||||
'Greedy(10)',
|
||||
'Greedy(20)',
|
||||
'Greedy(40)',
|
||||
'Greedy(80)'
|
||||
]
|
||||
title = "Greedy"
|
||||
kab_base.mp_simulate(bandits, k_arms, runs, steps, labels, title)
|
|
@ -0,0 +1,57 @@
|
|||
import numpy as np
|
||||
import bandit_23_Base as kab_base
|
||||
|
||||
class KAB_Softmax(kab_base.KArmBandit):
|
||||
def __init__(self, k_arms=10, alpha:float=0.1):
|
||||
super().__init__(k_arms=k_arms)
|
||||
self.alpha = alpha
|
||||
self.P = 0
|
||||
|
||||
def reset(self):
|
||||
super().reset()
|
||||
self.average_reward = 0
|
||||
|
||||
def select_action(self):
|
||||
q_exp = np.exp(self.Q - np.max(self.Q)) # 所有的值都减去最大值
|
||||
self.P = q_exp / np.sum(q_exp) # softmax 实现
|
||||
action = np.random.choice(self.k_arms, p=self.P) # 按概率选择动作
|
||||
return action
|
||||
|
||||
def update_Q(self, action, reward):
|
||||
self.steps += 1 # 迭代次数
|
||||
self.action_count[action] += 1 # 动作次数(action_count)
|
||||
self.average_reward += (reward - self.average_reward) / self.steps
|
||||
self.Q[action] += self.alpha * (reward - self.average_reward) * self.P[action]
|
||||
return
|
||||
# 是否要更新没有被选中的动作 Q 值
|
||||
for i in range(self.k_arms):
|
||||
if (i != action):
|
||||
self.Q[i] += self.alpha * (-self.average_reward) * (self.P[i])
|
||||
return
|
||||
# Sutton 的算法
|
||||
one_hot = np.zeros(self.k_arms)
|
||||
one_hot[action] = 1
|
||||
self.average_reward += (reward - self.average_reward) / self.steps
|
||||
self.Q += self.alpha * (reward - self.average_reward) * (one_hot - self.P)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
runs = 200
|
||||
steps = 1000
|
||||
k_arms = 10
|
||||
|
||||
bandits:kab_base.KArmBandit = []
|
||||
bandits.append(KAB_Softmax(k_arms, alpha=0.5))
|
||||
bandits.append(KAB_Softmax(k_arms, alpha=0.6))
|
||||
bandits.append(KAB_Softmax(k_arms, alpha=0.7))
|
||||
bandits.append(KAB_Softmax(k_arms, alpha=0.8))
|
||||
|
||||
labels = [
|
||||
'Softmax(0.5)',
|
||||
'Softmax(0.6)',
|
||||
'Softmax(0.7)',
|
||||
'Softmax(0.8)',
|
||||
]
|
||||
|
||||
title = 'Softmax'
|
||||
kab_base.mp_simulate(bandits, k_arms, runs, steps, labels, title)
|
|
@ -0,0 +1,57 @@
|
|||
from cProfile import label
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
from bandit_25_Softmax import KAB_Softmax
|
||||
import matplotlib as mpl
|
||||
|
||||
mpl.rcParams['font.sans-serif'] = ['SimHei']
|
||||
mpl.rcParams['axes.unicode_minus']=False
|
||||
|
||||
class KAB_Softmax_test(KAB_Softmax):
|
||||
def __init__(self, k_arms=10, alpha:float=0.1):
|
||||
super().__init__(k_arms=k_arms, alpha=alpha)
|
||||
self.Ps = []
|
||||
self.Qs = []
|
||||
|
||||
def select_action(self):
|
||||
q_exp = np.exp(self.Q - np.max(self.Q)) # 所有的值都减去最大值
|
||||
self.P = q_exp / np.sum(q_exp) # softmax 实现
|
||||
action = np.random.choice(self.k_arms, p=self.P) # 按概率选择动作
|
||||
self.Ps.append(self.P.copy())
|
||||
self.Qs.append(self.Q.copy())
|
||||
return action
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
runs = 1
|
||||
steps = 200
|
||||
k_arms = 3
|
||||
np.random.seed(15)
|
||||
bandit = KAB_Softmax_test(k_arms, alpha=0.15)
|
||||
bandit.simulate(runs, steps)
|
||||
|
||||
grid = plt.GridSpec(nrows=1, ncols=2)
|
||||
plt.subplot(grid[0, 0])
|
||||
Q_array = np.array(bandit.Qs)
|
||||
for i in range(k_arms):
|
||||
plt.plot(Q_array[:,i], label=str(i))
|
||||
plt.title(u'备选动作的价值变化')
|
||||
plt.xlabel(u'迭代次数')
|
||||
plt.ylabel(u'动作价值')
|
||||
plt.grid()
|
||||
plt.legend()
|
||||
print("最终的动作价值 =", np.round(Q_array[-1,:], 2))
|
||||
|
||||
plt.subplot(grid[0, 1])
|
||||
P_array = np.array(bandit.Ps)
|
||||
for i in range(k_arms):
|
||||
plt.plot(P_array[:,i], label=str(i))
|
||||
plt.title(u'备选动作概率的变化')
|
||||
plt.xlabel(u'迭代次数')
|
||||
plt.ylabel(u'被选概率')
|
||||
plt.grid()
|
||||
plt.legend()
|
||||
plt.show()
|
||||
|
||||
print("最终的备选概率 =", P_array[-1,:])
|
||||
print("备选概率的和 =", np.sum(P_array[-1,:]))
|
|
@ -0,0 +1,34 @@
|
|||
import numpy as np
|
||||
import math
|
||||
import bandit_23_Base as kab_base
|
||||
|
||||
class KAB_UCB(kab_base.KArmBandit):
|
||||
def __init__(self, k_arms=10, c=1):
|
||||
super().__init__(k_arms=k_arms)
|
||||
self.C = c
|
||||
|
||||
def select_action(self):
|
||||
ucb = self.C * np.sqrt(math.log(self.steps + 1) / (self.action_count + 1e-2))
|
||||
estimation = self.Q + ucb
|
||||
action = np.argmax(estimation)
|
||||
return action
|
||||
|
||||
if __name__ == "__main__":
|
||||
runs = 2000
|
||||
steps = 1000
|
||||
k_arms = 10
|
||||
|
||||
bandits:kab_base.KArmBandit = []
|
||||
bandits.append(KAB_UCB(k_arms, c=0.5))
|
||||
bandits.append(KAB_UCB(k_arms, c=0.7))
|
||||
bandits.append(KAB_UCB(k_arms, c=1))
|
||||
bandits.append(KAB_UCB(k_arms, c=1.2))
|
||||
|
||||
labels = [
|
||||
'UCB(c=0.5)',
|
||||
'UCB(c=0.7)',
|
||||
'UCB(c=1.0)',
|
||||
'UCB(c=1.2)'
|
||||
]
|
||||
title = "UCB"
|
||||
kab_base.mp_simulate(bandits, k_arms, runs, steps, labels, title)
|
|
@ -0,0 +1,72 @@
|
|||
import numpy as np
|
||||
from bandit_26_UCB import KAB_UCB
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
|
||||
class KAB_UCB_test(KAB_UCB):
|
||||
def select_action(self):
|
||||
ucb = self.C * np.sqrt(np.log(self.steps + 1) / (self.action_count + 1e-2))
|
||||
estimation = self.Q + ucb
|
||||
action = np.argmax(estimation)
|
||||
return action, self.Q, ucb
|
||||
|
||||
# 模拟运行
|
||||
def simulate(self, runs, steps):
|
||||
# 记录历史 reward,便于后面统计
|
||||
rewards = np.zeros(shape=(runs, steps))
|
||||
actions = np.zeros(shape=(runs, steps), dtype=int)
|
||||
values = np.zeros(shape=(runs, steps, 2, self.k_arms))
|
||||
|
||||
for r in range(runs):
|
||||
# 每次run都独立,但是使用相同的参数
|
||||
self.reset()
|
||||
# 测试 time 次
|
||||
for s in range(steps):
|
||||
action, mu, ucb = self.select_action()
|
||||
actions[r, s] = action
|
||||
values[r, s, 0] = mu
|
||||
values[r, s, 1] = ucb
|
||||
reward = self.pull_arm(action)
|
||||
rewards[r, s] = reward
|
||||
self.update_Q(action, reward)
|
||||
# end for t
|
||||
# end for r
|
||||
return rewards, actions, values
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
runs = 1
|
||||
steps = 100
|
||||
k_arms = 3
|
||||
|
||||
np.random.seed(13)
|
||||
bandit = KAB_UCB_test(k_arms, c=1)
|
||||
rewards, actions, values = bandit.simulate(runs, steps)
|
||||
|
||||
for step in range(steps):
|
||||
mu = values[0,step,0]
|
||||
ucb = values[0,step,1]
|
||||
action = actions[0,step]
|
||||
reward = rewards[0,step]
|
||||
|
||||
s = str.format("step={0:2d}, Q={1}, UCB={2}, Q+UCB={3}, a={4}, r={5:.2f}",
|
||||
step, np.around(mu,2), np.around(ucb,2), np.around(mu+ucb,2), action, reward)
|
||||
print(s)
|
||||
|
||||
|
||||
grid = plt.GridSpec(nrows=1, ncols=6)
|
||||
|
||||
for step in range(steps):
|
||||
if step > 5:
|
||||
continue
|
||||
mu = values[0,step,0]
|
||||
ucb = values[0,step,1]
|
||||
action = actions[0,step]
|
||||
reward = rewards[0,step]
|
||||
|
||||
plt.subplot(grid[0, step])
|
||||
plt.bar([0,1,2], mu + ucb, width=0.5, bottom=mu, color=['r','g','b'])
|
||||
plt.title(str.format("a={0},r={1:.2f}", action, reward))
|
||||
plt.grid()
|
||||
|
||||
plt.show()
|
|
@ -0,0 +1,46 @@
|
|||
from turtle import color
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
import numpy as np
|
||||
from scipy.stats import beta
|
||||
import matplotlib.pyplot as plot
|
||||
import matplotlib as mpl
|
||||
import matplotlib.colors as mcolors
|
||||
|
||||
mpl.rcParams['font.sans-serif'] = ['SimHei']
|
||||
mpl.rcParams['axes.unicode_minus']=False
|
||||
colors = list(mcolors.TABLEAU_COLORS.keys())
|
||||
|
||||
a = np.random.beta(10, 50,size=(10))
|
||||
print(a)
|
||||
plt.scatter(a, np.zeros_like(a), color=mcolors.TABLEAU_COLORS[colors[1]])
|
||||
|
||||
a = np.random.beta(105.3,32.5,size=(10))
|
||||
print(a)
|
||||
plt.scatter(a, np.zeros_like(a), color=mcolors.TABLEAU_COLORS[colors[2]])
|
||||
|
||||
|
||||
# 定义一组alpha 跟 beta值
|
||||
alpha_beta_values = [[50,50], [10,50], [105.3,32.5], [1,1], [10,10],]
|
||||
linestyles = []
|
||||
|
||||
# 定义 x 值
|
||||
x = np.linspace(0, 1, 1002)[1:-1]
|
||||
for alpha_beta_value in alpha_beta_values:
|
||||
print(alpha_beta_value)
|
||||
dist = beta(alpha_beta_value[0], alpha_beta_value[1])
|
||||
dist_y = dist.pdf(x)
|
||||
# 添加图例
|
||||
# plot.legend('alpha=')
|
||||
# 创建 beta 曲线
|
||||
plot.plot(x, dist_y, label=r'$\alpha=%.1f,\ \beta=%.1f$' % (alpha_beta_value[0], alpha_beta_value[1]))
|
||||
|
||||
# 设置标题
|
||||
plot.title(u'Beta分布')
|
||||
# 设置 x,y 轴取值范围
|
||||
plot.xlim(0, 1)
|
||||
#plot.ylim(0, 2.5)
|
||||
plot.legend()
|
||||
plot.grid()
|
||||
plot.show()
|
|
@ -0,0 +1,57 @@
|
|||
import numpy as np
|
||||
import bandit_23_Base as kab_base
|
||||
|
||||
|
||||
class KAB_Thompson(kab_base.KArmBandit):
|
||||
def __init__(self, k_arms=10, method=0):
|
||||
super().__init__(k_arms=k_arms)
|
||||
self.method = method # -1: 与自身均值比;0:与所有均值比;>0:期望的门限值
|
||||
|
||||
def reset(self):
|
||||
super().reset()
|
||||
self.total_average = 0
|
||||
self.alpha = np.ones(self.k_arms)
|
||||
self.beta = np.ones(self.k_arms)
|
||||
|
||||
def select_action(self):
|
||||
p_beta = np.random.beta(self.alpha, self.beta)
|
||||
action = np.argmax(p_beta)
|
||||
return action
|
||||
|
||||
def update_Q(self, action, reward):
|
||||
super().update_Q(action, reward)
|
||||
self.total_average += (reward - self.total_average) / self.steps
|
||||
|
||||
is_win = False
|
||||
if (self.method == -1): # 与整体均值比较
|
||||
if (reward >= self.total_average):
|
||||
is_win = True
|
||||
else: # 与输入的期望值比较
|
||||
if (reward >= self.method):
|
||||
is_win = True
|
||||
# 用reward计数
|
||||
if is_win:
|
||||
self.alpha[action] += abs(reward)
|
||||
else:
|
||||
self.beta[action] += abs(reward)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
runs = 2000
|
||||
steps = 1000
|
||||
k_arms = 10
|
||||
|
||||
bandits:kab_base.KArmBandit = []
|
||||
bandits.append(KAB_Thompson(k_arms, -1))
|
||||
bandits.append(KAB_Thompson(k_arms, 0))
|
||||
bandits.append(KAB_Thompson(k_arms, 0.5))
|
||||
bandits.append(KAB_Thompson(k_arms, 0.8))
|
||||
|
||||
labels = [
|
||||
'KAB_Thompson(-1)',
|
||||
'KAB_Thompson(0.0)',
|
||||
'KAB_Thompson(0.5)',
|
||||
'KAB_Thompson(0.8)',
|
||||
]
|
||||
title = "Thompson"
|
||||
kab_base.mp_simulate(bandits, k_arms, runs, steps, labels, title)
|
|
@ -0,0 +1,68 @@
|
|||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
from bandit_27_Thompson import KAB_Thompson
|
||||
import scipy.stats as ss
|
||||
|
||||
|
||||
class KAB_Thompson_test(KAB_Thompson):
|
||||
def __init__(self, k_arms=10, method=0):
|
||||
super().__init__(k_arms=k_arms, method=method)
|
||||
|
||||
def select_action(self):
|
||||
beta = np.random.beta(self.alpha, self.beta)
|
||||
action = np.argmax(beta)
|
||||
return action, beta
|
||||
|
||||
def simulate(self, runs, steps):
|
||||
rewards = np.zeros(shape=(runs, steps))
|
||||
actions = np.zeros(shape=(runs, steps), dtype=int)
|
||||
win_loss = np.zeros(shape=(runs, steps, 2, self.k_arms))
|
||||
beta_samples = np.zeros(shape=(runs, steps, self.k_arms))
|
||||
|
||||
for r in range(runs):
|
||||
# 每次run都清零计算 q 用的统计数据,并重新初始化奖励均值
|
||||
self.reset()
|
||||
self.alpha += 1
|
||||
self.beta += 1
|
||||
# 测试 time 次
|
||||
for s in range(steps):
|
||||
win_loss[r, s, 0] = self.alpha
|
||||
win_loss[r, s, 1] = self.beta
|
||||
action, beta = self.select_action()
|
||||
actions[r, s] = action
|
||||
beta_samples[r, s] = beta
|
||||
reward = self.pull_arm(action)
|
||||
rewards[r, s] = reward
|
||||
self.update_Q(action, reward)
|
||||
return rewards, actions, win_loss, beta_samples
|
||||
|
||||
if __name__ == "__main__":
|
||||
runs = 1
|
||||
steps = 20
|
||||
k_arms = 3
|
||||
|
||||
np.random.seed(5)
|
||||
bandit = KAB_Thompson_test(k_arms, 0)
|
||||
rewards, actions, win_loss, beta = bandit.simulate(runs, steps)
|
||||
|
||||
grid = plt.GridSpec(nrows=3, ncols=5)
|
||||
x = np.linspace(0,1,num=101)
|
||||
for i in range(steps):
|
||||
s = str.format("step:{5}\twin:loss={0}:{1}\tbeta={2}\ta={3}\tr={4}",
|
||||
np.round(win_loss[0,i,0],2),
|
||||
np.round(win_loss[0,i,1],2),
|
||||
np.round(beta[0,i], 2),
|
||||
actions[0,i],
|
||||
np.round(rewards[0,i],2), i)
|
||||
print(s)
|
||||
if (i >= 15):
|
||||
continue
|
||||
plt.subplot(grid[int(i/5), i%5])
|
||||
for j in range(k_arms):
|
||||
beta_pdf = ss.beta.pdf(x, win_loss[0,i,0,j], win_loss[0,i,1,j])
|
||||
plt.plot(x, beta_pdf, label=str(j))
|
||||
plt.scatter(beta[0,i,j], 0)
|
||||
plt.title(str.format("step:{0},a={1},r={2:.2f}",i,actions[0,i],rewards[0,i]))
|
||||
plt.grid()
|
||||
plt.legend()
|
||||
plt.show()
|
|
@ -0,0 +1,62 @@
|
|||
|
||||
import multiprocessing as mp
|
||||
import matplotlib.pyplot as plt
|
||||
import scipy.signal as ss
|
||||
import numpy as np
|
||||
|
||||
from bandit_23_Base import KArmBandit
|
||||
from bandit_24_Greedy import KAB_Greedy
|
||||
from bandit_24_E_Greedy import KAB_E_Greedy
|
||||
from bandit_25_Softmax import KAB_Softmax
|
||||
from bandit_26_UCB import KAB_UCB
|
||||
from bandit_27_Thompson import KAB_Thompson
|
||||
|
||||
|
||||
def run_algo(algo_name:KArmBandit, runs, steps, k_arms, parameters):
|
||||
all_mean_reward = []
|
||||
for p in parameters:
|
||||
bandit = algo_name(k_arms, p)
|
||||
rewards, _, _ = bandit.simulate(runs, steps)
|
||||
mean_reward = rewards.mean()
|
||||
all_mean_reward.append(mean_reward)
|
||||
return all_mean_reward
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
runs = 200
|
||||
steps = 1000
|
||||
k_arms = 10
|
||||
|
||||
algo_names = [KAB_Greedy, KAB_E_Greedy, KAB_Softmax, KAB_UCB, KAB_Thompson]
|
||||
algo_params = {
|
||||
KAB_Greedy: [10, 20, 30, 40, 50, 60, 70, 80, 90, 100],
|
||||
KAB_E_Greedy: [0.01, 0.02, 0.03, 0.04, 0.05, 0.08, 0.10, 0.15, 0.20, 0.25],
|
||||
KAB_Softmax: [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0],
|
||||
KAB_UCB: [0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.2],
|
||||
KAB_Thompson: [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]
|
||||
}
|
||||
|
||||
np.random.seed(5)
|
||||
|
||||
pool = mp.Pool(processes=4)
|
||||
results = []
|
||||
for algo_name in algo_names:
|
||||
params = algo_params[algo_name]
|
||||
results.append(pool.apply_async(run_algo, args=(algo_name,runs,steps,k_arms,params,)))
|
||||
pool.close()
|
||||
pool.join()
|
||||
# 收集结果
|
||||
algo_rewards = []
|
||||
for i in range(len(results)):
|
||||
algo_reward = results[i].get()
|
||||
algo_rewards.append(algo_reward)
|
||||
print(len(algo_rewards))
|
||||
for i, algo_name in enumerate(algo_names):
|
||||
smooth_data = ss.savgol_filter(algo_rewards[i], 10, 3)
|
||||
plt.plot(smooth_data, label=str(algo_name))
|
||||
print(algo_name)
|
||||
print(algo_rewards[i])
|
||||
plt.grid()
|
||||
plt.legend()
|
||||
plt.show()
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
|
||||
import multiprocessing as mp
|
||||
import numpy as np
|
||||
|
||||
from bandit_23_Base import KArmBandit, mp_simulate
|
||||
from bandit_24_Greedy import KAB_Greedy
|
||||
from bandit_25_Softmax import KAB_Softmax
|
||||
from bandit_26_UCB import KAB_UCB
|
||||
from bandit_27_Thompson import KAB_Thompson
|
||||
|
||||
|
||||
def staristic(k_arms, runs, steps):
|
||||
|
||||
bandits:KArmBandit = []
|
||||
bandits.append(KAB_Greedy(k_arms, 40))
|
||||
bandits.append(KAB_Softmax(k_arms, 1.0))
|
||||
bandits.append(KAB_UCB(k_arms, 1.0))
|
||||
bandits.append(KAB_Thompson(k_arms, 0.7))
|
||||
|
||||
# statistic
|
||||
all_rewards = []
|
||||
all_best = []
|
||||
all_actions = []
|
||||
|
||||
pool = mp.Pool(processes=4)
|
||||
results = []
|
||||
for i, bandit in enumerate(bandits):
|
||||
results.append(pool.apply_async(bandit.simulate, args=(runs,steps,)))
|
||||
pool.close()
|
||||
pool.join()
|
||||
|
||||
for i in range(len(results)):
|
||||
rewards, best_action, actions = results[i].get()
|
||||
all_rewards.append(rewards)
|
||||
all_best.append(best_action)
|
||||
all_actions.append(actions)
|
||||
|
||||
all_best_actions = np.array(all_best).mean(axis=1)
|
||||
all_mean_rewards = np.array(all_rewards).mean(axis=1)
|
||||
all_done_actions = np.array(all_actions)
|
||||
best_action_per_bandit = all_done_actions[:,k_arms-1]/all_done_actions.sum(axis=1)
|
||||
mean_reward_per_bandit = all_mean_rewards.sum(axis=1) / steps
|
||||
|
||||
features = np.zeros(shape=(len(bandits),8))
|
||||
# 0-100步的平均收益
|
||||
features[:,0] = all_mean_rewards[:,0:100].mean(axis=1)
|
||||
# 300-500步的平均收益
|
||||
features[:,1] = all_mean_rewards[:,300:500].mean(axis=1)
|
||||
# 700-900步的平均收益
|
||||
features[:,2] = all_mean_rewards[:,700:900].mean(axis=1)
|
||||
# 1000步的平均收益
|
||||
features[:,3] = mean_reward_per_bandit
|
||||
# 0-100步的最佳利用率
|
||||
features[:,4] = all_best_actions[:,0:100].mean(axis=1)
|
||||
# 300-500步的最佳利用率
|
||||
features[:,5] = all_best_actions[:,300:500].mean(axis=1)
|
||||
# 700-900步的最佳利用率
|
||||
features[:,6] = all_best_actions[:,700:900].mean(axis=1)
|
||||
# 1000步的最佳利用率
|
||||
features[:,7] = best_action_per_bandit
|
||||
|
||||
|
||||
print(np.round(features, 3))
|
||||
|
||||
X = features
|
||||
# X: 第一维是不同的算法,第二维是8个特征值
|
||||
# 归一化, 按特征值归一化
|
||||
Y = (X - np.min(X, axis=0, keepdims=True)) / (np.max(X, axis=0, keepdims=True) - np.min(X, axis=0, keepdims=True))
|
||||
print("Y.shape=", Y.shape)
|
||||
print(np.round(Y, 3))
|
||||
|
||||
# 计算权重值
|
||||
Z = Y / np.sqrt(np.sum(Y * Y))
|
||||
print("Z.shape=", Z.shape)
|
||||
print(np.round(Z, 3))
|
||||
|
||||
# Z+ Z-
|
||||
max_z = np.max(Z, axis=0)
|
||||
min_z = np.min(Z, axis=0)
|
||||
print("max_z.shape=", max_z.shape)
|
||||
print(max_z)
|
||||
print(min_z)
|
||||
|
||||
# D+, D-
|
||||
d_plus = np.sqrt(np.sum(np.square(Z - max_z), axis=1))
|
||||
d_minus = np.sqrt(np.sum(np.square(Z - min_z), axis=1))
|
||||
print("d_plus.shape=", d_plus.shape)
|
||||
print(d_plus)
|
||||
print(d_minus)
|
||||
|
||||
C = d_minus / (d_plus + d_minus)
|
||||
print("C=", C)
|
||||
sort = np.argsort(C)
|
||||
print("sort.shape=",sort.shape)
|
||||
best_to_worst = list(reversed(sort))
|
||||
print(best_to_worst)
|
||||
for i in best_to_worst:
|
||||
print(bandits[i].__class__.__name__)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
runs = 2000
|
||||
steps = 1000
|
||||
k_arms = 10
|
||||
|
||||
np.random.seed(515)
|
||||
bandits:KArmBandit = []
|
||||
bandits.append(KAB_Greedy(k_arms, 40))
|
||||
bandits.append(KAB_Softmax(k_arms, 1.0))
|
||||
bandits.append(KAB_UCB(k_arms, 1.0))
|
||||
bandits.append(KAB_Thompson(k_arms, 0.7))
|
||||
|
||||
labels = [
|
||||
'Greedy (40)',
|
||||
'Softmax (1.0)',
|
||||
'UCBound (1.0)',
|
||||
'Thompson(0.7)',
|
||||
]
|
||||
title = "Compare"
|
||||
mp_simulate(bandits, k_arms, runs, steps, labels, title)
|
||||
|
||||
# staristic(k_arms, runs, steps)
|
|
@ -0,0 +1,109 @@
|
|||
|
||||
import multiprocessing as mp
|
||||
import numpy as np
|
||||
|
||||
from bandit_23_Base import KArmBandit, mp_simulate
|
||||
from bandit_24_Greedy import KAB_Greedy
|
||||
from bandit_25_Softmax import KAB_Softmax
|
||||
from bandit_26_UCB import KAB_UCB
|
||||
from bandit_27_Thompson import KAB_Thompson
|
||||
|
||||
|
||||
def staristic(k_arms, runs, steps):
|
||||
|
||||
np.random.seed(515)
|
||||
|
||||
bandits:KArmBandit = []
|
||||
bandits.append(KAB_Greedy(k_arms, 40))
|
||||
bandits.append(KAB_Softmax(k_arms, 1.0))
|
||||
bandits.append(KAB_UCB(k_arms, 1.0))
|
||||
bandits.append(KAB_Thompson(k_arms, 0.7))
|
||||
|
||||
# statistic
|
||||
all_rewards = []
|
||||
all_best = []
|
||||
all_actions = []
|
||||
|
||||
pool = mp.Pool(processes=4)
|
||||
results = []
|
||||
for i, bandit in enumerate(bandits):
|
||||
results.append(pool.apply_async(bandit.simulate, args=(runs,steps,)))
|
||||
pool.close()
|
||||
pool.join()
|
||||
|
||||
for i in range(len(results)):
|
||||
rewards, best_action, actions = results[i].get()
|
||||
all_rewards.append(rewards)
|
||||
all_best.append(best_action)
|
||||
all_actions.append(actions)
|
||||
|
||||
all_best_actions = np.array(all_best).mean(axis=1)
|
||||
all_mean_rewards = np.array(all_rewards).mean(axis=1)
|
||||
all_done_actions = np.array(all_actions)
|
||||
best_action_per_bandit = all_done_actions[:,k_arms-1]/all_done_actions.sum(axis=1)
|
||||
mean_reward_per_bandit = all_mean_rewards.sum(axis=1) / steps
|
||||
|
||||
features = np.zeros(shape=(len(bandits),8))
|
||||
# 0-100步的平均收益
|
||||
features[:,0] = all_mean_rewards[:,0:100].mean(axis=1)
|
||||
# 300-500步的平均收益
|
||||
features[:,1] = all_mean_rewards[:,300:500].mean(axis=1)
|
||||
# 700-900步的平均收益
|
||||
features[:,2] = all_mean_rewards[:,700:900].mean(axis=1)
|
||||
# 1000步的平均收益
|
||||
features[:,3] = mean_reward_per_bandit
|
||||
# 0-100步的最佳利用率
|
||||
features[:,4] = all_best_actions[:,0:100].mean(axis=1)
|
||||
# 300-500步的最佳利用率
|
||||
features[:,5] = all_best_actions[:,300:500].mean(axis=1)
|
||||
# 700-900步的最佳利用率
|
||||
features[:,6] = all_best_actions[:,700:900].mean(axis=1)
|
||||
# 1000步的最佳利用率
|
||||
features[:,7] = best_action_per_bandit
|
||||
|
||||
print(np.round(features, 3))
|
||||
|
||||
X = features
|
||||
# X: 第一维是不同的算法,第二维是8个特征值
|
||||
# 归一化, 按特征值归一化
|
||||
Y = (X - np.min(X, axis=0, keepdims=True)) / (np.max(X, axis=0, keepdims=True) - np.min(X, axis=0, keepdims=True))
|
||||
print("Y.shape=", Y.shape)
|
||||
print(np.round(Y, 3))
|
||||
|
||||
# 计算权重值
|
||||
Z = Y / np.sqrt(np.sum(Y * Y, axis=0))
|
||||
print("Z.shape=", Z.shape)
|
||||
print(np.round(Z, 3))
|
||||
|
||||
# Z+ Z-
|
||||
max_z = np.max(Z, axis=0)
|
||||
min_z = np.min(Z, axis=0)
|
||||
print("max_z.shape=", max_z.shape)
|
||||
print(np.round(max_z,3))
|
||||
print("min_z.shape=", max_z.shape)
|
||||
print(min_z)
|
||||
|
||||
# D+, D-
|
||||
d_plus = np.sqrt(np.sum(np.square(Z - max_z), axis=1))
|
||||
d_minus = np.sqrt(np.sum(np.square(Z - min_z), axis=1))
|
||||
print("d_plus.shape=", d_plus.shape)
|
||||
print(d_plus)
|
||||
print("d_minus.shape=", d_plus.shape)
|
||||
print(d_minus)
|
||||
|
||||
D = d_minus / (d_plus + d_minus)
|
||||
print("D=", np.round(D,3))
|
||||
|
||||
sort = np.argsort(D)
|
||||
print("sort.shape=",sort.shape)
|
||||
best_to_worst = list(reversed(sort))
|
||||
print(best_to_worst)
|
||||
for i in best_to_worst:
|
||||
print(bandits[i].__class__.__name__)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
runs = 200
|
||||
steps = 1000
|
||||
k_arms = 10
|
||||
staristic(k_arms, runs, steps)
|
|
@ -1,5 +0,0 @@
|
|||
# 第 2 章 从概率计算到模拟验证
|
||||
|
||||
不瞒大家说,笔者有个洁癖,就是要给每一章取名字的时候,都用“xxxx问题”,最好是四个字。其实本章的真正名字叫做“**多臂赌博机**”问题(五个字),但是其英文 K-Arm Bandits,被某些翻译软件翻译成“武装匪徒”,正好四个字,笔者将错就错,用了这个名字,还请广大读者原谅。其实也是趁机讽刺了网络上那些不负责任的资料翻译行为。
|
||||
|
||||
探索与利用
|
|
@ -106,4 +106,4 @@ $$
|
|||
图 8.7.6 迷宫游戏中定义在状态节点上的奖励
|
||||
</center>
|
||||
|
||||
在这个例子中,中心绿色方格是智能体要达到的目标状态,给与奖励值 +1;深红色的方格是智能体要避开的状态,给与奖励值 -1。它不管是从哪里到达目标方格的,只要达到了该状态,就给与相应的奖励。因为 100 个方格(状态),每个方格都有 4 种动作,动作发生后的转移概率为 1,那么就有 400 个动作,如果给每个动作或者过程都定义奖励的话,就太麻烦了。而图 8.7.6 中,只需要定义 10 个奖励即可(其它 90 个奖励是 0)。
|
||||
在这个例子中,中心绿色方格是智能体要达到的目标状态,给与奖励值 $R=1.0$;深红色的方格是智能体要避开的状态,给与奖励值 $R=-1.0$。它不管是从哪里到达目标方格的,只要达到了该状态,就给与相应的奖励。因为 100 个方格(状态),每个方格都有 4 种动作,动作发生后的转移概率为 1,那么就有 400 个动作,如果给每个动作或者过程都定义奖励的话,就太麻烦了。而图 8.7.6 中,只需要定义 10 个奖励即可(其它 90 个奖励是 0)。
|
||||
|
|
|
@ -14,21 +14,22 @@
|
|||
|
||||
|状态序号|动作选择|奖励|转移|
|
||||
|-|-|-|-|
|
||||
|角落状态*|如果在角落处向远离中心的方向行驶,将会碰撞能量屏障使飞船受损,比如 $s_{24}$ 所示的角落处向右或向下行驶|-1|原地不动|
|
||||
|边界状态**|如果在边界处向远离中心的方向行驶,将会碰撞能量屏障使飞船受损,比如 $s_{9}$ 所示的边界处向右行驶|-1|原地不动|
|
||||
|角落和边界状态|向靠近中心的方向行驶,|0|1.0:动作方向|
|
||||
|虫洞状态 $s_{1}$|在 $s_1$ 处进行下一步行驶时,无论任何方向,将无条件地达到 $s_{12}$ 处,但后者并非终止状态。| +5 |1.0:到 $s_{12}$|
|
||||
|虫洞状态 $s_{21}$|在 $s_{21}$ 处进行下一步行驶时,无论任何方向,将无条件地达到 $s_{3}$ 处,但后者并非终止状态。| +10 |1.0:到 $s_{3}$|
|
||||
|角落状态*|如果在角落处向远离中心的方向行驶,将会碰撞能量屏障<br>使飞船受损,比如 $s_{24}$ 所示的角落处向右或向下行驶。|-1|原地不动|
|
||||
|边界状态**|如果在边界处向远离中心的方向行驶,将会碰撞能量屏障<br>使飞船受损,比如 $s_{9}$ 所示的边界处向右行驶。|-1|原地不动|
|
||||
|角落和边界状态|当向中心的方向行驶时,不会发生意外偏航。|0|1.0:动作方向|
|
||||
|虫洞状态 $s_{1}$|在 $s_1$ 处进行下一步行驶时,无论任何方向,将无条件地<br>达到 $s_{12}$ 处,但后者并非终止状态。| +5 |1.0:到 $s_{12}$|
|
||||
|虫洞状态 $s_{21}$|在 $s_{21}$ 处进行下一步行驶时,无论任何方向,将无条件地<br>达到 $s_{3}$ 处,但后者并非终止状态。| +10 |1.0:到 $s_{3}$|
|
||||
|中心状态|在上述三种特殊状态之外的所有其它状态,比如在 $s_{18}$ 处,如果选择向右行驶,将会达到 $s_{19}$,不会发生意外偏航。|0|1.0:动作方向|
|
||||
|
||||
表 9.1.1 描述了这些空间区域的基本特征,补充说明如下:
|
||||
|
||||
- 状态空间:5 x 5
|
||||
- 动作空间:4
|
||||
- 状态空间:5 x 5。
|
||||
- 动作空间:4。
|
||||
- 策略:随机,$\pi=1/4=0.25$
|
||||
- *:角落状态包括状态序号 {0,4,20,24}。
|
||||
- **:边界状态包括状态序号 {1,2,3,5,9,10,14,15,19,21,22,23}。
|
||||
- 所以序号为 1,21 的两个状态可以从边界状态中去掉。
|
||||
- 角落状态*:角落状态包括状态序号 {0,4,20,24}。
|
||||
- 边界状态**:边界状态包括状态序号 {1,2,3,5,9,10,14,15,19,21,22,23}。
|
||||
- 虫洞状态:{1, 21} 是两个虫洞的入口,出口没有特殊性质。
|
||||
- 所以序号为 {1, 21} 的两个状态可以从边界状态中去掉。
|
||||
|
||||
### 9.1.2 问题特点
|
||||
|
||||
|
@ -41,24 +42,28 @@
|
|||
2. 在每个状态(方格)下,可以随机选择 4 个方向中的任意一个移动,即动作空间为 4,动作为“上下左右”,策略概率为 0.25。
|
||||
3. 到达 $s_1,s_{21}$ 时,不是被立刻吸入虫洞,而是要进行下一步动作时才会时空转移。但是在 $s_1,s_{21}$ 并没有机会向其它方向行驶。
|
||||
4. 关于边角位置的状态,如图 9.1.2 所示,以状态 $s_2$ 为例:
|
||||
- 如果从该状态以 0.25 的概率选择向上移动(蓝色的实心圆旁标有 Up 字样)出界后,会以概率 1.0 回到 $s_2$,并有 -1 的奖励。所以说,这里面既有策略 $\pi$,又有转移概率 $p$,只不过动作发生后只有一个下游状态,没有分支。
|
||||
- 如果从 $s_2$ 以 0.25 的概率选择向下移动(蓝色的实心圆旁标有 Down 字样),会以 1.0 的概率转移到 $s_7$,得到 0 的奖励。
|
||||
- 如果从该状态以 0.25 的概率选择向上移动(蓝色的实心圆旁标有 Up 字样)出界后,会以概率 1.0 回到 $s_2$,并有 -1 的奖励。所以说,这里面既有策略 $\pi$,又有转移概率 $p$,只不过动作发生后只有一个下游状态,没有分支。即,$p(s_2,-1|s_2,Up)=1$。
|
||||
- 如果从 $s_2$ 以 0.25 的概率选择向下移动(蓝色的实心圆旁标有 Down 字样),会以 1.0 的概率转移到 $s_7$,得到 0 的奖励。即,$p(s_7,0|s_2,Down)=1$。
|
||||
<center>
|
||||
<img src="./img/ship-2.png">
|
||||
|
||||
图 9.1.2 边界状态的动作和概率转移
|
||||
</center>
|
||||
|
||||
所以,这个穿越虫洞问题的动作空间是 4,动作发生后的转移概率是 1,即,动作发生后,可以准确地到达目标,不会发生意外。而上一章中的射击气球问题,动作空间是 2,动作发生后的状态转移大于 2(两个动作分别是 2 和 3)。
|
||||
所以,这个穿越虫洞问题的动作空间是 4,动作发生后的转移概率是 1,即,动作发生后,可以准确地到达目标,不会发生意外。而上一章中的射击气球问题,动作空间是 2,动作发生后的状态转移分支数大于 2(两个动作分别是 2 和 3)。
|
||||
|
||||
最主要的是,读者一定要把这一章的问题与前面章节中学习的马尔科夫奖励过程中的没有动作而直接发生状态转移的情况分开。
|
||||
|
||||
|
||||
### 9.1.3 模型数据定义
|
||||
|
||||
在强化学习中,经常会用图 9.1.1 这种方格(或长方格)来研究各种算法,因为这种方式可以有效地把一些连续问题转变为离散问题,从而使用马尔科夫链来简化并描述问题。所以有必要建立一个通用的模型,用数据驱动的方式来定义模型的各种行为。
|
||||
在强化学习中的表格型问题中,经常会用图 9.1.1 这种方格(或长方格)来研究各种算法,因为这种方式可以有效地把一些连续问题转变为离散问题,从而使用马尔科夫链来简化并描述问题。所以有必要建立一个通用的模型,用数据驱动的方式来定义模型的各种行为。
|
||||
|
||||
模型定义可以分为四个小部分。
|
||||
模型定义可以分为四个小部分:
|
||||
- 状态空间定义
|
||||
- 动作空间定义
|
||||
- 奖励函数定义
|
||||
- 特殊移动定义
|
||||
|
||||
【代码位置】Wormhole_0_Data.py
|
||||
|
||||
|
@ -86,7 +91,7 @@ EndStates = []
|
|||
|
||||
- 关于序号的约定
|
||||
|
||||
- 可以用从 0 开始的序号,方格的左上角序号为 0,然后向右依次加 1,到最右侧边界后换行。右下角的序号为 GridWidth*GridHeight-1,比如 3x4-1=11。
|
||||
- 可以用从 0 开始的序号,**方格的左上角序号为 0**,然后**向右依次加 1**,到最右侧边界后换行。右下角的序号为 GridWidth*GridHeight-1,比如 3x4-1=11。
|
||||
|
||||
- 也可以用 $(x,y)$ 的方式定义每个状态的位置,方格的左上角坐标为 (0,0),以 3x4 为例,右下角为 (2,3)。
|
||||
|
||||
|
@ -98,12 +103,12 @@ Actions = [LEFT, UP, RIGHT, DOWN]
|
|||
# 初始策略
|
||||
Policy = [0.25, 0.25, 0.25, 0.25]
|
||||
# 状态转移概率: [SlipLeft, MoveFront, SlipRight, SlipBack]
|
||||
SlipProbs = [0.0, 1.0, 0.0, 0.0]
|
||||
Transition = [0.0, 1.0, 0.0, 0.0]
|
||||
```
|
||||
- 按中国人的习惯,定义左(LEFT)、上(UP)、右(RIGHT)、下(DOWN)顺时针顺序的四个方向。
|
||||
- 动作空间 Actions
|
||||
|
||||
由这上面四个动作组成。当然,在醉汉回家问题中,只有左、右两个动作。
|
||||
由这上面四个动作组成。在醉汉回家问题中,只有左、右两个动作。
|
||||
|
||||
- 初始策略 Policy
|
||||
|
||||
|
@ -111,7 +116,7 @@ SlipProbs = [0.0, 1.0, 0.0, 0.0]
|
|||
|
||||
- 状态转移概率 SlipProbs
|
||||
|
||||
在动作执行后,是否会出现偏差。举例来说,在冰面向前行走,很有可能冰面太滑而造成向左 0.2、向右 0.1、向前 0.7 的状态转移概率,那么该值就可以写成 [0.2, 0.7, 0.1, 0.0]。注意顺序不能乱,一定是“左上右下”。
|
||||
在动作执行后,是否会出现偏差。举例来说,在冰面向前行走,很有可能冰面太滑而造成向左 0.2、向右 0.1、向前 0.7 的状态转移概率,那么该值就可以写成 [0.2, 0.7, 0.1, 0.0]。注意顺序不能乱,一定是“左 SlipLeft、前 MoveFront、右 SlipRight、后 SlipBack”。这里的“前”,意思是与动作的方向一致,所以它的转移概率应该最大。
|
||||
|
||||
#### 奖励部分
|
||||
|
||||
|
@ -162,3 +167,4 @@ Blocks = []
|
|||
|
||||
用于搭建迷宫类场景。撞墙后一般原地不动。
|
||||
|
||||
在边角部分,如果飞船试图出界而由于环境限制被迫返回原地,这个算不算特殊移动呢?在一般的网格世界中,都是这样设计的,所以不能算是特殊移动。
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
|
||||
## 9.2 随机策略下的结果
|
||||
|
||||
### 9.2.1 通用的模型逻辑
|
||||
### 9.2.1 通用的模型环境
|
||||
|
||||
有了模型的数据定义后,需要把它转换成模型的代码逻辑。为此创建一个 GridWorld 类来读取 9.1.3 中的模型数据定义。需要保证这个类可以处理所有 GridWorld 形式的数据。
|
||||
有了模型的数据定义后,需要把它转换成模型环境的代码逻辑,便于在算法运行时可以快速地从环境得到状态更新和即时奖励。为此创建一个 GridWorld 类来读取 9.1.3 中的模型数据定义。需要保证这个类可以处理所有 GridWorld 形式的数据。
|
||||
|
||||
【代码位置】GridWorld_Model.py
|
||||
|
||||
|
@ -13,7 +13,7 @@ class GridWorld(object):
|
|||
def __init__(self, ...):
|
||||
...
|
||||
# 用于生成“状态->动作->转移->奖励”字典
|
||||
def __init_states(self, Probs, StepReward):
|
||||
def __init_states(self, Transition, StepReward):
|
||||
...
|
||||
# 用于计算移动后的下一个状态
|
||||
# 左上角为 [0,0], 横向为 x, 纵向为 y
|
||||
|
@ -23,13 +23,13 @@ class GridWorld(object):
|
|||
|
||||
这个类是可以应用到任何 GridWorld(方格世界)类型的强化学习场景中,读者如果希望自己构建更有趣的方格世界,可以只定义数据部分即可。
|
||||
|
||||
此时可以运行 Wormhole_0_Data.py,打印输出“状态->动作->转移->奖励字典”,用于观察我们在该模型中的数据设置是否正确。
|
||||
此时可以运行 Wormhole_0_Data.py,打印输出“状态->动作->转移->奖励 字典”,用于观察我们在该模型中的数据设置是否正确。
|
||||
【代码位置】 Wormhole_0_Data.py
|
||||
```python
|
||||
if __name__=="__main__":
|
||||
env = model.GridWorld(
|
||||
GridWidth, GridHeight, StartStates, EndStates, # 关于状态的参数
|
||||
Actions, Policy, SlipProbs, # 关于动作的参数
|
||||
Actions, Policy, Transition, # 关于动作的参数
|
||||
StepReward, SpecialReward, # 关于奖励的参数
|
||||
SpecialMove, Blocks) # 关于移动的限制
|
||||
model.print_P(env.P_S_R)
|
||||
|
@ -80,7 +80,7 @@ state = 24
|
|||
|
||||
在第 8 章的末尾,我们已经利用贝尔曼期望方程实现了迭代算法,**算法实现**代码单独存放在 Algo_PolicyValueFunction.py 中,就是为了可以在任何地方复用。
|
||||
|
||||
在本章中,我们又在前面创建了模型的**数据定义**和通用的**模型逻辑**,再加上上面的**算法实现**,万事俱备了。但是先捋清楚它们之间的调用关系。
|
||||
在本章中,我们又在前面创建了模型的**数据定义**和通用的**模型环境**,再加上上面的**算法实现**,万事俱备了。但是先捋清楚它们之间的调用关系。
|
||||
|
||||
<center>
|
||||
<img src="./img/grid_world_1.png">
|
||||
|
@ -90,28 +90,31 @@ state = 24
|
|||
|
||||
如图 9.2.1 所示。
|
||||
|
||||
- 数据定义:Wormhole_0_Data.py,定义穿越虫洞问题的模型。
|
||||
- 模型逻辑:GridWorld_Model.py,接收数据定义,用通用逻辑生成模型。
|
||||
- 数据定义:Wormhole_0_Data.py,定义穿越虫洞问题的数据。
|
||||
- 模型环境:GridWorld_Model.py,接收数据定义,用通用的网格世界逻辑生成模型。
|
||||
- 算法实现:Algo_PolicyValueFunction.py,接收模型,运行算法。
|
||||
- 过程控制与结果输出:Wormhole_1_VQ_pi.py,控制以上过程,输出结果。
|
||||
|
||||
这种设计的好处就是可以高度复用,比如:
|
||||
|
||||
- 更换数据定义,就可以把穿越虫洞问题变成悬崖行走问题或迷宫问题,但是模型逻辑部分和算法部分不需要改动。
|
||||
- 更换数据定义,就可以把穿越虫洞问题变成悬崖行走问题或迷宫问题,但是模型环境部分和算法部分不需要改动。
|
||||
|
||||
- 更换算法实现,就可以把计算贝尔曼价值函数问题变成后面要学习的计算贝尔曼最有价值函数问题。
|
||||
|
||||
过程控制代码如下:
|
||||
|
||||
【代码位置:Wormhole_1_VQ_pi.py】
|
||||
|
||||
|
||||
```Python
|
||||
import numpy as np
|
||||
import Wormhole_0_Data as data # 数据定义
|
||||
import GridWorld_Model as model # 模型逻辑
|
||||
import GridWorld_Model as model # 模型环境
|
||||
import Algo_PolicyValueFunction as algo # 算法实现
|
||||
import DrawQpi as drawQ # 结果输出
|
||||
|
||||
if __name__=="__main__":
|
||||
env = model.GridWorld(
|
||||
env = model.GridWorld( # 生成环境
|
||||
# 关于状态的参数
|
||||
data.GridWidth, data.GridHeight, data.StartStates, data.EndStates,
|
||||
# 关于动作的参数
|
||||
|
@ -121,7 +124,7 @@ if __name__=="__main__":
|
|||
# 关于移动的限制
|
||||
data.SpecialMove, data.Blocks)
|
||||
|
||||
gamma = 0.9 # 折扣,在本例中用1.0可以收敛,但是用0.9比较保险
|
||||
gamma = 0.9 # 折扣,在本例中用 1.0 也可以收敛
|
||||
iteration = 1000 # 算法最大迭代次数
|
||||
V_pi, Q_pi = algo.V_in_place_update(env, gamma, iteration) # 原地更新的迭代算法
|
||||
print("V_pi")
|
||||
|
@ -133,18 +136,23 @@ if __name__=="__main__":
|
|||
drawQ.draw(Q_pi, (data.GridWidth, data.GridHeight))
|
||||
```
|
||||
|
||||
在最开始的 import 部分,会把前面所述的“数据定义、模型逻辑、算法实现”三个模块都引入,然后在 main 下面先把数据“喂到”模型中(env=model.GridWorld(data...)),然后再把模型“喂到”算法中(algo.V_in_place_update(env...)),最后格式化输出模型的返回值 $V_\pi,Q_\pi$ 两个数组。
|
||||
过程说明:
|
||||
|
||||
- 在最开始的 import 部分,会把前面所述的“数据定义、模型环境、算法实现”三个模块都引入;
|
||||
- 然后在 main 下面先把数据“喂到”模型中:env=model.GridWorld(data...);
|
||||
- 然后再把模型“喂到”算法中:algo.V_in_place_update(env...);
|
||||
- 最后格式化输出模型的返回值 $V_\pi,Q_\pi$ 两个数组。
|
||||
|
||||
### 9.2.3 结果输出与分析
|
||||
|
||||
#### 状态价值函数 $v_\pi(s)$ 的结果
|
||||
|
||||
$V_\pi$ 其实是一个一维数组,按状态的顺序 [0,24] 存放着 $v_\pi(s)$ 的值。但是为了和方格世界对应,特地做了 5x5 的 reshape()。
|
||||
$V_\pi$ 其实是一个一维数组,按状态的顺序 $[0,\cdots,24]$ 存放着 $v_\pi(s)$ 的值。但是为了和方格世界的形状对应,特地做了 5x5 的 reshape()。
|
||||
|
||||
```
|
||||
迭代次数 = 36
|
||||
V_pi
|
||||
[[ 2.1 6. 1.7 -0.2 -1.4]
|
||||
[[ 2.1 6. 1.7 -0.2 -1.4]
|
||||
[ 1.3 2.3 1.2 0.1 -0.9]
|
||||
[ 1.3 1.9 1.1 0.2 -0.7]
|
||||
[ 2.2 3.6 1.9 0.4 -0.7]
|
||||
|
@ -160,13 +168,13 @@ V_pi
|
|||
|
||||
图 9.2.2 就是手绘的图形化结果,其中有几个相互有关系的格子被标上了不同的颜色,方便我们来验算一下 $v_\pi$ 的计算是否正确。
|
||||
|
||||
根据式 8.6.1:$v_\pi(s)=\sum_a \pi(a \mid s) \Big(\sum_{s'} p_{ss'}^a [r_{ss'}^a+\gamma v_\pi(s')]\Big)$,其中:
|
||||
根据式(8.6.1):$v_\pi(s)=\sum_a \pi(a \mid s) \Big(\sum_{s'} p_{ss'}^a [r_{ss'}^a+\gamma v_\pi(s')]\Big)$,其中:
|
||||
|
||||
- $\pi=0.25$
|
||||
- $p=1.0$
|
||||
- 而 $r$ 根据情况有所不同,可能是 0, -1, 5, 10 中的一个值。
|
||||
- $\pi=0.25$;
|
||||
- $p=1.0$;
|
||||
- 而 $r$ 根据情况有所不同,可能是 $[0, -1, 5, 10]$ 中的一个值。
|
||||
|
||||
下面以蓝色格子为例进行验算:$s=s_{21},s'=s_{3},\pi(a|s_{21})=0.25,p^a_{21,3}=1,r^a_{21,3}=10,\gamma=0.9$,代入式 8.6.1:
|
||||
下面以蓝色格子为例进行验算:$s=s_{21},s'=s_{3},\pi(a|s_{21})=0.25,p^a_{21,3}=1,r^a_{21,3}=10,\gamma=0.9$,代入式(8.6.1):
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
|
@ -174,41 +182,50 @@ v_\pi(s_{3})&=\sum_{a \in \{L,U,R,D\}} \pi(a|s_{21}) \Big(\sum_{s'=S_{21}} p^a_{
|
|||
\\
|
||||
&=4 \cdot 0.25 \cdot \Big(1 \cdot [10+0.9 \cdot (-0.2)]\Big)
|
||||
\\
|
||||
& \approx 9.8
|
||||
&=9.82 \approx 9.8
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
与$v_\pi(s_{21})$的值吻合。读者可以修改代码保留2位小数以上,会得到更准确的结果。
|
||||
|
||||
- 上式中外部的求和运算由 $4\times0.25$ 完成,是因为 4 个方向上的策略概率相等,所以简单地乘以 4 即可。
|
||||
- 上式中外部的求和运算由 $4 \cdot 0.25$ 完成,是因为 4 个方向上的策略概率相等,所以简单地乘以 4 即可。
|
||||
- 内部的求和运算,因为转移概率 $p^a_{21,3}=1$,所以只有一项状态转移,不需要求和。
|
||||
|
||||
橙色和绿色的部分的验证由读者在思考与练习中完成。
|
||||
再看绿色的的格子,中心位置为 18,所有的 $p=1$,所有的 $\pi=0.25$,所有的 $r=0$,$\gamma=0.9$,代入式(8.6.1):
|
||||
|
||||
再分析一下两个虫洞入口 $s_1,s_3$ 的状态价值函数值:
|
||||
$$
|
||||
\begin{aligned}
|
||||
v_\pi(s_{18})&= 0.25 \big(1 \cdot [0+0.9 \cdot 1.9]\big)+0.25 \big(1 \cdot [0+0.9 \cdot 0.2]\big)+0.25 \big(1 \cdot [0+0.9 \cdot -0.7]\big)+0.25 \big(1 \cdot [0+0.9 \cdot 0.4]\big)
|
||||
\\
|
||||
&=0.405 \approx 0.4
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
$v_\pi(s_{21})=9.8$,小于离开此状态的即时奖励($r=10$),而 $v_\pi(s_1)=6$,大于离开此状态时的即时奖励($r=5$)。这是为什么呢?
|
||||
|
||||
- 因为 $s_{21}$ 的状态价值函数由其下游状态 $s_{3}$ 决定,而飞船在 $s_{3}$ 有 0.25 的可能出界而得到负的奖励。如果目标状态是处于角落位置的 $s_20$,那么 $v_\pi(s_{21})$ 的值将会更小。
|
||||
再分析一下两个虫洞入口 $s_1,s_{21}$ 的状态价值函数值:
|
||||
|
||||
$v_\pi(s_{21})=9.8$,小于离开此状态的即时奖励($r=10$);而 $v_\pi(s_1)=6$,大于离开此状态时的即时奖励($r=5$)。这是为什么呢?
|
||||
|
||||
- 因为 $s_{21}$ 的状态价值函数由其下游状态 $s_{3}$ 决定,而飞船在 $s_{3}$ 有 0.25 的可能出界而得到负的奖励。如果目标状态是处于角落位置的 $s_{4}$,那么 $v_\pi(s_{21})$ 的值将会更小。
|
||||
|
||||
- 而 $s_1$ 的下游状态 $s_{12}$ 在中心区域,很不容易出界,状态价值为正数,所以 $v_\pi(s_1)$ 的值要大于即时奖励值。
|
||||
|
||||
整个方格世界的状态值分布,上半部分为正数,是因为有个两个虫洞入口的状态值很高;向下逐渐变成负数,是因为靠近边界的地方因为容易出界而得到负的奖励。
|
||||
整个方格世界的状态值分布,左半部分为正数,是因为有个两个虫洞入口的状态值很高;向右逐渐变成负数,是因为靠近边界的地方因为容易出界而得到负的奖励。
|
||||
|
||||
#### 动作价值函数 $q_\pi(s,a)$ 的结果
|
||||
|
||||
|
||||
$Q_\pi$ 是一个二维数组,行序号等于状态的顺序[0,24],列序号代表 4 个动作,每个单元存放的就是在策略 $\pi$ 下(本例中为四个方向随机的 0.25)的 4 个动作的价值函数值,顺序是“左上右下”。
|
||||
$Q_\pi$ 是一个二维数组,行序号等于状态的顺序 $[0,\cdots,24]$,列序号代表 4 个动作,每个单元存放的就是在策略 $\pi$ 下(本例中为四个方向随机的 0.25)的 4 个动作的价值函数值,顺序是“左上右下”。
|
||||
|
||||
```
|
||||
Q_pi
|
||||
[[ 0.9 0.9 5.4 1.2]
|
||||
[ 6. 6. 6. 6. ]
|
||||
[[ 0.9 0.9 5.4 1.2] # 状态 0 的四个动作的价值函数
|
||||
[ 6. 6. 6. 6. ] # 状态 1 的四个动作的价值函数
|
||||
[ 5.4 0.5 -0.2 1.1]
|
||||
......
|
||||
[ 8.8 1.7 0.4 1.9]
|
||||
[ 2.9 0.4 -0.9 -0.6]
|
||||
[ 0.4 -0.7 -1.9 -1.9]]
|
||||
[ 0.4 -0.7 -1.9 -1.9]] # 状态 24 的四个动作的价值函数
|
||||
```
|
||||
上面的输出为了节省篇幅,省略了中间的一些行。
|
||||
|
||||
|
@ -259,11 +276,7 @@ best_actions = np.argwhere(self.policy == np.max(self.policy)) #应该返回 [0
|
|||
|
||||
左图显示了每个状态内 4 个方向的动作价值。在一个方格内,4 个数据是有可比性的,不同方格内的数据没有可比性。正负符号也不表示其它意思,只看大小。
|
||||
|
||||
上半部分的动作价值函数值普遍为正数,下半部分普遍为负数,主要是和状态价值函数的分布有关。
|
||||
|
||||
绘制完全部 25 个状态的最佳动作后,我们来一起分析一下。
|
||||
|
||||
先看图 9.2.4。
|
||||
绘制完全部 25 个状态的最佳动作后,我们来一起分析一下。先看图 9.2.4。
|
||||
|
||||
<center>
|
||||
<img src="./img/ship-5.png">
|
||||
|
@ -279,19 +292,19 @@ $q_\pi$ 值是由其下游状态的 $v_\pi$ 所决定的,对于红色菱形区
|
|||
|
||||
$$
|
||||
\begin{aligned}
|
||||
q_\pi(s_8,a_{Down})&=\sum_{s'} p_{ss'}^a \big [r_{ss'}^a+\gamma v_\pi(s') \big ]
|
||||
q_\pi(s_8,a_{down})&=\sum_{s'} p_{ss'}^a \big [r_{ss'}^a+\gamma v_\pi(s') \big ]
|
||||
\\
|
||||
&=1.0 \times [0 + 0.9\times0.2]
|
||||
&=1.0 \cdot [0 + 0.9 \cdot 0.2]
|
||||
\\
|
||||
&\approx 0.2
|
||||
&=0.18\approx 0.2
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
同理,$q_\pi(s_{12},a_{Right}),q_\pi(s_{14},a_{Left}),q_\pi(s_{18},a_{Up})$ 都是相同的结果。
|
||||
同理,$q_\pi(s_{12},a_{right}),q_\pi(s_{14},a_{left}),q_\pi(s_{18},a_{up})$ 都是相同的结果。
|
||||
|
||||
黄色菱形也是如此。
|
||||
黄色菱形也是如此,不同的地方是菱形下顶点是 9.8,而其它三个顶点都是 3.2,这是因为在 $s_21$ 是个虫洞入口,它的下游状态不是 $s_{16}$,而是 $s_3$。
|
||||
|
||||
蓝色菱形因为位置靠边,只有三个顶点。在 $s_5$ 向左移动出界会得到 -1 的奖励,因此 $1.2-1=0.2$,正好是 $q_\pi(s_5,a_{Left})$ 的值。
|
||||
蓝色菱形因为位置靠边,只有三个顶点。在 $s_5$ 向左移动出界会得到 -1 的奖励,因此 $1.2-1=0.2$,正好是 $q_\pi(s_5,a_{left})$ 的值。
|
||||
|
||||
下面我们根据图 9.2.3,再看看哪些动作是合理的,哪些是有疑问的。
|
||||
|
||||
|
|
|
@ -120,18 +120,24 @@ if __name__=="__main__":
|
|||
打印输出结果:
|
||||
|
||||
```
|
||||
--------------------
|
||||
修改状态 0 的策略:[0.2 0.8]
|
||||
[1.22874 0.56 0.554 0.8 0.56 0.73 0. ]
|
||||
价值函数: [1.22874 0.56 0.554 0.8 0.56 0.73 0. ]
|
||||
--------------------
|
||||
修改状态 1 的策略:[0.5 0.5]
|
||||
[1.19228 0.55 0.554 0.8 0.56 0.73 0. ]
|
||||
价值函数: [1.19228 0.55 0.554 0.8 0.56 0.73 0. ]
|
||||
--------------------
|
||||
修改状态 2 的策略:[0.3 0.7]
|
||||
[1.19546 0.56 0.553 0.8 0.56 0.73 0. ]
|
||||
价值函数: [1.19546 0.56 0.553 0.8 0.56 0.73 0. ]
|
||||
--------------------
|
||||
修改状态 3 的策略:[0.1 0.9]
|
||||
[1.19548 0.56 0.554 0.8 0.56 0.73 0. ]
|
||||
价值函数: [1.19548 0.56 0.554 0.8 0.56 0.73 0. ]
|
||||
--------------------
|
||||
修改状态 4 的策略:[0.3 0.7]
|
||||
[1.19788 0.56 0.554 0.8 0.57 0.73 0. ]
|
||||
价值函数: [1.19788 0.56 0.554 0.8 0.57 0.73 0. ]
|
||||
--------------------
|
||||
修改状态 5 的策略:[0.6 0.4]
|
||||
[1.19188 0.56 0.554 0.8 0.56 0.72 0. ]
|
||||
价值函数: [1.19188 0.56 0.554 0.8 0.56 0.72 0. ]
|
||||
```
|
||||
|
||||
### 9.3.3 结果分析
|
||||
|
@ -171,11 +177,11 @@ $$
|
|||
\begin{aligned}
|
||||
v_\pi(s_0) &= \pi_0(a_0|s_0)q_\pi(s_0,a_0)+\pi_0(a_1|s_0)q_\pi(s_0,a_1)
|
||||
\\
|
||||
&=0.4\times1.0957+0.6\times1.262 = 1.19548
|
||||
&=0.4 \cdot 1.0957+0.6 \cdot 1.262 = 1.19548
|
||||
\\
|
||||
v'_\pi(s_0) &= \pi'(s_0)(a_0|s_0)q_\pi(s_0,a_0)+ \pi'(s_0)(a_1|s_0)q_\pi(s_0,a_1)
|
||||
\\
|
||||
&= 0.2 \times 1.0957 + 0.8 \times 1.262 = 1.22874
|
||||
&= 0.2 \cdot 1.0957 + 0.8 \cdot 1.262 = 1.22874
|
||||
\end{aligned}
|
||||
\tag{9.3.2}
|
||||
$$
|
||||
|
|
|
@ -136,7 +136,6 @@ def caculate_all_V_Q(all_policy_in_binary):
|
|||
上述代码将会输出所有策略组合及其状态价值函数:
|
||||
|
||||
```
|
||||
========================================
|
||||
OneHot形式的策略组与 V 函数值 :
|
||||
--------------------
|
||||
策略组-0: {0: [1, 0], 1: [1, 0], 2: [1, 0], 3: [1, 0], 4: [1, 0], 5: [1, 0]}
|
||||
|
@ -169,7 +168,6 @@ def find_best_v0_policy(V_values, all_policy_in_binary):
|
|||
```
|
||||
得到 $v_\pi(s_0)$ 的最大值为 1.29,并返回满足此条件的策略组序号 best_ids:
|
||||
```
|
||||
========================================
|
||||
v(s0)的最大 V 函数值 : 1.29
|
||||
```
|
||||
|
||||
|
@ -192,7 +190,6 @@ def all_best_v0(all_policy_in_binary, V_all_policy, Q_all_policy, best_ids):
|
|||
结果如下:
|
||||
|
||||
```
|
||||
========================================
|
||||
v(s0)等于最大值(1.29)的二进制形式的策略组与 V 函数值 :
|
||||
--------------------
|
||||
最优策略组-35: [1 0 0 0 1 1]
|
||||
|
|
|
@ -87,7 +87,7 @@ $$
|
|||
|
||||
表 9.5.2 只列出了 $q_\pi(s_0,a_0),q_\pi(s_0,a_1)$ 两个值,因为其它值都相等,为了节省篇幅,没有列出。
|
||||
|
||||
两列的最大值分别为 1.218,1.29,只有策略组 51,55 等于最大值, 而其它 6 个策略组的 $q_\pi(s_0,a_0)$ 函数值会小于最大值。因此,只有策略组 51,55 满足式(9.5.3),成为最优策略组,这也与用 $v_*$ 做评判标准得到的结果相同。
|
||||
两列的最大值分别为 1.218,1.29,只有策略组 $\pi_{51},\pi_{55}$ 等于最大值, 而其它 6 个策略组的 $q_\pi(s_0,a_0)$ 函数值会小于最大值。因此,只有策略组 $\pi_{51},\pi_{55}$ 满足式(9.5.3),成为最优策略组,这也与用 $v_*$ 做评判标准得到的结果相同。
|
||||
|
||||
这说明针对本例,最优策略并不是唯一的。在其它强化学习问题中,也是如此:
|
||||
|
||||
|
@ -99,7 +99,7 @@ $$
|
|||
|
||||
#### 为什么会有两个最优策略?
|
||||
|
||||
把策略组 51,55 的 V 函数和 Q 函数的晦涩数字标注在图 9.5.1 中,一目了然:
|
||||
把策略组 $\pi_{51},\pi_{55}$ 的 V 函数和 Q 函数的晦涩数字标注在图 9.5.1 中,一目了然:
|
||||
|
||||
<center>
|
||||
<img src="./img/shoot-result-search.png">
|
||||
|
@ -107,9 +107,9 @@ $$
|
|||
图 9.5.1 搜索最优策略的结果
|
||||
</center>
|
||||
|
||||
策略 51,55 的区别在表 9.5.2 中。
|
||||
策略 $\pi_{51},\pi_{55}$ 的区别在表 9.5.2 中。
|
||||
|
||||
表 9.5.2 策略51,55的区别
|
||||
表 9.5.2 策略 $\pi_{51},\pi_{55}$ 的区别
|
||||
|策略组合序号|$\pi(s_0)$|$\pi(s_1)$|$\pi(s_2)$|$\pi(s_3)$|$\pi(s_4)$|$\pi(s_5)$|
|
||||
|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
|
||||
|$\pi_{51}$|1|1|0|0|1|1|
|
||||
|
|
Двоичные данные
基础教程/A7-强化学习/90-穿越虫洞问题 - 从动作价值到最优策略/img/grid_world_1.png
До Ширина: | Высота: | Размер: 20 KiB После Ширина: | Высота: | Размер: 32 KiB |
Двоичные данные
基础教程/A7-强化学习/90-穿越虫洞问题 - 从动作价值到最优策略/img/ship-3.png
До Ширина: | Высота: | Размер: 19 KiB После Ширина: | Высота: | Размер: 18 KiB |
|
@ -7,7 +7,7 @@ class GridWorld(object):
|
|||
# 生成环境
|
||||
def __init__(self,
|
||||
GridWidth, GridHeight, StartStates, EndStates,
|
||||
Actions, Policy, SlipProbs,
|
||||
Actions, Policy, Transition,
|
||||
StepReward, SpecialReward,
|
||||
SpecialMove, Blocks):
|
||||
|
||||
|
@ -22,7 +22,7 @@ class GridWorld(object):
|
|||
self.SpecialMove = SpecialMove
|
||||
self.Blocks = Blocks
|
||||
self.Policy = self.__init_policy(Policy)
|
||||
self.P_S_R = self.__init_states(SlipProbs, StepReward)
|
||||
self.P_S_R = self.__init_states(Transition, StepReward)
|
||||
|
||||
# 把统一的policy设置复制到每个状态上
|
||||
def __init_policy(self, Policy):
|
||||
|
@ -32,7 +32,7 @@ class GridWorld(object):
|
|||
return PI
|
||||
|
||||
# 用于生成状态->动作->转移->奖励字典
|
||||
def __init_states(self, Probs, StepReward):
|
||||
def __init_states(self, Transition, StepReward):
|
||||
P = {}
|
||||
s_id = 0
|
||||
self.Pos2Sid = {}
|
||||
|
@ -49,7 +49,7 @@ class GridWorld(object):
|
|||
continue
|
||||
for action in self.Actions:
|
||||
list_probs = []
|
||||
for dir, prob in enumerate(Probs):
|
||||
for dir, prob in enumerate(Transition):
|
||||
if (prob == 0.0):
|
||||
continue
|
||||
s_next = self.__get_next_state(
|
||||
|
|
|
@ -1,69 +0,0 @@
|
|||
import numpy as np
|
||||
import GridWorld_Model as model # 模型逻辑
|
||||
import Algo_PolicyValueFunction as algo # 算法实现
|
||||
import Algo_OptimalValueFunction as algo2
|
||||
|
||||
# 状态空间(尺寸)S,终点目标T,起点S,障碍B,奖励R,动作空间A,转移概率P
|
||||
|
||||
# 空间宽度
|
||||
GridWidth = 2
|
||||
# 空间高度
|
||||
GridHeight = 2
|
||||
# 起点
|
||||
StartStates = []
|
||||
# 终点
|
||||
EndStates = []
|
||||
|
||||
LEFT, UP, RIGHT, DOWN = 0, 1, 2, 3
|
||||
# 动作空间
|
||||
Actions = [LEFT, UP, RIGHT, DOWN]
|
||||
Policy = [0.25, 0.25, 0.25, 0.25]
|
||||
# 转移概率
|
||||
# SlipLeft, MoveFront, SlipRight, SlipBack
|
||||
SlipProbs = [0.0, 1.0, 0.0, 0.0]
|
||||
|
||||
# 每走一步都-1,如果配置为0,则不减1,而是要在End处得到最终奖励
|
||||
StepReward = 0
|
||||
# from s->s', get r
|
||||
# s,s' 为状态序号,不是坐标位置
|
||||
SpecialReward = {
|
||||
(0,0):-1,
|
||||
(1,1):-1,
|
||||
(2,2):-1,
|
||||
(3,3):-1,
|
||||
(0,3):+5
|
||||
}
|
||||
|
||||
# 特殊移动,用于处理类似虫洞场景
|
||||
SpecialMove = {
|
||||
(0,LEFT): 3,
|
||||
(0,UP): 3,
|
||||
(0,RIGHT): 3,
|
||||
(0,DOWN): 3,
|
||||
}
|
||||
Blocks = []
|
||||
|
||||
|
||||
if __name__=="__main__":
|
||||
env = model.GridWorld(
|
||||
GridWidth, GridHeight, StartStates, EndStates, # 关于状态的参数
|
||||
Actions, Policy, SlipProbs, # 关于动作的参数
|
||||
StepReward, SpecialReward, # 关于奖励的参数
|
||||
SpecialMove, Blocks) # 关于移动的限制
|
||||
model.print_P(env.P_S_R)
|
||||
gamma = 0.5
|
||||
iteration = 1000
|
||||
V_pi, Q_pi = algo.calculate_VQ_pi(env, gamma, iteration)
|
||||
print("v_pi")
|
||||
print(np.reshape(np.round(V_pi,2), (GridWidth,GridHeight)))
|
||||
print("q_pi")
|
||||
print(np.round(Q_pi,2))
|
||||
|
||||
V_star, Q_star = algo2.calculate_VQ_star(env, gamma, 100)
|
||||
|
||||
print("v*=",np.round(V_star,5))
|
||||
policy = algo2.get_policy(env, V_star, gamma)
|
||||
print("policy")
|
||||
print(policy)
|
||||
print("q*=")
|
||||
print(np.round(Q_star,2))
|
|
@ -2,6 +2,7 @@ import numpy as np
|
|||
import copy
|
||||
import Shoot_2_DataModel as dataModel
|
||||
import Algorithm.Algo_PolicyValueFunction as algo
|
||||
import common.PrintHelper as helper
|
||||
|
||||
if __name__=="__main__":
|
||||
Policy = { # 原始状态
|
||||
|
@ -32,9 +33,10 @@ if __name__=="__main__":
|
|||
])
|
||||
# 每次只修改一个策略,保持其它策略不变,以便观察其影响
|
||||
for i in range(6):
|
||||
helper.print_seperator_line(helper.SeperatorLines.middle)
|
||||
print(str.format("修改状态 {0} 的策略:{1}", i, test_policy[i]))
|
||||
new_policy = copy.deepcopy(Policy) # 继承原始策略
|
||||
new_policy[i] = test_policy[i] # 只修改其中一个状态的策略
|
||||
env = dataModel.Env(new_policy)
|
||||
V,Q = algo.calculate_VQ_pi(env, gamma, max_iteration)
|
||||
print(np.round(V,5))
|
||||
print("价值函数:",np.round(V,5))
|
||||
|
|
|
@ -12,7 +12,7 @@ Actions = [LEFT, UP, RIGHT, DOWN]
|
|||
# 初始策略
|
||||
Policy = [0.25, 0.25, 0.25, 0.25]
|
||||
# 转移概率: [SlipLeft, MoveFront, SlipRight, SlipBack]
|
||||
SlipProbs = [0.0, 1.0, 0.0, 0.0]
|
||||
Transition = [0.0, 1.0, 0.0, 0.0]
|
||||
# 每走一步的奖励值,可以是0或者-1
|
||||
StepReward = 0
|
||||
# 特殊奖励 from s->s' then get r, 其中 s,s' 为状态序号,不是坐标位置
|
||||
|
@ -53,7 +53,7 @@ Blocks = []
|
|||
if __name__=="__main__":
|
||||
env = model.GridWorld(
|
||||
GridWidth, GridHeight, StartStates, EndStates, # 关于状态的参数
|
||||
Actions, Policy, SlipProbs, # 关于动作的参数
|
||||
Actions, Policy, Transition, # 关于动作的参数
|
||||
StepReward, SpecialReward, # 关于奖励的参数
|
||||
SpecialMove, Blocks) # 关于移动的限制
|
||||
model.print_P(env.P_S_R)
|
||||
|
|
|
@ -3,25 +3,29 @@ import Wormhole_0_Data as data # 数据定义
|
|||
import GridWorld_Model as model # 模型逻辑
|
||||
import Algorithm.Algo_PolicyValueFunction as algo # 算法实现
|
||||
import common.DrawQpi as drawQ # 结果输出
|
||||
import common.PrintHelper as helper
|
||||
|
||||
if __name__=="__main__":
|
||||
env = model.GridWorld(
|
||||
# 关于状态的参数
|
||||
data.GridWidth, data.GridHeight, data.StartStates, data.EndStates,
|
||||
# 关于动作的参数
|
||||
data.Actions, data.Policy, data.SlipProbs,
|
||||
data.Actions, data.Policy, data.Transition,
|
||||
# 关于奖励的参数
|
||||
data.StepReward, data.SpecialReward,
|
||||
# 关于移动的限制
|
||||
data.SpecialMove, data.Blocks)
|
||||
|
||||
gamma = 0.9 # 折扣,在本例中用1.0可以收敛,但是用0.9比较保险
|
||||
gamma = 0.9 # 折扣,在本例中用1.0可以收敛
|
||||
iteration = 1000 # 算法最大迭代次数
|
||||
V_pi, Q_pi = algo.calculate_VQ_pi(env, gamma, iteration) # 原地更新的迭代算法
|
||||
helper.print_seperator_line(helper.SeperatorLines.long)
|
||||
print("V_pi")
|
||||
V = np.reshape(np.round(V_pi,2), (data.GridWidth, data.GridHeight))
|
||||
V = np.reshape(np.round(V_pi,1), (data.GridWidth, data.GridHeight))
|
||||
print(V)
|
||||
helper.print_seperator_line(helper.SeperatorLines.long)
|
||||
print("Q_pi")
|
||||
print(np.round(Q_pi,2))
|
||||
print(np.round(Q_pi,1))
|
||||
# 字符图形化显示
|
||||
helper.print_seperator_line(helper.SeperatorLines.long)
|
||||
drawQ.draw(Q_pi, (data.GridWidth, data.GridHeight))
|
||||
|
|
|
@ -3,13 +3,14 @@ import Wormhole_0_Data as data # 数据定义
|
|||
import GridWorld_Model as model # 模型逻辑
|
||||
import Algorithm.Algo_OptimalValueFunction as algo # 算法实现
|
||||
import common.DrawQpi as drawQ # 结果输出
|
||||
import common.PrintHelper as helper
|
||||
|
||||
if __name__=="__main__":
|
||||
env = model.GridWorld(
|
||||
# 关于状态的参数
|
||||
data.GridWidth, data.GridHeight, data.StartStates, data.EndStates,
|
||||
# 关于动作的参数
|
||||
data.Actions, data.Policy, data.SlipProbs,
|
||||
data.Actions, data.Policy, data.Transition,
|
||||
# 关于奖励的参数
|
||||
data.StepReward, data.SpecialReward,
|
||||
# 关于移动的限制
|
||||
|
@ -18,10 +19,13 @@ if __name__=="__main__":
|
|||
gamma = 0.9 # 折扣,在本例中用1.0可以收敛,但是用0.9比较保险
|
||||
iteration = 1000 # 算法最大迭代次数
|
||||
V_star, Q_star = algo.calculate_VQ_star(env, gamma, iteration) # 原地更新的迭代算法
|
||||
helper.print_seperator_line(helper.SeperatorLines.long)
|
||||
print("V*")
|
||||
V = np.reshape(np.round(V_star,1), (data.GridWidth, data.GridHeight))
|
||||
print(V)
|
||||
helper.print_seperator_line(helper.SeperatorLines.long)
|
||||
print("Q*")
|
||||
print(np.round(Q_star,1))
|
||||
# 字符图形化显示
|
||||
helper.print_seperator_line(helper.SeperatorLines.long)
|
||||
drawQ.draw(Q_star, (data.GridWidth, data.GridHeight))
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import numpy as np
|
||||
import GridWorld_Model as model # 模型逻辑
|
||||
import Algo_PolicyValueFunction as algo # 算法实现
|
||||
import Algo_OptimalValueFunction as algo2
|
||||
import GridWorld_Model as model # 模型环境
|
||||
import Algorithm.Algo_PolicyValueFunction as algo # 算法实现
|
||||
import Algorithm.Algo_OptimalValueFunction as algo2
|
||||
|
||||
# 状态空间(尺寸)S,终点目标T,起点S,障碍B,奖励R,动作空间A,转移概率P
|
||||
|
||||
|
@ -53,13 +53,13 @@ if __name__=="__main__":
|
|||
model.print_P(env.P_S_R)
|
||||
gamma = 0.5
|
||||
iteration = 1000
|
||||
V_pi, Q_pi = algo.calculate_Vpi_Qpi(env, gamma, iteration)
|
||||
V_pi, Q_pi = algo.calculate_VQ_pi(env, gamma, iteration)
|
||||
print("v_pi")
|
||||
print(np.reshape(np.round(V_pi,2), (GridWidth,GridHeight)))
|
||||
print("q_pi")
|
||||
print(np.round(Q_pi,2))
|
||||
|
||||
V_star, Q_star = algo2.calculate_Vstar(env, gamma, 100)
|
||||
V_star, Q_star = algo2.calculate_VQ_star(env, gamma, 100)
|
||||
|
||||
print("v*=",np.round(V_star,5))
|
||||
policy = algo2.get_policy(env, V_star, gamma)
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
import gym
|
||||
env = gym.make("CartPole-v1")
|
||||
observation, info = env.reset(seed=42, return_info=True)
|
||||
R = 0
|
||||
for i in range(1000):
|
||||
env.render(mode="human")
|
||||
action = env.action_space.sample()
|
||||
observation, reward, done, info = env.step(action)
|
||||
#print(observation, reward, done, info)
|
||||
if done:
|
||||
observation, info = env.reset(return_info=True)
|
||||
R += reward
|
||||
if i % 10 == 0:
|
||||
print(i)
|
||||
env.close()
|
||||
print(R)
|