【问题标题】:Randomly shuffle a weighted array随机打乱加权数组
【发布时间】:2015-03-06 03:46:29
【问题描述】:

有一个带有 ID 和这些 ID 权重的哈希。

y = { 1 => 0.7, 2 => 0.2, 3 => 0.1 }

我想根据权重打乱这个哈希。

我尝试了许多不同的方法,所有这些方法都给了我类似的、意想不到的结果。这是我找到的最简洁的。

y.sort_by {|v| -v[1]*rand()}

当我运行这个一万次并挑选出第一个 ID 时,我得到以下计数:

{1=>8444, 2=>1316, 3=>240}

我希望这些计数能够反映上述权重(例如,1 => 7000)。对于为什么这种洗牌与这些权重不匹配,我有点困惑。有人能解开我的困惑并告诉我如何解决吗?

以下是我找到的一些有用的资源:

【问题讨论】:

  • 这个不会工作的例子。假设我们有哈希{ 1 => 0.7, 2 => 0.3}。当我们为 1 选择随机权重时,恰好 4/7 的时间它会大于 0.3,因此肯定大于我们为 2 选择的数字。另外 3/7 的时间,它将是随机的介于 0.0 和 0.3 之间,并且有 1/2 的机会大于我们为 2 选择的数字。所以它在时间上首先被订购 4/7 + (3/7)*(1/2) == 78.6%,而在 70% 的时间里应该首先订购它。
  • 你需要做的是构造一个(累积的)分布函数,然后让rn = rand0.01.0之间的一个数字),如果rn < 0.7选择12 if 0.7 <= rn < 0.93 如果 rn

标签: ruby arrays shuffle


【解决方案1】:

这是使用Enumerable#max_by 执行加权随机抽样的另一种方法,以及来自Efraimidis and Spirakis 的惊人结果:

给定一个哈希值表示概率之和为 1,我们可以得到一个加权随机抽样,如下所示:

# hash of ids with their respective weights that sum to 1
y = { 1 => 0.7, 2 => 0.2, 3 => 0.1 }

# lambda that randomly returns a key from y in proportion to its weight
wrs = -> { y.max_by { |_, weight| rand ** (1.0/weight) }.first }

# test run to see if it works
10_000.times.each_with_object(Hash.new(0)) { |_, freq| freq[wrs.call] += 1 }

# => {1=>6963, 3=>979, 2=>2058}

附带说明,talkArray#sample 添加加权随机采样,但该功能似乎在洗牌中丢失了。

进一步阅读:

  1. Ruby-Doc 代表 Enumerable#max_by — 特别是 wsample 示例
  2. Weighted Random Sampling 由 Efraimidis 和 Spirakis(2005 年)介绍算法
  3. New features for Array#sample, Array#choice 提到了将加权随机抽样添加到 Array#sample 的意图

【讨论】:

    【解决方案2】:

    这是一个最有可能效率低下但希望足够有效的解决方案: (尽管我不保证正确性!另外,代码不会让太多 Ruby 爱好者高兴...)。

    算法的本质很简单,就是根据权重随机选择一个元素,将其移除,然后与剩余的元素重复。

    def shuffle some_hash
       result = []
    
       numbers = some_hash.keys
       weights = some_hash.values
       total_weight = weights.reduce(:+)
    
       # choose numbers one by one
       until numbers.empty?
          # weight from total range of weights
          selection = rand() * total_weight
    
          # find which element this corresponds with
          i = 0
          while selection > 0
             selection -= weights[i]
             i += 1
          end
          i -= 1
    
          # add number to result and remove corresponding weight
          result << numbers[i]
          numbers.delete_at i
          total_weight -= weights.delete_at(i)
       end
    
       result
    end
    

    【讨论】:

    • 这很好用并且易于阅读。我跑了一堆,它按预期工作。谢谢。
    【解决方案3】:

    您给出了概率密度函数(P 表示“概率”):

    P(1) = 0.7
    P(2) = 0.3
    P(3) = 0.1
    

    您需要构造(累积)分布函数,如下所示:

    我们现在可以生成 0 和 1 之间的随机数,将它们绘制在 Y 轴上,向右画一条线以查看它们与分布函数相交的位置,然后读取关联的 X 坐标作为随机变量.所以如果随机数小于0.7,随机变量为1;如果介于 0.7 和 0.9 之间,则随机变量为 2,如果概率超过 0.9,则随机变量为 3。 (请注意,rand 等于 0.7(比如说)的概率几乎为零,因此我们不必为区分 &lt; 0.7&lt;= 0.7 而感到抱歉。)

    要实现它,首先计算哈希df

    y = { 1 => 0.7, 2 => 0.2, 3 => 0.1 }
    
    last = 0.0
    df = y.each_with_object({}) { |(v,p),h| last += p; h[last.round(10)] = v }
      #=> {0.7=>1, 0.9=>2, 1.0=>3}
    

    现在我们可以如下创建一个随机变量:

    def rv(df)
      rn = rand
      df.find { |p,_| rn < p }.last
    end
    

    让我们试试吧:

    def count(df,n)
      n.times.each_with_object(Hash.new(0)) { |_,count|
        count[rv(df)] += 1 }
    end
    
    n = 10_000
    count(df,n)
      #=> {1=>6993, 2=>1960, 3=>1047} 
    count(df,n)
      #=> {1=>6986, 2=>2042, 3=>972} 
    count(df,n)
      #=> {1=>6970, 2=>2039, 3=>991} 
    

    请注意,键值对count 的顺序是由前几个随机变量的结果决定的,因此键不一定是它们在此处的顺序。

    【讨论】:

    • 感谢您的详尽回复。该图表清楚地说明了为什么需要 CDF。最后,我选择了一个不同的响应,它使我可以轻松地设置一种基于 CDF 的哈希散列方法。再次感谢。
    【解决方案4】:

    如果您将权重设为整数值,如下所示:

    y = { 1 => 7, 2 => 2, 3 => 1 }
    

    然后您可以构造一个数组,其中数组中每个项目的出现次数基于权重:

    weighted_occurrences = y.flat_map { |id, weight| Array.new(weight, id) }
    # => [1, 1, 1, 1, 1, 1, 1, 2, 2, 3]
    

    然后做一个加权洗牌很简单:

    weighted_occurrences.shuffle.uniq
    

    经过 10,000 次随机播放并挑选出第一个 ID,我得到:

    {
      1 => 6988,
      2 => 1934,
      3 => 1078
    }
    

    【讨论】:

    • 感谢您的回答。我喜欢饥饿游戏风格的彩票,但最后我决定允许小数权重比将它们转换为整数更容易。
    • 很公平。感谢您提出有趣的问题!我很高兴想出一个答案。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-06-30
    • 1970-01-01
    • 1970-01-01
    • 2018-11-06
    • 2010-12-21
    • 1970-01-01
    • 2020-10-11
    相关资源
    最近更新 更多