贪婪算法
初始化每台机器,每个机器的每次中奖或不中奖用rewards来记录,初始值为1。
import numpy as np
#每个机器的中奖概率,0-1之间的均匀分布
probs = np.random.uniform(size=10)
#记录每个机器的返回值
rewards = [[1] for _ in range(10)]
probs, rewards
这个函数用来选择用哪一个机器来进行抽奖。分为两部分,一部分是随机选择,另一部分是根据现在的中奖情况来选择。
其中rewards_mean是来求平均值,也就是实际用到这个机器的时候的中奖情况,比如第3台机器,它被选出来了4次用来抽奖,其中中奖了两次,比如1,0,0,1,那么,它的rewards_mean就会变成0.5,是比初始的1要小的,那么下一次可能就不会再选它来进行抽奖了。
至于其中的随机选择的部分, 我目前实际上手来看,感觉对结果的影响并不大。
import random
#贪婪算法
def choose_one():
#有小概率随机选择一根拉杆
if random.random() < 0.01:
return random.randint(0, 9)
#计算每个老虎机的奖励平均
rewards_mean = [np.mean(i) for i in rewards]
#选择期望奖励估值最大的拉杆
return np.argmax(rewards_mean)
choose_one()
选择一个机器,并且用它来抽奖。
代码第6行是一个值得学习的设计方式,用random随机一个数,当这个数小于probs中的中奖率时,它就中奖,很巧妙的完成了是否中奖的判定。
def try_and_play():
i = choose_one()
#玩机器,得到结果
reward = 0
if random.random() < probs[i]:
reward = 1
#记录玩的结果
rewards[i].append(reward)
try_and_play()
rewards
执行了5000次之后,对比一下期望得到的最好结果和实际上用贪婪算法玩出来的最好结果。
一般来说在足够的次数之下,最好结果理应是优于实际玩出的结果的,不过由于是个概率问题,也可能后者大一些。
def get_result():
#玩N次
for _ in range(5000):
try_and_play()
#期望的最好结果
target = probs.max() * 5000
#实际玩出的结果
result = sum([sum(i) for i in rewards])
return target, result
get_result()
import numpy as np
import random
#每个机器的中奖概率,0-1之间的均匀分布
probs = np.random.uniform(size=10)
#记录每个机器的返回值
rewards = [[1] for _ in range(10)] #给rewards创建十个值为1的list
print(probs)
print(rewards)
#贪婪算法
def choose_one():
#有小概率随机选择一根拉杆
if random.random() < 0.05: #这一行代码是想给选择算法加上一个随机性,但是这个概率这么小,真的会对结果产生很大的影响吗?
return random.randint(0, 9)
#计算每个机器的奖励平均。 mean函数是求所有元素的平均值。这一行代码是把每一组数据中的平均值给求出来然后赋给一个列表。
rewards_mean = [np.mean(i) for i in rewards]
#print(rewards_mean) 如果不理解为什么要求平均奖励了,就把这个给执行一下
#选择期望奖励估值最大的拉杆
return np.argmax(rewards_mean)
#由于有些时候得到的reward是0,所以这个mean列表的里的元素的值,其实是在起伏的,且一开始是在下降。所以概率越大的机器一般来说最后剩下的mean里面的值越大。
def try_and_play():
i = choose_one()
#玩机器,得到结果
reward = 0
#左边函数生成个浮点数,如果小于第i个设备的中奖范围,那么就认为是中奖了,否则是没有中奖,用这种方式来模仿是否中奖,也可以让probs直接代指中奖概率啦
if random.random() < probs[i]: #random.random()函数每次调用生成一个0-1之间的浮点数
reward = 1
#记录玩的结果
rewards[i].append(reward) #.append函数可以再列表末尾添加一个元素
#try_and_play()
#print(rewards)
def get_result():
#玩N次
for _ in range(5000):
try_and_play()
#期望的最好结果
target = probs.max() * 5000
#实际玩出的结果。sum是一个内置函数,用于计算可迭代对象中数字的综合。它接收一个可迭代对象作为参数,并返回其中所有元素的总和
result = sum([sum(i) for i in rewards])
print(target,result)
return target, result
get_result()
概率递减
在随机“探索”环节,在无状态的情况下,理应是前期多多探索,后期趋于稳定,就找最大的羊毛薅。所以相比于初始的贪婪算法,这里增加了一行统计玩的次数。第七行那里修改了进入if的条件,随着玩的次数增加,进入if中“探索”就越难,这可能会比动态的制定一个常数得到的效果要好一些。
#随机选择的概率递减的贪婪算法
def choose_one():
#求出现在已经玩了多少次了
played_count = sum([len(i) for i in rewards])
#随机选择的概率逐渐下降
if random.random() < 1 / played_count:
return random.randint(0, 9)
#计算每个机器的奖励平均
rewards_mean = [np.mean(i) for i in rewards]
#选择期望奖励估值最大的拉杆
return np.argmax(rewards_mean)
上置信界算法 UCB算法
该算法的目的是“多探索玩的少的机器”。具体怎么要求已经写在注释上了。随着机器被使用次数的增多,它的UCB值就会下降,而它被选中的参考要素是UCB+rewars_mean,所以UCB一旦下降,其被选中的概率就会下降。换句话说,玩的越多,被探索的欲望就会越少。但是对于那些确实中奖概率很高的机器,它本身还是会因为其过硬的概率,而被选中。
注意第4和5行中,存在一个数据类型的转换,同样,第15行中也有。虽然一些东西看着像是列表、数组,但Python还是能直接对他们进行运算,这就是Python的强大和简洁之处吧。
UCB算法进行的乘法和开根号都是有数学推理证明过程的,还是不要修改了。
#随机选择的概率递减的贪婪算法
def choose_one():
#求出每个机器各玩了多少次
played_count = [len(i) for i in rewards] #类型是列表
played_count = np.array(played_count) #类型是数组
#求出上置信界
#分子是总共玩了多少次,取根号后让他的增长速度变慢
#分母是每台机器玩的次数,乘以2让他的增长速度变快
#随着玩的次数增加,分母会很快超过分子的增长速度,导致分数越来越小
#具体到每一台机器,则是玩的次数越多,分数就越小,也就是ucb的加权越小
#所以ucb衡量了每一台机器的不确定性,不确定性越大,探索的价值越大
fenzi = played_count.sum()**0.5 #类型是浮点数
fenmu = played_count * 2 #类型是数组
ucb = fenzi / fenmu
#ucb本身取根号
#大于1的数会被缩小,小于1的数会被放大,这样保持ucb恒定在一定的数值范围内
ucb = ucb**0.5 #开根号
#计算每个机器的奖励平均
rewards_mean = [np.mean(i) for i in rewards]
rewards_mean = np.array(rewards_mean)
#ucb和期望求和
ucb += rewards_mean
return ucb.argmax()
汤普森采样 Beta分布
def choose_one():
#求出每个机器出1的次数+1
count_1 = [sum(i) + 1 for i in rewards]
#求出每个机器出0的次数+1
count_0 = [sum(1 - np.array(i)) + 1 for i in rewards]
#按照beta分布计算奖励分布,这可以认为是每一台老虎机中奖的概率
beta = np.random.beta(count_1, count_0)
return beta.argmax()
什么是Beta分布呢,见下图。它有一个重要的思想就是,先科学假设,再根据实际其概况预测。简单来说,它会根据现在的一个a,b情况得出一个分布,然后随着a,b的变化,它这个分布也会发生变化。
具体这个分布的讲解可以看这个大佬写的文章:带你理解beta分布_Jie Qiao的博客-CSDN博客