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

问题介绍

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

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

二分查找法

对于这个问题,最简单的思路是将不同权重的样本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算法可以大大提高抽样的效率;在有放回抽取的情况中 ,如果样本量大、抽样次数不多,可以选择二分查找法;如果样本量中等,但抽样次数较多,则可以使用别名采样法。

相关推荐
didiplus1 分钟前
【趣学Python算法100例】百钱百鸡
python·算法·百钱百鸡
Monodye9 分钟前
【Java】网络编程:TCP_IP协议详解(IP协议数据报文及如何解决IPv4不够的状况)
java·网络·数据结构·算法·系统架构
pzx_00114 分钟前
【内积】内积计算公式及物理意义
数据结构·python·opencv·算法·线性回归
元气代码鼠16 分钟前
C语言程序设计(进阶)
c语言·开发语言·算法
十雾九晴丶1 小时前
攻防世界--->gametime
算法
Aurora_th2 小时前
树与图的深度优先遍历(dfs的图论中的应用)
c++·算法·深度优先·图论·dfs·树的直径
马剑威(威哥爱编程)3 小时前
除了递归算法,要如何优化实现文件搜索功能
java·开发语言·算法·递归算法·威哥爱编程·memoization
算法萌新——14 小时前
洛谷P2240——贪心算法
算法·贪心算法
湖北二师的咸鱼4 小时前
专题:二叉树递归遍历
算法·深度优先
重生之我要进大厂4 小时前
LeetCode 876
java·开发语言·数据结构·算法·leetcode