提问人:0xbadf00d 提问时间:4/29/2023 最后编辑:0xbadf00d 更新时间:4/30/2023 访问量:193
如何最小化此求和中的数值误差?
How can I minimize the numerical error in this summation?
问:
我在下面的代码示例中遇到了一个数字错误(我在下面添加了 Kahan 求和尝试和一个更聪明的幼稚但仍然幼稚的版本;但不幸的是,情况更糟):
#include <algorithm>
#include <iostream>
#include <random>
#include <vector>
int main()
{
std::random_device rd;
std::mt19937 g{ rd() };
std::uniform_real_distribution<> u;
static std::size_t constexpr n = 1000;
std::vector<double> q(n);
std::generate_n(q.begin(), q.size(), [&]() { return u(g); });
double average_of_q{};
for (auto const& q : q)
average_of_q += q;
average_of_q /= n;
std::vector<double> f(n);
std::generate_n(f.begin(), n, [&]() { return u(g); });
double sum1{};
for (std::size_t i = 0; i < n; ++i)
sum1 += std::abs(f[i] - q[i]);
sum1 /= n;
{
double sum2{};
for (std::size_t i = 0; i < n; ++i)
sum2 += std::abs(f[i] - q[i]) - q[i];
sum2 = sum2 / n + average_of_q;
std::cout << "naive: " << std::abs(sum1 - sum2) << std::endl;
}
{
double sum2{},
c{};
for (std::size_t i = 0; i < n; ++i)
{
double const x = std::abs(f[i] - q[i]) - q[i] - c,
s = sum2 + x;
c = (s - sum2) - x;
sum2 = s;
}
sum2 = sum2 / n + average_of_q;
std::cout << "kahan: " << std::abs(sum1 - sum2) << std::endl;
}
{
double sum2{};
for (std::size_t i = 0; i < n; ++i)
{
if (f[i] - q[i] >= 0)
sum2 += f[i] - 2 * q[i];
else
sum2 -= f[i];
}
sum2 = sum2 / n + average_of_q;
std::cout << "more clever, but still naive: " << std::abs(sum1 - sum2) << std::endl;
}
return 0;
}
输出是 ,而我们理论上期望它应该是 。如何优化此代码以使其尽可能小?1.11022e-16
0
std::abs(sum1 - sum2)
为了激励这一点:在我的实际应用程序中,我已经知道并且我不需要遍历每个 ,因为我知道对于大多数 来说,这是非常小的,这就是为什么我需要将公式用于 。average_of_q
i
std::abs(f[i] - q[i])
i
sum2
编辑:我也在MSE上询问了这个问题的理论部分(但略有不同;我不想在这里把事情搞得太复杂):https://math.stackexchange.com/q/4688917/47771。
编辑2:我还试图通过将它们乘以一个因子来“提升”总和的项:
{
double sum2{};
for (std::size_t i = 0; i < n; ++i)
sum2 += 1000 * (std::abs(f[i] - q[i]) - q[i]);
sum2 = sum2 / (1000 * n) + average_of_q;
std::cout << "boosted: " << std::abs(sum1 - sum2) << std::endl;
}
编辑3:
这可能是一个有用的信息:在我的实际应用中,与 .为简单起见,您可以假设所有 ,其中许多都在 1e-10 左右,但有一些接近 。f[i]
q[i]
q[i] = 1
f[i]
1
答:
这种情况基本上可以归结为比较计算相同数量的多种数学等效方法之间的累积舍入误差。正如在评论中指出的那样,为了尽量减少使用有限精度浮点计算时的数值差异,中间计算必须以高于目标精度的方式进行。具体而言,在此代码中,这需要应用于 、 和 的计算。average_of_q
sum1
sum2
此处的目标精度为 ,很可能映射到 IEEE-754 二进制浮点格式。各种编译器提供了某种形式的四精度浮点类型,这些类型可能映射到 IEEE-754 的 .例如,使用英特尔编译器 () 时,将提供该类型,并且适用于此代码。然而,更便携的解决方案可以使用 Kahan 求和来累积准四倍精度。下面演示了这一点。double
binary64
binary128
icl
_Quad
从软件的角度来看,重要的是指示编译器不要重新关联浮点表达式,以便保留 Kahan 求和的数值属性。要强制执行的命令行标志因编译器而异,在我的情况下是 ,并且以下代码的整个编译器调用是/fp:strict
icl /W4 /Ox /QxHOST /fp:strict array_sum_issue.cpp
有区别 ||通常为 0,但偶尔为 2-54 对应双精度 epsilon。n=1000000
sum1 - sum2
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <random>
#include <vector>
int main()
{
std::random_device rd;
std::mt19937 g{ rd() };
std::uniform_real_distribution<> u;
static std::size_t constexpr n = 1000000;
std::vector<double> q(n);
std::generate_n(q.begin(), q.size(), [&]() { return u(g); });
double average_of_q{};
{
double sum = 0, c = 0;
for (std::size_t i = 0; i < n; ++i) {
double y = q[i] - c;
double t = sum + y;
c = (t - sum) - y;
sum = t;
}
average_of_q = sum / n;
}
std::vector<double> f(n);
std::generate_n(f.begin(), n, [&]() { return u(g); });
double sum1{};
{
double sum = 0, c = 0;
for (std::size_t i = 0; i < n; ++i) {
double y = std::abs(f[i] - q[i]) - c;
double t = sum + y;
c = (t - sum) - y;
sum = t;
}
sum1 = sum / n;
}
double sum2{};
{
double sum = 0, c = 0;
for (std::size_t i = 0; i < n; ++i) {
double y = (std::abs(f[i] - q[i]) - q[i]) - c;
double t = sum + y;
c = (t - sum) - y;
sum = t;
}
sum2 = std::fma (sum, 1.0 / n, average_of_q);
}
double diff = std::abs (sum1 - sum2);
printf ("average_of_q = % 23.16e (% 23.13a)\n", average_of_q, average_of_q);
printf ("sum1 = % 23.16e (% 23.13a)\n", sum1, sum1);
printf ("sum2 = % 23.16e (% 23.13a)\n", sum2, sum2);
printf ("|sum1-sum2| = % 23.16e (% 23.13a)\n", diff, diff);
return EXIT_SUCCESS;
}
上述程序的示例输出(根据生成的随机数,数值会有所不同):
average_of_q = 5.0031728235599426e-01 ( 0x1.0029963aaf686p-1)
sum1 = 3.3347262871877092e-01 ( 0x1.5579d949d5452p-2)
sum2 = 3.3347262871877092e-01 ( 0x1.5579d949d5452p-2)
|sum1-sum2| = 0.0000000000000000e+00 ( 0x0.0000000000000p+0)
评论
std::mt19937 g{ rd() };
std::mt19937 g{ 0 };
diff
5.55112e-17
sum/n
std::fma
std::fma(a, b, c)
std::fma(b, a, c)
a
b
fma(a,b,c) == fma(b,a,c)
上一个:如何截断计算列中的小数点?
评论