如何随机更改numpy数组中某些元素的符号?

4

给定一个numpy数组

import numpy as np
from numpy.random import random
N = 5
x = random(N)

如何随机选择(x中的某些元素)的子集,并将它们乘以-1,以改变数组中一些元素的符号?
3个回答

3
或者这样:
x = np.where(random(N) > 0.5, -x, x)

(您可以将random(N) > 0.5更换为适合您的其他随机规则...)


3

假设你有一个布尔掩码,指示哪些元素需要翻转。那么你可以这样操作:

x[mask] *= -1

这种方法也适用于带有装饰索引的情况:
x[index] *= -1 

您也可以非常高效地使用 np.negative
np.negative(x, where=mask, out=x)

这种方法可能是最高效的。

生成掩码很简单。您可以编码一个简单的条件,比如

mask = np.random.random(N) >= 0.66

或者您可以使用np.random.choice来选择一个随机花式索引:

index = np.random.choice(N, size=N // 2, replace=False)

最后,你可以使用一些 XOR 的技巧来实现这个功能。IEEE 754 标准将符号位编码在数字的最高位中。通过使用浮点数的整数表示,您可以通过翻转该位来翻转符号。当然,这仅适用于浮点数。

您可以根据浮点数的大小调整整数的大小,或者只需使用 np.uint8 和位 0x80 即可。索引将按浮点数的大小进行缩放。

x.view(np.uint8)[index * x.itemsize] ^= 0x80

这假设使用小端字节序。对于大端字节序,请使用偏移量:
x.view(np.uint8)[(index + 1) * x.itemsize - 1] ^= 0x80

时间

这里是我在一台中等配置的笔记本电脑上运行的一些基准测试结果:

import numpy as np
from timeit import repeat

def where(x, mask):
    x = np.where(mask, -x, x)

def mask_(x, mask):
    x[mask] *= -1

def index(x, mask):
    x[np.flatnonzero(mask)] *= -1 

def negat(x, mask):
    np.negative(x, where=mask, out=x)

def xor__(x, mask):
    x.view(np.uint8)[np.flatnonzero(mask) * x.itemsize] ^= 0x80

for E in range(2, 7):
    N = 10**E
    x = np.random.random(N)

    for P in (0.1, 0.5, 0.9):
        mask = np.random.random(N) < P

        print(f'E = {E}, P = {P}:')

        for func in where, mask_, index, negat, xor__:
            B = 10**(7 - E)
            t = min(repeat(lambda: func(x, mask), number=B)) / B
            print(f'{func.__name__}: {t:.3g}')

结果按照P分隔:

P = 0.1
+-----+------+---------------------------------------+
|     |      |                  Func                 |
| Exp | Unit +-------+-------+-------+-------+-------+
|     |      | where | mask_ | index | negat | xor__ |
+-----+------+-------+-------+-------+-------+-------+
|  2  |  μs  |  4.11 |  6.20 |  12.5 | *3.06 |  14.9 |
|  3  |  μs  |  7.47 |  12.4 |  15.9 | *5.55 |  18.6 |
|  4  |  μs  | *32.3 |  94.0 |  41.6 |  38.9 |  49.8 |
|  5  |  ms  | *.258 |  1.06 |  .582 |  .575 |  .602 |
|  6  |  ms  |  15.7 |  10.5 |  6.44 | *5.87 |  6.57 |
+-----+------+-------+-------+-------+-------+-------+

P = 0.5
+-----+------+---------------------------------------+
|     |      |                  Func                 |
| Exp | Unit +-------+-------+-------+-------+-------+
|     |      | where | mask_ | index | negat | xor__ |
+-----+------+-------+-------+-------+-------+-------+
|  2  |  μs  |  4.11 |  6.53 |  13.0 | *3.48 |  15.4 |
|  3  |  μs  | *7.42 |  17.1 |  20.1 |  9.71 |  26.0 |
|  4  |  μs  | *32.0 |  234. |  140. |  130. |  150. |
|  5  |  ms  | *.268 |  2.41 |  1.27 |  1.43 |  1.36 |
|  6  |  ms  |  15.5 |  27.7 |  20.5 | *14.2 |  21.1 |
+-----+------+-------+-------+-------+-------+-------+

P = 0.9
+-----+------+---------------------------------------+
|     |      |                  Func                 |
| Exp | Unit +-------+-------+-------+-------+-------+
|     |      | where | mask_ | index | negat | xor__ |
+-----+------+-------+-------+-------+-------+-------+
|  2  |  μs  |  4.11 |  6.23 |  13.2 | *3.13 |  15.7 |
|  3  |  μs  |  7.81 |  15.0 |  23.6 | *6.40 |  28.4 |
|  4  |  μs  | *31.5 |  116. |  104. |  54.8 |  130. |
|  5  |  ms  | *.263 |  1.24 |  .882 |  .612 |  1.02 |
|  6  |  ms  |  16.6 |  18.4 |  21.0 | *6.24 |  22.9 |
+-----+------+-------+-------+-------+-------+-------+

结论:对于小数组(元素数量小于10^4)和大数组(元素数量大于10^6),np.negative通常是最快的方法。在元素数量在10^3-10^4左右的情况下,np.where最优。比较时间时,请记住方法indexxor__依赖于索引数组。如果这对您来说是一个输入,请减去调用np.flatnonzero所需的时间。
在所有情况下,由P确定的翻转元素的比例并不会对结果产生太大影响。
供参考,我还计算了使用np.random.choice创建索引与使用掩码之间差异的时间。这些时间略有误差,因为两种操作的结果并不完全相同:
def thresh(n, p):
    return np.flatnonzero(np.random.random(n) < p)

def choice(n, p):
    return np.random.choice(n, size=round(n * p), replace=False)

for E in range(2, 7):
    N = 10**E
    for P in (0.1, 0.5, 0.9):
        print(f'E = {E}, P = {P}:')
        for func in thresh, choice:
            B = 10**(7 - E)
            t = min(repeat(lambda: func(N, P), number=B)) / B
            print(f'{func.__name__}: {t:.3g}')

时间(聚合到表中):

+-----+------+-----------------------------------------------------+
|     |      |                          P                          |
|     |      +-----------------+-----------------+-----------------+
| Exp | Unit |       0.1       |       0.5       |        0.9      |
|     |      +--------+--------+--------+--------+--------+--------+
|     |      | thresh | choice | thresh | choice | thresh | choice |
+-----+------+--------+--------+--------+--------+--------+--------+
|  2  |  μs  |  14.8  |  35.2  |  15.3  |  34.9  |  14.7  |  34.9  |
|  3  |  μs  |  34.2  |  75.8  |  40.6  |  75.5  |  34.8  |  76.0  |
|  4  |  μs  |  214.  |  494.  |  267.  |  494.  |  206.  |  494.  |
|  5  |  ms  |  1.96  |  4.60  |  2.48  |  4.60  |  1.89  |  4.60  | 
|  6  |  ms  |  26.1  |  50.1  |  34.5  |  50.3  |  30.7  |  50.2  |
+-----+------+--------+--------+--------+--------+--------+--------+

将随机数组进行阈值处理并调用np.flatnonzero的速度始终比使用np.random.choice快约2倍。前一种方法允许您精确复制掩码,而后者则允许您设置确切数量的翻转元素。


@HeapOverflow。数组大小有多大?从根本上讲,它们的复杂度都是相同的,所以我猜处理布尔索引比只是浏览整个数组并否定整个数组要多一些开销。我已经更新了我的措辞。感谢您运行基准测试。 - Mad Physicist
嗯,我不知道flatnonzero,而且我其实是一个numpy新手,现在可能不会进行更多的测试了。我感兴趣的一件事是,为什么你的np.negative解决方案在一百万时胜过Julien的,在一千时输给了他。我怀疑Julien的循环在处理大数据时会更频繁,这会在缓存方面造成一些问题。 - Kelly Bundy
@HeapOverflow。我正在研究它的运行方式,我有一种感觉,在具有奇怪调度的共享机器上运行基准测试是个坏主意。我很快就会在我的笔记本电脑上发布计时结果。在回答中,我一直在使用移动设备。 - Mad Physicist
1
@HeapOverflow。我更新了时间、计算,并为不同的索引生成方法添加了基准测试。享受吧! - Mad Physicist
我真的很喜欢那个 :-). 整洁的表格。是手工制作的,还是有工具可以做到这一点? - Kelly Bundy
显示剩余15条评论

1

你可以做:

import random
x = [each*random.choice([-1,1]) for each in x]

一次性完成:

x = [each*random.choice([-1,1]) for each in random(N)]

其中random(N)是一个生成N个随机数的随机数生成器,例如可以使用numpy.random.random,就像问题中的示例一样。


我喜欢这个。如何将其更改为独立的生成器,而不是x的第二步转换?太遗憾了,random.random不能给出负元素。 - develarist
1
@develarist x = random(N) * 2 - 1?(不是完全等价,但也许足够好了?) - Kelly Bundy

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