深度学习 - 32.GraphEmbedding Alias 采样图文详解

63 篇文章 13 订阅
6 篇文章 3 订阅

一.引言

Alias Sample 即别名采样应用于离散采样,假设有一个随机事件包含 N 中情况,每种情况发生的概率为 P1,P2,...Pn 且其和为1,我们希望采样得到的事件能够符合随机事件的原始概率分布,这时候就需要 Alias 采样, Alias 是一个通过空间复杂度换取时间复杂度的算法,构造采样表的复杂度为 O(n),而采样的复杂度为 O(1)。在Graph Embedding 中,本质上节点用户对不同邻居节点的权重大多是不一致的,但之前提到的 DeepWalk 对用户的权重没有考虑或者默认节点之间权重均为1,所以 Alias 采样更适合于带权边的采样,Node2vec 就是采用该方法进行采样。

二.传统采样

1.建表

首先按照 1-N 的顺序将元素概率累加构造 N 个线段: 

CSDN-BITDDD

  

2.随机生成数字

随机生成 0-1 的随机数 p

3.采样

看对应的概率 p 落在哪个区间中,落入 P1 + P2 + ... + PK 到 P1 + P2 + ... + PK + P(K+1) 区间内,则取 K+1 号事件作为该次采样结果

4.复杂度与概率分布

A.复杂度

构造第一个查询表需要 O(n) 的复杂度,因为需要遍历全部的概率,随机生成数字 O(1),采样需要判断 p 落在哪个区间内,常规查询 O(n),二分查找 O(logN) 

B.概率分布

线段是通过 PDF 生成的 CDF,每一次随机采样落入线段内的事件 K 概率为 P(K) / 1,所以采样得到的结果理论上是服从原始概率分布的

三.Alias 采样分析

1.根据PDF构建表

Alias 采样与传统采样准备工作类似,先通过原始 PDF 构造原始表,各个矩形面积和为1

  

 2.将原始PDF表乘n

原始面积乘n后变为 1xn=n,这里由于篇幅设计,图像只显示扩大关系,并未严格按照扩大比例展示。其次准备一个 small 和 big 数组,用于存放 *n 后概率大于1和小于1的时间索引

3.多退少补进行填充补齐

已知原始 PDF 分布表中的概率 p 均为小于1的数且和为1,乘n后面积扩大n倍,正常情况下乘n后会得到大于1的面积和小于1的面积

A. 将大于1的面积补给面积小于1的部分

B. 对应大于1的面积减去刚才补走的面积,重新划分其为大于1还是小于1

C. 如此往复,最多经过 n-1 次即可将乘n的 PDF 表格填充为 1 x N,且每个面积和均为1的表格

不正常情况是P1-Pn的概率值相等,此时退化为均匀分布,就不用 Alias 了,随机游走就可以

经过上述操作后会得到两个表,一个是 Prob (有的也称为 Accept ),另一个是 Alias

-> Prop [P1, P2, P3, ..., Pn]

注意这里 P1 和前面提到的 p1 做了大小写区分,这里 P 代表事件 I 在第 I 列矩形中占的面积大小 

-> Alias [event_x, event_y, ... ,event_z ]

Alias 表中存储第 I 列矩阵中不是第 I 个事件的编号,如果 I 事件的 Prob 对应值为1,则 Alias 中存放 Null 或者事件 I 的编号

4.采样

A. 首先随机生成一个 1-N 的整数,决定在 1 x n 矩形中选择第几个矩阵,假定选择了第二行

B. 其次随机生成一个 0-1 的整数

假设生成值为 p-random,如果 p-random 小于第二列 event-2 的概率 P2,则采样 P2 对应的 event-2,反之则采样 Alias 表中对应的另一个事件的索引,上例中另一个事件为 P1 对应的 event-1 

5.复杂度与概率分布

A.复杂度

构建 Prob 和 Alias 表的复杂度为 O(n),而后续采样由于不需要根据随机概率在区分度为 N 的线段中寻找,只需要2选1,所以复杂度降低至 O(1),这也是其优于传统采样的原因

B.概率分布

事件 I 对应的概率 P1 乘 n 后在整体面积 n 中所占比例为 p1 * n / n 仍为 p1,所以 Alias 采样是符合原始概率分布要求的

  

四.Alias 采样代码图示详解

1.根据PDF构建表

这里通过 random.random() 随机构造节点之间的转移概率,即带权边的权重,由于关注关系图中,一个节点对其邻居节点的权重和不一定为1,所以 Alias 建表时需要对权重进行归一化,这里采用分量除以总量和的方式进行归一化。

    prob_num = 4
    transform_prob = []
    for i in range(prob_num):
        transform_prob.append(random.random())
    print("归一化前:", transform_prob)
    # 当前node的邻居的nbr的转移概率之和
    norm_const = sum(transform_prob)
    # 当前node的邻居的nbr的归一化转移概率
    normalized_prob = [float(u_prob) / norm_const for u_prob in transform_prob]
    print("归一化后:", normalized_prob)

 为了后续流程展示的清晰,这里给定一组 normalized_prob 当做测试概率:

    normalized_prob = [0.5, 0.3, 0.1, 0.1]

2. 将原始PDF表乘n

本例中 n=4,为了美观这里就不把上图扩大4倍了,并初始化 big,small 存放大于1与小于1的事件的索引,其对应的判断标准为上述的 normalized_prob

    length = len(transform_prob)
    # 建表复杂度o(N)
    accept, alias = [0] * length, [0] * length
    # small,big 存放比1小和比1大的索引
    small, big = [], []
    # 归一化转移概率 * 转移概率数
    transform_N = np.array(normalized_prob) * length
    print("归一化 * N 后:", transform_N)
    # 根据概率放入small large
    for i, prob in enumerate(transform_N):
        if prob < 1.0:
            small.append(i)
        else:
            big.append(i)

big: [0, 1]
small: [2, 3]

3.多退少补进行填充补齐

先分析一下执行方案的可行性,只要有一个大于1就必然有一个小于1,因为其总和为 N,这个可以用反证法简单证明,这里不多赘述,所以共有 N 个乘n后的概率P的分布表,每次用一个大于1的填充一个小于1的,理论上最多需要 n-1 次即可全部填平。

    # 只要有大于1和小于1的就说明还需要执行填平操作,所以 while small and big
    while small and big:
        small_idx, large_idx = small.pop(), big.pop()
        # 事件I的原始概率
        accept[small_idx] = transform_N[small_idx]
        # 大于1补充的事件J的编号
        alias[small_idx] = large_idx
        # J事件填充后需要对J对应的概率相减
        transform_N[large_idx] = transform_N[large_idx] - (1 - transform_N[small_idx])
        # 重新归并到 small 或 big
        if transform_N[large_idx] < 1.:
            small.append(large_idx)
        else:
            big.append(large_idx)

可以中途添加日志打印 accept,alias 和 transform_N 的具体变化情况:

start---------------------------------
epoch: 1
small  3 big  1
accept: [0, 0, 0, 0.4]
alias  [0, 0, 0, 1]
transpose: [2.  0.6 0.4 0.4]
epoch: 2
small  1 big  0
accept: [0, 0.6, 0, 0.4]
alias  [0, 0, 0, 1]
transpose: [1.6 0.6 0.4 0.4]
epoch: 3
small  2 big  0
accept: [0, 0.6, 0.4, 0.4]
alias  [0, 0, 0, 1]
transpose: [1.  0.6 0.4 0.4]
end--------------------------------------

原始概率:

big: [0, 1]
small: [2, 3]
accept: [0, 0, 0, 0]
alias  [0, 0, 0, 0]
transpose: [2.  1.2 0.4 0.4]

  

A. epoch1

big=1, small=3,所以用事件1补事件3,将3的面积补充到①

accept 加入事件3的概率 P4=0.4  [0, 0, 0, 0] -> [0, 0, 0, 0.4]

alias 加入给他填充的事件1的概率 [0, 0, 0, 0] -> [0, 0, 0, 1]

结束后将事件1对应的索引 1 加入到 small 中,转移概率 [2.  0.6 0.4 0.4]

B. epoch2 

 big=0,small=1, 所以用事件0补事件1,将事件0的面积补充到事件1

accept 加入事件1的概率 P1=0.6  [0, 0, 0, 0.4] -> [0, 0.6, 0, 0.4]

alias 加入给他填充的事件0的概率 [0, 0, 0, 0] -> [0, 0, 0, 1]

结束后将事件0对应的索引 0 加入到 big 中 ,转移概率 [1.6 0.6 0.4 0.4]

C. epoch3

big=0,small=2,所以用事件0补事件2,将事件0的面积补充到事件2

CSDN-BITDDD

accept 加入事件2的概率 P1=0.6  [0, 0.6, 0, 0.4] -> [0, 0.6, 0.4, 0.4]

alias 加入给他填充的事件0的概率 [0, 0, 0, 1] -> [0, 0, 0, 1]

结束后将事件0对应的索引 0 加入到 big 中,转移概率 [1.  0.6 0.4 0.4]

D.收尾

n=4,所以最多只需要 epoch=3 次即可填充完毕,但是我们看到 accept 还有一个位置的概率没有填充,所以需要收尾,将拼图的最后一块索引补充完毕。

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

 最终的事件0的对应概率为 1.0,根据逻辑放入 big 中,所以 while big 的逻辑将该索引添加到对应 Accept 数组中,由于随机概率值为0-1,所以 alias 中也不需要再存储了,因为当前索引只会发生对应索引一种事件 (P=1)

Tips: 

这里解释一下为什么会有 while small 这个逻辑,最后一个 epoch 填充完,上一个 big 对应索引的面积一定为1,按道理一定会到 big 中,为什么代码里都写到了 while small,一开始也是百思不得其解,后来打断点研究了一下,原来是因为万恶的 python 浮点数。

        if transform_N[large_idx] < 1.0:

该情况下,如果对应 prob 为1.,python会做精度转换。 [0.88304627 0.9873218 1. 0.41422909] 如果索引为2时,上述索引会得到 0.9999999999999997 ,所以顺理成章放到了 small 中。如果想解决这个问题可以采用 np.float32 对 transform_N 进行修饰,解决对应的浮点数问题。

4.采样

CSDN-BITDDD
def alias_sample(accept, alias):

    N = len(accept)
    # 生成随机索引
    i = int(np.random.random() * N)
    # 生成0-1随机数
    r = np.random.random()
    if r < accept[i]:
        return i
    else:
        return alias[i]

根据前三步得到的 porb(accept) 与 alias 执行 Alias 采样 ,这里模拟 10000 次采样看看概率分布如何:

all_sample = {}
for i in range(10000):
    sample = alias_sample(accept, alias)
    if sample not in all_sample:
        all_sample[sample] = 0
    all_sample[sample] += 1

k = list(range(4))
v = [all_sample[key] for key in k]
import matplotlib.pyplot as plt
plt.title("PDF")
plt.bar(k, v)
plt.show()

re -> {0: 5051, 1: 2989, 3: 988, 2: 972}
CSDN-BITDDD
# 原始转移概率
[0.5, 0.3, 0.1, 0.1]
# 采样10000次 PDF
[0.5072 0.2942 0.0985 0.1001]

可以看到 ALias 采样确实可以满足原始的概率分布采样要求。
 

5.全部代码

A.建表 O(n)

import numpy as np

def create_alias_table(transform_prob):
    print("归一化前:", transform_prob)
    # 当前node的邻居的nbr的转移概率之和
    norm_const = sum(transform_prob)
    # 当前node的邻居的nbr的归一化转移概率
    normalized_prob = [float(u_prob) / norm_const for u_prob in transform_prob]
    print("归一化后:", normalized_prob)
    length = len(transform_prob)
    # 建表复杂度o(N)
    accept, alias = [0] * length, [0] * length
    # small,big 存放比1小和比1大的索引
    small, big = [], []
    # 归一化转移概率 * 转移概率数
    transform_N = np.array(normalized_prob) * length
    print("归一化 * N 后:", transform_N)
    # 根据概率放入small large
    for i, prob in enumerate(transform_N):
        if prob < 1.0:
            small.append(i)
        else:
            big.append(i)

    while small and big:
        small_idx, large_idx = small.pop(), big.pop()
        accept[small_idx] = transform_N[small_idx]
        alias[small_idx] = large_idx
        transform_N[large_idx] = transform_N[large_idx] - (1 - transform_N[small_idx])
        if np.float32(transform_N[large_idx]) < 1.:
            small.append(large_idx)
        else:
            big.append(large_idx)

    while big:
        large_idx = big.pop()
        accept[large_idx] = 1
    while small:
        small_idx = small.pop()
        accept[small_idx] = 1
    return accept, alias

 B.采样 O(1)

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]

五.总结

Alias 采样大致就这么多,一开始看有点蒙圈,仔细了解后发现不仅简单且巧妙,了解了 Alias 采样,后续 GraphEmbedding 很多基于带权的关系图序列生成都会用到 Alias 采样,后续有空继续介绍~

更多推荐算法相关深度学习:深度学习导读专栏 

  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

BIT_666

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值