在我的课程项目中,我开始在C语言中实现“朴素贝叶斯分类器”。我的项目是使用大量训练数据实现一个文档分类应用程序(特别是垃圾邮件)。
现在我在实现算法时遇到了问题,因为C语言的数据类型有限制。
(我使用的算法在这里给出,http://en.wikipedia.org/wiki/Bayesian_spam_filtering)
问题陈述:该算法涉及到对文档中的每个单词进行处理,并计算其为垃圾邮件单词的概率。如果p1, p2, p3 …. pn分别是单词1, 2, 3 … n的概率。文档被判定为垃圾邮件或非垃圾邮件的概率是通过以下方式计算的:
这里,概率值很容易达到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; /* 模拟一些不太可能是垃圾邮件的单词 */ 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); } /* 模拟大量明显是垃圾邮件的单词 */ 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数据类型,但仍然存在相同的问题。
因此,假设我在分析一个包含10万个单词的文档,如果其中只有162个单词的垃圾邮件概率为1%,而剩下的99838个单词明显是垃圾邮件,那么由于精度错误(因为分子很容易变为零),我的应用程序仍然会将其判定为非垃圾邮件文档!!!
这是我第一次遇到这样的问题。那么,究竟应该如何解决这个问题呢?
回答:
你的问题是因为你收集了太多不考虑其大小的项。一个解决方案是取对数。另一个是排序你的各个项。首先,让我们将方程重写为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)
溢出时溢出。所以,如果中间结果溢出,就不会有后续的精度损失。