文章目录
- 前言
- 创建多臂老虎机环境
- 多臂老虎机算法基本框架(基类)
- 1. ε-贪心算法 (Epsilon-Greedy)
- 2. 随时间衰减的ε-贪婪算法 (Decaying ε-Greedy)
- 3. 上置信界算法 (Upper Confidence Bound, UCB)
- 4. 汤普森采样算法 (Thompson Sampling)
- 总结
前言
欢迎来到“从代码学习深度强化学习”系列!在本篇文章中,我们将深入探讨一个强化学习中的经典问题——多臂老虎机(Multi-Armed Bandit, MAB)。
多臂老虎机问题,顾名思义,源于一个赌徒在赌场面对一排老虎机(即“多臂老虎机”)的场景。每个老虎机(“臂”)都有其内在的、未知的获奖概率。赌徒的目标是在有限的回合内,通过选择拉动不同的老虎机,来最大化自己的总收益。
这看似简单的场景,却完美地诠释了强化学习中的一个核心困境:探索(Exploration)与利用(Exploitation)的权衡。
- 利用(Exploitation):选择当前已知收益最高的老虎机。这能保证我们在短期内获得不错的收益,但可能会错过一个潜在收益更高但尚未被充分尝试的选项。
- 探索(Exploration):尝试那些我们不确定其收益的老虎机。这可能会在短期内牺牲一些收益,但却有机会发现全局最优的选择,从而获得更高的长期总回报。
为了量化算法的性能,我们引入一个重要概念——累积懊悔(Cumulative Regret)。懊悔指的是在某一步选择的动作所带来的期望收益与“上帝视角”下最优动作的期望收益之差。一个优秀的算法,其目标就是最小化在整个过程中的累积懊悔。
在本篇博客中,我们将通过 Python 代码,从零开始实现一个多臂老虎机环境,并逐步实现和对比以下四种经典的求解策略:
- ε-贪心算法 (Epsilon-Greedy)
- 随时间衰减的ε-贪心算法 (Decaying Epsilon-Greedy)
- 上置信界算法 (Upper Confidence Bound, UCB)
- 汤普森采样算法 (Thompson Sampling)
关于 PyTorch: 尽管标题提及 PyTorch,但对于 MAB 这种基础问题,使用 NumPy 能更清晰地展示算法的核心逻辑,而无需引入深度学习框架的复杂性。本文中的实现将基于 NumPy,但其核心思想(如价值估计、策略选择)是构建更复杂的深度强化学习算法(如DQN)的基石,在那些场景中 PyTorch 将发挥关键作用。
让我们开始吧!
完整代码:下载链接
创建多臂老虎机环境
多臂老虎机问题可以表示为一个元组 ⟨ A , R ⟩ \langle\mathcal{A},\mathcal{R}\rangle ⟨A,R⟩ ,其中:
- A \mathcal{A} A为动作集合,其中一个动作表示拉动一个拉杆。若多臂老虎机一共有 K K K根拉杆,那动作空间就是集合 { a 1 , … , a K } \{a_1,\ldots,a_K\} {a1,…,aK},我们用 a t ∈ A a_t\in\mathcal{A} at∈A表示任意一个动作;
- R \mathcal{R} R为奖励概率分布,拉动每一根拉杆的动作 a a a都对应一个奖励概率分布 R ( r ∣ a ) \mathcal{R}(r|a) R(r∣a),不同拉杆的奖励分布通常是不同的。
假设每个时间步只能拉动一个拉杆,多臂老虎机的目标为最大化一段时间步 T T T内累积的奖励: max ∑ t = 1 T r t , r t ∼ R ( ⋅ ∣ a t ) \max\sum_{t=1}^Tr_t, r_t\sim\mathcal{R}\left(\cdot|a_t\right) max∑t=1Trt,rt∼R(⋅∣at)。其中 a t a_t at表示在第 t t t个时间步拉动某一拉杆的动作, r t r_t rt表示动作 a t a_t at获得的奖励。
首先,我们需要一个模拟环境。我们创建一个 BernoulliBandit
类来模拟一个拥有 K
个臂的老虎机。每个臂都服从伯努利分布,即每次拉动它,会以一个固定的概率 p
获得奖励 1
(获奖),以 1-p
的概率获得奖励 0
(未获奖)。在我们的环境中,这 K
个臂的获奖概率 p
是在初始化时随机生成的,并且对我们的算法(智能体)是未知的。
# 导入需要使用的库
import numpy as np # numpy是支持数组和矩阵运算的科学计算库
import matplotlib.pyplot as plt # matplotlib是绘图库class BernoulliBandit:"""伯努利多臂老虎机类该类实现了一个多臂老虎机问题的环境,每个拉杆都服从伯努利分布"""def __init__(self, K):"""初始化伯努利多臂老虎机参数:K (int): 拉杆个数,标量属性:probs (numpy.ndarray): 每个拉杆的获奖概率数组,维度为 (K,)best_idx (int): 获奖概率最大的拉杆索引,标量best_prob (float): 最大的获奖概率值,标量K (int): 拉杆总数,标量"""# 随机生成K个0~1之间的数,作为拉动每根拉杆的获奖概率# probs: (K,) - K个拉杆的获奖概率数组self.probs = np.random.uniform(size=K)# 找到获奖概率最大的拉杆索引# best_idx: 标量 - 最优拉杆的索引号self.best_idx = np.argmax(self.probs)# 获取最大的获奖概率# best_prob: 标量 - 最大获奖概率值self.best_prob = self.probs[self.best_idx]# 保存拉杆总数# K: 标量 - 拉杆个数self.K = Kdef step(self, k):"""执行一次拉杆动作当玩家选择了k号拉杆后,根据该拉杆的获奖概率返回奖励结果参数:k (int): 选择的拉杆编号,标量,取值范围为 [0, K-1]返回:int: 奖励结果,标量1 表示获奖0 表示未获奖"""# 根据k号拉杆的获奖概率进行伯努利采样# np.random.rand(): 标量 - 生成[0,1)之间的随机数# self.probs[k]: 标量 - k号拉杆的获奖概率if np.random.rand() < self.probs[k]:return 1 # 获奖else:return 0 # 未获奖# 设定随机种子,使实验具有可重复性
np.random.seed(1)# 设置拉杆数量
# K: 标量 - 多臂老虎机的拉杆个数
K = 10# 创建一个10臂伯努利老虎机实例
# bandit_10_arm: BernoulliBandit对象 - 包含10个拉杆的老虎机
bandit_10_arm = BernoulliBandit(K)# 输出老虎机的基本信息
print("随机生成了一个%d臂伯努利老虎机" % K)
print("获奖概率最大的拉杆为%d号,其获奖概率为%.4f" % (bandit_10_arm.best_idx, bandit_10_arm.best_prob))
运行以上代码,我们创建了一个10臂老虎机,并打印出了最优拉杆的信息。在我们的实验中,1号拉杆是收益最高的,其获奖概率为 0.7203。这个信息算法本身是不知道的,但我们可以用它来计算懊悔。
随机生成了一个10臂伯努利老虎机
获奖概率最大的拉杆为1号,其获奖概率为0.7203
多臂老虎机算法基本框架(基类)
为了方便实现和比较不同的算法,我们先定义一个 Solver
基类。这个基类包含了所有算法都需要共享的功能,例如记录每个臂被拉动的次数、记录历史动作以及计算和更新累积懊悔。具体的决策逻辑(run_one_step
)将由各个子类来实现, 需要求解的是选取某根拉杆的策略。
累积懊悔
对于每一个动作,我们定义其期望奖励为 Q ( a ) = E r ∼ R ( ⋅ ∣ a ) [ r ] {Q}(a)=\mathbb{E}_{r\sim\mathcal{R}(\cdot|a)}\begin{bmatrix}r\end{bmatrix} Q(a)=Er∼R(⋅∣a)[r]。于是,至少存在一根拉杆,它的期望奖励不小于拉动其他任意一根拉杆,我们将该最优期望奖励表示为 Q ∗ = max a ∈ A Q ( a ) Q^*=\max_{a\in\mathcal{A}}Q(a) Q∗=maxa∈AQ(a)。为了更加直观、方便地观察拉动一根拉杆的期望奖励离最优拉杆期望奖励的差距,我们引入懊悔(regret)概念。懊悔定义为拉动当前拉杆的动作与最优拉杆的期望奖励差,即 R ( a ) = Q ∗ − Q ( a ) R(a)=Q^*-Q(a) R(a)=Q∗−Q(a)。累积懊悔(cumulative regret)即操作 T T