RL_第二章_多臂老虎机_Part1

发布时间:2026/7/17 6:17:24
RL_第二章_多臂老虎机_Part1 本章作者从只有一个状态的简化情况下讨论强化学习中评估与反馈的诸多性质。这一章的读书笔记将从本章练习出发使用python搭建一个简单的多臂老虎机程序将强化学习的诸多元素可视化。Problem Formulation假设你面前有一台超级老虎机上面有k10k10k10个不同的拉杆。你的筹码 你有 1000 个游戏币意味着你可以总共玩 1000 次即T1000T1000T1000个时间步。你的动作 每一轮你只能从这 10 个拉杆中挑选 1个 拉下。游戏胜负目标 玩完这 1000 次后把你每次得到的金币加起来。总金币数最多你就赢了。 也就是最大化累积收益。这 10 个拉杆的背后没有固定的赔率表而是每一个拉杆都连接着一个“暗箱”。暗箱设定的出厂秘密真实价值q∗q_*q∗​在游戏还没开始时赌场老板给每个拉杆设定了一个“平均出币量数学期望”。比如拉杆 1 的平均出币量是 0.5 个。拉杆 2 的平均出币量是 -1.2 个倒扣钱。拉杆 3 的平均出币量是 1.5 个最好的拉杆。注意这个秘密设定你作为玩家是绝对不知道的代码里的 self.q_true 就是这个出厂秘密。在时间步ttt给定动作aaa的条件下收益RtR_tRt​的数学期望q∗(a)≐E[Rt∣Ata]q_*(a) \doteq \mathbb{E}[R_t \vert{} A_t a]q∗​(a)≐E[Rt​∣At​a]核心矛盾 如果你拥有“上帝视角”知道每一个拉杆的q∗(a)q_*(a)q∗​(a)那问题就不存在了——你只需要每次都选那个数学期望最大的拉杆即可。但现实是你不知道q∗(a)q_*(a)q∗​(a)。你只能通过不断地拉动拉杆收集历史的RtR_tRt​数据去估计出一个Qt(a)Q_t(a)Qt​(a)并希望让Qt(a)Q_t(a)Qt​(a)尽可能逼近q∗(a)q_*(a)q∗​(a)。多臂老虎机问题的终极目标不是去追求某单次抽样的最高收益而是在总共TTT个时间步的长期交互中最大化累积期望收益的总和。Maximize∑t1TRt\text{Maximize} \sum_{t1}^{T} R_tMaximizet1∑T​Rt​在算法理论中这个目标等价于最小化“遗憾” (Regret)。遗憾是指如果你每次都选理论上的最优拉杆和你实际为了探索而选择次优拉杆所产生的收益差额。探索次数越多短期遗憾就越大。Experiment Setupclass BanditEnvironment: 多臂老虎机环境模拟 完全对应 Sutton Barto 强化学习书中第 2 章的 10-armed Testbed def __init__(self, k_arms10, random_seedNone): self.k k_arms if random_seed is not None: np.random.seed(random_seed) # 1. 初始化真实价值 q*(a) # 每个动作的数学期望收益服从均值为0、方差为1的标准正态分布 self.q_true np.random.normal(loc0.0, scale1.0, sizeself.k) # 记录理论上的最优动作用于计算后续算法的“遗憾”和“最优动作选择率” self.optimal_action np.argmax(self.q_true) def step(self, action): 执行动作环境返回收益 :param action: 智能体选择的拉杆索引 (0 到 k-1) :return: 标量收益 R_t if action 0 or action self.k: raise ValueError(非法的动作索引) # 2. 生成即时收益 R_t # 收益从均值为 q_true[action]、方差为 1 的正态分布中采样 reward np.random.normal(locself.q_true[action], scale1.0) return reward def render_distributions(self): 可视化这 k 个拉杆的收益概率分布对应书中经典的小提琴图 data [] for a in range(self.k): # 为每个拉杆采样1000次用于绘制分布图 samples np.random.normal(locself.q_true[a], scale1.0, size1000) data.append(samples) plt.figure(figsize(10, 6)) plt.violinplot(data, showmeansTrue) plt.xlabel(Action) plt.ylabel(Reward Distribution) plt.title(f{self.k}-Armed Bandit Reward Distributions) plt.axhline(0, colorgray, linestyle--, alpha0.5) # 标记出最优的拉杆 plt.plot(self.optimal_action 1, self.q_true[self.optimal_action], r*, markersize15, labelOptimal Action $q_*(a)$) plt.xticks(range(1, self.k 1)) plt.legend() plt.show()动作价值算法 ——ε\varepsilonε-greedy算法开发和试探的平衡是强化学习中的一个问题。动作的价值的真实值是选择这个动作时的期望收益因此一种自然的方式就是通过计算实际收益的平均值来估计动作的价值。采样平均方法通过计算t时刻前执行动作a得到的平均收益作为估计动作价值的一种方法详见公式2.1但不一定是最好的方法。由大数定律可知当次数足够多时动作价值会收敛于最佳的动作评估价值。纯贪心算法会每次选择现有动作价值最大的最优动作但这好比一个偏执的人一直按照自己世界中最优的策略生活不愿尝试任何新鲜事物因此效果会很差。基于贪心算法的改进是ε\varepsilonε-greedy算法 即大多时候是贪心的偶尔往前随机试错。ε\varepsilonε-greedy 强制保留的探索概率能保证它在无限长的时间里一定能遍历所有状态最终收敛到全局最优。作者推导了ε\varepsilonε-greedy算法的增量式实现详见公式2.3具体伪代码如下算法: ε-greedy Action Selection (针对 k 臂老虎机) 输入: 动作数量 k, 探索率 ε (如 0.1), 总时间步 T 初始化: 对于所有的动作 a 1, 2, ..., k: Q(a) ← 0 // 初始化对每个动作的价值估计 N(a) ← 0 // 初始化每个动作被选择的次数 循环 对于 t 1, 2, ..., T: // 1. 动作选择 (Action Selection) 生成一个 [0, 1) 之间的均匀随机数 p 如果 p ε: A_t ← 从 {1, 2, ..., k} 中等概率随机选择一个动作 // 探索 (Exploration) 否则: A_t ← argmax_a Q(a) // 利用 (Exploitation) // (注若有多个动作的 Q(a) 并列最大则从中随机选择) // 2. 与环境交互 (Interaction) 执行动作 A_t环境返回即时收益 R_t // 3. 价值更新 (Value Update) N(A_t) ← N(A_t) 1 Q(A_t) ← Q(A_t) (1 / N(A_t)) * [R_t - Q(A_t)]上述的多臂老虎机问题则是模拟了一个平稳分布下的场景。在非平稳环境中环境底层的真实价值q∗(a)q_*(a)q∗​(a)是会随时间发生变化的比如老虎机 A 昨天很赚钱今天突然坏了。在这种环境下给近期的收益赋予比过去很久的收益更高的价值就是一种合理的处理方式有点像RL后续提出的折扣因子的概念。P.S. 这里如果用1/n1/n1/n来定义Qn1Q_{n1}Qn1​叫采样平均法这种方法认为每一步的收益对下一步action的价值定义贡献度一样但是一定会收敛详见公式2.7但是在非平稳环境中action的实际value无法收敛一定是随时间变化的在实际应用中很少用到如果用固定步长α\alphaα叫指数近因加权平均这种方法认为离当前步越近的reward贡献度越高reward的权值随着相隔时间的增加指数衰减。多臂老虎机代码模拟本部分对于ε\varepsilonε-贪心智能体实现了采样平均和固定步长两种方法。class EpsilonGreedyAgent: ε-greedy 多臂老虎机智能体 def __init__(self, k_arms, epsilon, alpha0.1, methodsample): self.k k_arms # 动作的数量 (拉杆数) self.epsilon epsilon # 探索的概率 ε # Q_t(a): 记录对每个动作价值的估计值。初始时全为 0。 self.q_estimates np.zeros(k_arms) # N(a): 记录每个动作被选择的次数用于计算增量更新的步长 (1/n) self.action_counts np.zeros(k_arms) self.method method self.alpha alpha def select_action(self): 核心逻辑根据 ε-greedy 策略选择动作 # 第一次采样生成一个 0 到 1 之间的均匀随机数 if np.random.rand() self.epsilon: # 【探索模式】 # 第二次采样从 0 到 k-1 所有动作中等概率随机盲抽一个 action np.random.choice(self.k) else: # 【利用模式 (贪心)】 # 找到当前 q_estimates 中估值最高的动作 max_q np.max(self.q_estimates) # 【细节警报】如果有多个动作的估值并列第一比如初始时全是 0 # 我们不能只选第一个而应该在所有最高分的动作里随机挑一个打破平局(Tie-breaking)。 # np.where 会返回所有等于 max_q 的动作索引 best_actions np.where(self.q_estimates max_q)[0] action np.random.choice(best_actions) return action def update(self, action, reward): 价值更新逻辑当执行动作并获得奖励后更新对该动作的价值估计 样本平均法: Q_{n1} Q_n (1/n) * [R_n - Q_n] 固定步长法: Q_{n1} Q_n alpha * [R_n - Q_n] # 1. 该动作的尝试次数 1 self.action_counts[action] 1 n self.action_counts[action] # 2. 计算误差 (TD Error 雏形): [真实目标 - 老估计值] error reward - self.q_estimates[action] if self.method sample: # 3. 增量更新 Q 值 self.q_estimates[action] (1.0 / n) * error else: self.q_estimates[action] self.alpha * error结果对比没有那种步长策略是普适最优的选哪个完全取决于环境是否平稳。样本平均法在平稳环境下随着n→∞n \to \inftyn→∞方差趋于 0收敛到真值无偏且最精确。在非平稳环境下10000 步前那个早就过时的收益和刚刚拿到的最新收益权重一模一样。固定步长法 展开后是指数加权移动平均因为步长永不缩小估值会一直带着收益噪声抖动永远不会完全收敛但会主动遗忘过时信息可以适应变化的环境。我们将两种方法分别在平稳环境和非平稳环境下选取ε0.1\varepsilon0.1ε0.1时的平均收益和最优动作选取比例进行了对比结果发现平稳环境下样本平均更优非平稳环境下固定步长法更优。哲学层面上这两种方法好像一位旧时代的学者记忆力很好对过去的知识掌握非常牢固但是一旦放在了信息指数爆炸的时代就没有优势另一种方法是紧跟先进技术的学者学习新知识很快特别擅长捕捉信息但学得快忘得也快在某个小领域信息收敛的情形下会显得没有那么踏实。