【问题标题】:map range of IEEE 32bit float [1:2) to some arbitrary [a:b)将 IEEE 32 位浮点数 [1:2) 的范围映射到任意 [a:b)
【发布时间】:2021-10-03 07:03:32
【问题描述】:

背景故事:具有任意端点的统一 PRNG

我有一个快速统一的伪随机数生成器,可以在 [1:2) 范围内创建统一的 float32 数字,即u : 1 <= u <= 2-eps。不幸的是,将端点 [1:2) 映射到任意范围 [a:b) 的端点在浮点数学中并非易事。我想通过简单的仿射计算来精确匹配端点。

正式声明

我想为1<=x<2 和任意a、b 创建一个IEEE-754 32 位浮点仿射函数f(x,a,b)正好 映射 1 -> anextlower(2) -> nextlower(b)

其中nextlower(q) 是下一个较低的 FP 可表示数字(例如,在 C++ 中 std::nextafter(float(q),float(q-1))

我尝试过的

简单映射f(x,a,b) = (x-1)*(b-a) + a 始终满足 f(1) 条件,但有时由于浮点舍入而无法满足 f(2) 条件。

本着Kahan summation 的精神,我尝试用免费的设计参数替换1 以消除FP 错误。 即与 f(x,c0,c1,c2) = (x-c0)*c1 + c2 一个数学解决方案是c0=1,c1=(b-a),c2=a(上面的简单映射), 但是额外的参数让我可以使用常量c0,c1,c2 来匹配端点。我不确定我是否充分理解 Kahan 求和背后的原理,以应用它们来确定参数,甚至确信存在解决方案。感觉就像我在黑暗中颠簸,其他人可能已经找到了光明。

除此之外:假设以下情况我很好

  • a
  • a 和 b 都远非零,即可以忽略次正规
  • a 和 b 相距足够远(以可表示的 FP 值衡量)以减轻非均匀量化并避免退化情况

更新

我正在使用 Chux 答案的修改形式来避免分裂。 虽然我不能 100% 确定我的重构保留了所有魔力,但它仍然适用于我的所有测试用例。

float lerp12(float x,float a,float b)
{
    const float scale = 1.0000001f;
    // scale = 1/(nextlower(2) - 1);
    const float ascale = a*scale;
    const float bscale = nextlower(b)*scale;
    return (nextlower(2) - x)*ascale + (x - 1.0f)*bscale;
}

请注意,只有最后一行 (5 FLOPS) 取决于 x,因此如果 (a,b) 保持不变,则可以重用其他行。

【问题讨论】:

  • [1...2) 有 N 个浮点成员。 [a...b) 有 M 个成员。将 N 映射到 M 或者稍微不均匀,或者某些 N 成员被忽略,需要重新调用 PSRN()。与此相关的编码目标是什么?
  • 是的,这两个范围内通常会有不同数量的可表示值。这会导致一些不均匀性。但我相信影响很小。我最关心的是匹配端点。
  • 当 a 和 b 靠得很近时,nextlower(2) -> nextlower(b) 在舍入到最近模式下不会自然地退出任何浮点计算(作为一个极端例如,考虑 a = nextlower (b))。您是否尝试过定向舍入模式?
  • @njuffa 我不太关心映射到数量如此之少的范围。重新采用不同的舍入模式:我不反对将其作为解决方案的一部分。
  • Mark,“将 [1:2) 映射到任意 [a:b)”和“1 -> a 和 nextlower(2) -> nextlower(b)”是矛盾的。你想要哪一个?假设这些相同是“有时不满足 f(2) 条件”问题的症结所在。这不是真正的 FP 舍入问题。

标签: random floating-point affinetransform


【解决方案1】:

OP 的目标

我想为 1 a 和 nextlower(2) - > nextlower(b)

这与“IEEE 32 位浮点 [1:2) 到任意 [a:b) 的映射范围”略有不同。


一般情况

x0 映射到y0x1y1 以及介于两者之间的各种xy

m = (y1 - y0)/(x1 - x0);
y = m*(x - x0) + y0;

OP的案例

// x0 = 1.0f;
// x1 = nextafterf(2.0f, 1.0f);
// y0 = a;
// y1 = nextafterf(b, a);

#include <math.h>  // for nextafterf()

float x = random_number_1_to_almost_2();
float m = (nextafterf(b, a) - a)/(nextafterf(2.0f, 1.0f) - 1.0f);
float y = m*(x - 1.0f) + a;

nextafterf(2.0f, 1.0f) - 1.0fx - 1.0fnextafterf(b, a) 是精确的,不会产生计算错误。
nextafterf(2.0f, 1.0f) - 1.0f 是一个略小于 1.0f 的值。


推荐

在端点处具有更好的对称性和数值稳定性的其他重组是可能的。

float x = random_number_1_to_almost_2();
float afactor = nextafterf(2.0f, 1.0f) - x;   // exact
float bfactor = x - 1.0f;                     // exact
float xwidth = nextafterf(2.0f, 1.0f) - 1.0f; // exact
// Do not re-order next line of code, perform 2 divisions
float y = (afactor/xwidth)*a + (bfactor/xwidth)*nextafterf(b, a);

注意afactor/xwidthbfactor/xwidth 在端点处都恰好为 0.0 或 1.0,因此满足“地图 1 -> a 和 nextlower(2) -> nextlower(b)”。不需要扩展精度。


OP 的 (x-c0)*c1 + c2 有问题,因为它将 (x-c0)*c1 除以 (2.0 - 1.0) 或 1.0(隐含),而它应该除以 nextafterf(2.0f, 1.0f) - 1.0f

【讨论】:

  • 谢谢。我在原始问题中添加了“更新”部分,以显示我对您的方法所做的一些细微修改。希望我的加速没有破坏任何东西。
  • 我不确定您更新中对比例的重新排序是否适用于所有 a,b - 也许会。
【解决方案2】:

基于融合乘加的简单 lerping 可以可靠地命中插值因子 0 和 1 的端点。对于 [1, 2) 中的 x,插值因子 x - 1 未达到统一,可以通过轻微修复通过将x-1(2.0f / nextlower(2.0f)) 相乘来进行拉伸。显然端点也需要调整为端点nextlower(b)。对于下面的 C 代码,我使用了问题中提供的 nextlower() 的定义,这可能不是提问者想要的,因为对于浮点数 q 足够大,q == (q - 1)

Asker 在 cmets 中表示,可以理解的是,这种映射不会导致伪随机数在区间 [a, b) 中完全均匀分布,只会大致如此,并且病理映射可能当 a 和 b 非常接近时发生。我还没有在数学上证明下面的map() 的实现保证了所需的行为,但对于大量随机测试用例来说似乎是这样。

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <math.h>

float nextlowerf (float q)
{
    return nextafterf (q, q - 1);
}

float map (float a, float b, float x)
{
    float t = (x - 1.0f) * (2.0f / nextlowerf (2.0f));
    return fmaf (t, nextlowerf (b), fmaf (-t, a, a));
}

float uint32_as_float (uint32_t a)
{
    float r;
    memcpy (&r, &a, sizeof(r));
    return r;
}

// George Marsaglia's KISS PRNG, period 2**123. Newsgroup sci.math, 21 Jan 1999
// Bug fix: Greg Rose, "KISS: A Bit Too Simple" http://eprint.iacr.org/2011/007
static uint32_t kiss_z=362436069, kiss_w=521288629;
static uint32_t kiss_jsr=123456789, kiss_jcong=380116160;
#define znew (kiss_z=36969*(kiss_z&65535)+(kiss_z>>16))
#define wnew (kiss_w=18000*(kiss_w&65535)+(kiss_w>>16))
#define MWC  ((znew<<16)+wnew )
#define SHR3 (kiss_jsr^=(kiss_jsr<<13),kiss_jsr^=(kiss_jsr>>17), \
              kiss_jsr^=(kiss_jsr<<5))
#define CONG (kiss_jcong=69069*kiss_jcong+1234567)
#define KISS ((MWC^CONG)+SHR3)

int main (void)
{
    float a, b, x, r;
    float FP32_MIN_NORM = 0x1.000000p-126f;
    float FP32_MAX_NORM = 0x1.fffffep+127f;
    
    do {
        do {
            a = uint32_as_float (KISS);
        } while ((fabsf (a) < FP32_MIN_NORM) || (fabsf (a) > FP32_MAX_NORM) || isnan (a));
        do {
            b = uint32_as_float (KISS);
        } while ((fabsf (b) < FP32_MIN_NORM) || (fabsf (b) > FP32_MAX_NORM) || isnan (b) || (b < a));

        x = 1.0f;
        r = map (a, b, x);
        if (r != a) {
            printf ("lower bound failed: a=%12.6a  b=%12.6a  map=%12.6a\n", a, b, r);
            return EXIT_FAILURE;
        }

        x = nextlowerf (2.0f);
        r = map (a, b, x);
        if (r != nextlowerf (b)) {
            printf ("upper bound failed: a=%12.6a  b=%12.6a  map=%12.6a\n", a, b, r);
            return EXIT_FAILURE;
        }
    } while (1);
    return EXIT_SUCCESS;
}

【讨论】:

    猜你喜欢
    • 2021-12-14
    • 2013-02-16
    • 1970-01-01
    • 1970-01-01
    • 2018-09-25
    • 1970-01-01
    • 2023-03-26
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多