WedX - журнал о программировании и компьютерных науках

Оценка умножения с экспоненциальной функцией

Я пытаюсь придумать хороший способ оценить следующую функцию

double foo(std::vector<double> const& x, double c = 0.95)
{
   auto N = x.size(); // Small power of 2 such as 512 or 1024
   double sum = 0;
   for (auto i = 0; i != N; ++i) {
     sum += (x[i] * pow(c, double(i)/N));
   }
   return sum;
}

Две мои основные проблемы с этой наивной реализацией — это производительность и точность. Поэтому я подозреваю, что самым тривиальным улучшением будет изменение порядка цикла: for (auto i = N-1; i != -1; --i) (-1 зацикливается, это нормально). Это повышает точность, добавляя сначала меньшие термины.

Хотя это хорошо для точности, это сохраняет проблему производительности pow. Численно pow(c, double(i)/N) равно pow(c, (i-1)/N) * pow(c, 1/N). И последнее является константой. Так что теоретически мы можем заменить pow повторным умножением. Хотя это хорошо для производительности, это вредит точности — ошибки будут накапливаться.

Я подозреваю, что здесь скрывается значительно лучший алгоритм. Например, тот факт, что N является степенью двойки, означает, что существует средний член x[N/2], который умножается на sqrt(c). Это намекает на рекурсивное решение.

В несколько связанном числовом наблюдении это выглядит как умножение сигнала на экспоненту, поэтому я, естественно, думаю: «БПФ, тривиальная свертка = сдвиг, ОБПФ», но это, похоже, не дает реальной выгоды с точки зрения точности или производительности.

Итак, это известная проблема с известными решениями?

10.07.2018

  • если c не сильно различаются, вы можете создать таблицы для pow(c, ...) 10.07.2018
  • Кстати, это не свертка, а просто скалярное произведение, поэтому я не думаю, что БПФ здесь имеет какое-либо значение. 10.07.2018
  • @geza: согласен. Это умножение; БПФ превратило бы его в (тривиальную) свертка. Но цена FFT и IFFT кажется высокой (O N log N). 10.07.2018
  • Ага. БПФ следует использовать только тогда, когда требуется свертка, чтобы сделать алгоритм от O (N ^ 2) до O (N logN). Но ваш алгоритм уже наилучший из возможных, O(N), так что вы не сможете улучшить его, переключив алгоритм. Вы можете просто оптимизировать его, поэтому постоянный коэффициент будет меньше. 10.07.2018
  • Каков диапазон c ? 10.07.2018
  • Это просто полиномиальная оценка. Схема Хорнера является эффективным методом для этого и уменьшает количество ошибок за счет минимального количества операций. 10.07.2018
  • @LutzL: значение переменной очень близко к 1, поэтому оценка мощностей как продуктов не будет такой точной. 10.07.2018
  • @YvesDaoust: [0.5 , 1.0) Думаю. Но он определенно неравномерно распределен в этом диапазоне. Я показал 0,95 по умолчанию, потому что это типичное значение. И действительно, 0,95^(1/1024) примерно равно 0,99995. 10.07.2018
  • @LutzL: Схема Хорнера - это один из способов заменить pow повторным умножением, как я упоминал во втором абзаце. Но Ив прав, проблема здесь в численной точности таких значений, как 0,99995. Это примерно 14 бит потерянной точности. 10.07.2018
  • [Человеку, который пытается удалить тег C++, см. ответ LutzL. Соответствующие функции C++, такие как expm1, оправдывают тег C++. ] 10.07.2018
  • @MSalters Вы действительно думаете, что этот вопрос является вопросом C ++, а не общим для большинства языков? 10.07.2018
  • @Pi: в C++11 не было функции expm1, поэтому даже версия имеет значение. 10.07.2018
  • @MSalters: в ​​большинстве математических библиотек это было раньше, поскольку C99 является стандартом для math.h/cmath, C++11 только унифицировал шаблон C++, так что больше нет expm1f. 10.07.2018
  • @MSalters cppref утверждает, что expm1 должен быть там, начиная с C++11. Или я как-то ошибаюсь? 10.07.2018
  • @Pi: Мог бы поклясться, что это было добавлено в 14 году. Тем не менее, это не функция, которую вы найдете в каждом языке, это было моей точкой зрения. Например. В Javascript его нет. 10.07.2018
  • @MSalters Понял. Извините за беспокойство :D 10.07.2018
  • Вы действительно вычисляете только одно значение или вычисляете экспоненциально затухающее скользящее среднее? В последнем случае целесообразны методы свертки с ускорениями методами БПФ. 10.07.2018
  • @LutzL: мы действительно вычисляем 1 значение для каждых N входных данных. Однако мы делаем это несколько тысяч раз в секунду, так как у нас легко могут быть миллионы входных данных в секунду. 11.07.2018
  • А входы всегда разные, не из сдвигающегося окна входной последовательности? То есть x[0..N-1], затем x[N..2N-1] и т.д. вместо x[0..N-1], затем x[1..N], затем x[2 ..N+1] и т. д.? А в массовых оценках ц всегда один и тот же на протяжении долгого времени или часто меняется? 11.07.2018
  • @LutzL: Действительно. Нулевое перекрытие. Каждый вход влияет на один выход, каждый выход зависит только от N входов и текущего значения c. 11.07.2018

Ответы:


1

Задача представляет собой полиномиальную оценку. Методом одиночной оценки с наименьшим количеством операций является схема Хорнера. Как правило, низкое количество операций уменьшает накопление шума с плавающей запятой.

Поскольку значение примера c=0.95 близко к 1, любой корень будет еще ближе к 1 и, таким образом, потеряет точность. Избегайте этого, вычисляя разницу в 1 напрямую, z=1-c^(1/n) через

z = -expm1(log(c)/N).

Теперь вам нужно вычислить многочлен

sum of x[i] * (1-z)^i

что можно сделать путем тщательной модификации схемы Горнера. Вместо

for(i=N; i-->0; ) {
  res = res*(1-z)+x[i]
}

использовать

for(i=N; i-->0; ) {
  res = (res+x[i])-res*z
}

что математически эквивалентно, но потеря цифр в 1-z происходит как можно позже без использования более сложного метода, такого как сложение с двойной точностью.


В тестах эти два метода, вопреки замыслу, дали почти одинаковые результаты, существенное улучшение можно было наблюдать, разделив результат на его значение в c=1, z=0 и кратное z, как в

double res0 = 0, resz=0;
int i;
for(i=N; i-->0; ) { 
    /* res0+z*resz = (res0+z*resz)*(1-z)+x[i]; */
    resz = resz - res0 -z*resz;
    res0 = res0 + x[i];
}

Тестовый пример, который показал это улучшение, был для последовательности коэффициентов

f(u) = (1-u/N)^(N-2)*(1-u)

где для N=1000 оценки приводят к

   c               z=1-c^(1/N)             f(1-z)         diff for 1st proc     diff for 3rd proc

  0.950000     0.000051291978909      0.000018898570629  1.33289104579937e-17  4.43845264361253e-19
  0.951000     0.000050239954368      0.000018510931892  1.23765066121009e-16  -9.24959978401696e-19
  0.952000     0.000049189034371      0.000018123700958  1.67678642238461e-17  -5.38712954453735e-19
  0.953000     0.000048139216599      0.000017736876972  -2.86635949350855e-17  -2.37169225231204e-19
...
  0.994000     0.000006018054217      0.000002217256601  1.31645860662263e-17  1.15619997300212e-19
  0.995000     0.000005012529261      0.000001846785028  -4.15668713370839e-17  -3.5363625547867e-20
  0.996000     0.000004008013365      0.000001476685973  8.48811716443534e-17    8.470329472543e-22
  0.997000     0.000003004504507      0.000001106958687  1.44711343873661e-17  -2.92226366802734e-20
  0.998000     0.000002002000667      0.000000737602425   5.6734266807093e-18  -6.56450534122083e-21
  0.999000     0.000001000499833      0.000000368616443  -3.72557383333555e-17  1.47701370177469e-20
10.07.2018
  • с наименьшим количеством операций: для оценки этих мощностей, что касается накопления ошибок, схема Хорнера достигает наихудшего количества операций, O(N). Можно разрабатывать решения только с операциями O(Log N). 10.07.2018
  • Для полиномиальной оценки с общей последовательностью коэффициентов вы получаете не лучше, чем O (N), поскольку каждый из N коэффициентов должен быть включен в процедуру. Таким образом, N умножений и N добавлений довольно мало. Многоточечная оценка - еще одна проблема, когда методы, основанные на Фурье или интерполяции, обеспечивают лучшую сложность, чем O (N) на точку выборки. 10.07.2018
  • Нет, я не говорю о временной сложности, я говорю о накоплении ошибок при одноточечной оценке. [Кстати, метод, о котором я говорю, сохраняет временную сложность O(N).] 10.07.2018
  • expm1 кажется важным шагом во избежание потери точности. 10.07.2018
  • @YvesDaoust: Вы думаете о попарном сложении? Я думаю, это может принести пользу моему ответу еще больше. Я бы сначала добавил x[i] + c^(1/N)*x[i+1] ко всем четным i, а результат сохранил в x[i]. Затем добавьте x[i] + c^(2/N) * x[i+2], сохраните в x[i] и так далее. 10.07.2018
  • @MSalters: известно ли что-нибудь о распространении Xi? 10.07.2018
  • @YvesDaoust: StDev(x) > Average(x), измеренные образцы непрерывного физического сигнала, поэтому нетривиальная корреляция между соседними значениями (определенно не IID). 10.07.2018
  • @MSalters: если диапазон значений велик, может быть полезно добавить от меньшего к большему, предпочтительно используя сортировку по линейному времени. 10.07.2018

  • 2

    Ответ Ива вдохновил меня.

    Кажется, что лучший подход — вычислять pow(c, 1.0/N) не напрямую, а косвенно:

    cc[0]=c; cc[1]=sqrt(cc[0]), cc[2]=sqrt(cc[1]),... cc[logN] = sqrt(cc[logN-1])

    Или в двоичном формате,

    cc[0]=c, cc[1]=c^0.1, cc[2]=c^0.01, cc[3]=c^0.001, ....

    Теперь, если нам нужно x[0b100100] * c^0.100100, мы можем рассчитать это как x[0b100100]* c^0.1 * c^0.0001. Мне не нужно предварительно вычислять таблицу размера N, как предложил geza. Таблицы размера log(N), вероятно, достаточно, и ее можно создать, многократно извлекая квадратные корни.

    [править] Как указано в ветке комментариев к другому ответу, попарное суммирование очень эффективно для контроля над ошибками. И это очень хорошо сочетается с этим ответом.

    Начнем с наблюдения, что мы суммируем

    x[0] * c^0.0000000
    x[1] * c^0.0000001
    x[2] * c^0.0000010
    x[3] * c^0.0000011
    ...
    

    Итак, мы запускаем log(N) итераций. На итерации 1 мы добавляем N/2 пар x[i]+x[i+1]*c^0.000001 и сохраняем результат в x[i/2]. На итерации 2 мы добавляем пары x[i]+x[i+1]*c^0.000010 и так далее. Основное отличие от обычного попарного суммирования состоит в том, что это умножение и сложение на каждом шаге.

    Теперь мы видим, что на каждой итерации мы используем один и тот же множитель pow(c, 2^i/N), что означает, что нам нужно вычислить только множители log(N). Это также довольно эффективно с точки зрения кэширования, поскольку мы выполняем доступ только к непрерывной памяти. Это также позволяет легко распараллеливать SIMD, особенно когда у вас есть инструкции FMA.

    10.07.2018

    3

    Если N является степенью 2, вы можете заменить оценки степеней геометрическими средствами, используя

    a^(i+j)/2 = √(a^i.a^j)
    

    и рекурсивно разделить от c^N/N.c^0/N. С рекурсией предварительного заказа вы можете накапливать, увеличивая веса.

    В любом случае, ускорение sqrt по сравнению с pow может быть незначительным.

    Вы также можете остановить рекурсию на определенном уровне и продолжить линейно с простыми произведениями.

    10.07.2018

    4

    Вы можете смешать многократное умножение на pow(c, 1./N) с некоторыми явными вызовами pow. т.е. каждая 16-я итерация или около того делает реальное pow и в противном случае продвигается вперед с умножением. Это должно дать большие преимущества в производительности при незначительной стоимости точности.

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

    10.07.2018
  • Несколько более логичным выбором будет каждая 8-я или 16-я итерация; 10 не делит 1024. 10.07.2018
  • Новые материалы

    Как создать диаграмму градиентной кисти с помощью D3.js
    Резюме: Из этого туториала Вы узнаете, как добавить градиентную кисть к диаграмме с областями в D3.js. Мы добавим градиент к значениям SVG и применим градиент в качестве заливки к диаграмме с..

    Я хотел выучить язык программирования MVC4, но не мог выучить его раньше, потому что это выглядит сложно…
    Просто начните и учитесь самостоятельно Я хотел выучить язык программирования MVC4, но не мог выучить его раньше, потому что он кажется мне сложным, и я бросил его. Это в основном инструмент..

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

    Объяснение документов 02: BERT
    BERT представил двухступенчатую структуру обучения: предварительное обучение и тонкая настройка. Во время предварительного обучения модель обучается на неразмеченных данных с помощью..

    Как проанализировать работу вашего классификатора?
    Не всегда просто знать, какие показатели использовать С развитием глубокого обучения все больше и больше людей учатся обучать свой первый классификатор. Но как только вы закончите..

    Работа с цепями Маркова, часть 4 (Машинное обучение)
    Нелинейные цепи Маркова с агрегатором и их приложения (arXiv) Автор : Бар Лайт Аннотация: Изучаются свойства подкласса случайных процессов, называемых дискретными нелинейными цепями Маркова..

    Crazy Laravel Livewire упростил мне создание электронной коммерции (панель администратора и API) [Часть 3]
    Как вы сегодня, ребята? В этой части мы создадим CRUD для данных о продукте. Думаю, в этой части я не буду слишком много делиться теорией, но чаще буду делиться своим кодом. Потому что..


    Для любых предложений по сайту: [email protected]