第二章 多臂tiger机问题

第一节 简介

强化学习是一种试错型学习范式。

第二节 问题介绍

多臂tiger机(multi-armed bandit,MAB)不存在状态信息,只有动作和奖励。有一个拥有K根拉杆的tiger机,拉动每一根拉杆都对应一个关于奖励的概率分布R。我们每次拉动其中一根拉杆,就可以从该拉杆对应的奖励概率分布中获得一个奖励r。在各根拉杆的奖励概率分布未知的情况下,从头开始尝试,目标是操作T次拉杆后尽可能高的累计奖励。由于奖励的概率分布是未知的,因此我们需要采取策略在“探索拉杆的获奖概率”和“根据经验选择获奖最多的拉杆”中进行权衡。

多臂tiger机问题可以表示为一个元组$<A,R>$,其中:

  • $A$为动作集合,其中一个动作表示拉动一根拉杆,若多臂tiger机一共有$K$根拉杆,那动作空间就是集合${a_1,...,a_i,...,a_K}$,我们用$a_t\in A$表示任意一个动作。
  • $R$为奖励概率分布,拉动每一根拉杆的动作$a$都对应一个奖励概率分布$R(r|a)$,拉动不同拉杆的奖励分布通常是不同的。

目标是最大化一段时间步$T$内累积的奖励:$\max\sum_{i=1}^{T}r_t,r_t~R(·|a_t)$,其中$a_t$表示在第$t$时间步拉动某一拉杆的动作,$r_t$表示动作$a_t$获得的奖励。

对于每一个动作$a$,我们定义其期望奖励为$Q(a)=E_{r~R(·|a)}[r]$。至少存在一根拉杆,它的期望奖励不小于拉动其他任意一根拉杆,我们将该最有期望奖励表示为$Q^*=\max_{a\in A}Q(a)$。懊悔被定义为拉动当前拉杆的动作$a$与最优拉杆的期望奖励差,即$R(a)=Q*-Q(a)$。累积懊悔是操作$T$次拉杆后累积的懊悔总量,即为:$\sigma_{R}=\sum_{t=1}R(a_t)$。所以问题从最大化累计奖励,变为最小化累积懊悔。

算法流程:

对于$\forall a\in A$,初始化计数器$N(a)=0$和期望奖励估值$\hat{Q}(a)=0$

for t=1→T do

​ 选取某根拉杆,该动作即为$a_t$

​ 得到奖励$r_t$

​ 更新计数器:$N(a_t)=N(a_t)+1$

​ *更新期望奖励估值:$\hat{Q}(a_t)=\hat{Q}(a_t)+\frac{1}{N(a_t)}[r_t-\hat{Q}(a_t)]$

end for

打*的那步是因为:

$Q_k=\frac{1}{k}\sum_{i=1}^{k}r_i$

$=\frac{1}{k}(r_k+\sum_{i=1}^{k-1}r_i)$

$=\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}(r_k-Q_{k-1})$

这样$O(1)$就求出新的均值了。

下面举个例子,有一个拉杆数为10的多臂tiger机,每根拉杆的奖励服从伯努利分布,即每次拉下拉杆有$p$的概率获得的奖励为1,有$1-p$的概率获得的奖励为0。显然1代表获奖,0代表没有获奖。于是代码如下:

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号拉杆后,根据拉动该拉杆获得奖励的概率返回1或0
if np.random.rand() < self.probs[k]:
return 1
else:
return 0 np.random.seed(42)
K = 10
bandit_10_arm = BernoulliBandit(K)
print(bandit_10_arm.best_idx, bandit_10_arm.best_prob) # 解决框架
class Solver:
def __init__(self, bandit):
self.bandit = bandit # 引入多臂tiger机
self.counts = np.zeros(self.bandit.K) # 每根拉杆的尝试次数
self.regret = 0. # 当前步的累积懊悔
self.actions = [] # 每一步动作
self.regrets = [] # 每一步的累积懊悔 # k为本次序号
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)

第三节 探索与利用的平衡

而具体的策略该如何设计呢?在多臂tiger机问题中,一个经典的问题就是探索与利用的平衡问题。探索(exploration)是指尝试拉动更多可能的拉杆,这跟拉杆不一定会获得最大的奖励,但这种方案能够摸清楚所有拉杆的获奖情况。利用(exploitation)是指拉动已知期望奖励最大的那根拉杆,由于已知的信息仅仅来自有限次的交互观测,所以当前的最优拉杆不一定是全局最优的。在多臂tiger机问题中,设计策略时就需要平衡探索和利用的次数,使得累积奖励最大化。

一个比较常用的思路是在开始时做比较多的探索,在对每根拉杆有比较准确的估计后,再进行利用。经典算法比如:$\epsilon-$贪婪算法、上置信界算法和汤普森采样算法等。

第四节 $\epsilon-$探索算法

完全贪婪算法即在每一时刻采取期望奖励估值最大的动作(这里指拉动拉杆),即纯粹地利用没有探索。在此基础上诞生了$\epsilon-Greedy$算法,具体是在完全贪婪算法上添加了噪声,每次以概率$1-\epsilon$选择以往经验中期望奖励估值最大的那根拉杆(利用),以概率$\epsilon$随机选择一根拉杆(探索)。

随着探索次数不断增加,我们对各个动作的奖励估计得越来越准,此时我们就没有必要花大力气进行探索。所以在具体实现中,令$\epsilon$随时间衰减,即探索的概率将会不断降低。但是却不让$\epsilon$不会在有限的步数内衰减至0,因为基于优先步数观测得完全贪婪算法仍然是一个局部信息的贪婪算法,永远距离最优解有一个固定的差距。

具体实现,这里依然是10臂tiger机,设置$\epsilon=0.01,T=5000$:

class EpsilonGreedy(Solver):  # 继承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) # 初始化拉动所有拉杆的期望奖励估值,因为这个1会在无穷次之后的贡献变为0,而如果初始化为0,则探索的时候发现别的没有当前高,就不会探索了 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)
# counts[k]多算1,因为没算这次呢 然后是之前推的公式
self.estimates[k] += 1. / (self.counts[k] + 1) * (r - self.estimates[k])
return k

通过实验可知,无论$\epsilon$取值多少,累积懊悔都是线性增长的。随着$\epsilon$的增大,累积懊悔增长的速率也会增大。

接下来尝试反比例衰减,即$\epsilon_{t}=\frac{1}{t}$。代码如下:

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:
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(42)
decaying_epsilon_greedy_solver = DecayingEpsilonGreedy(bandit_10_arm)
decaying_epsilon_greedy_solver.run(5000)
print(decaying_epsilon_greedy_solver.regret)

经过实验发现,反比例衰减能够使累积懊悔与时间步的关系变成次线性的(对数形式增长),明显优于固定值的贪婪算法。

第五节 上置信界算法

对于一台双臂tiger机,第一根拉杆只被拉动过一次,得到的奖励为0,第二根拉杆被拉动过很多次,我们对它的奖励分布已经有了大致的把握。这里基于不确定性,因为第一根拉杆的不确定性很高,很具有探索价值,因为探索之后我们可能发现它的期望价值很高。我们在此引入不确定性度量$U(a)$,它会随着一个动作被尝试次数的增加而减小。我们可以使用一种基于不确定性的策略来综合考虑现有的期望奖励估值和不确定性。其核心问题是如何估计不确定性。

上置信界算法(upper confidence bound,UCB)算法是一种经典的基于不确定性的策略算法,它的思想用到了一个非常著名的数学原理:霍夫定不等式。霍夫定不等式:令$X_1,X_2,...,X_n$为$n$个独立同分布的随机变量,取值范围为$[0,1]$,其经验期望为$\bar{x_n}=\frac{1}{n}\sum_{j=1}^{n}X_j$,则有:$P(E[X]\ge \bar{x_t}+u)\le e{-2nu2}$,其中$u\ge0$。

将此不等式运用到多臂tiger机问题中,将$\hat{Q}(a_t)$代入$\bar{x_t}$,不等式中的参数$u=\hat{U}(a_t)$代表不确定性度量。给定一个概率$p=e{-2N(a_t)U(a_t)2}$,根据上述不等式,$Q(a_t)\lt \hat{Q}(a_t)+\hat{U}(a_t)$至少以概率$1-p$成立。当$p$很小时,$Q(a_t)\lt \hat{Q}(a_t)+\hat{U}(a_t)$就以很大概率成立,$\hat{Q}(a_t)+\hat{U}(a_t)$便是期望奖励上界。于是上置信界算法便选取期望奖励上届最大的动作,即$a_t=\arg\max_{a\in A}[\hat{Q}(a)+\hat{U}(a)]$。而这个$\hat{U}(a)$是怎么来的呢,是根据$p=e{-2N(a_t)U(a_t)2}$得到的,即给定概率$p$,可以求出$\hat{U}(a)=\sqrt{\frac{-\log p}{2N(a_t)}}$。

同样随时间反比例递减,设置$p=\frac{1}{t}$,并且在分母中为拉动每根拉杆的次数加上常数1,以免出现分母为0的情形,即此时$\hat{U}(a_t)=\sqrt{\frac{-\log p}{2(N(a_t)+1)}}$,同时设定一个常数$c$来控制不确定性比重,此时$a_t=\arg\max_{a\in A}[\hat{Q}(a)+c\times \hat{U}(a)]$。

实现如下:

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):
self.total_count += 1
ucb = self.estimates + self.coef * np.sort(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(42)
coef = 1 # 权重c
UCB_solver = UCB(bandit_10_arm, coef)
UCB_solver.run(5000)
print(UCB_solver.regret)

发现累积懊悔随时间次线性增长(对数形式增长)。

第六节 汤普森采样算法

MAB中还有一种经典算法——汤普森采样(Thompson sampling),先假设拉动每根拉杆的奖励服从一个特定的概率分布,然后根据拉动每根拉杆的期望奖励来进行选择。但是由于计算所有拉杆的期望奖励的代价比较高,汤普森采样算法使用采样的方式,这样得到一组各根拉杆的奖励样本,选择其中奖励最大的动作。汤普森采样是一种计算所有拉杆的最高奖励概率的蒙特克罗采样方法。

在实际情况中,我们通常用$Beta$分布对当前每个动作的奖励概率分布进行建模,具体来说,若某拉杆被选择了$k$次,其中$m_1$次奖励为1,$m_2$次奖励为0,则该拉杆的奖励服从参数为$(m_1+1,m_2+1)$的$Beta$分布。

实现如下:

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
self._b[k] += (1 - r)
return k np.random.seed(42)
thompson_sampling_solver = ThompsonSampling(bandit_10_arm)
thompson_sampling_solver.run(5000)
print(thompson_sampling_solver.regret)

发现累积懊悔随时间次线性增长(对数形式增长)。

第七节 小结

探索与利用是强化学习试错法中的必备技术,其中反比例衰减的$\epsilon-$贪婪算法、上置信界算法和汤普森采样方法均能保证对数的渐进最优累积懊悔。

多臂tiger机问题与强化学习的一大区别在于其与环境的交互并不会改变环境,即多臂tiger机的每次交互的结果和以往的动作无关,所以可看作无状态的强化学习。下一章讨论有状态的环境下的强化学习即马尔可夫决策过程。

动手学强化学习 第二章 多臂tiger机问题 阅读笔记的更多相关文章

  1. 小白学习之pytorch框架(2)-动手学深度学习(begin-random.shuffle()、torch.index_select()、nn.Module、nn.Sequential())

    在这向大家推荐一本书-花书-动手学深度学习pytorch版,原书用的深度学习框架是MXNet,这个框架经过Gluon重新再封装,使用风格非常接近pytorch,但是由于pytorch越来越火,个人又比 ...

  2. oracle学习 第二章 限制性查询和数据的排序 ——03

    这里.我们接着上一小节2.6留下的问题:假设要查询的字符串中含有"_"或"%".又该如何处理呢? 開始今天的学习. 2.7  怎样使用转义(escape)操作符 ...

  3. 对比《动手学深度学习》 PDF代码+《神经网络与深度学习 》PDF

    随着AlphaGo与李世石大战的落幕,人工智能成为话题焦点.AlphaGo背后的工作原理"深度学习"也跳入大众的视野.什么是深度学习,什么是神经网络,为何一段程序在精密的围棋大赛中 ...

  4. 【动手学深度学习】Jupyter notebook中 import mxnet出错

    问题描述 打开d2l-zh目录,使用jupyter notebook打开文件运行,import mxnet 出现无法导入mxnet模块的问题, 但是命令行运行是可以导入mxnet模块的. 原因: 激活 ...

  5. mxnet 动手学深度学习

    http://zh.gluon.ai/chapter_crashcourse/introduction.html 强化学习(Reinforcement Learning) 如果你真的有兴趣用机器学习开 ...

  6. Asp.Net MVC4 + Oracle + EasyUI 学习 第二章

    Asp.Net MVC4 + Oracle + EasyUI 第二章 --使用Ajax提升网站性能 本文链接:http://www.cnblogs.com/likeli/p/4236723.html ...

  7. Ruby学习-第二章

    第二章 类继承,属性,类变量 1.如何声明一个子类 class Treasure < Thing 这样Thing类中的属性name,description都被Treasure继承 2.以下三种方 ...

  8. Java基础知识二次学习-- 第二章 基础语法与递归补充

    第二章 基础语法与递归补充   时间:2017年4月24日10:39:18 章节:02章_01节,02章_02节 视频长度:49:21 + 15:45 内容:标识符,关键字与数据类型 心得:由字母,下 ...

  9. 一起来学Spring Cloud | 第二章:服务注册和发现组件 (Eureka)

    本篇文章,很浅显的一步步讲解如何搭建一个能运行的springcloud项目(带所有操作截图).相信!看完本篇之后,你会觉得springcloud搭建如此简单~~~~ 一. Eureka简介: 1.1  ...

  10. C#高级编程 (第六版) 学习 第二章:C#基础

    第二章 基础 1,helloworld示例: helloworld.cs using System; using System.Collections.Generic; using System.Li ...

随机推荐

  1. css3自动滚动

    <!DOCTYPE html> <html lang="en"><div class="wrap"> <ul clas ...

  2. Android:Activity

    什么是 Activity 简单来说 Activity 类似于一个 Web 的页面,表示拥有整个屏幕的界面.电子邮件应用可能有一个显示新电子邮件列表的 Activity.一个用于撰写电子邮件的 Acti ...

  3. 【补题】The 2022 SDUT Summer Trials

    比赛链接 The 2022 SDUT Summer Trials A. Ginger's number 样例恶臭(恼) 签到题 简单分解因数就会发现要求的就是\(gcd\),直接算即可,时间复杂度\( ...

  4. 001、nodelocaldns(/etc/resolv.conf)

    nodelocaldns  pod 中的 /etc/resolv.conf 虽然读取的是 宿主机的/etc/resolv.conf,但是不是实时同步更新的.可能同步更新会有延迟 所以如果  /etc/ ...

  5. MSF后渗透常用命令

    一.在meterpreter命令行下常用的操作 ps # 查看当前活跃进程 migrate pid # 将Meterpreter会话移植到进程数位pid的进程中 execute -H -i -f cm ...

  6. Windows.h 文件学习

    SDk :软件开发工具包 Api  :Windows操作系统提供给应用程序编程的接口,windows.h 窗口:窗口是屏幕上的一块矩形区域,是Windows应用程序与用户进行交互的接口,分为客户区与非 ...

  7. springboot-maven打包项目

    在project 标签内,新增一下内容 <build> <plugins> <!--打包项目--> <plugin> <groupId>or ...

  8. 十二、21.提交本地代码到Git仓库并推送到码云

    查看分支 运行git add . 把所有修改过后文件添加到暂存区 git commit 把当前所有的代码提交到rights分支 加-m加一个消息 到此所有的功能模块都已经提交到了rights这个分支里 ...

  9. 单元测试成神之路——Golang篇

    这位大佬写的太好了 https://zhangyuyu.github.io/golang-unit-test/

  10. 硬件IIC的重映射使用问题

    目录 沁恒的蓝牙系列芯片,有映射硬件模块去其他引脚的功能,可以配置各芯片的功能引脚重映射寄存器(R16_PIN_ALTERNATE),或者使用函数GPIOPinRemap函数进行配置. 比如说想要配置 ...