问题描述
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
返回值会立即使用所需RAM的3倍。
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处进行/不进行微小更改。但是,实际上,我只是希望一个麻木的专家可以识别出一些不直观的极端情况,其中会发生一些非直觉的事情,或者提供一些可验证的相同理论。)
有什么想法,建议的测试或可能的解决方案吗?
解决方法
让我们为这两种方法打印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
类型,它只能容纳 3 个有效位(认为范围为非零位),例如数字(二进制)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)中。并且[em>没有在[0.5,1)范围内没有其他可表示的数字,因为这些数字都以“ 0.1”开头,因此任何其他1位只能位于第二或第三小数位。例如0.1001将无法表示,因为它具有四个有效位。
因此,如果生成器还从上述八个数字中选择一个,则它们都必须位于[0,0.5)中,从而产生偏差。它可以从该范围中的不同中选择四个数字,或者可以包括具有适当概率的该范围中的所有可表示数字,但是无论哪种方式,您都将有一个“ gap bias” ”,则从[0,0.5)选取的数字可能比从[0.5,1)选取的数字具有较小或更大的间隙。不确定“间隙偏差”是事物还是正确的术语,但重点是[0,0.5)中的分布看起来与[0.5,1)中的分布不同。使它们看起来相同的唯一方法是,如果您坚持从上面等距的八个数字中进行选择。在[0.5,1)中的分布/可能性决定了在[0,0.5)中应使用什么。
所以... float3
的随机数生成器将从这8个数字中选取,而不会生成例如0.0000111。但是现在想象一下,我们还有一个类型float5
,它可以容纳 5 个有效位。然后,一个随机数生成器可以选择0.00001。然后,如果将其转换为我们的float3
,那将继续存在,那么您将获得0.00001作为float3
。但是在[0.5,1)范围内,生成float5
数字并将其转换为float3
的过程仍将仅产生数字0.100、0.101、0.110和0.111,因为float3
仍然不能代表该范围内的任何其他数字。
因此,仅凭float32
和float64
就可以得到。您的两种方法给您不同的分布。我会说第二种方法的分布实际上更好,因为第一种方法具有我所说的“间隙偏差”。因此,也许不是您破坏的新方法,而是测试。如果是这样,请修复测试。否则,解决您的情况的想法可能是使用旧的float64
到float32
方式,但不能一次完成所有操作。取而代之的是,准备float32
结构,使其各处都只有0.0,然后将其填充为用新方法生成的较小块。
小警告,顺便说一句:NumPy中似乎有一个bug用于生成随机的float32
值,而不使用最低位置位。因此,这可能是测试失败的另一个原因。您可以使用(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
调整这些块的大小以保持性能。
我使用以下值运行您的代码:
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,...]
基于此,我推测您的随机测试用例只能用一个种子来测试您的解决方案,因为您用新的解决方案生成了不同的序列。结果导致测试用例失败。