【问题标题】:How to verify that a shuffling algorithm is uniform?如何验证洗牌算法是统一的?
【发布时间】:2018-05-30 16:08:10
【问题描述】:

我有一个Knuth's shuffling algorithm 的简单 Python 实现:

def knuth_shuffle(ar):
    num = len(ar)
    for i in range(num):
        index = random.randint(0, i)
        ar[i], ar[index] = ar[index], ar[i]
    return ar

如何测试(使用scipy 或任何其他包)洗牌确实是统一的?我找到了一些相关的帖子(12),但他们没有回答我的问题。了解一般情况下如何执行此类检查会很棒。

【问题讨论】:

  • 1 怎么没有回答你的问题?
  • Knuth 证明了 shuffle 的一致性。这是通过证明它与从一组 N 个项目中挑选 N 个项目而不进行替换来完成的。这与从递减范围生成一系列随机数相同,然后形成排列的阶乘基索引。
  • math.stackexchange.com/questions/2435/… 是相关的。对于小向量,可能的排列很少,您可以只计算每个不同的打乱结果。对于较大的向量,抽查一些子集(例如元素 。)

标签: python algorithm shuffle


【解决方案1】:

编辑:

作为 cmets 中的Paul Hankin,我最初的测试只检查每个元素落入每个位置的概率,而不是所有排列的可能性相同,这是一个更强的要求。下面的 sn-p 计算了每个排列的频率,这是我们应该关注的:

import math
import random

def knuth_shuffle(ar):
    num = len(ar)
    for i in range(num):
        index = random.randint(0, i)
        ar[i], ar[index] = ar[index], ar[i]
    return ar

# This function computes a unique index for a given permutation
# Adapted from https://www.jaapsch.net/puzzles/compindx.htm#perm
def permutation_index(permutation):
    n = len(permutation)
    t = 0
    for i in range(n):
      t = t * (n - i)
      for j in range(i + 1, n):
        if permutation[i] > permutation[j]:
            t += 1
    return t

N = 6  # Test list size
T = 1000  # Trials / number of permutations

random.seed(100)
n_perm = math.factorial(N)
trials = T * n_perm
ar = list(range(N))
freq = [0] * n_perm
for _ in range(trials):
    ar_shuffle = ar.copy()
    knuth_shuffle(ar_shuffle)
    freq[permutation_index(ar_shuffle)] += 1

如果 shuffle 是统一的,则生成的 freq 向量的值应根据 T * N! 试验和成功概率 1 / (N!) 的二项分布进行分布。这是上一个示例的分布估计图(使用Seaborn 完成),其中频率值应该在 1000 左右:

我认为这看起来不错,但同样,对于定量结果,您需要更深入的统计分析,例如 Pearson's chi-squared test,正如 David Eisenstat 所建议的那样。


原始答案:

我将在这里提出一些基本的想法,但我没有最强大的统计背景,所以有人可能想补充或纠正任何错误的地方。

您可以制作一个包含每个值的频率矩阵,以进行多次试验:

def knuth_shuffle(ar):
    num = len(ar)
    for i in range(num):
        index = random.randint(0, i)
        ar[i], ar[index] = ar[index], ar[i]
    return ar

N = 100  # Test list size
T = 10000  # Number of trials
ar = list(range(N))
freq = [[0] * N for _ in range(N)]

for _ in range(T):
    ar_shuffle = ar.copy()
    kunth_shuffle(ar_shuffle)
    for i, j in enumerate(ar_shuffle):
        freq[i][j] += 1

一旦您可以做到这一点,您可以采取多种方法。一个简单的想法是,如果洗牌是统一的,freq / T 应该趋向于1 / N,因为T 趋向于无穷大。因此,您可以只使用“非常大”的值 T 并看到这些值“足够接近”。或者检查freq / T - 1 / N的标准差是否“足够小”。

这些“足够接近”和“足够小”虽然不是很可靠的概念。扎根分析需要更多的统计工具。我认为您需要test the hipothesis,每个频率值都是从binomial distribution 中采样的,T 试验1 / N 成功概率。正如我所说,没有完整解释的背景,这可能不是它的地方,但如果你真的需要一个彻底的分析,你可以阅读这个主题。

【讨论】:

  • 如果您将 shuffle 实现为 r=random.randint(len(ar)); ar = ar[r:] + ar[:r](即,选择输入的随机循环排列),您将通过此测试。这可能是一个人为的例子,但结果中存在大量统计错误,仅通过检查每个项目进入哪个槽的频率是无法捕捉到的。
  • @PaulHankin 我明白了,你是对的,谢谢。我更改了代码以实际测试每种可能排列的频率。
  • @DavidEisenstat 谢谢,是的,我想这就是我在想的那种事情,但我太生疏了,也记错了。
【解决方案2】:

您可以通过将所有可能的随机数序列注入knuth_shuffle 来准确检查这一点,然后验证您是否只获得了每个排列一次。

这段代码就是这样做的:

import collections
import itertools
import random

def knuth_shuffle(ar, R=random.randint):
    num = len(ar)
    for i in range(num):
        index = R(0, i)
        ar[i], ar[index] = ar[index], ar[i]
    return ar

def fact(i):
    r = 1
    while i > 1:
        r *= i
        i -= 1
    return r

def all_random_seqs(N):
    for r in range(fact(N)):
        seq = []
        for i in range(N):
            seq.append(r % (i+1))
            r //= (i+1)
        it = iter(seq)
        yield lambda x, y: next(it)

for N in range(1, 6):
    print N
    results = collections.Counter()
    for R in all_random_seqs(N):
        a = list('ABCDEFG'[:N])
        knuth_shuffle(a, R)
        results[''.join(a)] += 1
    print 'checking...'
    if len(results) != fact(N):
        print 'N=%d. Not enough results. %s' % (N, results)
    if any(c > 1 for c in results.itervalues()):
        print 'N=%d. Not all permutations unique. %s' % (N, results)
    if any(sorted(c) != list('ABCDEFG'[:N]) for c in results.iterkeys()):
        print 'N=%d. Some permutations are illegal. %s' % (N, results)

此代码检查大小为 1、2、3、4、5 的输入列表的精确正确性。您可能会在 N 之前走得更远!变得太大了。

您还需要使用 random.randint 对代码版本执行完整性检查(例如,生成 500 次“ABCD”随机播放,并确保您至少获得每个排列一次)。

【讨论】:

    【解决方案3】:

    如果你从给定的固定顺序中随机打乱相同的项目,那么在打乱的项目中一个固定位置的每个项目的计数应该趋向于相同的值。

    下面我将列表 0..9 洗牌几次并打印输出:

    from random import shuffle  # Uses Fischer-Yates
    
    tries = 1_000_000
    intcount = 10
    first_position_counts = {n:0 for n in ints}
    ints = range(intcount)
    for _ in range(tries):
        lst = list(ints)   # [0, 1, ...9] In that order
        shuffle(lst)
        first_position_counts[lst[0]] += 1
    
    print(f'{tries} shuffles of the ints 0..{intcount-1} should have each int \n',
          'appear in the first position {tries/intcount} times.')
    for item in first_position_counts.items():
        print(' %i: %5i' % item)
    

    运行一次,你可能会得到类似的东西:

     0: 99947
     1:100522
     2:99828
     3:100123
     4:99582
     5:99635
     6:99991
     7:100108
     8:100172
     9:100092

    再说一遍:

     0: 100049
     1:99918
     2:100053
     3:100285
     4:100293
     5:100034
     6: 99861
     7: 99584
     8:100055
     9: 99868

    现在,如果您有数千个项目要洗牌,那么它们应该以n! 排列之一结束,但是n! 很快就会变大;如果它是“可比的”,肯定比随机数生成器的可能范围更大,那么它就会崩溃。

    【讨论】:

    • 单个位置是一致的这一事实并不能说明排列的一致性。假设 shuffle only 产生了旋转,但它产生了均匀分布的旋转。那么每个位置也会均匀分布,但是shuffle算法明显不够用。
    • 是什么让您认为 shuffle 会产生旋转?这是极不可能的——你自己去看看算法。如果您仍然认为轮换是最有可能的结果,那么请设计一个轮换测试。我认为没有必要。 (另请参见上面 Dan D 的证明建议)。
    • 正确实现,当然,shuffle算法完美运行(取决于随机数生成器的质量)。但是,如果我们只是相信这一点,那么测试就毫无意义了。旋转示例是极端的,但它表明要测试随机洗牌的均匀性不仅仅是每个位置的均匀性。您还需要测试每个排列中的元素是否相互不相关。 (而且一个糟糕的随机数生成器可能会导致这种相关性。)
    • “你还需要测试一下……”:通常有人与自己有不同的需求。对隐藏模式的另一项测试可能是为起始列表中的每个项目分配颜色并为连续生成的排列绘制颜色。我并不是说不能设计其他测试,只是您可能想“先采摘低垂的果实”,并且必须在某个阶段停下来,因为知道有人可以设计另一种测试......
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-07-08
    • 1970-01-01
    相关资源
    最近更新 更多