使用NumPy进行快速蒙特卡罗模拟?

3
我正在使用R和Python跟随《贝叶斯数据分析实例》中的练习。
我想找到一种使用恒定空间的快速蒙特卡罗模拟方法。
下面的问题很简单,但可以作为不同方法的良好测试:
ex 4.3
确定从洗牌后的pinochle牌组中抽取一个10的确切概率。 (在pinochle牌组中,有48张牌。有六个值:9、10、Jack、Queen、King、Ace。每个标准的四种花色:红心,方块,梅花,黑桃中都有两个相同的值。)
(A)获取10的概率是多少?
当然,答案是1/6。
我能找到的最快解决方案(与R的速度相当)是使用np.random.choice生成大量的卡牌抽取数组,然后应用Counter。我不喜欢不必要地创建数组的想法,因此我尝试使用字典和for循环,一次抽取一张牌并增加该类型牌的计数。令我惊讶的是,它慢得多!
下面是我测试的三种方法的完整代码。有没有一种方法可以像method1()一样高效,但使用恒定空间Python代码:Google Colab链接
deck = [c for c in ['9','10','Jack','Queen','King','Ace'] for _ in range(8)]
num_draws = 1000000

def method1():
  draws = np.random.choice(deck, size=num_draws, replace=True)
  df = pd.DataFrame([Counter(draws)])/num_draws
  print(df)
  
def method2():
  card_counts = defaultdict(int)
  for _ in range(num_draws):
    card_counts[np.random.choice(deck, replace=True)] += 1
  df = pd.DataFrame([card_counts])/num_draws
  print(df)
  
def method3():
  card_counts = defaultdict(int)
  for _ in range(num_draws):
    card_counts[deck[random.randint(0, len(deck)-1)]] += 1
  df = pd.DataFrame([card_counts])/num_draws
  print(df)

Python timeit() 结果:

方法1: 1.2997

方法2: 23.0626

方法3: 5.5859

R 代码:

card = sample(deck, numDraws, replace=TRUE)
print(as.data.frame(table(card)/numDraws))

样本“deck”和“num_draws”用于计时的目的是什么? - Divakar
更新问题并提供信息:num_draws = 1000000``` - Jules Kuehn
你可以在你的牌组中放置0和1,然后计算所抽取的牌的值之和。这样算可以吗? - Davis Herring
@DavisHerring,你能进一步解释一下吗? - Jules Kuehn
1
numpy 中创建大数组是一项功能,而不是错误。 - hpaulj
1
@JulesKuehn:不要制作一个字符串列表,而是制作一个数字列表,指示您刚刚抽取的卡片中有多少张是10。 (当然,其中大部分数字为0,其余几个数字为1。)然后求和并除以抽牌次数。 - Davis Herring
2个回答

3

这里有一个使用 np.unique+np.bincount 的例子:

def unique():    
    unq,ids = np.unique(deck, return_inverse=True)
    all_ids = np.random.choice(ids, size=num_draws, replace=True)
    ar = np.bincount(all_ids)/num_draws
    return pd.DataFrame(ar[None], columns=unq)

NumPy如何帮助解决问题?

这里有两个主要的改进措施:

  1. 我们将字符串数据转换为数字数据。NumPy 能够很好地处理此类数据,我们使用 np.unique 来实现。

  2. 我们使用 np.bincount 替换计数步骤。同样,它能够很好地处理数字数据,并且我们从这个方法开始时的数字转换中已经得到了数字数据。

  3. 总的来说,NumPy 能够很好地处理大型数据,这也是这里的情况。


使用给定的示例数据集进行计时,与最快的 method1 进行比较 -

In [177]: %timeit method1()
328 ms ± 16.3 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

In [178]: %timeit unique()
12.4 ms ± 265 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

通过将字符串/计数器从等式中剔除,速度得到了惊人的提升!但是这仍然使用O(num_draws)的空间。有没有什么方法可以避免这种情况? - Jules Kuehn
我的问题是要寻找一个O(1)空间复杂度的解决方案。我认为你的解决方案可以被改进来限制空间,但这会牺牲掉需要运行前三行代码多次的代价。 - Jules Kuehn
@JulesKuehn 当您谈到“空间”时,您是在谈论内存空间还是其他内容?如果您担心内存空间,创建all_ids可能会成为一个问题。如果您担心运行时/性能效率,则必须了解NumPy如何获得性能。它是通过数组数据的时间和空间局部性来实现的。 all_ids是上下文中的数组。因此,内存和性能之间总是存在权衡。但是,我不太清楚您对“空间”的概念。 - Divakar
1
@JulesKuehn,你不关心性能吗?我以为这是这篇文章的主要关注点。如果你同时关心内存,请坚持使用基于循环的那些。如果你关心性能,请选择这个。 - Divakar
您可以通过将绘制分成固定大小的块来进行权衡。这样,您应该使用恒定的内存:计数器和绘制缓冲区。对于外部块循环(在Python中),您仍将稍微减慢速度,但对于内部绘制循环,仍将有一些加速。 - Ronan Paixão
显示剩余2条评论

0
Numpy通过其数值引擎中的C代码实现效率。Python很方便,但比C慢几个数量级。 在Numpy和其他高性能Python库中,Python代码主要由胶水代码组成,准备被调度的任务。由于存在开销,一次性绘制许多样本会更快。 记住,为Numpy提供100万个元素的缓冲区仍然是恒定空间。然后,您可以通过循环1亿次进行采样。 这种额外的内存分配通常不是问题。如果您必须避免以任何代价使用内存,同时仍然从Numpy获得性能优势,可以尝试使用Numba或Cython来加速它。
from numba import jit
@jit(nopython=True)
def method4():
    card_counts = np.zeros(6)
    for _ in range(num_draws):
        card_counts[np.random.randint(0, 6)] += 1
    return card_counts/num_draws

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接