2

我有一些代码随机初始化一些 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()返回np.float64,尽管下游步骤只需要np.float32,而且在某些情况下,这个数组非常大(想想数百万个 400 维的词向量)。因此临时np.float64返回值暂时使用 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 行更改可能会带来哪些非直觉性的变化,而这些变化可能会出现在下游。

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

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

4

3 回答 3

2

让我们打印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只能保存三个有效位的类型(想想非零位的跨度),例如数字(二进制​​)1.01 或 11100.0 或 0.0000111,但不是 10.01,因为它有四个有效位。

然后范围 [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) 范围内没有其他可表示的数字,因为这些数字都以“0.1”开头,因此任何其他 1 位只能位于第二个或第三个小数位。例如 0.1001 将无法表示,因为它有四个有效位。

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

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

所以这就是你得到的,只是用float32and float64。您的两种方法为您提供了不同的分布。我想说第二种方法的分布实际上更好,因为第一种方法有我所说的“差距偏差”。所以也许不是你的新方法坏了,而是测试。如果是这种情况,请修复测试。否则,解决您的情况的一个想法可能是使用旧的float64方式float32,但不会立即产生所有内容。取而代之的是,在float32任何地方只用 0.0 准备结构,然后用新方法生成的更小的块填充它。

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

顶部的实验程序(也在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)
于 2020-10-07T10:12:40.260 回答
2

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

例如:

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)以保持性能。

于 2020-10-07T11:10:01.200 回答
1

我使用以下值运行您的代码:

seed = 0
target_shape = [100]
vector_size = 3

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

具体来说,它看起来保留了随机数生成器中使用相同种子uniform的一半值。random这可能是因为 numpy 的随机生成器中的实现细节。

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

[            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, ...]

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

于 2020-10-06T22:27:53.507 回答