【问题标题】:Speed up random weighted choice without replacement in python加速随机加权选择而不用在 python 中替换
【发布时间】:2021-01-15 23:23:30
【问题描述】:

我想从约 10⁷ 整数的总体中抽样约 10⁷ 次,没有替换和权重,每次选择 10 个元素。每次采样后,我都会更改权重。我在以下脚本中计时了两种方法(python3 和 numpy)。这两种方法对我来说似乎都慢得令人痛苦,你有没有加快速度的方法?

import numpy as np
import random

@profile
def test_choices():
    population = list(range(10**7))
    weights = np.random.uniform(size=10**7)
    np_weights = np.array(weights)

    def numpy_choice():
        np_w = np_weights / sum(np_weights)
        c = np.random.choice(population, size=10, replace=False, p=np_w)

    def python_choice():
        c = []
        while len(c) < 10:
            c += random.choices(population=population, weights=weights, k=10 - len(c))
            c = list(set(c))

    for i in range(10**1):

        numpy_choice()
        python_choice()

        add_weight = np.random.uniform()
        random_element = random.randint(0, 10**7)
        weights[random_element] += add_weight
        np_weights[random_element] += add_weight


test_choices()

有计时器结果:

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
    24        10   20720062.0 2072006.2     56.6          numpy_choice()
    25        10   15593925.0 1559392.5     42.6          python_choice()

【问题讨论】:

  • 只是为了澄清,您在每个 10 个样本后替换?
  • 是的,每个样本的总体都是一样的,所以我在每个 10 个样本后替换。但是每个样本本身是没有替换的(= 一个样本的 10 个元素是唯一的)
  • 顺便说一下,weightsnp_weights 似乎是同一个数组的副本。另外,您需要population 成为列表吗?看来您可以通过使用np.arange 来提高效率。部分开销是 numpy 到列表到 numpy 的转换。
  • 还有,@profile是什么?
  • 是的 np_weights 是一个副本,但是它被 numpy_choice 修改了。 @profile 是线分析器的装饰器。 github.com/rkern/line_profiler

标签: python python-3.x numpy random


【解决方案1】:

这只是对 jdhesa 答案的评论。问题是考虑只增加一个权重的情况是否有用 -> 是的!

示例

@nb.njit(parallel=True)
def numba_choice_opt(population, weights, k,wc,b_full_wc_calc,ind,value):
    # Get cumulative weights
    if b_full_wc_calc:
        acc=0
        for i in range(weights.shape[0]):
            acc+=weights[i]
            wc[i]=acc
    #Increase only one weight (faster than recalculating the cumulative  weight)
    else:
        weights[ind]+=value
        for i in nb.prange(ind,wc.shape[0]):
            wc[i]+=value

    # Total of weights
    m = wc[-1]
    # Arrays of sample and sampled indices
    sample = np.empty(k, population.dtype)
    sample_idx = np.full(k, -1, np.int32)
    # Sampling loop
    i = 0
    while i < k:
        # Pick random weight value
        r = m * np.random.rand()
        # Get corresponding index
        idx = np.searchsorted(wc, r, side='right')
        # Check index was not selected before
        # If not using Numba you can just do `np.isin(idx, sample_idx)`
        for j in range(i):
            if sample_idx[j] == idx:
                continue
        # Save sampled value and index
        sample[i] = population[idx]
        sample_idx[i] = population[idx]
        i += 1
    return sample

示例

np.random.seed(0)
population = np.random.randint(100, size=1_000_000)
weights = np.random.rand(len(population))
k = 10
wc = np.empty_like(weights)

#Initial calculation 
%timeit numba_choice_opt(population, weights, k,wc,True,0,0)
#1.41 ms ± 9.21 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)

#Increase weight[100] by 3 and calculate
%timeit numba_choice_opt(population, weights, k,wc,False,100,3)
#213 µs ± 6.06 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

#For comparison
#Please note that it is the memory allcocation of wc which makes
#it so much slower than the initial calculation from above
%timeit numba_choice(population, weights, k)
#4.23 ms ± 64.9 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)

【讨论】:

  • 啊,这是个好主意,考虑到改变重量的“增量”,所以基本上你只需要做wc[ind:] += value
  • 将所有内容放在一起,与原始方法相比,运行时间提高了近 100 倍。感谢大家的帮助!
【解决方案2】:

你可以试试这样的。我已经使用 Numba 加速了我的功能,但在我的测试中,如果没有它,它也会更快。

import numpy as np
import numba as nb

@nb.njit
def numba_choice(population, weights, k):
    # Get cumulative weights
    wc = np.cumsum(weights)
    # Total of weights
    m = wc[-1]
    # Arrays of sample and sampled indices
    sample = np.empty(k, population.dtype)
    sample_idx = np.full(k, -1, np.int32)
    # Sampling loop
    i = 0
    while i < k:
        # Pick random weight value
        r = m * np.random.rand()
        # Get corresponding index
        idx = np.searchsorted(wc, r, side='right')
        # Check index was not selected before
        # If not using Numba you can just do `np.isin(idx, sample_idx)`
        for j in range(i):
            if sample_idx[j] == idx:
                continue
        # Save sampled value and index
        sample[i] = population[idx]
        sample_idx[i] = population[idx]
        i += 1
    return sample

这是一个快速比较

def python_choice(population, weights, k):
    c = []
    while len(c) < 10:
        c += random.choices(population=population, weights=weights, k=10 - len(c))
        c = list(set(c))
    return c

def numpy_choice(population, weights, k):
    w = weights / weights.sum()
    return np.random.choice(population, size=k, replace=False, p=w)

# Test
np.random.seed(0)
population = np.random.randint(100, size=1_000_000)
weights = np.random.rand(len(population))
k = 10
print(python_choice(population, weights, k))
# [96, 99, 90, 46, 78, 16, 17, 22, 58, 30]
print(numpy_choice(population, weights, k))
# [ 9 61  1 18 41 89 55  4 53 40]
print(numba_choice(population, weights, k))
# [66 82 91 62  9 56 71 14 32 26]

%timeit python_choice(population, weights, k)
# 198 ms ± 19.3 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
%timeit numpy_choice(population, weights, k)
# 13.4 ms ± 65.7 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit numba_choice(population, weights, k)
# 2.08 ms ± 27.9 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

编辑:这是没有 Numba 的情况:

import numpy as np

def loop_choice(population, weights, k):
    wc = np.cumsum(weights)
    m = wc[-1]
    sample = np.empty(k, population.dtype)
    sample_idx = np.full(k, -1, np.int32)
    i = 0
    while i < k:
        r = m * np.random.rand()
        idx = np.searchsorted(wc, r, side='right')
        if np.isin(idx, sample_idx):
            continue
        sample[i] = population[idx]
        sample_idx[i] = population[idx]
        i += 1
    return sample

# Setup from before...
%timeit loop_choice(population, weights, k)
# 3.55 ms ± 23.1 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

编辑:只是一个小测试来检查样本是否调整到权重:

import numpy as np
import matplotlib.pyplot as plt

np.random.seed(0)
n = 200
population = np.arange(n)
weights = np.sin(np.linspace(0, 2 * np.pi, n)) + 1
k = 15
r = 1600
a = np.zeros(n, np.int32)
for _ in range(r):
    c = numba_choice(population, weights, k)
    np.add.at(a, c, 1)
plt.figure()
plt.plot(weights / weights.sum(), label='Weights')
plt.plot(a / (k * r), label='Samples')
plt.legend()
plt.tight_layout()
plt.show()

结果:

【讨论】:

  • 谢谢,这确实显着提高了速度。我想知道是否可以通过缓存一些值来进一步改进它。我试试看
  • Numba 中的 Cumsum 非常慢,但是您可以使用像 wc = np.empty(len(weights) + 1, weights.dtype);wc[0]=0;acc=0;for i in range(weights.shape[0]):;acc+=weights[i];wc[i+1]=acc 这样的简单循环将其写出来
  • @max9111 我刚刚对其进行了测试,但它似乎对我的测量结果没有影响(使用 NumPy 1.18.1 和 Numba 0.48.0 和 Anaconda for Windows)。也许它变得更好了?
  • @Nik 原来我不需要用wc 开头的额外零来做那件事,这意味着我可以在 Numba 中保存额外的中间数组,所以现在更快了。我目前正在检查这是否真的给出了一个公平的样本......
  • 每次抽奖后,增加一个权重。因此,理论上,每次绘制后不必为整个权重向量重新计算累积和,而只需为增加值之后的权重向量重新计算。但我不知道这是否可以加快进程
猜你喜欢
  • 2015-07-05
  • 2010-09-08
  • 2017-12-26
  • 2017-09-18
  • 2020-05-25
  • 2023-03-31
  • 2020-01-22
相关资源
最近更新 更多