【动手学强化学习】第二章 多臂老虎机问题知识点总结
本章知识点:探索与利用的含义、多臂老虎机问题、累积懊悔的定义、增量式更新期望奖励、经典探索策略( ϵ \epsilon ϵ-贪婪算法、上置信界UCB算法、汤普森采样算法)
多臂老虎机
- 多臂老虎机是简化的强化学习,与强化学习不同,多臂老虎机不存在状态信息,只有动作和奖励,算是最简单的“和环境交互中的学习”的一种形式
- 多臂老虎机问题与强化学习的一大区别在于其与环境的交互并不会改变环境,即多臂老虎机的每次交互的结果和以往的动作无关,所以可看作无状态的强化学习(stateless reinforcement learning)
- 多臂老虎机中表现出明显的探索与利用(exploration vs. exploitation)问题
累计懊悔
- 对于每个动作 a a a,其期望奖励为 Q ( a ) = E r ∼ R ( ⋅ ∣ a ) [ r ] Q(a) = E_{r \sim R(\cdot|a)}[r] Q(a)=Er∼R(⋅∣a)[r],最优期望奖励 Q ∗ = max a ∈ A Q ( a ) Q^* = \max_{a \in A}Q(a) Q∗=maxa∈AQ(a)
- 懊悔定义为拉动当前拉杆的动作与最优拉杆的期望奖励差 R ( a ) = Q ∗ − Q ( a ) R(a) =Q^* -Q(a) R(a)=Q∗−Q(a)
- 累计懊悔(cumulative regret):即操作T 次拉杆后累积的懊悔总量 σ R = ∑ t = 1 T R ( a t ) \sigma_{R} = \sum_{t=1}^T R(a_t) σR=∑t=1TR(at)
- 多臂老虎机问题的目标为最大化累积奖励,等价于最小化累积懊悔
- 增量式期望奖励更新公式推导
增量式更新期望奖励
- 为了知道拉动哪一根拉杆能获得更高的奖励,需要估计拉动这根拉杆的期望奖励。由于只拉动一次拉杆获得的奖励存在随机性,所以需要多次拉动一根拉杆,然后计算得到的多次奖励的期望
- 增量式更新推导公式
Q k = 1 k ∑ i = 1 k r i = 1 k [ r k + ∑ i = 1 k − 1 r i ] = 1 k ( r k + ( k − 1 ) Q k − 1 ) = 1 k ( r k + k Q k − 1 − Q k − 1 ) = Q k − 1 + 1 k [ r k − Q k − 1 ] \begin{aligned} Q_k &= \frac{1}{k}\sum_{i=1}^{k}{r_i} \\ &= \frac{1}{k}\left[ r_k + \sum_{i=1}^{k-1}r_i \right] \\ &= \frac{1}{k}( r_k +(k-1)Q_{k-1})\\ &= \frac{1}{k}(r_k + kQ_{k-1} - Q_{k-1}) \\ &= Q_{k-1} + \frac{1}{k}\left[ r_k - Q_{k-1} \right] \end{aligned} Qk=k1i=1∑kri=k1[rk+i=1∑k−1ri]=k1(rk+(k−1)Qk−1)=k1(rk+kQk−1−Qk−1)=Qk−1+k1[rk−Qk−1] - 如果将所有数求和再除以次数,其缺点是每次更新的时间复杂度和空间复杂度均为 O ( n ) O(n) O(n)。而采用增量式更新,时间复杂度和空间复杂度均为 O ( 1 ) O(1) O(1)
探索与利用
- 探索与利用的平衡, 探索–增加对环境的知识;利用–根据已有知识选择目前来看奖励最大的动作
- 多臂老虎机问题中:探索(exploration)是指尝试拉动更多可能的拉杆,这根拉杆不一定会获得最大的奖励,但这种方案能够摸清楚所有拉杆的获奖情况
- 多臂老虎机问题中:利用(exploitation)是指拉动已知期望奖励最大的那根拉杆,由于已知的信息仅仅来自有限次的交互观测,所以当前的最优拉杆不一定是全局最优的
- 在多臂老虎机问题中,设计策略时就需要平衡探索和利用的次数,使得累积奖励最大化。常用的思路是在开始时做比较多的探索,在对每根拉杆都有比较准确的估计后,再进行利用
经典探索策略
ϵ \epsilon ϵ-贪婪策略
- ϵ \epsilon ϵ-贪婪策略:完全贪婪算法即在每一时刻采取期望奖励估值最大的动作(拉动拉杆),这就是纯粹的利用,而没有探索
- ϵ-贪婪算法在完全贪婪的基础上添加噪声 每次以 1 − ϵ 1-\epsilon 1−ϵ的概率根据以往经验选择期望奖励估值最大的动作(利用),以 ϵ \epsilon ϵ概率随机选择动作(探索)
- 随着探索次数的不断增加,我们对各个动作的奖励估计得越来越准(掌握环境的信息增加),此时我们就没必要继续花大力气进行探索。所以在ϵ-贪婪算法的具体实现中,我们可以令 ϵ \epsilon ϵ随时间衰减,即探索的概率将会不断降低
- 通过实验结果可以发现,基本上无论 ϵ取值多少,累积懊悔都是线性增长的。在这个例子中,随着 ϵ的增大,累积懊悔增长的速率也会增大
- 个人思考:这是因为懊悔是和探索挂钩,随机探索因为没有选择已知最优的动作,造成懊悔。ϵ越大,探索的概率越大,探索越多,前期懊悔累计越多,导致其增大,而在后期掌握了环境知识,应减少探索。这里因为ϵ值固定,导致一直在线性增长
- 在探索期,算法会随机选择动作,这意味着它可能会选择到次优动作。每次选择次优动作时,都会产生一定的懊悔。由于 epsilon 是固定的,算法会以一定的概率持续探索,因此这种损失会随着时间线性增长。
- 改进:epsilon的值随时间衰减
随时间成反比例衰减的epsilon -贪婪算法能够使累积懊悔与时间步的关系变成次线性(sublinear)的,这明显优于固定 epsilon值的 -贪婪算法
下图中可以看到在前期epsilon较大时累计懊悔增长的较快,而随时间增大后,epsilon减小,探索减少,累计懊悔增长变缓,这与模拟退火的思想很类似
- epsilon只会决定策略收敛的快慢,对于最终的奖励值(收敛结果)并没有一定的强相关联系
上置信界算法
-
上置信界算法(Upper confidence bound,UCB)是一种经典的基于不确定性的策略算法,引入不确定性度量 U ( a ) U(a) U(a),其会随着一个动作被尝试次数的增加而减小
-
引入不确定性度量U(a),不确定性越高,就越具有探索的价值,因为探索之后可能发现它的期望奖励很大。随着一个动作被尝试次数的增加,不确定性度量减小。
-
使用一种基于不确定性的策略来综合考虑现有的期望奖励估值和不确定性,其核心问题是如何估计不确定性。
-
UCB算法,思想用到了一个非常著名的数学原理:霍夫丁不等式(Hoeffding’s inequality), u u u为不确定性度量, x ˉ \bar{x} xˉ为经验期望
-
将霍夫丁不等式运用到多臂老虎机问题中,以期望奖励估值 Q ^ t ( a ) \hat Q_t(a) Q^t(a)代入 x ˉ \bar{x} xˉ
-
首先要估计期望奖励上界,上置信界算法便选取期望奖励上界最大的动作 a t = a r g m a x a ∈ A [ Q ^ ( a ) + U ^ ( a t ) ] a_t = argmax_{a \in A}[\hat Q(a) + \hat U(a_t)] at=argmaxa∈A[Q^(a)+U^(at)]
-
UCB 算法在每次选择拉杆前,先估计每根拉杆的期望奖励的上界,使得拉动每根拉杆的期望奖励只有一个较小的概率p超过这个上界,(因为当p很小时,1-p很大,则Q(at)<Qhat(at)+ Uhat(at)成立的概率很大)接着选出期望奖励上界最大的拉杆,从而选择最有可能获得最大期望奖励的拉杆。上标^hat代表估计量
汤普森采样算法(Thompson sampling)
- 汤普森采样,先假设拉动每根拉杆的奖励服从一个特定的概率分布,然后根据拉动每根拉杆的期望奖励来进行选择。
- 由于计算所有拉杆的期望奖励的代价比较高,汤普森采样算法使用采样的方式,即根据当前每个动作 a a a的奖励概率分布进行一轮采样,得到一组各根拉杆的奖励样本,再选择样本中奖励最大的动作。
- 汤普森采样是一种计算所有拉杆的最高奖励概率的蒙特卡洛采样方法。
代码实现
- 构造每根拉杆的奖励服从伯努利分布(Bernoulli distribution)的多臂老虎机
每次拉下拉杆有 p p p的概率获得的奖励为 1,有 1 − p 1-p 1−p的概率获得的奖励为 0。奖励为 1 代表获奖,奖励为 0 代表没有获奖
# 导入需要使用的库,其中numpy是支持数组和矩阵运算的科学计算库,而matplotlib是绘图库
import numpy as np
import matplotlib.pyplot as plt
class BernoulliBandit:
""" 伯努利多臂老虎机,输入K表示拉杆个数 """
def __init__(self, K):
self.probs = np.random.uniform(size=K) # 随机生成K个0~1的数,作为拉动每根拉杆的获奖
# 概率
self.best_idx = np.argmax(self.probs) # 获奖概率最大的拉杆
self.best_prob = self.probs[self.best_idx] # 最大的获奖概率
self.K = K
def step(self, k):
# 当玩家选择了k号拉杆后,根据拉动该老虎机的k号拉杆获得奖励的概率返回1(获奖)或0(未
# 获奖)
if np.random.rand() < self.probs[k]:
return 1
else:
return 0
np.random.seed(1) # 设定随机种子,使实验具有可重复性
K = 10
bandit_10_arm = BernoulliBandit(K)
print("随机生成了一个%d臂伯努利老虎机" % K)
print("获奖概率最大的拉杆为%d号,其获奖概率为%.4f" %
(bandit_10_arm.best_idx, bandit_10_arm.best_prob))
- Solver基础类达成多臂老虎机的求解:实现根据策略选择动作、根据动作获取奖励、更新期望奖励估值、更新累积懊悔和计数
将根据策略选择动作、根据动作获取奖励和更新期望奖励估值放在run_one_step()
函数中,由每个继承 Solver 类的策略具体实现。而更新累积懊悔和计数则直接放在主循环run()
中
class Solver:
""" 多臂老虎机算法基本框架 """
def __init__(self, bandit):
self.bandit = bandit
self.counts = np.zeros(self.bandit.K) # 每根拉杆的尝试次数
self.regret = 0. # 当前步的累积懊悔
self.actions = [] # 维护一个列表,记录每一步的动作
self.regrets = [] # 维护一个列表,记录每一步的累积懊悔
def update_regret(self, k):
# 计算累积懊悔并保存,k为本次动作选择的拉杆的编号
self.regret += self.bandit.best_prob - self.bandit.probs[k]
self.regrets.append(self.regret)
def run_one_step(self):
# 返回当前动作选择哪一根拉杆,由每个具体的策略实现
raise NotImplementedError
def run(self, num_steps):
# 运行一定次数,num_steps为总运行次数
for _ in range(num_steps):
k = self.run_one_step()
self.counts[k] += 1
self.actions.append(k)
self.update_regret(k)
- ϵ − 贪婪算法 \epsilon-贪婪算法 ϵ−贪婪算法
ϵ-贪婪算法在完全贪婪的基础上添加噪声 每次以 1 − ϵ 1-\epsilon 1−ϵ的概率根据以往经验选择期望奖励估值最大的动作(利用),以 ϵ \epsilon ϵ概率随机选择动作(探索)
#实现epsilon-贪婪算法 继承Solver类
class EpsilonGreedy(Solver):
def __init__(self, bandit,epsilon=0.01, init_prob=1.0) -> None:
super(EpsilonGreedy,self).__init__(bandit)
self.epsilon = epsilon
self.estimates = np.array([init_prob] * self.bandit.K) #初始化拉动所有拉杆的期望奖励估值
def run_one_step(self):
if np.random.random() < self.epsilon: #探索
k = np.random.randint(0, self.bandit.K)
else: #利用
k = np.argmax(self.estimates)
r = self.bandit.step(k) #奖励
self.estimates[k] += 1. / (self.counts[k]+1) * (r - self.estimates[k]) # 增量式更新 猜测 self.counts[k]要+1的原因是避免分母为0
return k
- 可视化累计懊悔与时间变化
def plot_results(solvers, solver_names):
"""生成累积懊悔随时间变化的图像。输入solvers是一个列表,列表中的每个元素是一种特定的策略。
而solver_names也是一个列表,存储每个策略的名称"""
for idx, solver in enumerate(solvers):
time_list = range(len(solver.regrets))
plt.plot(time_list,solver.regrets, label=solver_names[idx])
plt.xlabel('Time Steps')
plt.ylabel('Culmulative regrets')
plt.title("%d-armed bandit" %solvers[0].bandit.K)
plt.legend()
plt.show()
np.random.seed(1)
epsilon_greedy_solver = EpsilonGreedy(bandit_10_arm,epsilon=0.01)
epsilon_greedy_solver.run(5000)
print('epsilon-贪婪算法的累积懊悔为:', epsilon_greedy_solver.regret)
plot_results([epsilon_greedy_solver], ["EpsilonGreedy"])
上述为10臂老虎机,策略为EpsilonGreedy, ϵ = 0.01 \epsilon=0.01 ϵ=0.01的累计懊悔
通过上面的实验可以发现,在经历了开始的一小段时间后, ϵ \epsilon ϵ-贪婪算法的累积懊悔几乎是线性增长的。这是 ϵ = 0.01 \epsilon=0.01 ϵ=0.01时的结果(每次以 1 − ϵ 1-\epsilon 1−ϵ的概率 根据以往经验选择期望奖励估值最大的动作, ϵ = 0.01 \epsilon=0.01 ϵ=0.01时会进行很多利用),一旦做出了随机拉杆的探索,产生的懊悔值是固定的,导致累计懊悔线性增长
- 不同的 ϵ \epsilon ϵ取值的累计懊悔对比
np.random.seed(0)
epsilons = [1e-4, 0.01, 0.1, 0.25, 0.5]
epsilon_greedy_solver_list = [
EpsilonGreedy(bandit_10_arm, epsilon=e) for e in epsilons
]
epsilon_greedy_solver_names = ["epsilon={}".format(e) for e in epsilons]
for solver in epsilon_greedy_solver_list:
solver.run(5000)
plot_results(epsilon_greedy_solver_list, epsilon_greedy_solver_names)
无论 ϵ \epsilon ϵ取值多少,累积懊悔都是线性增长的
-
个人思考:这是因为懊悔是和探索挂钩,随机探索因为没有选择已知最优的动作,造成懊悔。ϵ越大,探索的概率越大,探索越多,前期懊悔累计越多,导致其增大,而在后期掌握了环境知识,应减少探索。这里因为ϵ值固定,导致一直在线性增长
-
改进:epsilon的值随时间衰减
随时间成反比例衰减的epsilon -贪婪算法能够使累积懊悔与时间步的关系变成次线性(sublinear)的,这明显优于固定 epsilon值的 -贪婪算法
# epsilon随时间衰减的贪婪算法
class DecayingEpsilonGreedy(Solver):
def __init__(self, bandit,init_prob=1.0) -> None:
super(DecayingEpsilonGreedy,self).__init__(bandit)
self.estimates = np.array([init_prob]* self.bandit.K)
self.total_count = 0
def run_one_step(self):
self.total_count += 1
if np.random.random() < 1 / self.total_count : #epsilon的值随时间衰减 epsilon = 1/ t
k = np.random.randint(0, self.bandit.K)
else:
k = np.argmax(self.estimates)
r = self.bandit.step(k)
self.estimates[k] += 1./ (self.counts[k]+1) * (r - self.estimates[k]) # 增量式更新公式
return k
np.random.seed(1)
decaying_epsilon_greedy_solver = DecayingEpsilonGreedy(bandit_10_arm)
decaying_epsilon_greedy_solver.run(5000)
print('epsilon值衰减的贪婪算法的累积懊悔为:', decaying_epsilon_greedy_solver.regret)
plot_results([decaying_epsilon_greedy_solver], ["DecayingEpsilonGreedy"])
下图中可以看到在前期epsilon较大时累计懊悔增长的较快,而随时间增大后,epsilon减小,探索减少,累计懊悔增长变缓,这与模拟退火的思想很类似
- UCB上置信界算法
p = 1 t p = \frac{1}{t} p=t1,在分母中为拉动每根拉杆的次数加上常数 1,以免出现分母为 0 的情形
class UCB(Solver):
""" UCB算法,继承Solver类 """
def __init__(self, bandit, coef, init_prob=1.0):
super(UCB, self).__init__(bandit)
self.total_count = 0
self.estimates = np.array([init_prob] * self.bandit.K)
self.coef = coef
def run_one_step(self):
self.total_count += 1
ucb = self.estimates + self.coef * np.sqrt(
np.log(self.total_count) / (2 * (self.counts + 1))) # 计算上置信界
k = np.argmax(ucb) # 选出上置信界最大的拉杆
r = self.bandit.step(k)
self.estimates[k] += 1. / (self.counts[k] + 1) * (r - self.estimates[k])
return k
np.random.seed(1)
coef = 1 # 控制不确定性比重的系数
UCB_solver = UCB(bandit_10_arm, coef)
UCB_solver.run(5000)
print('上置信界算法的累积懊悔为:', UCB_solver.regret)
plot_results([UCB_solver], ["UCB"])
- 汤普森采样算法
# 汤普森采样算法
class ThompsonSampling(Solver):
def __init__(self, bandit) -> None:
super(ThompsonSampling, self).__init__(bandit)
self._a = np.ones(self.bandit.K) # 列表,表示每根拉杆奖励为1的次数
self._b = np.ones(self.bandit.K) # 列表,表示每根拉杆奖励为0的次数
def run_one_step(self):
samples = np.random.beta(self._a,self._b) # 按照Beta分布采样一组奖励样本
k = np.argmax(samples) #选出采样奖励最大的拉杆
r = self.bandit.step(k)
self._a[k] += r # 更新beta分布的第一个参数
self._b[k] += (1-r)# 更新beta分布的第二个参数
return k
np.random.seed(1)
thompson_sampling_solver = ThompsonSampling(bandit_10_arm)
thompson_sampling_solver.run(5000)
print('汤普森采样算法的累积懊悔为:', thompson_sampling_solver.regret)
plot_results([thompson_sampling_solver], ["ThompsonSampling"])
总结
- ϵ \epsilon ϵ-贪婪算法的累积懊悔是随时间线性增长的,而另外 3 种算法( ϵ \epsilon ϵ -衰减贪婪算法、上置信界算法、汤普森采样算法)的累积懊悔都是随时间次线性增长的(具体为对数形式增长)
- 上置信界算法和汤普森采样方法均能保证对数的渐进最优累积懊悔
- 多臂老虎机问题与强化学习的一大区别在于其与环境的交互并不会改变环境,即多臂老虎机的每次交互的结果和以往的动作无关,所以可看作无状态的强化学习(stateless reinforcement learning)