常见的加权随机抽样算法(附代码)

问题介绍

在游戏开发中经常会遇到一个场景,需要以不等概率抽取某些物品(即带权重的随机抽取),如:抽奖、装备洗练等;对应的问题随之而生,在大量抽取的情况下,选择什么方法效率最高?

其次,随机抽取可以分为有放回抽取和无放回抽取,不同方法在不同的抽取方式下是否有显著的差异?

二分查找法

对于这个问题,最简单的思路是将不同权重的样本ID映射到数组索引上,通过随机生产数组索引返回对应的样本ID,实现加权随机;

如:{A: 1, B: 2, C: 3, D: 4} 四个样本可以对应构造数组 [A,B,B,C,C,C,D,D,D,D] ,生成一个0~9的随机数,并返回数组中索引对应的样本。但是当样本量增加时,构造数组既费时间也费空间。

因此,我们可以将构造数组优化成区间段,如上述例子的构造数组优化为区间段后为:[(0,1), (1,3), (3,6), (6,9)] ,再通过二分查找查找随机数随机数落入哪个区间,并返回对应的样本。

示例代码:以有放回抽取为例

python 复制代码
# 二分查找法
class BinarySearchALG():

    def __init__(self, samepleSet):
        self.m_sampleSet = samepleSet
        self.m_result = []
        self.m_edge = 0
        self.m_start = 0
        self.m_end = 0
        self.m_maxNum = 0

    # 初始化二分查找数组
    def initSearchArray(self):
        edge = 0
        searchAarry = []
        for sampleId, weight in self.m_sampleSet.items():
            searchAarry.append([sampleId, range(edge, edge+weight)])
            edge += weight
        return searchAarry

    def initWeightedRandom(self):
        self.m_searchAarry = self.initSearchArray()
        self.m_edge = len(self.m_searchAarry)
        self.m_end = self.m_edge-1
        self.m_maxNum = self.m_searchAarry[-1][1][-1]

    def weightedRandom(self, count):
        self.initWeightedRandom()
        for _ in range(count):
            self.doWeightedRandom(self.m_start, self.m_end, self.m_maxNum)
        return self.m_result

    def doWeightedRandom(self, start, end, maxNum):
        randomNum = random.randint(0, maxNum)
        res = self.bindarySearch(randomNum, start, end)
        self.m_result.append(res)
  
    def bindarySearch(self, target, start, end):
        length = end - start
        mid = length // 2
        idx = mid + start
        weightRange = self.m_searchAarry[idx][1]
        if target in weightRange:
            sampleId = self.m_searchAarry[idx][0]
            return sampleId
        if target > weightRange[-1]:
            return self.bindarySearch(target, idx+1, end)
        return self.bindarySearch(target, start, idx)

A-RES算法

A-RES算法利用了蓄水池思想来解决大数据的加权随机抽样问题,但是这个方法更适用于一次抽取多个的不放回抽取。具体步骤简化如下:

假设当前有n个不同权重的样本,需要不放回随机抽取m个

1、遍历样本数组,生成n个0~1的随机数

2、计算特征值 <math xmlns="http://www.w3.org/1998/Math/MathML"> E i = R 1 W i E_i = R^\frac{1}{W_i} </math>Ei=RWi1;其中R为每次生成的随机数, <math xmlns="http://www.w3.org/1998/Math/MathML"> W i W_i </math>Wi为样本权重, <math xmlns="http://www.w3.org/1998/Math/MathML"> E i E_i </math>Ei为计算的特征值

3、对特征值进行排序,前m个即本次随机抽样的结果

示例代码:以无放回抽取为例

python 复制代码
# A-Res算法
class A_RES_ALG():

    def __init__(self, samepleSet):
        self.m_sampleSet = samepleSet
        self.m_result = []

    def calcEigenValue(self):
        pool = {}
        for sampleId, weight in self.m_sampleSet.items():
            eigen = pow(random.random(), 1/weight)
            pool[sampleId] = eigen
        return pool

    def weightedRandom(self, count):
        pool = self.calcEigenValue()
        pool = sorted(pool.items(), key=lambda x:x[1], reverse=True)
        self.m_result = [val[0] for val in pool[0: count]]
        return self.m_result

别名采样算法

别名采样算法将一次抽取分成了两个概率分布,分别为样本为n的随机分布和二项分布。具体步骤简化为以下几点:

假设当前有 {A: 1, B: 2, C: 3, D: 4} 四个不同权重的样本

1、四个样本被抽到的概率为 {A: 0.1, B: 0.2, C: 0.3, D: 0.4}

2、分别将每个样本的概率乘以样本总数,得每个样本的图形面积 {A: 0.4, B: 0.8, C: 1.2, D: 1.6}

3、将面积大于1的部分补充到小于1的部分,并保证每个单位不超过两个样本,得

css 复制代码
{
    {A:0.4, C:0.6},
    {B:0.8, D:0.2},
    {C:0.6, D:0.4},
    {D:1}
}

4、随机抽取一个1~4的整数,并返回对应单位,如:抽到 3,返回 {C: 0.6, D: 0.4}

5、随机生成一个0~1之间的小数,并返回对应样本,如:抽到 0.35,返回样本C

  # 计算一下抽到C的总概率:P = 0.25 * 0.6 + 0.25 * 0.6 = 0.3,与1中计算的概率相同

示例代码:以有放回抽取为例

python 复制代码
# 别名采样算法
class AliasSamplingALG():
  
    def __init__(self, sampleSet):
        self.m_sampleSet = sampleSet
        self.m_aliasTable = []
        self.m_littleQueue = []  # 存放面积小于1的图形的队列
        self.m_largeQueue = []  # 存放面积大于等于1的图形的队列
        self.m_result = []
        self.initQueue()

    def initQueue(self):
        demon = sum(self.m_sampleSet.values())
        total = len(self.m_sampleSet)
        for sampleId, weight in self.m_sampleSet.items():
            area = weight / demon * total
            if area > 1:
                self.m_largeQueue.append([sampleId, area])
            elif area < 1:
                self.m_littleQueue.append([sampleId, area])
            else:
                self.m_aliasTable.append([[sampleId, area]])

    def constructAliastable(self):
        while self.m_littleQueue and self.m_largeQueue:
            largeInfo = self.m_largeQueue.pop(0)
            littleInfo = self.m_littleQueue.pop(0)
            restArea = largeInfo[1] - (1 - littleInfo[1])
            self.m_aliasTable.append([[largeInfo[0], 1-littleInfo[1]], littleInfo])
            if restArea > 1:
                self.m_largeQueue.append([largeInfo[0], restArea])
            elif restArea < 1:
                self.m_littleQueue.append([largeInfo[0], restArea])
            else:
                self.m_aliasTable.append([[largeInfo[0], restArea]])

    def weightedRandom(self, count):
        for _ in range(count):
            self.constructAliastable()
            total = len(self.m_aliasTable)
            idx = random.randint(0, total)
            area = self.m_aliasTable[idx]
            rand = random.random()
            if rand < area[0][1]:
                self.m_result.append(area[0][0])
                continue
            self.m_result.append(area[1][0])
        return self.m_result

性能分析

1、 有放回抽取

使用10w个样本,初始权重为500,每2w个样本权重加500,根据不同权重将样本分为五类, 一共抽样1000个

采样结果

二分查找法时间消耗

A-RES算法时间消耗

别名采样算法时间消耗

可以清楚地看到,在有放回抽取的情况下,二分查找法用时0.17s,别名采样算法用时0.7s,A-RES算法用时高达193.8s;二分查找法和别名采样算法的时间消耗主要集中在构造抽样条件数组 上,分别占用了总时间的76%和98.9%;而A-RES算法时间主要消耗在两部分:计算特征值和对本轮特征值进行排序 ,分别占用58.5%和41.5%,由于A-RES算法每次抽取都要计算全部样本的特征值,因此耗时大大增加了。

2、无放回抽取:

使用10w个样本,初始权重为500,每2w个样本权重加500,根据不同权重将样本分为五类, 一共抽样500个

采样结果

二分查找法时间消耗

A-RES算法时间消耗

别名采样算法时间消耗

同样可以看到,在无放回抽取的情况下,由于二分查找法和别名采样算法每次抽取结束后都要剔除掉已抽取的样本重新构造条件数组 ,用时分别高达65.2s和720.5s;而A-RES算法只需计算一轮特征值 ,在不放回抽取的情况中具有非常大的优势,用时仅0.17s。

总结

在无放回抽取中 ,选择A-RES算法可以大大提高抽样的效率;在有放回抽取的情况中 ,如果样本量大、抽样次数不多,可以选择二分查找法;如果样本量中等,但抽样次数较多,则可以使用别名采样法。

相关推荐
小码农<^_^>17 分钟前
优选算法精品课--滑动窗口算法(一)
算法
羊小猪~~20 分钟前
神经网络基础--什么是正向传播??什么是方向传播??
人工智能·pytorch·python·深度学习·神经网络·算法·机器学习
软工菜鸡1 小时前
预训练语言模型BERT——PaddleNLP中的预训练模型
大数据·人工智能·深度学习·算法·语言模型·自然语言处理·bert
南宫生1 小时前
贪心算法习题其三【力扣】【算法学习day.20】
java·数据结构·学习·算法·leetcode·贪心算法
AI视觉网奇1 小时前
sklearn 安装使用笔记
人工智能·算法·sklearn
JingHongB2 小时前
代码随想录算法训练营Day55 | 图论理论基础、深度优先搜索理论基础、卡玛网 98.所有可达路径、797. 所有可能的路径、广度优先搜索理论基础
算法·深度优先·图论
weixin_432702262 小时前
代码随想录算法训练营第五十五天|图论理论基础
数据结构·python·算法·深度优先·图论
小冉在学习2 小时前
day52 图论章节刷题Part04(110.字符串接龙、105.有向图的完全可达性、106.岛屿的周长 )
算法·深度优先·图论
Repeat7152 小时前
图论基础--孤岛系列
算法·深度优先·广度优先·图论基础
小冉在学习2 小时前
day53 图论章节刷题Part05(并查集理论基础、寻找存在的路径)
java·算法·图论