问题描述
有一个拥有K根拉杆的老虎机,拉动每一根拉杆都对应一个关于奖励的概率分布R。我们每拉动其中一根拉杆,就可以从该拉杆对应的奖励概率分布中获得一个奖励r。我们在各根拉杆的奖励概率分布未知的情况下,从头开始尝试,目标是在操作T次拉杆后获得尽可能高的累计奖励
形象化描述
Target: 最大化一段时间步T内累积的激励
其中
表示在第t时间步拉动某一拉杆的动作,
表示动作
获得的奖励
累积懊悔
懊悔:拉动当前拉杆的动作a与最优拉杆的期望奖励差
累积懊悔:
MAB问题的目标为最大化累积奖励,等价于最小化累积懊悔
估计期望奖励
为了知道拉动哪一根拉杆能获得更高的奖励,我们需要估计拉动这根拉杆的期望奖励。由于只拉动一次拉杆获得的奖励存在随机性,所以需要多次拉动一根拉杆,然后计算奖励期望
增量式更新:时间复杂度为O(1)
因此有: 其中Q表示期望奖励,N表示计数器
策略设计
MAB问题的求解流程:根据策略选择动作,根据动作获取奖励,更新期望奖励估值,更新累积懊悔和计数
探索:尝试拉动更多的拉杆,尽可能的摸清楚所有拉杆的获奖情况
利用:拉动已知期望奖励最大的那根拉杆,但由于已知的信息仅仅辣子有限次的交互观察,所以当前的最优拉杆不一定是全局最优的
设计策略时需要平衡探索和利用的次数,使得累计奖励最大化
(1)
-贪婪算法
完全贪婪算法的思想是在每一时刻采取期望奖励最大的动作(拉动拉杆),即纯粹的“利用”,没有“探索”。-贪婪算法实在完全贪婪算法的基础上添加了噪声,每次以概率1-
选择以往经验中期望奖励估值最大的那根拉杆(action:利用),以概率
随机选择一根拉杆(action:探索)
随着探索次数的不断增加,我们队各个动作的奖励估计会越来越准,此时不必画大力气进行探索,所以可以令随时间衰减(注意:不要在有限步数衰减到0)
(2)上置信界算法
引入不确定性度量U(a),它会随着一个动作被尝试次数的增加而减小。(一根拉杆的不确定性越大,它就越有探索的价值,因为探索之后我们可能发现它的期望奖励很大)
如何估计不确定性:
霍夫丁不等式:令
为n个独立同分布的随机变量,取值范围为[0,1],其经验期望为
,则有
将代入
,不等式中的参数
代表不确定性度量。给定一个概率
,根据上述不等式,
至少以概率1-p成立。当p很小时,
就以很大的概率成立,
即为期望奖励上界。此时,上置信界算法便选取期望奖励上界最大的动作,即
。根据公式
,解得
。因此,设定一个概率p后,就可以计算相应的不确定性度量
了。
UCB算法的思想是在每次选择拉杆前,先估计拉动每根拉杆的期望奖励上界,使得拉动每根拉杆的期望奖励只有一个较小的概率p超过这个上界,接着选出期望奖励上界最大的拉杆,从而选择最有可能获得最大期望奖励的拉杆。
在编写代码时,设置,并且为了避免
分母为0,
(在分母上+1)。同时还设置了一个权重系数coef来控制不确定性比重,此时
(3)汤普森采样算法
先假设拉动每根拉杆的奖励服从一个特定的奖励分布,然后根据拉动每根拉杆的期望奖励来进行选择。但是由于计算所有拉杆的期望奖励的代价比较高,汤普森采样算法使用采样的方式,即根据当前每个动作a的奖励概率分布进行一轮采样,得到一组各根拉杆的奖励样本,再选择样本中奖励最大的动作,可以看出,汤普森采样是一种计算所有拉杆的最高奖励概率的蒙特卡洛采样方法。
在实际情况中,通常用Beta分布对当前每个动作的奖励概率分布进行建模。具体来说,若拉杆被选择了k次,其中m1次奖励为1,m2次奖励为0,则该拉杆的奖励服从参数为(m1+1,m2+1)的Beta分布。
代码实现
import numpy as np
import matplotlib.pyplot as plt
class BernoulliBandit: # 期望奖励满足伯努利分布的多臂老虎机问题
def __init__(self, K):
self.probs = np.random.uniform(size=K) # 随机生成每根拉杆的获奖概率
self.best_idx = np.argmax(self.probs) # 期望奖励最大的拉杆的index
self.best_prob = self.probs[self.best_idx] # 期望奖励最大的拉杆的获奖概率
self.K = K # 拉杆的根数
def step(self, K):
if np.random.rand() < self.probs[K]:
return 1
else:
return 0
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):
self.regret += self.bandit.best_prob - self.bandit.probs[k] # 累计懊悔
self.regrets.append(self.regret)
def run_one_step(self):
return NotImplementedError
def run(self, num_steps):
for _ in range(num_steps):
k = self.run_one_step()
self.counts[k] += 1
self.actions.append(k)
self.update_regret(k)
class EpsilonGreedy(Solver):
def __init__(self, bandit, epsilon=0.01, init_prob=1.0):
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]) # 增量式更新期望奖励
return k
class DecayingEpsilonGreedy(Solver):
def __init__(self, bandit, init_prob=1.0):
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随时间衰减
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
class 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): # 取p=1/t t为拉杆次数
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
class ThompsonSampling(Solver):
def __init__(self, bandit):
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)
K = 10
bandit_10_arm = BernoulliBandit(K)
solver1 = DecayingEpsilonGreedy(bandit_10_arm)
coef = 1
solver2 = UCB(bandit_10_arm, coef)
solver3 = ThompsonSampling(bandit_10_arm)
num_step = 5000
solver1.run(num_step)
print("epsilon-贪婪算法的累计懊悔:", solver1.regret)
solver2.run(num_step)
print("上置信界算法的累计懊悔:", solver2.regret)
solver3.run(num_step)
print("汤普森采样算法的累计懊悔:", solver3.regret)