【问题标题】:Return majority weighted vote from array based in columns从基于列的数组中返回多数加权投票
【发布时间】:2020-09-11 23:23:04
【问题描述】:

我有一个矩阵x3 x 3 维度和一个向量w3,

x = np.array([[1, 2, 1],
              [3, 2 ,1],
              [1, 2, 2]])

w = np.array([0.3, 0.4, 0.3])

我需要为x 的每一行生成另一个向量yx 的每一列都由w 中的对应值加权。像这样的:

对于y[0],它应该寻找X[0] => [1, 2, 1]

  • 值为 1 的列 = 第一个和第三个 [0,2]
  • 值为 2 的列 = 秒 [1]
  • 值为 3 的列 = 无

对按 X 中的值分组的列的权重(w)求和:

  • 值为 1 的列的权重总和:0.3 + 0.3 = 0.6
  • 值为 2 的列的权重总和:0.4
  • 值为 3 的列的权重总和:0

由于值为 1 的列的权重总和最高,y[0] = 1。以此类推。

【问题讨论】:

  • 仅供参考,在接受的答案中有一个错误(下面的评论和解决方案)。我已经发布了一个可能对你也有用的替代方案。
  • 该错误尚未被接受 ;-) 但至少从讨论中我包括了没有获胜者时的额外情况,我想你会希望你的代码处理。
  • @joann255,也许有不止一行的预期结果是个好主意,并包括下面提到的测试用例 (x=[[2, 2, 4, 1]] , w=[0.1, 0.2, 0.3, 0.4])。

标签: python numpy numpy-ndarray


【解决方案1】:

如果你理解broadcasting,你可以用 numpy 来做。缺点是因为代码是矢量化的,所以你做的计算比你需要的要多。如果w 向量的大小非常大,这将很重要。

也许有人想出了一种更简单的方法来编写它,但这是我不用想太多就可以做到的。

答案第一:

i = np.arange(3) + 1
m = (x.reshape((1,4,3)) == i.reshape((3,1,1)))
np.argmax(np.sum(m, axis=2).T*w, axis=1) + 1

现在是一步一步的解释......请注意,通常最好从零开始计数,但我遵循你的约定。

  1. 我添加了一行,所以数组不是对称的(更容易检查形状)

     In [1]: x = np.array([[1, 2, 1],
        ...:               [3, 2 ,1],
        ...:               [1, 2, 2],
        ...:               [3, 1, 3]])
        ...:
        ...: w = np.array([0.3, 0.4, 0.3])
    
  2. 第一步是拥有索引数组i。您的会议从一开始。

     In [2]: i = np.arange(3) + 1
    
  3. 棘手的步骤:创建一个形状为 (3,4,3) 的数组,其中数组的第 i 个条目是所有条目为 0 或 1 的 (4,3) 数组. 当且仅当 x == i 时为 1。这是通过向xi 添加维度来完成的,这样它们就可以被广播。该操作基本上比较了xi的所有组合,因为x的所有维度都匹配i的1个维度,反之亦然:

     In [3]: m = (x.reshape((1,4,3)) == i.reshape((3,1,1)))*1
    
     In [4]: m
     Out[4]:
     array([[[1, 0, 1],
             [0, 0, 1],
             [1, 0, 0],
             [0, 1, 0]],
    
            [[0, 1, 0],
             [0, 1, 0],
             [0, 1, 1],
             [0, 0, 0]],
    
            [[0, 0, 0],
             [1, 0, 0],
             [0, 0, 0],
             [1, 0, 1]]])
    
  4. 现在您沿行求和(axis=2)以获得每个选择在x 的每一行中出现的次数(请注意,当您将其与x 进行比较时,结果会被转置):

     In [5]: np.sum(m, axis=2)
     Out[5]:
     array([[2, 1, 1, 1],
            [1, 1, 2, 0],
            [0, 1, 0, 2]])
    
  5. 我希望你已经知道这是怎么回事。可以直接阅读:x的第一行,1出现两次,2出现一次。 x第二行全部出现一次,x第三行1出现一次,2出现两次,以此类推

  6. 将其乘以权重:

     In [7]: np.sum(m, axis=2).T*w
     Out[7]: 
     array([[0.6, 0.4, 0. ],
            [0.3, 0.4, 0.3],
            [0.3, 0.8, 0. ],
            [0.3, 0. , 0.6]])
    
  7. 沿行获取最大值(添加一个以符合您的约定):

     In [8]: np.argmax(np.sum(m, axis=2).T*w, axis=1) + 1
     Out[8]: array([1, 2, 2, 3])
    

特例:平局

在cmets中提出了以下案例:

x = np.array([[2, 2, 4, 1]])
w = np.array([0.1, 0.2, 0.3, 0.4])

权重之和为:

[0.1, 0.4, 0., 0.4]

所以在这种情况下没有赢家。从这个问题中不清楚在这种情况下会做什么。可以全部拿走,什么都不拿……最后可以看看这些案例:

final_w = np.sum(m, axis=2).T*w
result = np.argmax(np.sum(m*w, axis=2), axis=0) + 1
special_cases = np.argwhere(np.sum(final_w == np.max(final_w), axis=1) > 1)

注意:为了可读性,我使用了reshape方法,但我经常使用np.expand_dims或np.newaxis。像这样的:

i = np.arange(3) + 1
m = (x[np.newaxis] == i[:, np.newaxis, np.newaxis])
np.argmax(np.sum(m, axis=2).T*w, axis=1) + 1

另一种选择:您也可以使用某种已编译的代码。例如,在这种情况下,numba 非常容易使用。

【讨论】:

  • 我认为您的解决方案是巧合。您应该考虑将np.argmax(np.sum(m, axis=2).T*w, axis=1) + 1 更改为np.argmax(np.sum(m*w, axis=2), axis=0) + 1。这是一个测试用例:w = [0.1, 0.2, 0.3, 0.4]x = [[[2, 2, 4, 1]]。我适当调整了您的索引以处理 1x4 案例
  • 我认为您可能对 + 1 感到困惑,因此索引从 1 开始。您的测试用例很有趣,因为它是平局,并且计算不会抱怨它。在您的测试用例中,最终权重为 [0.1, 0.4, 0. , 0.4],因此结果应为 24。我的代码只给出2。你的建议给了1。必须对此进行检查。
  • OP 没有将权重乘以xx 只是一个标签。最终的权重是 [0.3, 0.3, 0.4],这不是平局。
  • 我不考虑出现平局的情况,因为它们发生的可能性很小。在我的真实场景中,权重是至少有 5 个十进制数字的浮点数。为简化起见,我只是给了你小数点后 1 位的权重。
【解决方案2】:

这是一种非常疯狂的方法,它涉及排序和索引,而不是添加新维度。这有点像np.unique 使用的基于排序的方法。

首先找到每一行的排序索引:

rows = np.repeat(np.arange(x.shape[0]), x.shape[1])  # [0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3]
cols = np.argsort(x, axis=1).ravel()                 # [0, 2, 1, 2, 1, 0, 0, 1, 2, 1, 0, 2]

现在您可以为每列创建一个排序元素数组,包括未加权和加权。前者用于求和的指标,后者实际是求和的。

u = x[rows, cols]                            # [1, 1, 2, 1, 2, 3, 1, 2, 2, 1, 3, 3]
v = np.broadcast_to(w, x.shape)[rows, cols]  # [0.3, 0.3, 0.4, 0.3, 0.4, 0.3, 0.3, 0.4, 0.3, 0.4, 0.3, 0.3]

您可以在以下位置找到要应用np.add.reduce 的断点:

row_breaks = np.diff(rows).astype(bool)            # [0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0]
col_breaks = np.diff(u).astype(bool)               # [0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0]
break_mask = row_breaks | col_breaks               # [0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0]
breaks = np.r_[0, np.flatnonzero(break_mask) + 1]  # [ 0,  2,  3,  4,  5,  6,  7,  9, 10]

现在您有了每行中相同数字的权重总和:

sums = np.add.reduceat(v, breaks)  # [0.6, 0.4, 0.3, 0.4, 0.3, 0.3, 0.7, 0.4, 0.6]

但是您需要根据每行唯一元素的数量将它们分解为段:

unique_counts = np.add.reduceat(break_mask, np.arange(0, x.size, x.shape[1]))
unique_counts[-1] += 1  # The last segment will be missing from the mask: # [2, 3, 2, 2]

unique_rows = np.repeat(np.arange(x.shape[0]), unique_counts)  # [0, 0, 1, 1, 1, 2, 2, 3, 3]

您现在可以对每个段进行排序以找到最大值:

indices = np.lexsort(np.stack((sums, unique_rows), axis=0))  # [1, 0, 2, 4, 3, 5, 6, 7, 8]

每次运行结束时的索引由下式给出:

max_inds = np.cumsum(unique_counts) - 1  # [1, 4, 6, 8]

所以最大总和是:

sums[indices[max_inds]]  # [0.6, 0.4, 0.7, 0.6]

您可以解开索引内的索引以从每一行中获取正确的元素。请注意max_inds,以及依赖它的所有内容都与x.shape[1] 一样大小,正如预期的那样:

result = u[breaks[indices[max_ind]]]

这种方法看起来不是很漂亮,但它可能比在数组上使用额外维度更节省空间。此外,无论x 中的数字如何,它都能正常工作。请注意,我从未以任何方式减去任何内容或调整x。实际上,所有行都是独立处理的,构造breaks时,row_breaks打破了最大元素与下一个元素相同的重合。

TL;DR

享受:

def weighted_vote(x, w):
    rows = np.repeat(np.arange(x.shape[0]), x.shape[1])
    cols = np.argsort(x, axis=1).ravel()
    u = x[rows, cols]
    v = np.broadcast_to(w, x.shape)[rows, cols]
    row_breaks = np.diff(rows).astype(bool)
    col_breaks = np.diff(u).astype(bool)
    break_mask = row_breaks | col_breaks
    breaks = np.r_[0, np.flatnonzero(break_mask) + 1]
    sums = np.add.reduceat(v, breaks)
    unique_counts = np.add.reduceat(break_mask, np.arange(0, x.size, x.shape[1]))
    unique_counts[-1] += 1
    unique_rows = np.repeat(np.arange(x.shape[0]), unique_counts)
    indices = np.lexsort(np.stack((sums, unique_rows), axis=0))
    max_inds = np.cumsum(unique_counts) - 1
    return u[breaks[indices[max_inds]]]

基准测试

基准测试以下列格式运行:

rows = ...
cols = ...
x = np.random.randint(cols, size=(rows, cols)) + 1
w = np.random.rand(cols)
%timeit weighted_vote_MP(x, w)
%timeit weighted_vote_JG(x, w)
assert (weighted_vote_MP(x, w) == weighted_vote_JG(x, w)).all()

我对@9​​87654340@ 使用了以下概括,并进行了适当的更正:

def weighted_vote_JG(x, w):
    i = np.arange(w.size) + 1
    m = (x[None, ...] == i.reshape(-1, 1, 1))
    return np.argmax(np.sum(m * w, axis=2), axis=0) + 1

行:100,列:10

  MP: 440 µs ± 5.12 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
* JG: 153 µs ± 796 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)

行:1000,列:10

  MP: 2.53 ms ± 43.7 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
* JG: 1.03 ms ± 12 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

行:10000,列:10

  MP: 23.5 ms ± 200 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
* JG: 16.6 ms ± 67.4 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

行:100000,列:10

  MP: 322 ms ± 3.11 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
* JG: 188 ms ± 858 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

行:100,列:100

* MP: 3.31 ms ± 257 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
  JG: 12.6 ms ± 244 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

行:1000,列:100

* MP: 31 ms ± 159 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
  JG: 134 ms ± 581 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

行:10000,列:100

* MP: 417 ms ± 7.06 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
  JG: 1.42 s ± 126 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

行:100000,列:100

* MP: 4.94 s ± 25.9 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
  JG: MemoryError: Unable to allocate 7.45 GiB for an array with shape (100, 100000, 100) and data type float64

故事的寓意:对于少量的列和权重,扩展解决方案更快。对于更多列,请改用我的版本。

【讨论】:

  • 很高兴看到基准,看到列数变大的点。我仍然不明白为什么 weighted_vote([[2, 2, 4, 1]], [0.1, 0.2, 0.3, 0.4]) == [1]。在这种情况下,1 出现一次 (0.1),2 出现两次 (2*0.2),3 从不出现,4 出现一次 (0.4)。所以最终的权重是 [0.1, 0.4, 0., 0.4]。最大值是 2 和 4。
  • @哈维尔。查看 OP 的示例:“值为 2 的列的权重总和:0.4”。请注意,它不是 0.8。 OP 仅使用 x 作为标签,而不是总和的一部分。
  • @哈维尔。标题中的单词应该是“调节”而不是“重量”
  • 在OP中:在第一行,值2出现一次。这对应于 i==1 和 w[1] == 0.4。我不知道从哪里可以得到 0.8。我在之前的评论中所做的是遵循与 OP 中相同的程序,但使用您的测试用例,因此不会与索引混淆。按照该程序,24 首先以 0.4 的总权重并列。你的方法给出了1 作为结果。
  • 也许需要用更长的工作示例扩展 OP,也许添加几行的预期结果,以确保我们正确理解它。换句话说:我知道您的测试用例的结果应该是 2 和 4 之间的平局。
猜你喜欢
  • 1970-01-01
  • 2013-03-08
  • 1970-01-01
  • 2021-12-09
  • 2011-05-06
  • 2021-06-15
  • 1970-01-01
  • 2020-08-03
  • 1970-01-01
相关资源
最近更新 更多