Проблема с точной работой с плавающей запятой в C

Для одного из моих курсовых проектов я начал внедрять "Наивный байесовский классификатор" в C. Мой проект - реализовать приложение классификатора документов (особенно Spam), используя огромные данные обучения.

Теперь у меня проблема с реализацией алгоритма из-за ограничений в типе C.

(Используется алгоритм, который я использую здесь, http://en.wikipedia.org/wiki/Bayesian_spam_filtering)

ЗАЯВЛЕНИЕ О ПРОБЛЕМЕ: Алгоритм включает в себя принятие каждого слова в документе и вычисление вероятности его спам-слова. Если p1, p2 p3.... pn - вероятности слова-1, 2, 3... n. Вероятность того, что doc является спамом или нет, вычисляется с использованием

alt text

Здесь значение вероятности может быть очень легко около 0,01. Поэтому, даже если я использую datatype "double", мой расчет будет идти за броском. Чтобы подтвердить это, я написал пример кода, приведенный ниже.

#define PROBABILITY_OF_UNLIKELY_SPAM_WORD     (0.01)
#define PROBABILITY_OF_MOSTLY_SPAM_WORD     (0.99)

int main()
{
    int index;
    long double numerator = 1.0;
    long double denom1 = 1.0, denom2 = 1.0;
    long double doc_spam_prob;

    /* Simulating FEW unlikely spam words  */
    for(index = 0; index < 162; index++)
    {
        numerator = numerator*(long double)PROBABILITY_OF_UNLIKELY_SPAM_WORD;
        denom2    = denom2*(long double)PROBABILITY_OF_UNLIKELY_SPAM_WORD;
        denom1    = denom1*(long double)(1 - PROBABILITY_OF_UNLIKELY_SPAM_WORD);
    }
    /* Simulating lot of mostly definite spam words  */
    for (index = 0; index < 1000; index++)
    {
        numerator = numerator*(long double)PROBABILITY_OF_MOSTLY_SPAM_WORD;
        denom2    = denom2*(long double)PROBABILITY_OF_MOSTLY_SPAM_WORD;
        denom1    = denom1*(long double)(1- PROBABILITY_OF_MOSTLY_SPAM_WORD);
    }
    doc_spam_prob= (numerator/(denom1+denom2));
    return 0;
}

Я попробовал Float, двойные и даже длинные двойные типы данных, но все еще та же проблема.

Следовательно, скажем, в документе на 100 тыс. слов, который я анализирую, если только 162 слова имеют 1% вероятности спама, а оставшиеся 99838 явно спам-слова, то мое приложение скажет это как не спам-документ из-за ошибки Precision (как числитель легко переходит в ZERO)!!!.

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

Ответы

Ответ 1

Ваша проблема вызвана тем, что вы собираете слишком много условий без учета их размера. Одним из решений является логарифмы. Другое - сортировать свои индивидуальные условия. Во-первых, перепишем уравнение как 1/p = 1 + ∏((1-p_i)/p_i). Теперь ваша проблема в том, что некоторые из этих терминов малы, а другие большие. Если у вас слишком много мелких терминов подряд, вы будете переполнены, и при слишком большом количестве терминов вы переполните промежуточный результат.

Итак, не ставьте слишком много одного порядка в строке. Отсоедините термины (1-p_i)/p_i. В результате первым будет наименьший срок, последний самый большой. Теперь, если вы сразу их размножаете, у вас все равно будет недостаток. Но порядок расчета не имеет значения. Используйте два итератора в свою временную коллекцию. Один начинается с начала (т.е. (1-p_0)/p_0), другой в конце (т.е. (1-p_n)/p_n), а ваш промежуточный результат начинается с 1.0. Теперь, когда ваш промежуточный результат равен >= 1.0, вы берете термин с фронта, и когда ваш итоговый результат равен < 1.0 вы берете результат со спины.

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

Там, конечно, реальная возможность переполнения. Если вход совершенно не является спамом (p = 1E-1000), тогда 1/p будет переполняться, потому что ∏((1-p_i)/p_i) переполняется. Но поскольку термины сортируются, мы знаем, что промежуточный результат будет переполняться только, если ∏((1-p_i)/p_i) переполняется. Таким образом, если промежуточный результат переполняется, то последующая потеря точности отсутствует.

Ответ 2

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

Я решил сделать математику,

Исходное уравнение:

Problem

Я немного изменяю его:

enter image description here

Взятие журналов с обеих сторон:

enter image description here

Пусть,

enter image description here

Подставив,

enter image description here

Следовательно, альтернативная формула для вычисления комбинированной вероятности:

enter image description here

Если вам нужно, чтобы я расширил это, оставьте комментарий.

Ответ 3

Вот трюк:

for the sake of readability, let S := p_1 * ... * p_n and H := (1-p_1) * ... * (1-p_n), 
then we have:

  p = S / (S + H)
  p = 1 / ((S + H) / S)
  p = 1 / (1 + H / S)

let`s expand again:

  p = 1 / (1 +  ((1-p_1) * ... * (1-p_n)) / (p_1 * ... * p_n))
  p = 1 / (1 + (1-p_1)/p_1 * ... * (1-p_n)/p_n)

Итак, в основном вы получите произведение довольно больших чисел (между 0 и, для p_i = 0.01, 99). Идея состоит в том, чтобы не умножать тонны небольших чисел друг на друга, чтобы получить, ну, 0, но сделать частное из двух небольших чисел. Например, если n = 1000000 and p_i = 0.5 for all i, указанный выше метод даст вам 0/(0+0), который равен NaN, тогда как предлагаемый метод даст вам 1/(1+1*...1), который равен 0.5.

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

  p = 1 / (1 + (1-p_1)/p_n * ... * (1-p_n)/p_1)

таким образом вы делите большие числители (малые p_i) с большими знаменателями (большие p_(n+1-i)) и малые числители с малыми знаменателями.

edit: MSalter предложил полезную дальнейшую оптимизацию в своем ответе. Используя это, формула выглядит следующим образом:

  p = 1 / (1 + (1-p_1)/p_n * (1-p_2)/p_(n-1) * ... * (1-p_(n-1))/p_2 * (1-p_n)/p_1)

Ответ 4

Попробуйте вычислить обратный 1/p. Это дает вам уравнение вида 1 + 1/(1-p1) * (1-p2)...

Если вы посчитаете появление каждой вероятности - похоже, что у вас есть небольшое количество возвращаемых значений, вы можете использовать функцию pow() - pow (1-p,хождения_of_p) * pow (1 -q, occences_of_q) - и избегать индивидуального округления с каждым умножением.

Ответ 5

Вы можете использовать вероятность в процентах или обещаниях:

doc_spam_prob= (numerator*100/(denom1+denom2));

или

doc_spam_prob= (numerator*1000/(denom1+denom2));

или используйте какой-либо другой коэффициент

Ответ 6

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

http://www.nongnu.org/hpalib/ а также http://www.tc.umn.edu/~ringx004/mapm-main.html