这些交替的 numpy `uniform` 与 `random` 结构可能有何不同?

How could these alternate numpy `uniform` vs `random` constructions possibly differ?

我有一些随机初始化一些 numpy 数组的代码:

rng = np.random.default_rng(seed=seed)
new_vectors = rng.uniform(-1.0, 1.0, target_shape).astype(np.float32)  # [-1.0, 1.0)
new_vectors /= vector_size

一切都运行良好,所有项目测试都通过了。

不幸的是,uniform() returns np.float64,尽管下游步骤只需要 np.float32,并且在某些情况下,这个数组非常大(想想数百万个 400 -维词向量)。所以临时 np.float64 return-value 暂时使用 3X 所需的 RAM。

因此,我将上面的内容替换为定义上应该等效的内容:

rng = np.random.default_rng(seed=seed)
new_vectors = rng.random(target_shape, dtype=np.float32)  # [0.0, 1.0)                                                 
new_vectors *= 2.0  # [0.0, 2.0)                                                                                  
new_vectors -= 1.0  # [-1.0, 1.0)
new_vectors /= vector_size

并且在此更改之后,所有密切相关的功能测试仍然通过,但是依赖于如此初始化的向量的远下游计算的单个远程边缘测试开始失败。并以一种非常可靠的方式失败。这是一个随机测试,在顶部情况下以较大的误差余量通过,但在底部情况下总是失败。所以:有些事情发生了变化,但以某种非常微妙的方式。

new_vectors 的表面值似乎在两种情况下都正确且相似地分布。同样,所有功能的“特写”测试仍然通过。

因此,我喜欢关于这 3 行更改可能造成的非直觉更改的理论,这些更改可能会出现在下游。

(我仍在尝试找到一个最小的测试来检测任何不同之处。如果您喜欢深入研究受影响的项目,看到成功的确切特写测试和一个边缘测试失败,并提交 with/without 微小的变化,在 https://github.com/RaRe-Technologies/gensim/pull/2944#issuecomment-704512389。但实际上,我只是希望 numpy 专家可能会认识到一些微小的极端情况,在这些情况下会发生一些非直觉的事情,或者提供一些可测试的理论相同。)

有什么想法、建议的测试或可能的解决方案吗?

我 运行 您的代码具有以下值:

seed = 0
target_shape = [100]
vector_size = 3

我注意到您的第一个解决方案中的代码与第二个解决方案生成的代码不同new_vectors。

具体来说,uniform 保留了 运行dom 数字生成器的一半值,而 random 使用相同的种子。这可能是因为 numpy 的 运行dom 生成器中的一个实现细节。

在下面的代码片段中,我只插入了空格来对齐相似的值。可能还有一些浮点数舍入使结果看起来不相同。

[            0.09130779,              -0.15347552,             -0.30601767,              -0.32231492,              0.20884682, ...]
[0.23374946, 0.09130772, 0.007424275, -0.1534756, -0.12811375, -0.30601773, -0.28317323, -0.32231498, -0.21648853, 0.20884681, ...]

基于此,我推测您的随机测试用例仅使用一个种子测试您的解决方案,因为您使用新解决方案生成了不同的序列。此结果导致测试用例失败。

让我们为这两种方法打印 new_vectors * 2**22 % 1,也就是说,让我们看看前 22 个小数位之后还剩下什么(程序在最后)。用第一种方法:

[[0.         0.5        0.25       0.         0.        ]
 [0.5        0.875      0.25       0.         0.25      ]
 [0.         0.25       0.         0.5        0.5       ]
 [0.6875     0.328125   0.75       0.5        0.52539062]
 [0.75       0.75       0.25       0.375      0.25      ]]

使用第二种方法:

[[0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0.]]

差别很大!第二种方法在前 22 个小数位之后不产生任何带 1 位的数字。

假设我们有一个类型 float3,它只能容纳 三个 有效位(想想 non-zero 位的跨度),例如数字(二进制)1.01 或 11100.0 或 0.0000111,但不是 10.01,因为它有 4 个有效位。

然后范围 [0, 1) 的随机数生成器将从这八个数字中选择:

0.000
0.001
0.010
0.011
0.100
0.101
0.110
0.111

等等,等等。为什么只有这八个?例如前面提到的 0.0000111 呢?那在 [0, 1) 中,可以表示,对吗?

是的,但请注意,它在 [0, 0.5) 中。在 [0.5, 1) 范围内没有 no 个可表示的数字,因为这些数字都以“0.1”开头,因此任何进一步的 1 位只能在第二个或第三个小数位。例如 0.1001 将不可表示,因为它有 4 个有效位。

因此,如果生成器还从上述八个数字以外的任何其他数字中进行选择,则它们都必须在 [0, 0.5) 内,从而产生偏差。它可以从该范围内的 不同 四个数字中选择,或者可能包括该范围内具有适当概率的 所有 可表示数字,但无论哪种方式你d 有一个“间隙偏差”,其中从 [0, 0.5) 中选取的数字可以比从 [0.5, 1) 中选取的数字具有更小或更大的间隙。不确定“差距偏差”是一个事物还是正确的术语,但关键是 [0, 0.5) 中的分布看起来与 [0.5, 1) 中的分布不同。使它们看起来相同的唯一方法是坚持从上面的 equally-spaced 八个数字中挑选。 [0.5, 1) 中的 distribution/possibilities 指示您应该在 [0, 0.5] 中使用什么。

所以... float3 的随机数生成器会从这八个数字中选择,并且永远不会生成例如 0.0000111。但现在假设我们还有一个类型 float5,它可以容纳 5 个有效位。然后一个随机数生成器可以选择 0.00001。如果您随后将其转换为我们的 float3,那将继续存在,您将拥有 0.00001 作为 float3。但是在 [0.5, 1) 范围内,这个生成 float5 数字并将它们转换为 float3 的过程仍然只会产生数字 0.100、0.101、0.110 和 0.111,因为 float3 仍然不能表示该范围内的任何其他数字。

这就是你得到的,只是 float32float64。你的两种方法给你不同的分布。我会说第二种方法的分布实际上更好,因为第一种方法有我所说的“差距偏差”。所以也许不是你的新方法坏了,而是测试。如果是这种情况,请修复测试。否则,解决您的情况的一个想法可能是使用旧的 float64-to-float32 方式,但不要一次生成所有内容。相反,准备 float32 结构,到处都只有 0.0,然后将其填充到用你的新方法生成的更小的块中。

小提示,顺便说一句:看起来 NumPy 中有一个 bug 用于生成随机 float32 值,而不是使用 lowest-position 位。所以这可能是测试失败的另一个原因。您可以尝试使用 (rng.integers(0, 2**24, target_shape) / 2**24).astype(np.float32) 而不是 rng.random(target_shape, dtype=np.float32) 的第二种方法。我认为这等同于固定版本(因为它显然目前正在这样做,除了 23 而不是 24)。

顶部的实验程序(也at repl.it):

import numpy as np

# Some setup
seed = 13
target_shape = (5, 5)
vector_size = 1

# First way
rng = np.random.default_rng(seed=seed)
new_vectors = rng.uniform(-1.0, 1.0, target_shape).astype(np.float32)  # [-1.0, 1.0)
new_vectors /= vector_size

print(new_vectors * 2**22 % 1)

# Second way
rng = np.random.default_rng(seed=seed)
new_vectors = rng.random(target_shape, dtype=np.float32)  # [0.0, 1.0)                                                 
new_vectors *= 2.0  # [0.0, 2.0)                                                                                  
new_vectors -= 1.0  # [-1.0, 1.0)
new_vectors /= vector_size

print(new_vectors * 2**22 % 1)

一种保持精度和节省内存的方法可能是创建大型目标数组,然后使用更高精度的块填充它。

例如:

def generate(shape, value, *, seed=None, step=10):
  arr = np.empty(shape, dtype=np.float32)
  rng = np.random.default_rng(seed=seed)
  (d0, *dr) = shape
  for i in range(0, d0, step):
    j = min(d0, i + step)
    arr[i:j,:] = rng.uniform(-1/value, 1/value, size=[j-i]+dr)
  return arr

可用作:

generate((100, 1024, 1024), 7, seed=13)

您可以调整这些块的大小(通过 step)以保持性能。