朴素贝叶斯的实现 - 准确率问题

3
编辑:可运行的正确代码版本位于以下链接:https://github.com/a7x/NaiveBayes-Classifier 我使用了来自openClassroom的数据,并开始在Python中开发一个小型的朴素贝叶斯分类器。步骤是通常的训练和预测。我有几个问题,想知道为什么准确率相当糟糕。
  1. For training, I calculated the log likelihood by the formula :

    log( P ( word | spam ) +1 ) /( spamSize + vocabSize .)

    My question is: why did we add the vocabSize in this case :( and is this the correct way of going about it? Code used is below:

    #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")
    
  2. For prediction, I just took a mail , split it into words, added all the probabilities, separately for spam/non-spam, and multiplied them by 0.5. Whichever was higher was the class label. Code is below:

    http://pastebin.com/8Y6Gm2my ( Stackoverflow was again playing games for some reason :-/)

编辑:我已经移除了 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%的准确率意味着实现中一定存在一些明显的错误。


你并不是在做“朴素贝叶斯”,而是在使用拉普拉斯平滑的贝叶斯算法,其中“k”(拉普拉斯平滑参数)等于1。这就是为什么你要将分子(k)加1,将词汇量大小(k * set_size)加到分母中。 - Jim Clay
你运行了上面的代码吗?它没有给你一个数学域错误吗?你需要将计数收集为浮点数,否则在除法中一切都会变成零。 - abhaga
前往https://www.ai-class.com/home/观看有关“机器学习”(第5课)的视频,了解更多关于拉普拉斯平滑的信息,以及它是否是正确的选择。使用它有利有弊。 - Jim Clay
@JimClay:在多项式朴素贝叶斯的几乎所有实际实现中,都会执行拉普拉斯或利德斯通平滑。 - Fred Foo
我认为“from future import division”部分已经解决了除法的问题。是的,我添加了拉普拉斯来弥补缺席者的不足。谢谢。 - crazyaboutliv
2个回答

2
你的程序存在多个错误和不良假设,分别在两个部分中。以下是其中几个:
1. 你在程序中硬编码了垃圾邮件和非垃圾邮件的数量相同这一事实。我建议不要硬编码这种假设。虽然这不是绝对必要的,但在更一般的情况下,你需要将其删除。
2. 你在程序中硬编码了某个数字,将其视为词汇量大小。我不建议这样做,因为这个数字可能会在训练集的任何修改上发生变化。此外,实际上它是不正确的。我建议在学习过程中计算它。
3. 这可能不是一个错误,但你似乎有一个包含训练集中所有单词的词汇表。这可能是次优的;实际上,你所参考的页面建议只考虑所有电子邮件中前2500个单词。然而,即使没有这种过滤,我的实现也只有几个邮件未被分类,这并不影响结果的正确性。
4. 你错误地计算了仅在垃圾邮件或非垃圾邮件中观察到的单词。它们在另一个子集中被发现的对数概率不是你添加的1,而是log(1/(spamSize+vocabSize))log(1/(nonspamSize+vocabSize)),具体取决于它所在的组。这实际上非常重要——你需要将这个概率与你的数据一起存储,以使程序正确运行。
5. 你不忽略从未出现在训练集中的单词。实际上,这些单词可以用不同的方式处理,但你应该考虑它们。
6. 由于预测函数中缩进的错误,你只预测消息的第一行而不是整个消息。这只是一个编程错误。
更新。你已经修复了6。同时,在处理此数据集时,1并不严格需要修复,3也不需要。你的修改没有正确修复4和5。首先,如果某个集合中从未观察到某个单词,那么它在该集合中的概率应该降低。忽略这个单词并不是一个好主意,你需要将其视为高度不可能的单词。其次,你当前的代码是不对称的,因为在垃圾邮件中不存在的单词会取消对非垃圾邮件的检查(但反之则不然)。如果你需要在异常处理程序中什么都不做,请使用pass而不是continue,因为后者会立即进入下一个for w in words:迭代。问题2仍然存在——你使用的词汇量大小与实际不符。它必须是在训练集中观察到的不同单词的数量,而不是所有邮件中单词的总数。

我将其硬编码,因为它实际上有相等数量的垃圾邮件和非垃圾邮件。 - crazyaboutliv
  1. 我会按照网站建议只取2500。
  2. 我已经更改了缩进,但是答案仍然不变,很遗憾:( 我将修改仅在垃圾邮件/非垃圾邮件中找到的单词的计算方式。谢谢。
- crazyaboutliv
目前主要的问题似乎是编号4。如果您从未在垃圾邮件中看到某些单词,则会使其高度不可能,您需要通过严厉惩罚此概率而不是通过添加一个来增加它。 - Tanriol
不,你错了。有问题的代码行是在predict函数中的except子句中的spamP = spamP + 1。还有一个关于非垃圾邮件的问题。 - Tanriol
嗨,谢谢。我已经更改了代码,只考虑前2500个单词,并且更改了词汇量。最终成功得到了正确的答案!所以再次感谢! - crazyaboutliv
显示剩余4条评论

2
这里至少有一个你正在犯的错误:你正在将对数概率存储在模型文件中(正如你应该这样做),但是在预测代码中,你却假装它们是直接的概率。
totalSpamP = spamP * 0.5

应该是

totalSpamP = spamP + math.log(0.5)

另外,我不明白这一行是做什么的:
spamP = spamP + 1

似乎是在弥补训练集中垃圾邮件部分没有找到的特征,但这些单词应该被忽略。现在,它正在将e (exp(1))添加到概率中,根据定义是无效的。
(顺便说一下,我刚刚使用我的朴素贝叶斯实现对这个训练集进行了分类,并获得了97.6%的准确率,所以这就是你应该瞄准的数字 :))

嗨,让我做出更改并回复你!非常感谢! - crazyaboutliv
顺便问一下,为什么我们要添加词汇大小?是拉普拉斯平滑导致了这个添加吗? - crazyaboutliv
是的,spamP = spamP + 1 此处将其加了1,现在我已经将其删除。 - crazyaboutliv
嗨,我已经按照你的建议进行了更改。准确性肯定有所提高,至少对于非垃圾邮件情况是这样,但垃圾邮件情况仍然没有改善 :( 还需要做哪些更改? - crazyaboutliv
嗨,由于一个小错误,非垃圾邮件准确性出现了问题。已经修复了。但是垃圾邮件的准确性仍然非常糟糕 :( - crazyaboutliv

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接