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

3 投票
2 回答
2643 浏览
提问于 2025-04-17 09:16

编辑:可以在以下链接找到正确的代码版本: https://github.com/a7x/NaiveBayes-Classifier

我使用了来自openClassroom的数据,开始在Python中做一个简单的朴素贝叶斯分类器。步骤就是常规的训练和预测。我有几个问题,想知道为什么准确率这么差。

  1. 在训练时,我通过以下公式计算了对数似然:

    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")
    
  2. 在预测时,我只是把一封邮件拆分成单词,分别计算垃圾邮件和非垃圾邮件的概率,然后把它们相乘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 个回答

2

你可能犯了一个错误:你在模型文件中存储了对数概率(这是对的),但是在预测代码中,你却把它们当成了普通概率来用:

totalSpamP = spamP * 0.5

应该是

totalSpamP = spamP + math.log(0.5)

另外,我不太明白这一行代码在干嘛:

spamP = spamP + 1

这行代码似乎是在弥补训练集中垃圾邮件部分缺失的某个特征,但这些词应该直接被忽略。现在,它在一个概率上加了eexp(1)),这从定义上来说是不对的。

顺便说一下,我刚刚用我自己实现的朴素贝叶斯在这个训练集上进行了分类,得到了97.6%的准确率,所以你应该朝这个目标努力哦 :)

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点仍然存在——你使用的词汇量与实际的词汇量不匹配。它应该是训练集中观察到的不同单词的数量,而不是所有消息中单词的总数。

撰写回答