Alias Method: 非均匀随机抽样算法

在游戏中经常遇到按一定的掉落概率来随机掉落指定物品的情况,例如按照:钻石10%,金币20%,银币50%,饰品10%,装备10% 来计算掉落物品的类型。
问题的抽象
给定一个集合,要从中以给定的概率分布随机抽取其中的元素。
给定一个离散型随机变量的概率分布 P ( X = i ) = p i , i 1 , . . . N ;    i = 1 N p i = 1. P(X=i)=p_i,\quad i\in 1,...N; \,\,\sum_{i=1}^{N} p_i = 1. ,从离散集合中进行采样,要求采样结果尽可能服从概率分布P.

平凡解法(Trivial Solution)

比较容易想到的方法是

  • 根据要求的概率分布计算累积分布,映射到[0,1]的线段上;
  • 然后通过一个[0,1]之间均匀分布的随机生成器,生成一个[0,1]之间的随机数;
  • 判断这个数落在[0,1]线段上的哪个分段,就输出相应的元素。

例如【钻石,金币,银币,饰品,装备】的概率[0.1, 0.2, 0.5, 0.1, 0.1],其累积分布的列表[0.1, 0.3, 0.8, 0.9, 1];
然后生成一个随机数比如0.618,那么累加概率列表中第一个大于0.618的数为0.8,对应的元素类别是银币。又生成一个随机数0.816,则找到0.9对应的元素是饰品。
复杂度主要来自判断随机数落在哪个分段,线性搜索时是O(n),用BST(二分查找)存储时,复杂度降为O(logN).

一个朴素的想法

将上面的线段扩展成二维的矩形方块。

原论文中的例子:
一个随机事件包含四种情况A,B,C,D,每种情况发生的概率分别为: 1/2,1/3,1/12,1/12,问怎么用产生符合这个概率的采样方法。

第一步:将四个事件排列成1-4,扔两次骰子,第一次扔1~4之间的整数,决定落在哪一列;
在这里插入图片描述
第二步:按照四种概率中最大的那个概率进行归一化。第一次扔骰子决定好了哪一列,第二次扔骰子,得到[0~1]之间的随机数。如图,如果落在了第一列上,这次不论随机数是多少,都采样事件A。
如果落在第二列上,第二次的随机数如果小于2/3则采样成功,取事件B;如果超过2/3则失败,重新采样。依次类推。
在这里插入图片描述

这样算法复杂度最好为O(1)最坏有可能无穷次,平均意义上需要采样O(N)次。怎样优化呢?

Alias Method

虽然非均匀分布的平凡解法最好能做到对数时间复杂度,但对于非均匀的伯努利实验(随机变量可能取值只有2种),我们仍能在常数时间内解决问题。

若随机变量的取值可能有 k 个,那必然有部分取值的概率小于 1 k \frac{1}{k} ,同时有另一些不小于 1 k \frac{1}{k}
我们可以通过拆借的方法,把概率大于 1 k \frac{1}{k} 的部分借给概率小于 1 k \frac{1}{k} 的部分,使得所有取值上的概率都恰好等于 1 k \frac{1}{k} ;从而使非均匀采样问题变成均匀采样问题。

Alias Method 充分利用概率分布加和为1的性质,通过空间换时间的方法,在常数时间内,完成非均匀到均匀采样的映射。

还是如上面的那样的思路,但是如果我们不按照其中最大的值去归一化,而是按照其均值归一化
按照 1 N \dfrac{1}{N} 归一化,即是所有概率乘以N。

上面的例子,四种事件均值是1/4,[1/2,1/3,1/12,1/12]分别乘以4得到下图:

在这里插入图片描述

其总面积为N,然后可以将其分成一个1*N的长方形。将前两个多出来的部分补到后面两个缺失的部分中。
先将1(第一列)中的部分补充到4中:
在这里插入图片描述
这时如果,将1,2中多出来的部分,补充到3中,就麻烦了,因为又陷入到如果从中采样超过2个以上的事件这个问题中,所以
Alias Method一定要保证:每列中最多只放两个事件

所以此时需要将1中的补到3中去,不管1中是不是少于1,先把3填满;再将2中的补到1中:

在这里插入图片描述

Alias Method具体算法如下:

  1. 按照上面说的方法,将整个概率分布拉平成为一个1*N的长方形即为Alias Table,
    构建上面那张图之后,储存两个数组accept和alias,accept[i]里面存着第i列对应的事件i矩形占的面积百分比【也即其概率】,上图的话数组就为 P r a b [ 2 3 , 1 , 1 3 , 1 3 ] Prab[\frac{2}{3},1,\frac{1}{3},\frac{1}{3}] ,另一个数组alias[i]里面储存着第i列不是事件i的另外一个事件的标号,像上图就是Alias[2 NULL 1 1]
  2. 产生两个随机数,第一个产生1~N 之间的整数i,决定落在哪一列。扔第二次骰子,0~1之间的任意数,判断其与Prab[i]大小,如果小于Prab[i],则采样i,如果大于Prab[i],则采样Alias[i]。

构建方法:
1.找出其中面积小于等于1的列,如i列,这些列说明其一定要被别的事件矩形填上,所以在Prab[i]中填上其面积
2.然后从面积大于1的列中,选出一个,比如j列,用它将第i列填满,然后Alias[i] = j,第j列面积减去填充用掉的面积。
以上两个步骤一直循环,直到所有列的面积都为1了为止。

如果按照上面的方法去构建Alias Table,算法复杂度是 O ( n 2 ) O(n^2) 的,因为最多需要跑N轮,而每跑一轮的最大都需要遍历N次。一个更好的办法是用两个队列A,B去储存面积大于1的节点标号,和小于1的节点标号,每次从两个队列中各取一个,将大的补充到小的之中,小的出队列,再看大的减去补给之后,如果大于1,继续放入A中,如果等于1,则也出去,如果小于1则放入B中。
这样算法复杂度为 O ( n ) \large O(n)

原论文中的算法:
Algorithm: Vose’s Alias Method

  • Initialization:
  1. Create arrays Alias and Prob, each of size n.
  2. Create two worklists, Small and Large.
  3. Multiply each probability by n.
  4. For each scaled probability p i < 1 p_i <1 :
    1. If p i < 1 p_i <1 , add i i to Small.
    2. Otherwise (pi≥1), add i to Large.
  5. While Small and Large are not empty: (Large might be emptied first)
    1. Remove the first element from Small; call it l l .
    2. Remove the first element from Large; call it g g .
    3. Set P r o b [ l ] = p l Prob[l]=p_l .
    4. Set A l i a s [ l ] = g Alias[l]=g .
    5. Set p g : = ( p g + p l ) 1 p_g:=(p_g+p_l)−1 . (This is a more numerically stable option.)
    6. If p g < 1 p_g<1 , add g to Small.
    7. Otherwise ( p g 1 p_g\geq 1 ), add g to Large.
  6. While Large is not empty:
    1. Remove the first element from Large; call it g.
    2. Set P r o b [ g ] = 1 Prob[g]=1 .
  7. While Small is not empty: This is only possible due to numerical instability.
    1. Remove the first element from Small; call it l l .
    2. Set P r o b [ l ] Prob[l] =1.
  • Generation:
    1. Generate a fair die roll from an n-sided die(n面骰子); call the side i i .
    2. Flip a biased coin that comes up heads with probability P r o b [ i ] Prob[i] .
    3. If the coin comes up “heads,” return i.
    4. Otherwise, return A l i a s [ i ] Alias[i] .

c++实现:
https://liam.page/2019/12/02/non-uniform-random-choice-in-constant-time-complexity/

python实现:

import numpy as np

def gen_prob_dist(N):
    p = np.random.randint(0,100,N)
    return p/np.sum(p)

def create_alias_table(area_ratio):

    l = len(area_ratio)
    accept, alias = [0] * l, [0] * l
    small, large = [], []

    for i, prob in enumerate(area_ratio):
        if prob < 1.0:
            small.append(i)
        else:
            large.append(i)

    while small and large:
        small_idx, large_idx = small.pop(), large.pop()
        accept[small_idx] = area_ratio[small_idx]
        alias[small_idx] = large_idx
        area_ratio[large_idx] = area_ratio[large_idx] - (1 - area_ratio[small_idx])
        if area_ratio[large_idx] < 1.0:
            small.append(large_idx)
        else:
            large.append(large_idx)

    while large:
        large_idx = large.pop()
        accept[large_idx] = 1
    while small:
        small_idx = small.pop()
        accept[small_idx] = 1

    return accept,alias

def alias_sample(accept, alias):
    N = len(accept)
    i = int(np.random.random()*N)
    r = np.random.random()
    if r < accept[i]:
        return i
    else:
        return alias[i]

def simulate(N=100,k=10000,):

    truth = gen_prob_dist(N)

    area_ratio = truth*N
    accept,alias = create_alias_table(area_ratio)

    ans = np.zeros(N)
    for _ in range(k):
        i = alias_sample(accept,alias)
        ans[i] += 1
    return ans/np.sum(ans),truth

if __name__ == "__main__":
    alias_result,truth = simulate()



Ref:

Darts, Dice, and Coins: Sampling from a Discrete Distribution

时间复杂度为O(1)的离散采样算法

浅梦-Alias Method

猜你喜欢

转载自blog.csdn.net/rover2002/article/details/106760664
今日推荐