RL代码实践 01——ε-greedy算法及其优化

发布于:2025-08-11 ⋅ 阅读:(39) ⋅ 点赞:(0)

目录

一、问题描述

二、问题分析和解决

1、ε-greedy算法

2、改进的ε-greedy算法

(1)衰减的探索概率

(2)上置信界(Upper Confidence Bound,UCB)

(3)汤普森采样(Thompson Sampling)


一、问题描述

多臂老虎机(MAB,muti-armed bandit)

K个拉杆,中奖概率分布未知,如何在T次拉杆后获得尽可能高的累计奖励?(也就是在有限的尝试次数里,让中奖次数尽可能多)

二、问题分析和解决

最简的RL系统,只有reward,没有state,更能体现纯粹的探索利用权衡。

关键就在于找到一个合适的exploration和exploitation的比例。

1、ε-greedy算法

大概率选目前中奖概率高的,小概率随机探索

1、如何判断中奖?

因为获奖概率是在 [ 0, 1 ) 中随机生成的数,假设为 x,

所以可以用随机投点模拟,落在区间 [ 0, x ) 为中奖,落在区间 [ x, 1 )为不中奖。

可不可以是 [ 0, x ] 中奖?不可以。

如果中奖概率x是0,按照左闭右闭的说法,则随机投点落在 [0 , 0]就中奖,即点数0可以中奖,但是实际上和中奖概率为0矛盾。所以不能是<=,只能是<。

2、计算历史中奖概率时,如果该拉杆从未被访问过,中奖概率赋值为0.1比0更好。为什么?

如果不设置为0.1,就很有可能,有些拉杆一直很难被选上。因为大概率是exploit的,而exploit又是选择历史中奖率最高的,而一直没被选中过的拉杆,如果设置概率为0,就不可能被选到(除了全为0时,迫不得已会选一个)。设置0.1可以鼓励初始阶段有更多的探索。

import numpy as np
from click.testing import Result

# 模拟K个拉杆的老虎机
K=10
probs = np.random.uniform(size=K) # 各个拉杆对应的获奖概率,为在[0,1)内随机生成的浮点数(均匀分布)
rewards = [[] for _ in range(K)] # 初始化各个拉杆的历史获奖记录,大数组内包含K个小数组,先设为空

# 选择哪个拉杆(贪婪算法)
def choose_one():
    # 小概率(0.01)explore 随机挑选一个拉杆
    if np.random.uniform()<0.01:
        return np.random.randint(0,10)
    else:
    # 大概率(0.99)exploit 选择历史中奖概率最大的拉杆
        rewards_mean = [np.mean(i) if i else 0.1 for i in rewards]
        return np.argmax(rewards_mean)

# 玩一次
def play_once():
    i = choose_one()
    # 生成结果
    cur_reward = 1 if np.random.uniform()<probs[i] else 0
    # 记录结果
    rewards[i].append(cur_reward)

if __name__=="__main__":
    # 总共T次拉杆机会
    T = 5000
    for _ in range(T):
        play_once()

    # 真实中奖次数
    true_result = np.sum([np.sum(i) for i in rewards])

    # 理论中奖次数
    theory_result = np.max(probs) * T

    print(f"true_result: {true_result}")
    print(f"theory_result: {theory_result}")

2、改进的ε-greedy算法

(1)衰减的探索概率

随着经验的积累增加,我们的探索欲望是可以减弱的。

只需要修改小概率探索部分的代码:

# 玩的总次数(积累的经验有多少)
played_count = np.sum([len(i) for i in rewards])
# 探索概率随次数增加而下降
if np.random.uniform()<1/(played_count + 1): # +1避免除以0
    return np.random.randint(0,10)

(2)上置信界(Upper Confidence Bound,UCB)

多探索玩得少的机器。此时不是针对所有老虎机一概而论的,而是针对每台老虎机来说的,各自有各自的“探索欲望值”。玩得多的少探索,玩得少的仍想去多探索。

对于老虎机的场景:

动作a当前的平均奖励,即某拉杆对应的历史中奖次数的平均(中奖概率)

动作a被选择的次数,即某拉杆被play的次数

总时间步数,即所有拉杆被play次数的总和

特点:

不再显式区分探索(Explore)和利用(Exploit),而是通过一个统一的数学公式动态平衡二者。即UCB算法里面的explore和exploit不是分开的,不是小概率explore大概率exploit,而是综合考虑一个UCB值。

1、关于探索:

分母n小,即该拉杆的尝试次数少时,探索项大,UCB大,越有可能选择这个动作;

分子会随着时间缓慢增大(增长速度比分母慢),即总体的探索充足时,探索欲望会下降,UCB值会更倾向于平均奖励项,注重exploit。

超参数c越大,探索越激进;c越小,探索越保守。

def choose_one():
    # 各个老虎机玩的次数
    played_counts = np.array([len(i) for i in rewards])

    # 优先探索未尝试过的拉杆(也是处理分母可能为0的情况)
    if np.any(played_counts == 0):
        return np.argmin(played_counts)

    # 当所有拉杆都尝试过后,进入UCB算法
    a = np.log(np.sum(played_counts)) # 分子是各台玩的次数求和的对数值,增长较慢
    b = played_counts # 分母是每一台玩的次数,增长较快
    # 对每一台老虎机,玩的次数越多,分母很快会超过分子,分数就越小,探索欲望就越低
    rewards_mean = [np.mean(i) for i in rewards]
    c = 2**0.5
    ucb = rewards_mean + c * np.sqrt(a/b)

    return np.argmax(ucb)  # 选择UCB最大的拉杆

(3)汤普森采样(Thompson Sampling)

根据历史成功次数和失败次数来生成Beta分布,该分布描述了“成功概率的概率分布”。

1、什么是Beta分布?

这里仅从直观上理解:它是“成功概率的概率分布”,所以必然是限制在[0,1)上的。

假设

  • a:为 “成功次数 + 1”(+1 是为了避免初始值为 0 导致的极端情况)。
  • b:为 “失败次数 + 1”。

a和b越大,分布越集中(因为尝试的次数越多,结果越明显,不确定性越低);

a和b越小,分布越分散(不确定性越高)。

初始时,a=b=1,成功概率的期望是0.5,但是是均匀分布(完全分散状态);

随着实验次数增多,a=b时,成功概率的期望仍然是0.5,但是会呈现峰值在0.5的对称分布(更集中)。

a>b,成功的次数多于失败,则成功概率会偏向1的一边;

a<b,失败的次数多于成功,则成功概率会偏向0的一边。

np.random.beta(a, b)会返回随机数,它是在beta分布上采的样值。

2、汤普森采样是怎么隐式平衡exploit和explore的?

 在MAB问题中,该算法根据“中奖概率的Beta概率分布”来生成一个随机数,当作中奖概率。然后选取中奖概率最大的拉杆来拉。

关键在于它是用随机数来模拟概率,并不是用准确的历史中奖概率,或者是Beta峰值对应的数(其实就是接近 成功次数/(成功次数+失败次数),即历史中奖概率)。

这种随机采样是 “带着不确定性做决策”,既优先选择当前看起来好的拉杆(利用),又给不确定的拉杆留了机会(探索)。

对于历史表现好(a>b)且稳定的(不确定性低),随机数很可能落在高概率区,易被选中(exploit);

对于不确定性高的,随机数的概率分布较分散,仍有一定可能采到高值,导致被选中(explore)。

通过概率分布的 “不确定性” 自动把握探索与利用的比例,无需人工设定参数。

3、和UCB对比

汤普森采样:更像 “凭直觉的赌徒”,通过概率猜测拉杆好坏,灵活性高、实践效果好,适合复杂或实时场景。

探索力度可以动态自适应,计算简单,对奖励噪声更稳健(因为概率分布天然平滑了随机波动),理论保证相对较弱,但在实践中往往表现更好

UCB:更像 “严谨的统计学家”,通过公式严格计算每个拉杆的潜力,理论扎实,适合对可解释性和收敛性要求高的场景。

探索力度由公式强制,计算复杂,对极端值较敏感,有严格的理论证明解释

def choose_one():
    # 每个拉杆中奖的次数
    counts_1 = [sum(i)+1 for i in rewards]
    # 每个拉杆不中奖的次数
    counts_0 = [sum(1-np.array(i))+1 for i in rewards]
    # 对每个拉杆,根据 beta 分布随机生成中奖概率
    beta = np.random.beta(counts_1,counts_0)
    return np.argmax(beta)


网站公告

今日签到

点亮在社区的每一天
去签到