Быстрый расчет RMS дает NaN в Java - ошибка с плавающей запятой?

Я получаю озадачивающий результат, выполняя математику с поплавками. У меня есть код, который никогда не должен давать отрицательное число, генерирующее отрицательное число, что вызывает NaN, когда я пытаюсь извлечь квадратный корень.

Этот код очень хорошо работает в тестах. Однако при работе с реальными числами (т. е. потенциально очень маленькими, с семью и восемью отрицательными показателями) числами в конечном итоге сумма становится отрицательной, что приводит к NaN. Теоретически шаг вычитания удаляет только число, которое уже было добавлено к sum; это проблема с ошибкой с плавающей запятой? Есть ли способ это исправить?

Код:

public static float[] getRmsFast(float[] data, int halfWindow) {
    int n = data.length;
    float[] result = new float[n];
    float sum = 0.000000000f;
    for (int i=0; i<2*halfWindow; i++) {
        float d = data[i];
        sum += d * d;
    }
    result[halfWindow] = calcRms(halfWindow, sum);

    for (int i=halfWindow+1; i<n-halfWindow; i++) {
        float oldValue = data[i-halfWindow-1];
        float newValue = data[i+halfWindow-1];
        sum -= (oldValue*oldValue);
        sum += (newValue*newValue);
        float rms = calcRms(halfWindow, sum);
        result[i] = rms;
    }

    return result;
}

private static float calcRms(int halfWindow, float sum) {
    return (float) Math.sqrt(sum / (2*halfWindow));
}

Для некоторого фона: я пытаюсь оптимизировать функцию, которая вычисляет функцию скользящего среднеквадратичного значения (RMS) для данных сигнала. Оптимизация очень важна; это горячая точка в нашей обработке. Основное уравнение простое: http://en.wikipedia.org/wiki/Root_mean_square - Sum квадраты данных над окном, разделить сумму на размер окна, затем взять квадрат.

Исходный код:

public static float[] getRms(float[] data, int halfWindow) {
    int n = data.length;
    float[] result = new float[n];
    for (int i=halfWindow; i < n - halfWindow; i++) {
        float sum = 0;
        for (int j = -halfWindow; j < halfWindow; j++) {
            sum += (data[i + j] * data[i + j]);
        }
        result[i] = calcRms(halfWindow, sum);
    }
    return result;
}

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

Я довольно тщательно проверил индексы массива в новой версии. Кажется, он работает так, как задумано, но я, конечно, могу ошибаться в этой области!

Обновление: с нашими данными было достаточно изменить тип sum на двойной. Не знаю, почему мне это не пришло в голову. Но я оставил отрицательную проверку. И FWIW, я также смог реализовать решение, в котором пересчет суммы каждые 400 выборок давал большое время выполнения и достаточную точность. Спасибо.


person AbGator    schedule 14.03.2013    source источник
comment
попробуйте с double вместо float. Но проверка на негатив и тогда понадобится наверное.   -  person Joop Eggen    schedule 14.03.2013
comment
Каков диапазон ваших данных и каково максимальное значение halfWindow? Ваши данные float имеют 24-битные значащие символы. Их точные квадраты имеют 48 бит или меньше. Если вы масштабируете float до целого числа и конвертируете в long, у вас остается 15 свободных битов, поэтому может быть возможно сохранить сумму с точной арифметикой в ​​long, если диапазон диапазона не слишком велик, а половина окна не слишком велика. большой. Скорее всего, это возможно только в том случае, если все ваши данные близки к 1e-7 и 1e-8, которые вы упомянули. Большие данные сделают диапазон слишком большим. Может подойти подход «голова и хвост» с double.   -  person Eric Postpischil    schedule 14.03.2013


Ответы (3)


это проблема с ошибкой с плавающей запятой?

Да это так. Из-за округления вы вполне можете получить отрицательные значения после вычитания предыдущего слагаемого.

Например:

    float sum = 0f;
    sum += 1e10;
    sum += 1e-10;
    sum -= 1e10;
    sum -= 1e-10;
    System.out.println(sum);

На моей машине это печатает

-1.0E-10

хотя математически результат ровно нулевой.

Такова природа плавающей запятой: 1e10f + 1e-10f дает точно такое же значение, как 1e10f.

Что касается стратегий смягчения последствий:

  1. Вы можете использовать double вместо float для повышения точности.
  2. Время от времени вы можете полностью пересчитать сумму квадратов, чтобы уменьшить влияние ошибок округления.
  3. Когда сумма становится отрицательной, вы можете либо выполнить полный пересчет, как в (2) выше, либо просто установить сумму равной нулю. Последнее безопасно, поскольку вы знаете, что будете приближать сумму к ее истинному значению и никогда не отклоняться от него.
person NPE    schedule 14.03.2013
comment
Любые предложения по ее решению? Я мог бы действительно использовать выигрыш во время выполнения от этого, если бы я мог заставить его работать. Я мог бы просто округлить до нуля, если бы он стал отрицательным, но мне это немного не нравится. - person AbGator; 14.03.2013
comment
@AbGator: Установка суммы на ноль - это не хак, чем кажется. См. (3) в моем последнем редактировании. - person NPE; 14.03.2013

Попробуйте проверить свои индексы во втором цикле. Последнее значение i будет n-halfWindow-1, а n-halfWindow-1+halfWindow-1 равно n-2.

Возможно, вам придется изменить цикл на for (int i=halfWindow+1; i<n-halfWindow+1; i++).

person user1149913    schedule 14.03.2013
comment
Хотя это и не настоящая причина плохого поведения... Думаю, вы правы! - person AbGator; 14.03.2013

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

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

Или просто поверьте мне на слово и знайте, что каждое число с плавающей запятой «довольно близко» к запрошенному значению, причем некоторые из них «абсолютно точны», но большинство «в основном» точны. Это означает, что вам нужно учитывать ошибку измерения и помнить о ней после вычислений, иначе вы рискуете полагать, что у вас есть точный результат в конце вычисления значения (чего у вас нет).

person Edwin Buck    schedule 14.03.2013