多臂老虎机
有n根拉杆的的老虎机,每根拉杆获得奖励(值为1)的概率各不相同。
期望奖励更新
Q
k
=
1
k
∑
i
=
1
k
r
i
=
1
k
(
r
k
+
∑
i
=
1
k
−
1
r
i
)
=
1
k
(
r
k
+
k
Q
k
−
1
−
Q
k
−
1
)
=
Q
k
−
1
+
1
k
[
r
k
−
Q
k
−
1
]
Q_k=\frac 1k \sum^{k}_{i=1}r_i\\ =\frac 1k (r_k+\sum^{k-1}_{i=1}r_i)\\ =\frac 1k( r_k+kQ_{k-1}-Q_{k-1})\\ =Q_{k-1}+\frac 1k[r_k-Q_{k-1}]
Qk=k1i=1∑kri=k1(rk+i=1∑k−1ri)=k1(rk+kQk−1−Qk−1)=Qk−1+k1[rk−Qk−1]
累计懊悔:
至少存在一根拉杆,它的期望奖励不小于拉动其他任意一根拉杆,设最优期望为 Q ∗ = m a x 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),累计懊悔 σ R = ∑ t = 1 T R ( a t ) \sigma_R=\sum^T_{t=1}R(a_t) σR=∑t=1TR(at),则多臂老虎机可等价为最小化累积期望。
基本框架
多臂老虎机的基本框架如下
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的数作为概率,获奖积1分,没获奖0分
self.best_idx = np.argmax(self.probs) # 获奖概率最大的拉杆
self.best_prob = self.probs[self.best_idx] # 最大获奖概率
self.K = K
def step(self, K):
# 进行一步,根据选择的第k号拉杆获奖的概率进行返回
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))
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 # ?问下gpt
def run(self, num_steps):
# 运行一定次数
for _ in range(num_steps):
k = self.run_one_step()
self.counts[k] += 1
self.actions.append(k)
self.actions.append(k)
self.update_regret(k)
def plot_results(solvers, solver_names):
# 输入的solvers是一个列表,每个元素是一个特定策略,solver_names也是一个列表,存储每个策略的名称
for idx, solver in enumerate(solvers):
"""enumerate() 函数用于将一个可遍历的数据对象(如列表、元组或字符串)组合为一个索引序列,同时列出数据和数据下标,一般在循环中使用。此处将索引存在idx,具体的策略存在solver中"""
time_list = range(len(solver.regrets)) # 生成一个包含从 0 到 solver.regrets 的大小减去 1 的整数序列。
plt.plot(time_list, solver.regrets, label=solver_names[idx])
plt.xlabel("Time steps")
plt.ylabel("Cumulative regrets")
plt.title("%d-armed bandit" % solvers[0].bandit.K)
plt.legend() # 用于添加图例到 Matplotlib 图表中的函数,即使用label标签标记的。
plt.show()
对于多臂老虎机的决策,有如下方法
ϵ \epsilon ϵ-贪婪算法
这个算法设置一个随机概率 ϵ \epsilon ϵ,每次做决策时,有 1 − ϵ 1-\epsilon 1−ϵ的概率按已有经验做出最优决策,在这个问题中,即在拉动过的杆中选出最优的,以概率 ϵ \epsilon ϵ随机选择一根拉杆。
本例中先选择 ϵ = 0.01 , T = 5000 \epsilon =0.01,T=5000 ϵ=0.01,T=5000:
class EpsilonGreedy(Solver):
"""epsilon贪婪算法,需要继承Solver类"""
def __init__(self, bandit, epsilon=0.01, init_prob=1.0):
super(EpsilonGreedy, self).__init__(bandit) # 调用父类的构造函数,用bandit作为参数
self.epsilon = epsilon
self.estimates = np.array([init_prob] * self.bandit.K) # 初始化所有拉杆的期望奖励估值,设置为一样的?
def run_one_step(self):
# 以贪婪算法运行一步
if np.random.rand() < 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 # 返回选择的杆的编号
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"])
实际上,尝试次数越多,越不需要冒险去探险,因为这无疑会降低收益,那么我们希望 ϵ \epsilon ϵ随时间逐渐变小。即 ϵ t = 1 t \epsilon_t = \frac 1t ϵt=t1,一般使用随时间衰减的贪婪算法:
class DecayingEpsilonGreedy(Solver):
"""epsilon值随时间衰减的贪婪算法,需要继承Solver类"""
def __init__(self, bandit, init_prob=1.0):
super(DecayingEpsilonGreedy, self).__init__(bandit) # 调用父类的构造函数,用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.rand() < 1 / self.total_count: # 随机选择
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)
decaysing_epsilon_greedy_solver = DecayingEpsilonGreedy(bandit_10_arm)
decaysing_epsilon_greedy_solver.run(5000)
print('epsilon值衰减的-贪婪算法的累计懊悔为:', decaysing_epsilon_greedy_solver.regret)
plot_results([decaysing_epsilon_greedy_solver], ["DecayingEpsilonGreedy"])
上置信界算法(UCB)
在探索时,我们总会有这样的一种想法,如果第1根拉杆只被拉动过1次,但奖励为0,第2根被拉动过很多次,我们对它的奖励分布已经有了比较确定的把握,或许我们更愿意尝试拉动第一根拉杆,可能他的收益更高。为了定量的描述这种“潜藏“的更优解,引入了不确定性度量
U
(
a
)
U(a)
U(a),它会随着一个动作被尝试次数的增加而减小。
那么我们可以使用一种基于不确定性的策略来综合考虑现有的期望奖励估值和不确定性,其核心问题是如何估计不确定性。
霍夫丁不等式
令
X
1
,
⋯
,
X
n
X_1,\cdots,X_n
X1,⋯,Xn为
n
n
n个独立同分布的随机变量,取值范围为
[
0
,
1
]
[0,1]
[0,1],若经验期望为
x
‾
=
1
x
∑
j
=
1
n
X
j
\overline{x}=\frac 1x\sum^n_{j=1}X_j
x=x1∑j=1nXj,则有
P
(
E
[
X
]
≥
x
‾
t
+
u
)
≤
e
−
2
n
u
2
P(E[X]\ge \overline{x}_t +u)\le e^{-2nu^2}
P(E[X]≥xt+u)≤e−2nu2
期望的奖励上界
在这个例子中,经验期望就是 Q ^ ( a t ) \hat{Q}(a_t) Q^(at),将 u = U ^ ( a t ) u=\hat{U}(a_t) u=U^(at)代表不确定性度量,给定一个概率 p = e − 2 N ( a t ) U ( a t ) 2 p=e^{-2N(a_t)U(a_t)^2} p=e−2N(at)U(at)2,其中 N ( a t ) N(a_t) N(at)表示拉动某一编号杆的次数。根据霍夫丁不等式, Q ( a t ) < Q ^ ( a t ) + U ^ ( a t ) Q(a_t)<\hat{Q}(a_t)+\hat{U}(a_t) Q(at)<Q^(at)+U^(at)至少以概率 1 − p 1-p 1−p成立,当 p p p很小时,以很大概率成立,那么我们可以认为 Q ^ ( a t ) + U ^ ( a t ) \hat{Q}(a_t)+\hat{U}(a_t) Q^(at)+U^(at)便是期望的奖励上界。
此时,UCB算法选取上界最大的动作,即 a t = a r g m a x a ∈ A [ Q ^ ( a ) + U ^ ( a ) ] a_t = arg\ max_{a\in A}[\hat{Q}(a)+\hat{U}(a)] at=arg maxa∈A[Q^(a)+U^(a)]
其中 U ^ ( a t ) = − l o g p 2 N ( a t ) \hat{U}(a_t)=\sqrt{\cfrac{-log\ p}{2N(a_t)}} U^(at)=2N(at)−log p,也就是说,设定一个概率 p p p后,就可计算了。
总的来说,UCB算法在每次决策前,都会估计每根杆的期望上界,使得拉动每根杆的期望奖励只有一个较小的概率 p p p超过上界,并选取最优可能获得最大期望奖励的拉杆。
UCB算法的代码实现
容易发现的是,随着时间的增长,我们将对期望有着越来越确定的把握,那么 p p p的设置也应该是随时间增长减少的,则设 p = 1 t p=\frac 1t p=t1
对于 U ^ ( a t ) \hat{U}(a_t) U^(at),我们在分母加上常数1,避免出现分母为0的情况,则 U ^ ( a t ) = l o g t 2 ( N ( a t ) + 1 ) \hat{U}(a_t)=\sqrt{\frac{log t}{2(N(a_t)+1)}} U^(at)=2(N(at)+1)logt。
为了控制不确定性的比重,引入系数 c c c,此时, a t = a r g m a x a ∈ A [ Q ^ ( a ) + c ⋅ U ^ ( a ) ] a_t = arg\ max_{a\in A}[\hat{Q}(a)+c\cdot \hat{U}(a)] at=arg maxa∈A[Q^(a)+c⋅U^(a)]
class UCB(Solver):
# UCB算法,继承Solver类
def __init__(self, bandit, coef, init_prob=1.0):
super(UCB, self).__init__(bandit)
self.total_count = 0 # 时间计数t
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))) # 计算上置信界
# np.log()默认以e为底,以其他为底可使用换底公式
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 = 0.7 # 不确定性权重的参数
UCB_solver = UCB(bandit_10_arm, coef)
UCB_solver.run(5000)
print("上置信界算法的累积懊悔为:", UCB_solver.regret)
plot_results([UCB_solver], ["UCB"])