我在cudnnBatchNormalizationForwardTraining
函数中使用CUDNN_BN_MIN_EPSILON值时遇到了问题(请参阅the docs here),事实证明这是因为我传递的是float
值1e-5f
而不是 double 值(我正在使用float
值来节省内存和速度向上计算),并且此值一旦转换为float,则略小于1e-5
,后者是该常量的实际值。
经过一番尝试和错误之后,我发现我现在正在使用一个不错的近似值:
const float CUDNN_BN_MIN_EPSILON = 1e-5f + 5e-13f;
我确信有更好的方法来解决这样的问题,所以问题是:
解决此问题的另一种方法是,给定
double
值d1
和float
值f1
,d1 - (float)f1
应该是可能的最小负值(否则,这意味着f1
小于d1,这不是我们的意思)。重新寻找)。我做了一些基本的试验和错误(使用
1e-5
作为我的目标值):// Check the initial difference
> 1e-5 - 1e-5f
2,5262124918247909E-13 // We'd like a small negative value here
// Try to add the difference to the float value
> 1e-5 - (1e-5f + (float)(1e-5 - 1e-5f))
2,5262124918247909E-13 // Same, probably due to approximation
// Double the difference (as a test)
> 1e-5 - (1e-5f + (float)((1e-5 - 1e-5f) * 2))
-6,5687345259044915E-13 // OK
通过这种近似,最终的
float
值为1,00000007E-05
,看起来不错。但是,对我来说
* 2
乘法完全是任意的,我不确定这样做是可靠的还是最佳的选择。有没有更好的方法来实现这一目标?
谢谢!
编辑:这是我现在正在使用的(不良)解决方案,很乐意用更好的解决方案替换它!
/// <summary>
/// Returns the minimum possible upper <see cref="float"/> approximation of the given <see cref="double"/> value
/// </summary>
/// <param name="value">The value to approximate</param>
public static float ToApproximatedFloat(this double value)
=> (float)value + (float)((value - (float)value) * 2);
解决方案:这是最终的正确实现(感谢John Bollinger):
public static unsafe float ToApproximatedFloat(this double value)
{
// Obtain the bit representation of the double value
ulong bits = *((ulong*)&value);
// Extract and re-bias the exponent field
ulong exponent = ((bits >> 52) & 0x7FF) - 1023 + 127;
// Extract the significand bits and truncate the excess
ulong significand = (bits >> 29) & 0x7FFFFF;
// Assemble the result in 32-bit unsigned integer format, then add 1
ulong converted = (((bits >> 32) & 0x80000000u)
| (exponent << 23)
| significand) + 1;
// Reinterpret the bit pattern as a float
return *((float*)&converted);
}
最佳答案
由于您似乎对表示形式级别的细节感兴趣,因此您将依赖于float
和double
类型的表示形式。但是,实际上,很可能归结为IEEE-754的基本“binary32”和“binary64”格式。它们的一般形式是一个符号位,几位偏置指数和一堆有效位,对于归一化的值,包括一个隐含的有效位。
简单的情况
给定一个IEEE-754 binary64格式的double
,其值不少于+ 2-126,您要做的是
double
值的位模式。例如,作为无符号的64位整数。double d = 1e-5;
uint64_t bits;
memcpy(&bits, &d, 8);
uint64_t exponent = ((bits >> 52) & 0x7FF) - 1023 + 127;
uint64_t significand = (bits >> 29) & 0x7fffff;
uint32_t float_bits = ((bits >> 32) & 0x80000000u)
| (exponent << 23)
| significand;
double
的结果,所以这是正确的,无论所有截断的有效位是否都为0。如果加法溢出有效位,它将正确地增加指数字段。但是,它可能会产生无穷大的位模式。float_bits += 1;
float
float f;
memcpy(&f, &float_bits, 4);
负数
给定二进制64格式的负
double
,其大小不小于2-126,请按照上述过程操作,但从float_bits
中减去1而不是加1。请注意,恰好对于-2-126,这会生成次标准的binary32(请参见下文),这是正确的结果。零和非常小的数字,包括次态
IEEE 754提供了幅度非常小的非零数字的精度降低表示。这种表示称为次正规的。在某些情况下,超过给定输入binary64的最小binary32是次正规的,包括对于不是binary64次正规的某些输入。
同样,IEEE 754提供带符号的零,并且-0是一种特殊情况:严格大于-0(两种格式)的最小binary32是最小的次正数。注意:不是+0,因为根据IEEE 754,+ 0和-0通过常规比较运算符进行比较比较相等。最小正,非零,低于标准的binary32值的位模式为0x00000001。
受到这些考虑的binary64值已对binary64指数字段进行了偏置,其值小于或等于binary64指数偏置和binary32指数偏置之间的差(896)。其中包括那些指数恰好为0的指数,这些指数表征了binary64零和次正规量。对简单案例过程中的重新偏置步骤进行检查可以使您正确地得出结论,该过程将对此类输入产生错误的结果。
这些案例的代码留作练习。
无限和NaN
带有偏置的binary64指数字段集的所有位的输入表示正无穷大或负无穷大(binary64有效位数未设置任何位)或非数字(NaN)值。 Binary64 NaN和正无穷大应转换为它们的binary32等效项。负无穷大可能应转换为最大幅度的负binary32值。这些需要作为特殊情况处理。
这些案例的代码留作练习。