作为一个新手,写这个强化学习-基础知识专栏是想和大家分享一下自己学习强化学习的学习历程,希望对大家能有所帮助。这个系列后面会不断更新,希望自己在2021年能保证平均每日一更的更新速度,主要是介绍强化学习的基础知识,后面也会更新强化学习的论文阅读专栏。本来是想每一篇多更新一点内容的,后面发现大家上CSDN主要是来提问的,就把很多拆分开来了(而且这样每天任务量也小一点哈哈哈哈偷懒大法)。但是我还是希望知识点能成系统,所以我在目录里面都好按章节系统地写的,而且在github上写成了书籍的形式,如果大家觉得有帮助,希望从头看的话欢迎关注我的github啊,谢谢大家!另外我还会分享深度学习-基础知识专栏以及深度学习-论文阅读专栏,很早以前就和小伙伴们花了很多精力写的,如果有对深度学习感兴趣的小伙伴也欢迎大家关注啊。大家一起互相学习啊!可能会有很多错漏,希望大家批评指正!不要高估一年的努力,也不要低估十年的积累,与君共勉!
K臂老虎机介绍及其Python实现
如果大家想对K臂老虎机做一个比较深入的了解的话,建议大家去阅读这篇博客,作者写的挺清楚的,而且还推荐了很多的其他材料,我这里主要是对K臂老虎机做一个简要的介绍。
定义
K臂老虎机(Multi-armed bandit,简称MAB)最早的场景是在赌场里面。赌场里面有K台老虎机,每次去摇老虎机都需要一个代币,且老虎机都会以一定概率吐出钱,你手上如果有T个代币,也就是你一共可以摇T次,你怎么才能使你的期望回报最大?当然我们要先假设每个老虎机吐钱的概率是不一样的,不然你怎么摇就都是一样的了。
我们一般也将所有的下面这种形式的问题成为K臂老虎机问题: 你可以反复面对 k 种不同的选择或行动。在每次选择之后,你会收到一个数值奖励,该奖励取决于你选择的行动的固定概率分布。 你的目标是在一段时间内最大化预期的总奖励。
如果我们是贝叶斯人,我们在实际对老虎机进行操作之前其实对老虎机吐钱的概率就已经有了一个先验的分布,然后我们不断地进行试验,根据试验的结果去调整我们前面的分布;而如果我们是频率学家,那我们一开始对这些机器吐钱的概率其实是没有先验的,我们会通过实验去预测出每台机器吐钱的概率,然后根据这个概率去不断优化我们的决策。
但不管从哪种角度出发,K臂老虎机的问题其实就是一个探索与利用的问题,就比如说我们先进行了m次实验(m<T),发现了第一个臂吐钱的频率更高,那接下来我们是一直去摇第一个臂(利用:exploitation)还是说我们还去试着摇一下其他的臂(探索:exploration),从短期来看利用是好的,但是从长期来看探索是好的。
基本概念
在我们的K臂老虎机中,只要选择了该动作,
k
k
k 个动作的每一个都有预期的或平均的奖励, 让我们称之为该行动的价值。我们将在时间步
t
t
t 选择的动作表示为
A
t
,
A_{t},
At, 并将相应的奖励表示为
R
t
∘
R_{t_{\circ}}
Rt∘ 然 后, 对于任意动作
a
a
a 的价值, 定义
q
∗
(
a
)
q_{*}(a)
q∗(a) 是给定
a
a
a 选择的预期奖励:
q
∗
(
a
)
≐
E
[
R
t
∣
A
t
=
a
]
q_{*}(a) \doteq \mathbb{E}\left[R_{t} \mid A_{t}=a\right]
q∗(a)≐E[Rt∣At=a]
如果我们知道每个动作的价值, 那么解决 K臂老虎机将是轻而易举的:你总是选择具有最高价值 的动作。但是我们不知道实际动作价值, 尽管你可能有估计值。 我们将在时间步骤
t
t
t 的动作
a
a
a 的估计值表示为
Q
t
(
a
)
Q_{t}(a)
Qt(a) 。 我们希望
Q
t
(
a
)
Q_{t}(a)
Qt(a) 接近
q
∗
(
a
)
q_{*}(a)
q∗(a) 。
K臂老虎机的变种
我们在上面定义中介绍的K臂老虎机其实是最简单的一种场景,K臂老虎机还有很多其他的变形:
- 如果那些臂的吐钱的概率分布在一开始就设定好了,而且之后不再改变,则称为oblivious adversary setting。
- 如果那些臂吐钱的概率设定好了之后还会发生变化,那么称为adaptive adversary setting。
- 如果把待推荐的商品作为MAB问题的arm,那么我们在推荐系统中我们就还需要考虑用户作为一个活生生的个体本身的兴趣点、偏好、购买力等因素都是不同的,也就是我们需要考虑同一臂在不同上下文中是不同的。
- 如果每台老虎机每天摇的次数有上限,那我们就得到了一个Bandit with Knapsack问题。
greedy和 ϵ − g r e e d y \epsilon-greedy ϵ−greedy
greedy(贪婪)的算法也就是选择具有最高估计值的动作之一: A t = argmax a Q t ( a ) A_{t}=\underset{a}{\operatorname{argmax}} Q_{t}(a) At=aargmaxQt(a),也就是相当于我们只做exploitation ; 而 ϵ − g r e e d y \epsilon-greedy ϵ−greedy以较小的概率 ϵ \epsilon ϵ地从具有相同概率的所有动作中随机选择, 相当于我们在做exploitation的同时也做一定程度的exploration。greedy的算法很容易陷入执行次优动作的怪圈,当reward的方差更大时,我们为了做更多的探索应该选择探索度更大的 ϵ − g r e e d y \epsilon-greedy ϵ−greedy,但是当reward的方差很小时,我们可以选择更greedy的方法,在实际当中我们很多时候都会让 ϵ \epsilon ϵ 从一个较大的值降低到一个较小的值,比如说从1降低到0.1,相当于我们在前期基本上只做探索,后期只做利用。
-
Trade-off between exploration and exploitation
-
ϵ \epsilon ϵ -Greedy Exploration: Ensuring continual exploration
- Q All actions are tried with non-zero probability
- With probability 1 − ϵ 1-\epsilon 1−ϵ choose the greedy action
- B With probability ϵ \epsilon ϵ choose an action at random
π ( a ∣ s ) = { ϵ / ∣ A ∣ + 1 − ϵ if a ∗ = arg max a ∈ A Q ( s , a ) ϵ / ∣ A ∣ otherwise \pi(a \mid s)=\left\{\begin{array}{ll} \epsilon /|\mathcal{A}|+1-\epsilon & \text { if } a^{*}=\arg \max _{a \in \mathcal{A}} Q(s, a) \\ \epsilon /|\mathcal{A}| & \text { otherwise } \end{array}\right. π(a∣s)={ϵ/∣A∣+1−ϵϵ/∣A∣ if a∗=argmaxa∈AQ(s,a) otherwise
Policy improvement theorem: For any
ϵ
\epsilon
ϵ -greedy policy
π
,
\pi,
π, the
ϵ
\epsilon
ϵ -greedy policy
π
′
\pi^{\prime}
π′ with respect to
q
π
q_{\pi}
qπ is an improvement,
v
π
′
(
s
)
≥
v
π
(
s
)
v_{\pi^{\prime}}(s) \geq v_{\pi}(s)
vπ′(s)≥vπ(s)
q
π
(
s
,
π
′
(
s
)
)
=
∑
a
∈
A
π
′
(
a
∣
s
)
q
π
(
s
,
a
)
=
ϵ
∣
A
∣
∑
a
∈
A
q
π
(
s
,
a
)
+
(
1
−
ϵ
)
max
a
q
π
(
s
,
a
)
≥
ϵ
∣
A
∣
∑
a
∈
A
q
π
(
s
,
a
)
+
(
1
−
ϵ
)
∑
a
∈
A
π
(
a
∣
s
)
−
ϵ
∣
A
∣
1
−
ϵ
q
π
(
s
,
a
)
=
∑
a
∈
A
π
(
a
∣
s
)
q
π
(
s
,
a
)
=
v
π
(
s
)
\begin{aligned} q_{\pi}\left(s, \pi^{\prime}(s)\right) &=\sum_{a \in \mathcal{A}} \pi^{\prime}(a \mid s) q_{\pi}(s, a) \\ &=\frac{\epsilon}{|\mathcal{A}|} \sum_{a \in \mathcal{A}} q_{\pi}(s, a)+(1-\epsilon) \max _{a} q_{\pi}(s, a) \\ & \geq \frac{\epsilon}{|\mathcal{A}|} \sum_{a \in \mathcal{A}} q_{\pi}(s, a)+(1-\epsilon) \sum_{a \in \mathcal{A}} \frac{\pi(a \mid s)-\frac{\epsilon}{|\mathcal{A}|}}{1-\epsilon} q_{\pi}(s, a) \\ &=\sum_{a \in \mathcal{A}} \pi(a \mid s) q_{\pi}(s, a)=v_{\pi}(s) \end{aligned}
qπ(s,π′(s))=a∈A∑π′(a∣s)qπ(s,a)=∣A∣ϵa∈A∑qπ(s,a)+(1−ϵ)amaxqπ(s,a)≥∣A∣ϵa∈A∑qπ(s,a)+(1−ϵ)a∈A∑1−ϵπ(a∣s)−∣A∣ϵqπ(s,a)=a∈A∑π(a∣s)qπ(s,a)=vπ(s)
Therefore,
v
π
′
(
s
)
≥
v
π
(
s
)
v_{\pi^{\prime}}(s) \geq v_{\pi}(s)
vπ′(s)≥vπ(s) from the policy improvement theorem
softmax 方法
softamx是另一种兼顾探索与利用的方法,它既不像greedy算法那样贪婪,也没有像
ϵ
−
\epsilon-
ϵ− greedy那样在探索阶段做随机动作而是使用 softmax函数计算每一个arm被选中的概率,以更高的概率去摇下平均收益高的臂,以更地的概率去摇下平均收益低的臂。
a
r
m
i
a r m_{i}
armi 表示第i 个手柄,
U
i
\quad U_{i}
Ui 表示手柄的平均收益, k是手柄总数。
p
(
a
r
m
i
)
=
e
u
i
∑
j
k
e
u
i
p\left(a r m_{i}\right)=\frac{e^{u_{i}}}{\sum_{j}^{k} e^{u_{i}}}
p(armi)=∑jkeuieui
当然这里有一个问题是为什么要用softmax,我们直接用某一个臂得到的平均收益除以总的平均收益不行吗?我理解上感觉softmax方法是在agrmax方法和直接除这种方法之间的方法,因为softmax加上e之后其实会让平均收益低的臂和平均收益高的臂走向极端,也就是让策略越来越激进,甚至到最终收敛成argmax?而且我感觉图像分类里面经常用softmax一方面是因为求梯度比较好计算,另一方面是因为有时候softmax之前得到的分数可能有负数,那我们这里的好处是我们可以在刚开始某个壁的奖励是0的时候,我们依旧会有一定概率选它而不会像下面公式里面的这种一样不选它。
p
(
a
r
m
i
)
=
u
i
∑
j
k
u
i
p\left(a r m_{i}\right)=\frac{{u_{i}}}{\sum_{j}^{k} {u_{i}}}
p(armi)=∑jkuiui
所以总的来说softmax有三个好处:
- 便于求梯度
- 在刚开始某一个臂收益为0的时候这个臂依旧有被选上的可能
- softmax算法让平均收益低的臂和平均收益高的臂走向极端,也就是让策略越来越激进,甚至到最终收敛成argmax,就有点像 ϵ − g r e e d y \epsilon-greedy ϵ−greedy中 ϵ \epsilon ϵ不断下降。
一个简单的赌博机算法
循环的最后一步其实用到了
Q n + 1 = 1 n ∑ i = 1 n R i = 1 n ( R n + ∑ i = 1 n − 1 R i ) = 1 n ( R n + ( n − 1 ) 1 n − 1 ∑ i = 1 n − 1 R i ) = 1 n ( R n + ( n − 1 ) Q n ) = 1 n ( R n + n Q n − Q n ) = Q n + 1 n ( R n − Q n ) \begin{aligned} Q_{n+1} &=\frac{1}{n} \sum_{i=1}^{n} R_{i} \\ &=\frac{1}{n}\left(R_{n}+\sum_{i=1}^{n-1} R_{i}\right) \\ &=\frac{1}{n}\left(R_{n}+(n-1) \frac{1}{n-1} \sum_{i=1}^{n-1} R_{i}\right) \\ &=\frac{1}{n}\left(R_{n}+(n-1) Q_{n}\right) \\ &=\frac{1}{n}\left(R_{n}+n Q_{n}-Q_{n}\right) \\ &=Q_{n}+\frac{1}{n}\left(R_{n}-Q_{n}\right) \end{aligned} Qn+1=n1i=1∑nRi=n1(Rn+i=1∑n−1Ri)=n1(Rn+(n−1)n−11i=1∑n−1Ri)=n1(Rn+(n−1)Qn)=n1(Rn+nQn−Qn)=Qn+n1(Rn−Qn)
也就是:新估计←旧估计+步长[目标−旧估计]
Python 代码实现
在代码里面实现了 ϵ − g r e e d y \epsilon-greedy ϵ−greedy、softmax,以及直接根据当前各个臂的平均收益去决策三种方法,完整的代码放在github上了,写的比较匆忙,后面会再更新一下放到github的仓库之中
# 作者:Yunhui
# 创建时间:2021/1/13 23:54
# IDE:PyCharm
# encoding: utf-8
import random
import math
import numpy as np
import matplotlib.pyplot as plt
ARM_NUM = 5
E_GREEDY_FACTOR = 0.9
SEED = random.randint(1, 10000)
TEST_STEPS = 1000
class MAB:
def __init__(self, arm_num: int) -> None:
"""
:param arm_num: the number of arms 臂的数量
"""
self.arm_num = arm_num # 设置臂的数量
self.probability = dict({}) # 设置每个臂能摇出一块钱的概率
self.try_time = dict({}) # 每个臂已经摇过的次数
self.reward = dict({}) # 每个臂已经获得的钱
self.reward_all = 0 # 所有臂获得的收益之和
self.try_time_all = 0 # 总的尝试的次数
def reset(self, seed: int) -> None:
"""
Each arm is initialized, and each arm is set the same when passing in the same random seed
对每一个臂进行初始化,当传入的随机种子一样时,每个臂的设置相同
:param seed: random seed 传入的随机种子
"""
print("We have %d arms" % self.arm_num)
for num in range(self.arm_num):
random.seed(num+seed)
self.probability[str(num + 1)] = random.random()
self.try_time[str(num + 1)] = 0
self.reward[str(num + 1)] = 0
def step(self, arm_id: str):
"""
Change the arm according to the arm_id
当传入每次要摇下的臂的编号后,老虎机的状态发生变化
:param arm_id: the id of the arm in this step 这一步控制摇下杆的id
"""
self.try_time[arm_id] += 1
self.try_time_all += 1
if random.random() < self.probability[arm_id]:
self.reward[arm_id] += 1
self.reward_all += 1
def render(self):
"""
draw the multi-armed bandit,including tried times and reward
for each arm, and total tried times and rewards.
"""
if self.arm_num <= 10:
print('*' * 8 * (self.arm_num + 1) + '**')
title = str(self.arm_num) + " arm bandit"
title_format = '{:^' + str(8 * (self.arm_num + 1)) + 's}'
print('*' + title_format.format(title) + '*')
print('*' + ' ' * 8 * (self.arm_num + 1) + '*')
print('*{:^8s}'.format('arm'), end='')
for arm in range(self.arm_num):
print('{:^8d}'.format(arm + 1), end='')
print('*\n')
print('*{:^8s}'.format('tried'), end='')
for arm in range(self.arm_num):
print('{:^8d}'.format(self.try_time[str(arm + 1)]), end='')
print('*\n')
print('*{:^8s}'.format('reward'), end='')
for arm in range(self.arm_num):
print('{:^8d}'.format(self.reward[str(arm + 1)]), end='')
print('*\n')
print('*' + title_format.format("total tried:" + str(self.try_time_all)) + '*')
print('*' + title_format.format("total rewards:" + str(self.reward_all)) + '*')
print('*' + ' ' * 8 * (self.arm_num + 1) + '*')
print('*' * 8 * (self.arm_num + 1) + '**')
def e_greedy_method(mab):
"""
e greedy method: define a e_greedy_factor and create a random number,
when the random number is less then e_greedy_factor, then pick a arm
randomly, else pick the arm with argmax q_table.
:param mab: the class MBA
:return: selected arm_id
"""
q_table = []
for arm_num in range(mab.arm_num):
if mab.try_time[str(arm_num+1)] != 0:
q_table.append(mab.reward[str(arm_num+1)]/mab.try_time[str(arm_num+1)])
else:
q_table.append(0)
if random.random() < E_GREEDY_FACTOR:
arm_id = random.randint(1, mab.arm_num)
else:
arm_id = np.argmax(q_table) + 1
return arm_id
def sofmax_method(mab):
"""
softmax method: calculate the softmax value of each arm's avarage reward,
and pick the arm with greatest softmax value.
:param mab: the class MBA
:return: selected arm_id
"""
exp_sum = 0
softmax_list = []
for arm_num in range(mab.arm_num):
if mab.try_time[str(arm_num+1)] > 0:
exp_sum += math.exp(mab.reward[str(arm_num+1)] / mab.try_time[str(arm_num+1)])
else:
exp_sum += math.exp(0)
assert exp_sum > 0
for arm_num in range(mab.arm_num):
if mab.try_time[str(arm_num+1)] == 0:
avg_reward_temp = 0
else:
avg_reward_temp = mab.reward[str(arm_num+1)] / mab.try_time[str(arm_num+1)]
softmax_list.append(math.exp(avg_reward_temp) / exp_sum)
arm_id = np.random.choice(mab.arm_num, 1, p=softmax_list)[0]
print("The softmax list is", softmax_list)
print("The id of returned arm is ", arm_id+1)
return arm_id + 1
def average_method(mab):
"""
decide the arm_id according to the average return of each arm but don't do the math.exp() operation like softmax
:param mab: the class MBA
:return: selected arm_id
"""
sum_average = 0
softmax_list = []
for arm_num in range(mab.arm_num):
if mab.try_time[str(arm_num + 1)] > 0:
sum_average += (mab.reward[str(arm_num + 1)] / mab.try_time[str(arm_num + 1)])
else:
sum_average += 0
if sum_average == 0:
arm_id = np.random.choice(mab.arm_num) + 1
else:
for arm_num in range(mab.arm_num):
if mab.try_time[str(arm_num + 1)] == 0:
avg_reward_temp = 0
else:
avg_reward_temp = mab.reward[str(arm_num + 1)] / mab.try_time[str(arm_num + 1)]
softmax_list.append(avg_reward_temp / sum_average)
arm_id = np.random.choice(mab.arm_num, 1, p=softmax_list)[0]
print("The softmax list is", softmax_list)
print("The id of returned arm is ", arm_id + 1)
return arm_id + 1
if __name__ == '__main__':
reward_list = []
mab_test = MAB(ARM_NUM)
print("****Multi-armed Bandit***")
mab_test.reset(SEED)
mab_test.render()
for i in range(TEST_STEPS):
mab_test.step(str(e_greedy_method(mab_test)))
reward_list.append(mab_test.reward_all/mab_test.try_time_all)
if (i+1) % 20 == 0:
print("We have test for %i times" % (i+1))
mab_test.render()
plt.plot(reward_list)
plt.show()