我有一个矩阵x
,具有3 x 3
维数和一个3,
的向量w
:
x = np.array([[1, 2, 1],
[3, 2 ,1],
[1, 2, 2]])
w = np.array([0.3, 0.4, 0.3])
我需要生成另一个向量y
,即每行x
的多数票。x
的每一列都按相应的值加权,以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
.等等。
如果你懂广播,你可以用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
现在分步解释...请注意,通常最好从零开始计数,但我遵循了您的惯例。
我添加了一行,因此数组不对称(更容易检查形状)
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])
第一步是让索引数组
i
。您的约定从 1 开始。In [2]: i = np.arange(3) + 1
棘手的步骤:创建一个形状为 (3,4,3) 的数组,其中数组的第 i 个条目是 (4,3) 数组,所有条目均为 0 或 1。它是 1 当且仅当 x == i。这是通过向
x
和i
添加维度来完成的,以便可以广播它们。该操作基本上比较了x
和i
的所有组合,因为x
的所有维度都匹配 size=1 维度的i
,反之亦然: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]]])
现在,您沿行求和(即 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]])
我希望你已经可以看到这是怎么回事。您可以直接阅读:在
x
的第一行中,1
出现两次,2
出现一次。在第二行x
,都出现一次,在x
的第三行,1
出现一次,2
出现两次,以此类推。将其乘以权重:
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]])
获取行的最大值(添加一个以符合您的约定):
In [8]: np.argmax(np.sum(m, axis=2).T*w, axis=1) + 1 Out[8]: array([1, 2, 2, 3])
特殊情况:领带
评论中提到了以下案例:
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.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;博士
享受:
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()
我对weighted_vote_JG
使用了以下概括,并进行了适当的更正:
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
故事的寓意:对于少量的列和权重,扩展的解决方案更快。对于更多的列,请改用我的版本。