web-dev-qa-db-ja.com

多くの数の幾何平均を計算する効率的な方法

値が事前に制限されていない多数の数値の幾何平均を計算する必要があります。素朴な方法は

_double geometric_mean(std::vector<double> const&data) // failure
{
  auto product = 1.0;
  for(auto x:data) product *= x;
  return std::pow(product,1.0/data.size());
}
_

ただし、これは、蓄積されたproductのアンダーフローまたはオーバーフローが原因で失敗する可能性があります(注:_long double_は実際にはこの問題を回避しません)。したがって、次のオプションは対数を合計することです。

_double geometric_mean(std::vector<double> const&data)
{
  auto sumlog = 0.0;
  for(auto x:data) sum_log += std::log(x);
  return std::exp(sum_log/data.size());
}
_

これは機能しますが、すべての要素に対してstd::log()を呼び出します。これは、潜在的に低速です。 それを回避できますか?たとえば、累積されたproductの指数と仮数(と同等)を別々に追跡することによって?

28
Walter

「指数と仮数の分割」ソリューション:

double geometric_mean(std::vector<double> const & data)
{
    double m = 1.0;
    long long ex = 0;
    double invN = 1.0 / data.size();

    for (double x : data)
    {
        int i;
        double f1 = std::frexp(x,&i);
        m*=f1;
        ex+=i;
    }

    return std::pow( std::numeric_limits<double>::radix,ex * invN) * std::pow(m,invN);
}

exがオーバーフローする可能性がある場合は、long longではなくdoubleとして定義し、すべてのステップでinvNを掛けることができますが、精度が大幅に低下する可能性があります。このアプローチで。

[〜#〜] edit [〜#〜]大きな入力の場合、計算をいくつかのバケットに分割できます。

double geometric_mean(std::vector<double> const & data)
{
    long long ex = 0;
    auto do_bucket = [&data,&ex](int first,int last) -> double
    {
        double ans = 1.0;
        for ( ;first != last;++first)
        {
            int i;
            ans *= std::frexp(data[first],&i);
            ex+=i;
        }
        return ans;
    };

    const int bucket_size = -std::log2( std::numeric_limits<double>::min() );
    std::size_t buckets = data.size() / bucket_size;

    double invN = 1.0 / data.size();
    double m = 1.0;

    for (std::size_t i = 0;i < buckets;++i)
        m *= std::pow( do_bucket(i * bucket_size,(i+1) * bucket_size),invN );

    m*= std::pow( do_bucket( buckets * bucket_size, data.size() ),invN );

    return std::pow( std::numeric_limits<double>::radix,ex * invN ) * m;
}
12
sbabbi

私はそれを行う方法を考え出したと思います。それは、ピーターの考えと同様に、質問の2つのルーチンを組み合わせたものです。これがサンプルコードです。

double geometric_mean(std::vector<double> const&data)
{
    const double too_large = 1.e64;
    const double too_small = 1.e-64;
    double sum_log = 0.0;
    double product = 1.0;
    for(auto x:data) {
        product *= x;
        if(product > too_large || product < too_small) {
            sum_log+= std::log(product);
            product = 1;      
        }
    }
    return std::exp((sum_log + std::log(product))/data.size());
}

悪いニュースは、これにはブランチが付属しているということです。良いニュース:分岐予測子はこれをほぼ常に正しくする可能性があります(分岐がトリガーされることはめったにありません)。

分岐は、製品に一定数の用語があるというPeterの考えを使用して回避できます。それに関する問題は、値によっては、オーバーフロー/アンダーフローがまだ数期間以内に発生する可能性があることです。

11
Walter

元のソリューションのように数値を乗算し、特定の乗算数ごとに対数に変換するだけで、これを加速できる場合があります(初期数値のサイズによって異なります)。

4
Peter de Rivaz

対数法よりも優れた精度とパフォーマンスを提供する別のアプローチは、範囲外の指数を固定量で補正し、キャンセルされた超過分の正確な対数を維持することです。そのようです:

const int EXP = 64; // maximal/minimal exponent
const double BIG = pow(2, EXP); // overflow threshold
const double SMALL = pow(2, -EXP); // underflow threshold

double product = 1;
int excess = 0; // number of times BIG has been divided out of product

for(int i=0; i<n; i++)
{
    product *= A[i];
    while(product > BIG)
    {
        product *= SMALL;
        excess++;
    }
    while(product < SMALL)
    {
        product *= BIG;
        excess--;
    }
}

double mean = pow(product, 1.0/n) * pow(BIG, double(excess)/n);

BIGSMALLによるすべての乗算は正確であり、log(超越関数、したがって特に不正確な関数)への呼び出しはありません。

3
Sneftel

非常に高価な対数を使用する代わりに、2の累乗で結果を直接スケーリングできます。

double geometric_mean(std::vector<double> const&data) {
  double huge = scalbn(1,512);
  double tiny = scalbn(1,-512);
  int scale = 0;
  double product = 1.0;
  for(auto x:data) {
    if (x >= huge) {
      x = scalbn(x, -512);
      scale++;
    } else if (x <= tiny) {
      x = scalbn(x, 512);
      scale--;
    }
    product *= x;
    if (product >= huge) {
      product = scalbn(product, -512);
      scale++;
    } else if (product <= tiny) {
      product = scalbn(product, 512);
      scale--;
    }
  }
  return exp2((512.0*scale + log2(product)) / data.size());
}
1
Jeffrey Sax

ログを合計して製品を安定して計算することは完全に問題なく、かなり効率的です(これだけでは不十分な場合:いくつかのSSE操作-IntelMKLのベクトル操作もあります)でベクトル化された対数を取得する方法があります)。

オーバーフローを回避するための一般的な手法は、すべての数値を事前に最大または最小の大きさのエントリで除算することです(または、ログの差をlogmaxまたはlogminに合計します)。数値が大きく異なる場合は、バケットを使用することもできます(たとえば、小さい数値と大きい数値のログを別々に合計します)。 doubleのログは決して巨大ではないため(たとえば-700から700の間)、非常に大きなセットを除いて、通常、これはどちらも必要ないことに注意してください。

また、標識を個別に追跡する必要があります。

log xの計算では、x1に近い場合を除いて、通常はxと同じ有効桁数を維持します。小さなstd::log1pprod(1 + x_n)を計算する必要がある場合はx_nを使用します。 。

最後に、合計時に丸め誤差の問題がある場合は、 Kahan summation またはバリアントを使用できます。

1
Alexandre C.

計算を減らし、オーバーフローを防ぐための簡単なアイデアがあります。一度に少なくとも2つの数値をグループ化し、それらの対数を計算してから、それらの合計を評価することができます。

log(abcde) = 5*log(K)

log(ab) + log(cde)  = 5*log(k)
1
Vikram Bhat