Эффективный способ вычисления среднего геометрического числа чисел

Мне нужно вычислить среднее геометрическое для большого набора чисел, значения которых не ограничены априори. Наивный способ был бы

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 отдельно?

Ответы

Ответ 1

Решение "раскол экспоненты и мантиссы":

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 может переполняться, вы можете определить его как double вместо long long и умножить на invN на каждом шаге, но вы можете потерять много точности с этим подход.

РЕДАКТИРОВАТЬ. Для больших входов мы можем разбить вычисление в нескольких ведрах:

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;
}

Ответ 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());
}

Плохая новость: это связано с веткой. Хорошая новость: предиктор отрасли, скорее всего, почти всегда будет прав (ветвь должна срабатывать только редко).

Филиалу можно было бы избежать, используя идею Петра о постоянном числе терминов в продукте. Проблема в том, что переполнение/недополнение может все еще происходить только в нескольких терминах, в зависимости от значений.

Ответ 3

Вы можете ускорить это путем умножения чисел, как в исходном решении, и только преобразования в логарифмы каждого определенного количества умножений (в зависимости от размера ваших начальных чисел).

Ответ 4

Другой подход, который обеспечивал бы лучшую точность и производительность, чем метод логарифма, заключался бы в том, чтобы компенсировать экспоненты вне диапазона на фиксированную сумму, поддерживая точный логарифм отмененного избытка. Например:

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);

Все умножения на BIG и SMALL точны, и нет вызовов на log (трансцендентальная и, следовательно, особенно неточная) функция.

Ответ 5

Существует простая идея сократить вычисления, а также предотвратить переполнение. Вы можете группировать числа, как минимум, по два, и вычислять их журнал, а затем оценивать их сумму.

log(abcde) = 5*log(K)

log(ab) + log(cde)  = 5*log(k)

Ответ 6

Суммирование журналов для вычисления продуктов стабильно прекрасно и довольно эффективно (если этого недостаточно: существуют способы получения векторизованных логарифмов с несколькими операциями SSE - есть также векторные операции Intel MKL).

Чтобы избежать переполнения, общей методикой является деление каждого числа на максимальную или минимальную запись заблаговременно (или разницу в сумме в лог-маке или лог-мин). Вы также можете использовать ведра, если цифры сильно различаются (например, суммируйте журнал небольших чисел и больших чисел отдельно). Обратите внимание, что обычно ни одно из них не требуется, за исключением очень больших наборов, поскольку журнал double никогда не бывает огромным (между -700 и 700).

Кроме того, вам нужно отслеживать знаки отдельно.

Вычисление log x обычно имеет такое же количество значащих цифр, что и x, за исключением случаев, когда x близок к 1: вы хотите использовать std::log1p, если вам нужно вычислить prod(1 + x_n) с небольшим x_n.

Наконец, если у вас возникли проблемы с округлением при суммировании, вы можете использовать суммирование Kahan или варианты.

Ответ 7

Вместо использования логарифмов, которые являются очень дорогими, вы можете напрямую масштабировать результаты по степеням двух. В псевдокоде:

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