C中精度浮点运算的问题

对于我的课程项目之一,我开始在C中实现“朴素贝叶斯分类器”。我的项目是使用大量训练数据实现文档分类器应用程序(尤其是垃圾邮件)。

现在由于C数据类型的限制,我在实现算法时遇到了问题。

(我在这里使用的算法, http://en.wikipedia.org/wiki/Bayesian_spam_filtering )

问题陈述:该算法涉及获取文档中的每个单词并计算它是垃圾邮件单词的概率。 如果p1,p2 p3 …. pn是word-1,2,3 … n的概率。 使用以下方法计算doc是否为垃圾邮件的概率

替代文字

这里,概率值可以非常容易地在0.01左右。 因此,即使我使用数据类型“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,double甚至是long double数据类型,但仍然存在同样的问题。

因此,在我正在分析的100K字文件中,如果只有162个单词具有1%的垃圾邮件概率而剩余的99838个是明显的垃圾邮件单词,那么我的应用程序仍然会因为精度错误而将其称为非垃圾邮件doc(因为分子容易进行)到零)!!!

这是我第一次遇到这样的问题。 那么究竟应该如何解决这个问题呢?

您的问题是由于您收集太多条款而不考虑其大小而引起的。 一种解决方案是采用对数。 另一个是对您的个人条款进行排序。 首先,让我们将等式重写为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)溢出时,中间结果才会溢出。 因此,如果中间结果溢出,则不会出现后续的精度损失。

这通常发生在机器学习中。 AFAIK,关于精度损失你无能为力。 因此,为了绕过这一点,我们使用log函数并将除法和乘法转换为减法和加法。

所以我决定做数学,

原始等式是:

问题

我稍微修改一下:

在此处输入图像描述

记录两侧的日志:

在此处输入图像描述

让,

在此处输入图像描述

代,

在此处输入图像描述

因此,计算组合概率的替代公式:

在此处输入图像描述

如果您需要我对此进行扩展,请发表评论。

这是一个诀窍:

 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 )。 我们的想法是,不要将大量的小数字彼此相乘,以获得0 ,但是要得到两个小数的商。 例如,如果n = 1000000 and p_i = 0.5 for all in = 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) )和小分子与小分母分开。

编辑: 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) 

尝试计算逆1 / p。 这给你一个1 + 1 /(1-p1)*(1-p2)forms的等式……

如果你然后计算每个概率的出现 – 看起来你有少量的值重复 – 你可以使用pow()函数 – pow(1-p,occurences_of_p)* pow(1-q, occurrences_ofs) – 并避免每次乘法的单独舍入。

您可以使用概率百分比或promiles:

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

要么

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

或使用其他系数

我在数学方面不强,所以我无法对可能消除或减少问题的公式进行简化评论。 但是,我熟悉long double类型的精度限制,并且知道C的几个任意和扩展的精度数学库。检查:

http://www.nongnu.org/hpalib/和http://www.tc.umn.edu/~ringx004/mapm-main.html