多臂老虎机
第一部分
需要的库包
# 导入需要使用的库,其中numpy是支持数组和矩阵运算的科学计算库,而matplotlib是绘图库
import numpy as np
import matplotlib.pyplot as plt
1.设置伯努利老虎机
拉动每根拉杆的奖励服从伯努利分布 即每次拉下拉杆有的概率获得的奖励为 1,有1-的概率获得的奖励为 0。奖励为 1 代表获奖,奖励为 0 代表没有获奖。
# 先设置一个具有10个拉杆数量的老虎机 并且设置好哪个拉杆输出好的效果的概率最大
class BernoulliBandit:
""" 伯努利多臂老虎机,输入K表示拉杆个数 """
def __init__(self, K):
self.probs = np.random.uniform(size=K) # 随机生成K个0~1的数,作为拉动每根拉杆的获奖概率
#print(self.probs)
self.best_idx = np.argmax(self.probs) # 获奖概率最大的拉杆 拉杆的序号
#print(self.best_idx)
self.best_prob = self.probs[self.best_idx] # 最大的获奖概率 拉杆序号对应的最大概率
#print(self.best_prob)
self.K = K
#print(self.K)
def step(self, k):
# 当玩家选择了k号拉杆后,根据拉动该老虎机的k号拉杆获得奖励的概率返回1(获奖)或0(未获奖)
if np.random.rand() < self.probs[k]:# rand 通过本函数可以返回一个或一组服从“0-1”均匀分布的随机样本值 随机样本取值范围[0,1),不包括1
return 1
else:
return 0
np.random.seed(1) # 设定随机种子,使实验具有可重复性 self.probs = np.random.uniform(size=K)使这条语句每次生成的最大概率的拉杆序号保持一致
K = 10
bandit_10_arm = BernoulliBandit(K)
print("随机生成了一个%d臂伯努利老虎机" % K)
print("获奖概率最大的拉杆为%d号,其获奖概率为%.4f" % (bandit_10_arm.best_idx, bandit_10_arm.best_prob) )
运行结果:
2.算法框架solver
class Solver:
""" 多臂老虎机算法基本框架 """
def __init__(self, bandit):
self.bandit = bandit
# K 为上面伯努力类中的K即老虎机的臂数
self.counts = np.zeros(self.bandit.K) # 每根拉杆的尝试次数 这里的banit为老虎机 是用来引用前面的BernoulliBandit类 K个为零的数据
# print("couts",self.counts)
self.regret = 0. # 当前步的累积懊悔
self.actions = [] # 维护一个列表,记录每一步的动作
# print("action",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 # 相当于C++中的虚函数 子类函数中的同名函数运行
def run(self, num_steps):
# 运行一定次数,num_steps为总运行次数
for _ in range(num_steps):
k = self.run_one_step()
# print(k)
self.counts[k] += 1
self.actions.append(k)
self.update_regret(k)
最后在引用之后使用这个类生成一个列表 用来存储数据
动手学强化学习网站上的内容:接下来我们用一个 Solver 基础类来实现上述的多臂老虎机的求解方案。根据前文的算法流程,我们需要实现下列函数功能:根据策略选择动作、根据动作获取奖励、更新期望奖励估值、更新累积懊悔和计数。在下面的 MAB 算法基本框架中,我们将根据策略选择动作、根据动作获取奖励和更新期望奖励估值放在 run_one_step()
函数中,由每个继承 Solver 类的策略具体实现。而更新累积懊悔和计数则直接放在主循环 run()
中。
3. ϵ-贪心算法
完全贪婪算法即在每一时刻采取期望奖励估值最大的动作(拉动拉杆),这就是纯粹的利用,而没有探索,所以我们通常需要对完全贪婪算法进行一些修改,其中比较经典的一种方法为 ϵ-贪婪( ϵ-Greedy)算法。ϵ-贪婪算法在完全贪婪算法的基础上添加了噪声,每次以1-ϵ概率 选择以往经验中期望奖励估值最大的那根拉杆(利用),以概率 随机选择一根拉杆(探索),公式如下:
采样概率:从中随机选择,采样概率:
随着探索次数的不断增加,我们对各个动作的奖励估计得越来越准,此时我们就没必要继续花大力气进行探索。所以在 ϵ-贪婪算法的具体实现中,我们可以令 ϵ随时间衰减,即探索的概率将会不断降低。但是请注意, ϵ 不会在有限的步数内衰减至 0,因为基于有限步数观测的完全贪婪算法仍然是一个局部信息的贪婪算法,永远距离最优解有一个固定的差距。
class EpsilonGreedy(Solver):
""" epsilon贪婪算法,继承Solver类 """
def __init__(self, bandit, epsilon=0.01, init_prob=1.0):
super(EpsilonGreedy, self).__init__(bandit) # 初始化父类中的变量 # 这里的banit为老虎机 是用来引用前面的BernoulliBandit类
self.epsilon = epsilon
# 初始化拉动所有拉杆的期望奖励估值
self.estimates = np.array([init_prob] * self.bandit.K) # 这里的K是上面设置完老虎机之后设置的K
# print(self.estimates)
# print(self.bandit.K)
def run_one_step(self):
if np.random.random() < self.epsilon: # e-贪心算法 以概率e(epsilon)随机选择一根拉杆 以概率1-e选择以往经验中期望奖励估值最大的那根拉杆
k = np.random.randint(0, self.bandit.K) # 随机选择一根拉杆 0为随机生成整数的最小值 self.bandit.K为随机生成整数的最大值
else:
k = np.argmax(self.estimates) # 选择期望奖励估值最大的拉杆
r = self.bandit.step(k) # 得到本次动作的奖励 BernoulliBandit类中的奖励值的设置
self.estimates[k] += 1. / (self.counts[k] + 1) * (r - self.estimates[k]) # 更新期望奖励Q值
return k
ϵ-贪心算法继承了算法框架solver 在初始化的函数中设置了 bandit 这代表的是老虎机 还有贪心值 初始化值
初始化了拉动所有拉杆的期望奖励估值
K表示的是拉杆个数 k表示的是当前的拉杆序号
4.绘图
def plot_results(solvers, solver_names):
"""生成累积懊悔随时间变化的图像。输入solvers是一个列表,列表中的每个元素是一种特定的策略。
而solver_names也是一个列表,存储每个策略的名称"""
for idx, solver in enumerate(solvers):
time_list = range(len(solver.regrets)) # self.regrets记录的是每一步的懊悔值 len是计算它的长度 为5000步
# print(time_list)
# 输出时range(0,5000)
# print(idx)
# 输出是 0
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() # 设置图例
plt.show() # 显示图像
np.random.seed(0)
epsilon_greedy_solver = EpsilonGreedy(bandit_10_arm, epsilon=0.01) # 这里的bandit_10_arm为前面设置好的10臂老虎机
epsilon_greedy_solver.run(5000)
print('epsilon-贪婪算法的累积懊悔为:', epsilon_greedy_solver.regret)
plot_results([epsilon_greedy_solver], ["EpsilonGreedy"])
运行结果:
图像:
5.多个不同贪心概率
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)
6.epsilon值衰减的贪婪算法
class DecayingEpsilonGreedy(Solver):
""" epsilon值随时间衰减的epsilon-贪婪算法,继承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
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"])
代码的逻辑和上面代码的逻辑是一样的
运行结果:
从实验结果图中可以发现,随时间做反比例衰减的ϵ -贪婪算法能够使累积懊悔与时间步的关系变成次线性(sublinear)的,这明显优于固定ϵ 值的 ϵ-贪婪算法。
虽然最后懊悔值没有完全收敛,但是结果也是比 ϵ-贪婪算法的线性效果好的
第二部分 上至信界算法
设想这样一种情况:对于一台双臂老虎机,其中第一根拉杆只被拉动过一次,得到的奖励为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)))
# 计算上置信界 为上面a的公式
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"])
第三部分 汤普森算法
MAB 中还有一种经典算法——汤普森采样(Thompson sampling),先假设拉动每根拉杆的奖励服从一个特定的概率分布,然后根据拉动每根拉杆的期望奖励来进行选择。但是由于计算所有拉杆的期望奖励的代价比较高,汤普森采样算法使用采样的方式,即根据当前每个动作a 的奖励概率分布进行一轮采样,得到一组各根拉杆的奖励样本,再选择样本中奖励最大的动作。可以看出,汤普森采样是一种计算所有拉杆的最高奖励概率的蒙特卡洛采样方法。
class ThompsonSampling(Solver):
""" 汤普森采样算法,继承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)
thompson_sampling_solver = ThompsonSampling(bandit_10_arm)
thompson_sampling_solver.run(5000)
print('汤普森采样算法的累积懊悔为:', thompson_sampling_solver.regret)
plot_results([thompson_sampling_solver], ["ThompsonSampling"])
运行结果:
结论
通过实验我们可以得到以下结论:ϵ -贪婪算法的累积懊悔是随时间线性增长的,而另外 3 种算法( ϵ-衰减贪婪算法、上置信界算法、汤普森采样算法)的累积懊悔都是随时间次线性增长的(具体为对数形式增长)。
多臂老虎机问题与强化学习的一大区别在于其与环境的交互并不会改变环境,即多臂老虎机的每次交互的结果和以往的动作无关,所以可看作无状态的强化学习(stateless reinforcement learning)。
python知识点
self
self是指类本身
和普通函数相比,在类中定义函数只有一点不同,就是第一参数永远是类的本身实例变量self,并且调用时,不用传递该参数。除此之外,类的方法(函数)和普通函数没啥区别,既可以用默认参数、可变参数或者关键字参数
self相当于c++语言中的this
def __init__(self):
相当于c++中的构造函数 不需要调用就会调用有print还会输出
变量名称
变量名称前加上两个下划线__name 该变量为私有变量
name前后都有两个下划线是特殊变量 不能随便定义
函数
np.random.uniform
第一部分中的np.random.uniform(size=K)
函数是指从一个均匀分布的区域中随机采样
用法:
np.random.uniform(low, high ,size)
`其形成的均匀分布区域为[low, high)
1.low:采样区域的下界,float类型或者int类型或者数组类型或者迭代类型,默认值为0 2.high:采样区域的上界,float类型或者int类型或者数组类型或者迭代类型,默认值为1 3.size:输出样本的数目(int类型或者tuple类型或者迭代类型) 4.返回对象:ndarray类型,形状和size中的数值一样 假设 size为10 那就是10个数据 那返回对象就是十个数据
self.X = np.random.uniform((10, 3))
这行函数是说随机生成两个数据,第一个数据不大于10,第二个数据不大于3
np.argmax
获奖概率最大的拉杆 拉杆的序号 从给定的数据中获取最大概率所对应的索引 本实例中是指所对应的拉杆序号 也就是上一个函数中的K值
np.argmax(self.probs) # 获奖概率最大的拉杆 拉杆的序号
(1)遵循运算之后降一维的原则
(2)函数返回的是最大值的索引,而不是最大值本身。
(3)0代表对行进行最大值选取,1代表对列最大值进行选取
super
super(A,self).__init__()
这个函数类似于c++中的菱形继承
遵循以下三条原则:
-
子类永远在父类前面
-
如果有多个父类,会根据它们在列表中的顺序被检查
-
如果对下一个类存在两个合法的选择,选择第一个父类
class Base(object):
def __init__(self):
print("enter Base")
print("leave Base")
class A(Base):
def __init__(self):
print("enter A")
super(A,self).__init__()
print("leave A")
class B(Base):
def __init__(self):
print("enter B")
super(B,self).__init__()
print("leave B")
class C(A,B):
def __init__(self):
print("enter C")
super(C,self).__init__()
print("leave C")
c=C()
运行结果是:
再输入
print(C.mro())
运行结果:
np.zeros()
numpy.zeros(shape, dtype=float)
各个参数意义: shape
:创建的新数组的形状(维度)。 dtype
:创建新数组的数据类型。 返回值:给定维度的全零数组。
numpy.random.seed()
seed( ) 函数用于指定随机数生成时所用算法开始的整数值,如果使用相同的 seed() 值,则每次生成的随机数都相同,如果不设置这个值,则系统根据时间来自己选择这个值,此时每次生成的随机数因时间差异而不同。
但是,只在调用的时候seed()一下并不能使生成的随机数相同,需要每次调用都seed()一下,表示种子相同,从而生成的随机数相同。
-
一个代码程序中相同的随机种子下生成的随机数相同
-
一个随机种子在代码中只作用一次,只作用于其定义位置的下一次随机数生成
from numpy import *
num=0
print(random.random())
random.seed(5)
while(num<5):
print(random.random())
num+=1
运行结果:
0.03448381943112333
0.22199317108973948 # 随机种子为 5 下生成的随机数,与实例 1 中的数相同
0.8707323061773764
0.20671915533942642
0.9186109079379216
0.48841118879482914
只有第二行是因为随机种子而生成的数据
enumerate()
-
enumerate()是python的内置函数
-
enumerate在字典上是枚举、列举的意思
-
对于一个可迭代的(iterable)/可遍历的对象(如列表、字符串),enumerate将其组成一个索引序列,利用它可以同时获得索引和值
-
enumerate多用于在for循环中得到计数
range
range(start, stop, step)
参数名称 | 说明 | 备注 |
---|---|---|
start | 计数起始位置 | 整数参数,可省略。省略时默认从0开始计数 |
stop | 计数终点位置 | 不可省略的整数参数。计数迭代的序列中不包含stop |
step | 步长 | 可省略的整数参数,默认时步长为1 |
由上表可以得知stop不可省略,其他可以省略
返回值
range函数返回一个range对象实例。实例包含了计数的起始位置、终点位置和步长等信息。
format
format是字符串内嵌的一个方法,用于格式化字符串。以大括号{}来标明被替换的字符串。
s = "{} is a {}".format('Tom', 'Boy')
print(s) # Tom is a Boy
s1 = "{} is a {}".format('Tom')
# 抛出异常, Replacement index 1 out of range for positional args tuple
print(s1)
参考链接:python中format的用法详解_python format-CSDN博客v96pc_search_result_base6&utm_term=format%E5%9C%A8python%E4%B8%AD%E7%9A%84%E7%94%A8%E6%B3%95&spm=1018.2226.3001.4187