朴素贝叶斯实现 - 准确性问题
编辑:可以在以下链接找到正确的代码版本: https://github.com/a7x/NaiveBayes-Classifier
我使用了来自openClassroom的数据,开始在Python中做一个简单的朴素贝叶斯分类器。步骤就是常规的训练和预测。我有几个问题,想知道为什么准确率这么差。
在训练时,我通过以下公式计算了对数似然:
log( P ( word | spam ) +1 ) /( spamSize + vocabSize .)
我的问题是:为什么在这种情况下要加上
vocabSize
呢?这是正确的做法吗?下面是使用的代码:#This is for training. Calculate all probabilities and store them in a vector. Better to store it in a file for easier access from __future__ import division import sys,os ''' 1. The spam and non-spam is already 50% . So they by default are 0.5 2. Now we need to calculate probability of each word , in spam and non-spam separately 2.1 we can make two dictionaries, defaultdicts basically, for spam and non-spam 2.2 When time comes to calculate probabilities, we just need to substitute values ''' from collections import * from math import * spamDict = defaultdict(int) nonspamDict = defaultdict(int) spamFolders = ["spam-train"] nonspamFolders = ["nonspam-train"] path = sys.argv[1] #Base path spamVector = open(sys.argv[2],'w') #WRite all spam values into this nonspamVector = open(sys.argv[3],'w') #Non-spam values #Go through all files in spam and iteratively add values spamSize = 0 nonspamSize = 0 vocabSize = 264821 for f in os.listdir(os.path.join(path,spamFolders[0])): data = open(os.path.join(path,spamFolders[0],f),'r') for line in data: words = line.split(" ") spamSize = spamSize + len(words) for w in words: spamDict[w]+=1 for f in os.listdir(os.path.join(path,nonspamFolders[0])): data = open(os.path.join(path,nonspamFolders[0],f),'r') for line in data: words = line.split(" ") nonspamSize = nonspamSize + len(words) for w in words: nonspamDict[w]+=1 logProbspam = {} logProbnonSpam = {} #This is to store the log probabilities for k in spamDict.keys(): #Need to calculate P(x | y = 1) numerator = spamDict[k] + 1 # Frequency print 'Word',k,' frequency',spamDict[k] denominator = spamSize + vocabSize p = log(numerator/denominator) logProbspam[k] = p for k in nonspamDict.keys(): numerator = nonspamDict[k] + 1 #frequency denominator = nonspamSize + vocabSize p = log(numerator/denominator) logProbnonSpam[k] = p for k in logProbnonSpam.keys(): nonspamVector.write(k+" "+str(logProbnonSpam[k])+"\n") for k in logProbspam.keys(): spamVector.write(k+" "+str(logProbspam[k])+"\n")
在预测时,我只是把一封邮件拆分成单词,分别计算垃圾邮件和非垃圾邮件的概率,然后把它们相乘0.5。哪个概率更高就标记为那个类别。下面是代码:
http://pastebin.com/8Y6Gm2my(由于某种原因,Stackoverflow又出问题了 :-/)
编辑:我已经去掉了 spam = spam + 1 的部分。现在我只是忽略那些单词
问题:我的准确率非常差。如下面所述。
No of files in spam is 130
No. of spam in ../NaiveBayes/spam-test is 53 no. of non-spam 77
No of files in non-spam is 130
No. of spam in ../NaiveBayes/nonspam-test/ is 6 no. of non-spam 124
请告诉我我哪里出错了。我觉得准确率低于50%说明实现中一定有明显的错误。
2 个回答
你可能犯了一个错误:你在模型文件中存储了对数概率(这是对的),但是在预测代码中,你却把它们当成了普通概率来用:
totalSpamP = spamP * 0.5
应该是
totalSpamP = spamP + math.log(0.5)
另外,我不太明白这一行代码在干嘛:
spamP = spamP + 1
这行代码似乎是在弥补训练集中垃圾邮件部分缺失的某个特征,但这些词应该直接被忽略。现在,它在一个概率上加了e(exp(1)
),这从定义上来说是不对的。
顺便说一下,我刚刚用我自己实现的朴素贝叶斯在这个训练集上进行了分类,得到了97.6%的准确率,所以你应该朝这个目标努力哦 :)
你的程序里有很多错误和不合理的假设,两个部分都有。以下是一些问题。
- 你在程序中硬编码了垃圾邮件和非垃圾邮件数量相同的假设。我建议不要这样做。虽然这不是绝对必要的,但在更一般的情况下,你需要去掉这个假设。
- 你在程序中硬编码了一个被当作词汇量的数字。我不建议这样做,因为这个数字在训练集修改时可能会改变。而且,这样做其实是不正确的。我建议在学习过程中计算这个值。
- 这可能不是错误,但你似乎把训练集中的所有单词都当作词汇。这可能不是最优的;实际上,你提到的页面建议只考虑所有邮件中的前2500个单词。不过,这对获得正确结果并不是必需的——即使不进行这样的筛选,我的实现也只会有几封邮件未被分类。
- 你错误地计算了只在垃圾邮件或非垃圾邮件中出现的单词的概率。它们在另一组中出现的对数概率不是你加的
1
,而是log(1/(spamSize+vocabSize))
或log(1/(nonspamSize+vocabSize))
,具体取决于它属于哪个组。这一点非常重要——你需要将这个概率与数据一起存储,以便程序正常运行。 - 你没有忽略在训练集中从未出现过的单词。实际上,这些单词可以用不同的方式处理,但你应该考虑它们。
- 由于预测函数中的缩进错误,你预测时使用的不是整个消息,而只是消息的第一行。这只是一个编程错误。
更新。你已经修复了第6点。同时,第1点在处理这个数据集时并不是严格必要的,第3点也不需要修复。
你的修改没有正确修复第4点或第5点。首先,如果某个单词在某个集合中从未出现,那么该消息的概率应该降低。忽略这个单词并不是一个好主意,你需要把它当作一个非常不可能的单词来考虑。
其次,你当前的代码是不对称的,因为在垃圾邮件中缺少的单词会取消对非垃圾邮件的检查(但反过来不成立)。如果你在异常处理程序中不需要做任何事情,使用pass
,而不是continue
,因为后者会立即跳到下一个for w in words:
的循环。
问题第2点仍然存在——你使用的词汇量与实际的词汇量不匹配。它应该是训练集中观察到的不同单词的数量,而不是所有消息中单词的总数。