【问题标题】:What's the numerically best way to calculate the average计算平均值的最佳数值方法是什么
【发布时间】:2020-12-23 19:42:34
【问题描述】:

计算平均值的最佳方法是什么?有了这个问题,我想知道哪种计算平均值的算法在数值意义上是最好的。它应该具有最小的舍入误差,不应该对上溢或下溢等敏感。

谢谢。


附加信息:首选增量方法,因为值的数量可能不适合 RAM(对大于 4 GB 的文件进行多次并行计算)。

【问题讨论】:

  • 谁投票决定关闭,因为没有建设性,大错特错了。这是一个很好且恰当的问题。
  • 请注意,提出的不同算法并不相互排斥。读取 1 MB 块,对它们进行排序,对它们求和,然后对所有部分和使用 Kahan 求和是完全可行的。
  • 感谢您的所有 cmets。他们帮助我理解了我的问题。我会接受这篇论文作为答案,因为它提供了处理总和的不同方法的分析。

标签: c++ numerical-methods


【解决方案1】:

如果您想要 O(N) 算法,请查看 Kahan summation

【讨论】:

  • 显然 O(N) 会比排序方法更快。你知道 Kahan 是否比求和前排序更准确吗?
  • @David Heffernan:来自维基百科条目:“虽然 Kahan 的算法实现了对 n 个数字求和的 O(1) 误差增长,但通过成对求和只能实现稍差的 O(logn) 增长:递归将一组数字分成两半,将每一半相加,然后将两个和相加。”。我怀疑排序方法方法可以做到O(1)。
  • @yi_H 我不太遵循那里的逻辑。那里描述的成对求和似乎不涉及排序。无论如何,与 Kahan 或成对求和相比,排序显然非常昂贵。
  • @David:取决于您在排序后使用的确切求和算法。有关详细信息,请参阅citeseerx.ist.psu.edu/viewdoc/…。 100% 的准确度是可能的(即唯一的错误是由于结果的存储量有限)
【解决方案2】:

您可以查看http://citeseer.ist.psu.edu/viewdoc/summary?doi=10.1.1.43.3535(Nick Higham,“浮点求和的准确性”,SIAM Journal of Scientific Computation,1993 年)。

如果我没记错的话,如果所有数字都是正数,补偿求和(Kahan 求和)就很好,至少与对它们进行排序并按升序添加它们一样好(除非有非常多的数字)。如果有些数字是正数而有些是负数,那么这个故事就会复杂得多,这样你就会被取消。在这种情况下,有一个参数可以按降序添加它们。

【讨论】:

  • 将正数和负数分别相加总是有一种廉价的技巧。在这种情况下,算法的速度并不那么关键,只要它是 O(N);磁盘 I/O 将支配几乎任何数量的 FP 操作。
  • @MSalters 为什么要将它们分别相加?如果要最小化舍入误差,则中间结果应尽可能小(绝对值)。将它们分别相加会产生相反的效果。
  • 正如您自己所注意到的,如果所有数字都具有相同的符号,则补偿总和是好的。
  • 补偿总和仍然适用于正负项。它的误差是cond(S_n)u,其中cond(S_n)是求和的条件数,u是单位舍入。正则求和有错误~ncond(S_n)u。如果总和是病态的,那么任何建议都没有帮助。
  • 再想一想,关于将正面和负面部分分开求和的想法仍然存在问题。您会得到两个准确的结果,但是当这些子和大致相等时,最终结果中仍然会出现灾难性的抵消。
【解决方案3】:

按数量级升序对数字进行排序。将它们相加,首先是低震级。除以计数。

【讨论】:

  • 这里的数字升序排序和加法有何不同?
  • 为什么要排序?这不是容易溢出(总和可能溢出)吗?
  • @Jan 它减少了舍入。想想吧。
  • @Tobias 溢出对你来说真的是个问题吗?
  • @David:是的,可以。该计算可能用于数百万个样本。
【解决方案4】:

我总是使用下面的伪代码:

float mean=0.0; // could use doulbe
int n=0;  // could use long

for each x in data:
    ++n;
    mean+=(x-mean)/n;

我没有其稳定性的正式证明,但您可以看到,假设数据值表现良好,我们不会遇到数值溢出问题。它在 Knuth 的计算机编程艺术

中有所提及

【讨论】:

  • 请注意,对于floatn 越接近 2^23,准确度就越低。例如,一千万个值1.0 后跟一千万个值2.0 的序列将平均为1.269。这是因为(x-mean)/n 接近于零,而mean 在添加时不会改变。
  • @jpa 如果你担心这个,你可以先洗牌。
  • +1 因为这几乎与没有累积积累的天真实现一样简单。 Knuth 的哪本书,第 2 卷?
  • 这篇博文提供了这种方法的推理。 diego.assencio.com/?index=c34d06f4f4de2375658ed41f70177d59
【解决方案5】:

只是添加一个可能的答案以供进一步讨论:

逐步计算每一步的平均值:

AVG_n = AVG_(n-1) * (n-1)/n + VALUE_n / n

或成对组合

AVG_(n_a + n_b) = (n_a * AVG_a + n_b * AVG_b) / (n_a + n_b)

(希望公式够清楚)

【讨论】:

  • 诚然,我自己并没有解决,但是增量形式的重复划分似乎会产生更多的准确性损失。部分问题是1/n 在最低有效位中引入了错误,因此n/n != 1,至少当它作为三步操作(除-存储-乘)执行时。如果只执行一次除法,这将最小化,但您将在 GB 的数据上执行此操作。
  • 使用此公式,您不会遇到数据类型溢出的风险。
  • 很棒的公式。这是我在 C# 中的第一个实现:double average(IList<int> numbers, int avgUpTo1BasedIndex) { return (avgUpTo1BasedIndex == 1) ? numbers[0] : average(numbers, avgUpTo1BasedIndex - 1) * (avgUpTo1BasedIndex - 1) / avgUpTo1BasedIndex + (double)numbers[avgUpTo1BasedIndex - 1] / avgUpTo1BasedIndex; }
【解决方案6】:

一个很晚的帖子,但由于我没有足够的声誉来发表评论,@Dave 的方法是 Gnu Scientific Library 使用的方法(截至 2020 年 12 月)。

这是从mean_source.c中提取的代码:

double FUNCTION (gsl_stats, mean) (const BASE data[], const size_t stride, const size_t size)
{
/* Compute the arithmetic mean of a dataset using the recurrence relation mean_(n) = mean(n-1) + (data[n] - mean(n-1))/(n+1)   */

long double mean = 0;
size_t i;

for (i = 0; i < size; i++)
{
  mean += (data[i * stride] - mean) / (i + 1);
}

return mean;
}

GSL 使用相同的算法来计算方差,毕竟这只是给定数字的平方差的平均值。

【讨论】:

    猜你喜欢
    • 2018-07-28
    • 2019-12-05
    • 1970-01-01
    • 2022-01-27
    • 1970-01-01
    • 2020-04-25
    • 1970-01-01
    • 2014-05-26
    • 2023-03-13
    相关资源
    最近更新 更多