朴素贝叶斯法初探

0. 从贝叶斯最优分类器说起

讨论朴素贝叶斯之前,我们先来讨论一下贝叶斯最优分类器,它和朴素贝叶斯一样,都是一种生成式模型。

对于给定的特征向量,我们的目的是预测样本的标签。假定每个都属于{0,1},,贝叶斯最优分类器的函数形式是:

为了描述概率函数,我们需要个参数,每个对应于给定一个时概率函数的值,这意味着,我们所需的样本数量随特征个数呈现指数型增长。

这对样本的数量往往会有很大的要求,在实际的工程中很多时候是不可实现的。

0x1:如何对贝叶斯最优分类器的假设类进行限制?

我们知道,在PAC可学习理论中,通过限制假设类可以缓解欠拟合问题,同时减小逼近误差,虽然可能会引入估计误差增大的问题。

在朴素贝叶斯方法中,我们给出的朴素的生成式假设是:对于给定的标签,各特征之间是彼此独立的,即:

有了这种约束假设,并使用贝叶斯法则,贝叶斯最优分类器可简化为:

也就是说,对于朴素贝叶斯生成式推测来说,需要估计的参数个数只有 2d+1 个。这个独立假设显著减少了需要学习的参数数量。

因此,当我们使用最大似然原则进行参数估计时,得到的分类被称为朴素贝叶斯分类器。

 

1. 朴素贝叶斯法的模型定义

朴素贝叶斯(naive Bayes)法是基于贝叶斯定理与特征条件独立假设的分类方法。对于给定的训练数据集,首先基于特征条件独立假设学习输入/输出的联合概率分布;然后基于此模型,对给定的输入x,利用贝叶斯定理求出后验概率最大的输出y

0x1:朴素贝叶斯模型是生成模型(generative model)

朴素贝叶斯法实际上学习到生成数据的机制,所以属于生成模型

朴素贝叶斯法模型训练的目标是学习到样本中包含的输入/输出的联合概率分布,这就对训练样本的质量提出了要求,即要求样本(提取的特征)需要包含真实有效的概率分布信息

所有的机器学习算法的前提都是概率同分布,就是训练样本的分布空间必须与实际问题的分布空间相同
只有我们的训练样本中的的确确包含了代表问题本质的规律结构,算法才有机会去学习到这种模式) 这样训练出来的模型泛化性能才会好,也就是模型预测未知数据的准确性好

我们在应用机器学习/深度学习来解决安全问题的时候,首先需要思考的问题是:我们的样本数据是怎么样的?我们要用什么方式去抽象我们的样本数据?不同的抽象方式又代表了我们如何看待样本的角度。如果我们采样抽取的样本特征空间不足以真实地描述我们要预测的问题本质;或者是我们的样本量不够,在这种情况下,再好的算法也无法获得一个好的效果

0x2:朴素贝叶斯模型

1. 模型训练 - 输入X/输出Y的联合概率分布

设输入空间为 n 维向量的集合,输出空间为类标记集合 Y = {C1,C2,...,Ck},输入为特征向量,输出为类标记(class label)是X和Y的联合概率分布,训练数据集独立同分布产生。

朴素贝叶斯法通过训练数据集学习联合概率分布, 但联合概率不能直接产生(事实上它是未知的),朴素贝叶斯通过以下先验概率及条件概率分布,并通过贝叶斯定理计算得到联合概率

先验概率分布:

条件概率分布:

先验概率和条件概率相乘得到联合概率

2. 模型预测 - 选择后验概率最大对应的类

朴素贝叶斯法分类时,对给定的输入x,通过学习到的模型计算后验概率分布,将后验概率最大的类作为 x 的预测类输出,后验概率计算根据贝叶斯定理进行:

,将条件独立假设带入公式得:

这是朴素贝叶斯法分类预测的基本公式,得到当时输入对应的每个类别的后验概率后,朴素贝叶斯从中选出后验概率最大的那一个类作为预测结果,即:

同时,注意到上式中分母对所有Ck都是相同的,因此分母可约掉,得:

0x3:朴素贝叶斯的条件独立性假设

 条件概率有指数级数量的参数(所有参数的排列组合),直接估计实际上是不可行的。为了解决这个问题,朴素贝叶斯非对条件概率分布加了一个条件独立性假设,由于这是一个较强的假设,朴素贝叶斯法也因此得名

条件独立假设等于是说用于分类的特征在类确定的条件下都是条件独立的,这一假设使朴素贝叶斯法变得简单,但有时也会牺牲一些分类的准确率

 

2. 朴素贝叶斯法的策略

0x1:期望风险最小化 - 根据模型进行判断预测过程

朴素贝叶斯法预测时将实例分到后验概率最大的类中,这等价于期望风险最小化。假设选择0-1损失函数: ,这时,期望风险是:。该式中期望是对联合分布P(X,Y)取得,而联合概率分布我们是未知的,因此取等价的后验概率条件期望:

为了使期望风险最小化,只需对X = x逐个极小化,由此得到

等式推导的最后一步就是最后后验概率的意思,由此,根据期望风险最小化准则就推导出了等价的后验概率最大化准则:,这就是朴素贝叶斯法所采用的数学原理

0x2:经验风险最小化 - 模型建立过程

当模型是条件概率分布,损失函数对数损失函数时,经验风险最小化就等价于极大似然估计

0x3:结构风险最小化 - 模型建立过程

当模型是条件概率分布时、损失函数是对数损失函数时、模型复杂度由模型的先验概率表示时,结构化风险最小化就等价于最大后验概率估计

Relevant Link:

https://www.google.com.hk/url?sa=t&rct=j&q=&esrc=s&source=web&cd=1&cad=rja&uact=8&ved=0ahUKEwj8m-mjmrvVAhUBzIMKHWiqCIoQFgglMAA&url=https%3a%2f%2fzh%2ewikipedia%2eorg%2fzh-cn%2f%25E6%259C%25B4%25E7%25B4%25A0%25E8%25B4%259D%25E5%258F%25B6%25E6%2596%25AF%25E5%2588%2586%25E7%25B1%25BB%25E5%2599%25A8&usg=AFQjCNGICcTloX_Ty9_xQ06UnWbSWHkcWw

 

3. 朴素贝叶斯法的算法 - 参数估计

朴素贝叶斯法的策略是最大后验概率准则,但是为了能够计算每个类的后验概率,需要先得到联合概率分布的,而联合概率分布不能直接求得,我们需要应用一些算法来辅助我们逼近这个结果

0x1: 极大似然估计

在朴素贝叶斯训练学习过程中,学习本质上就是在估计,可以应用极大似然估计法估计相应的概率

先验概率的极大似然估计是:

 

条件概率的极大似然估计:实际场景中,输入变量X可能是一个多维度特征向量,因此设第j个特征可能取值的集合为,条件概率的极大似然估计是:式中,是第 i 个样本的第 j 个特征;是第 j 个特征可能取的第 l 个值;I 为指示函数。

1. 极大似然参数的例子

我们先计算先验概率:P(Y = 1) = 9 / 15;P(Y = -1) = 6 / 15

然后计算输入变量各个特征的条件概率:

= 2 / 9; = 3 / 9; = 4 / 9

= 1 / 9; = 4 / 9; = 4 / 9

= 3 / 6; = 2 / 6; = 1 / 6

= 3 / 6; = 2 / 6; = 1 / 6

根据最大后验概率公式,对于给定的,计算其最大后验概率:

= 9 / 15 * 3 / 9 * 1 / 9 = 1 / 45

= 6 / 15 * 2 / 6 * 3 / 6 = 1 / 15

朴素贝叶斯法取后验概率最大的对应的类,所以 y = -1

0x2: 贝叶斯估计 - 平滑化的极大似然估计

极大似然估计是一种单纯从训练样本推导概率分布的算法,但是用极大似然估计可能会出现所要估计的概率值为0的情况(如果样本数量不足,某些特征在样本中出现次数为0的时候,或者遇到样本量不足导致预测偏差的情况),这时会影响联合概率的估计,进一步影响后验概率的计算结果,使分类产生偏差。解决这一问题的方法是采用贝叶斯估计(即加入平滑因子)

条件概率的贝叶斯估计公式:,式中>=0,等价于在随机变量各个取值的频数上赋予一个正数>0,当=0时公式退化到极大似然估计。通常取=1,这时被称为拉普拉斯平滑(Laplace smoothing)

先验概率的贝叶斯估计是:

0x3:极大后验概率估计(MAP) - 带惩罚项的极大似然估计

参数估计算法的另一个思路是MAP,我个人觉得它正好和贝叶斯估计反过来,即取Bayes估计中参数后验分布中概率最大的点来估计参数就是最大后验估计,和极大似然估计最大的不同是:最大后验估计的融入了要估计量的先验分布在其中。故最大后验估计可以看做规则化的(带惩罚项)最大似然估计
这里写图片描述 
对这个似然函数左右两边同时求log对数,对数函数是单调的,对最大估计无影响: 
这里写图片描述
等式最右边第一项是标准的对数似然函数,第二项是先验分布的对数。这表明对参数的估计不再仅仅考虑样本的统计结果,还要考虑进先验分布,它如同一个惩罚项。 

Relevant Link: 

http://blog.csdn.net/juanjuan1314/article/details/78189527?locationNum=4&fps=1
http://www.cnblogs.com/liliu/archive/2010/11/24/1886110.html

 

4. 朴素贝叶斯法的具体应用 - Spam fileter(垃圾分类demo)

0x1: 分类原理

我们知道,我们如果假设用于预测的所有词(例如15个词)互相之间都是有相关依存关系的,则朴素贝叶斯法的公式如下

但是了简便实现,我们可以假设所有词之间是互相独立的,则计算过程会大大简化

下面我们简述原理与预测过程

1. 计算所有词的 P(wordN | S)、以及 P(wordN | H) 

1. 输入所有邮件,然后得到邮件中每个单词出现在垃圾邮件中的次数(条件概率 P(wordN | S) ),出现在正常邮件中的次数( P(wordN | H) )
2. 设置默认概率,即如果在预测时遇到训练语料库中未见过的词,则赋值默认概率例如: 3.7e-4
3. 先验概率,设置Spam为0.2,Ham为0.8,即根据经验,100封邮件里有20封是垃圾邮件

有了这些参数,模型就训练出来了。朴素贝叶斯分类器的训练就是在计算条件概率

2. 预测新邮件,获取所有关键字

然后输入一封待处理邮件,找到里面所有出现的关键词

3. 计算独立联合条件概率

求出,A为一封邮件是垃圾邮件的事件,T为关键词出现在一封邮件中的事件。是多个关键词。A和T是关联的事件。每个关键词根据朴素贝叶斯的假设,是相互独立的。这些关键词同时出现的情况下A是垃圾邮件的概率。

代表了从待预测email中切出的词,A代表Spam或者Ham,上面公式要分别计算A = S和A = H时的独立联合条件概率,在工程上也十分简单,就是把每个词在语料库中出现的次数比例,累乘起来
有因为分母是已经确定的事实,概率乘积为1,可忽略

0x2: 利用朴素贝叶斯进行垃圾邮件分类 - 以单个词为依据

我们用一个例子来说明朴素贝叶斯是怎么应用在垃圾邮件分类中的

贝叶斯过滤器是一种统计学过滤器,建立在已有的统计结果之上。所以,我们必须预先提供两组已经识别好的邮件,一组是正常邮件(ham),另一组是垃圾邮件(spam)
我们用这两组邮件,对过滤器进行"训练"。这两组邮件的规模越大,训练效果就越好。这里我们假设正常邮件和垃圾邮件各4000封

1. 训练

"训练"过程很简单。首先,解析所有邮件,提取每一个词(word)。然后,计算每个词语在正常邮件和垃圾邮件中的出现频率。比如,我们假定"sex"这个词,在4000封垃圾邮件中,有200封包含这个词,那么它的spam出现频率就是5%;而在4000封正常邮件中,只有2封包含这个词,那么ham出现频率就是0.05%。(如果某个词只出现在垃圾邮件中,就假定它在正常邮件的出现频率是1%,反之亦然。这样做是为了避免概率为0。随着邮件数量的增加,计算结果会自动调整。)
有了这个初步的统计结果,过滤器就可以投入使用了

2. 概率计算

现在,我们收到了一封新邮件。在未经统计分析之前,我们假定它是垃圾邮件的概率为50%。(有研究表明,用户收到的电子邮件中,80%是垃圾邮件。但是,这里仍然假定垃圾邮件的"先验概率"为50%。)
我们用S表示垃圾邮件(spam),H表示正常邮件(healthy)。因此,P(S)和P(H)的先验概率,都是50%

然后,对这封邮件进行解析,发现其中包含了sex这个词,请问这封邮件属于垃圾邮件的概率有多高?
我们用W表示"sex"这个词,那么问题就变成了如何计算P(S|W)的值,即在某个词语(W)已经存在的条件下,垃圾邮件(S)的概率有多大。
根据条件概率公式,马上可以写出

公式中,P(W|S)和P(W|H)的含义是,这个词语在垃圾邮件和正常邮件中,分别出现的概率。这两个值可以从历史资料库中得到,对sex这个词来说,上文假定它们分别等于5%和0.05%。另外,P(S)和P(H)的值,前面说过都等于50%。所以,马上可以计算P(S|W)的值:

因此,这封新邮件是垃圾邮件的概率等于99%。这说明,sex这个词的推断能力很强,将50%的"先验概率"一下子提高到了99%的"后验概率"

0x3: 利用朴素贝叶斯进行垃圾邮件分类 - 以多个词为依据

在上面的例子中,基于一个词就推断一个email是否是垃圾邮件未免太过武断,在实际使用中很容易出现误报。因为在一封邮件里往往会包含很多词,为了能降低误报。更实际的做法是选出这封信中P(S|W)最高的15个词(对推断共享最高的15个词),计算它们的联合条件概率。如果有的词是第一次出现则初始化为0.4,因为垃圾邮件使用的往往是固定的语句,如果出现了训练库中从未出现的词,则有很大概率是正常的词

而所谓联合概率,就是指在多个事件发生的情况下,另一个事件发生概率有多大。比如,已知W1和W2是两个不同的词语,它们都出现在某封电子邮件之中,那么这封邮件是垃圾邮件的概率,就是联合概率。

在已知W1和W2的情况下,无非就是两种结果:垃圾邮件(事件E1)或正常邮件(事件E2)

其中,W1、W2和垃圾邮件的概率分别如下:

如果假定所有事件都是独立事件(这是一个强假设,可能会导致预测的准确性下降),那么就可以计算P(E1)和P(E2):

由于在W1和W2已经发生的情况下,垃圾邮件的概率等于下面的式子:

将上式带入即

将先验概率P(S)等于0.5代入,得到

将P(S|W1)记为P1,P(S|W2)记为P2,公式就变成

这就是联合概率的计算公式,将情况从二词扩展到15词,最终公式为

0x4: 代码示例

# -*- coding: utf-8 -*-

# for tokenize
import nltk
from nltk.corpus import stopwords
from nltk.tokenize import wordpunct_tokenize

# for reading all the files
from os import listdir
from os.path import isfile, join

# add path to NLTK file
nltk.data.path = ['nltk_data']
# load stopwords
stopwords = set(stopwords.words('english'))

# path for all the training data sets
# 用于训练模型的语料集
spam_path = 'data/spam/'
easy_ham_path = 'data/easy_ham/'

# change it to the type of mails you want to classify
# path to the hard ham mails
# 用于预测检验效果4个语料库
spam2_path = 'data/spam_2/'
easy_ham2_path = 'data/easy_ham_2/'
hard_ham2_path = 'data/hard_ham_2/'
hard_ham_path = 'data/hard_ham/'

# 分别测试模型在4种不同类型的数据集中的分类效果
test_paths = [spam2_path, easy_ham2_path, hard_ham_path, hard_ham2_path]



def get_words(message):
    """
    Extracts all the words from the given mail and returns it as a set.
    """

    # thanks http://slendermeans.org/ml4h-ch3.html

    # remove '=' symbols before tokenizing, since these
    # sometimes occur within words to indicate, e.g., line-wrapping
    # also remove newlines
    all_words = set(wordpunct_tokenize(message.replace('=\\n', '').lower()))

    # remove the stopwords
    msg_words = [word for word in all_words if word not in stopwords and len(word) > 2]

    return msg_words


def get_mail_from_file(file_name):
    """
    Returns the entire mail as a string from the given file.
    """

    message = ''

    with open(file_name, 'r') as mail_file:

        for line in mail_file:
            # the contents of the actual mail start after the first newline
            # so find it, and then extract the words
            if line == '\n':
                # make a string out of the remaining lines
                for line in mail_file:
                    message += line

    return message


def make_training_set(path):
    """
    Returns a dictionary of <term>: <occurrence> of all
    the terms in files contained in the directory specified by path.
    path is mainly directories to the training data for spam and ham folders.
    occurrence is the percentage of documents that have the 'term' in it.
    frequency is the total number of times the 'term' appears across all the
    documents in the path
    """

    # initializations
    training_set = {}

    mails_in_dir = [mail_file for mail_file in listdir(path) if isfile(join(path, mail_file))]

    # count of cmds in the directory
    cmds_count = 0
    # total number of files in the directory
    total_file_count = len(mails_in_dir)

    for mail_name in mails_in_dir:

        if mail_name == 'cmds':
            cmds_count += 1
            continue

        # get the message in the mail
        message = get_mail_from_file(path + mail_name)

        # we have the message now
        # get the words in the message
        terms = get_words(message)

        # what we're doing is tabulating the number of files
        # that have the word in them
        # add these entries to the training set
        for term in terms:
            if term in training_set:
                training_set[term] = training_set[term] + 1
            else:
                training_set[term] = 1

    # reducing the count of cmds files from file count
    total_file_count -= cmds_count
    # calculating the occurrence for each term
    for term in training_set.keys():
        training_set[term] = float(training_set[term]) / total_file_count

    return training_set




# c is an experimentally obtained value
# 贝叶斯估计P(S) = P(H) = 0.5
# 当一个新词出现时,我们假定为3.7e-4
def classify(message, training_set, prior=0.5, c=3.7e-4):
    """
    Returns the probability that the given message is of the given type of
    the training set.
    """
    # 获取word token list
    msg_terms = get_words(message)

    msg_probability = 1

    for term in msg_terms:
        if term in training_set:
            msg_probability *= training_set[term]
        else:
            msg_probability *= c

    return msg_probability * prior


spam_training_set, ham_training_set = {}, {}
def trainingProess():
    global spam_training_set, ham_training_set
    print 'Loading training sets...',
    spam_training_set = make_training_set(spam_path)
    ham_training_set = make_training_set(easy_ham_path)
    print 'done.'



def testProcess():
    global spam_training_set, ham_training_set
    SPAM = 'spam'
    HAM = 'ham'
    for mail_path in test_paths:
        mails_in_dir = [mail_file for mail_file in listdir(mail_path) if isfile(join(mail_path, mail_file))]

        results = {}
        results[SPAM] = 0
        results[HAM] = 0

        print 'Running classifier on files in', mail_path[5:-1], '...'

        for mail_name in mails_in_dir:
            if mail_name == 'cmds':
                continue
            # 获取email的全文
            mail_msg = get_mail_from_file(mail_path + mail_name)

            # 0.2 and 0.8 because the ratio of samples for spam and ham were the same
            # 贝叶斯估计P(S) = 0.2;P(H) = 0.8
            # 当一个新词出现时,我们假定为3.7e-4
            spam_probability = classify(mail_msg, spam_training_set, 0.2)
            ham_probability = classify(mail_msg, ham_training_set, 0.8)
            # 计算得到独立联合条件概率:
            # P(T1|S) * P(T2|S) * ... * P(Tn|S)
            # P(T1|H) * P(T2|H) * ... * P(Tn|H)

            # 根据贝叶斯估计概率分别对S和H类的概率预测结果,判定属于垃圾邮件还是正常邮件
            if spam_probability > ham_probability:
                results[SPAM] += 1
            else:
                results[HAM] += 1

            total_files = results[SPAM] + results[HAM]
            spam_fraction = float(results[SPAM]) / total_files
            ham_fraction = 1 - spam_fraction

        print 'Fraction of spam messages =', spam_fraction
        print 'Fraction of ham messages =', ham_fraction
        print ''


if __name__ == '__main__':
    # training process
    # 训练模型,得到训练集中每个词的条件概率
    trainingProess()

    # test process
    # 测试模型
    testProcess()

    # 接收手工输入,并基于当前模型判定是否是恶意邮件
    mail_msg = raw_input('Enter the message to be classified:')
    print ''

    ## 0.2 and 0.8 because the ratio of samples for spam and ham were the 0.2-0.8
    spam_probability = classify(mail_msg, spam_training_set, 0.2)
    ham_probability = classify(mail_msg, ham_training_set, 0.8)
    if spam_probability > ham_probability:
        print 'Your mail has been classified as SPAM.'
    else:
        print 'Your mail has been classified as HAM.'
    print ''

Relevant Link:

http://www.ganecheng.tech/blog/53219332.html
https://en.wikipedia.org/wiki/Naive_Bayes_spam_filtering
https://github.com/aashishsatya/Bayesian-Spam-Filter/blob/master/ClassifierDemo.py
http://www.ganecheng.tech/blog/53219332.html
https://github.com/dwhitena/bayes-spam-filter
http://www2.aueb.gr/users/ion/data/enron-spam/
http://www.ruanyifeng.com/blog/2011/08/bayesian_inference_part_two.html
http://www.voidcn.com/blog/win_in_action/article/p-5800906.html

Copyright (c) 2017 LittleHann All rights reserved

posted @ 2017-08-04 09:50 骑着蜗牛逛世界 阅读(...) 评论(...) 编辑 收藏