朴素贝叶斯算法:

一、原理简介

朴素贝叶斯分类器是生成学习算法,是最简单的有向概率图模型。

假设:特征条件独立性假设

对已知类别,假设所有属性相互独立。

1.1 原理



朴素贝叶斯算法 java 朴素贝叶斯算法流程图_模型预测


原理

1.2 期望风险最小化


朴素贝叶斯算法 java 朴素贝叶斯算法流程图_朴素贝叶斯算法_02

期望风险最小化


1.3 算法


朴素贝叶斯算法 java 朴素贝叶斯算法流程图_数据_03

算法

1.4 贝叶斯估计


朴素贝叶斯算法 java 朴素贝叶斯算法流程图_朴素贝叶斯算法 java_04

贝叶斯估计

二、算法流程

1. 对数据进行类别编码及单词编码,同时划分训练集和测试集;

2. 统计频次(2种方式:多项式和伯努利+拉普拉斯平滑),得到先验概率和类条件概率;

3.

三、代码实例

1. 数据

链接:https://pan.baidu.com/s/1X5FtrhhhCzlYC1-Y1jIPfQ

提取码:a9oh

新闻数据的一部分,只为测试代码用。

2. python实现

2.1 数据转换

convert_data函数:实现类别编码及单词编码,同时划分训练集和测试集,并保存。

数据格式:类别标签+单词编码+“#”+文章名,每篇文章一行,每行的元素空格分隔


import os, random, sys

# 测试数据路径
file_path = 'E:/Pycharm/badou/Data/NewsData/test_newsdata'
# 新闻数据
# file_path = 'E:/Pycharm/badou/Data/NewsData/all_NewsData'
# 训练集和测试集的输出文件路径
TrainOutFilePath = './NB_mid_data/train.data'
TestOutFilePath = './NB_mid_data/test.data'
# 划分数据集的比例,0.8为训练
TrainPercent = 0.8
# 打开文件,往里面写数据
train_out_file = open(TrainOutFilePath, 'w', encoding='utf-8')
test_out_file = open(TestOutFilePath, 'w', encoding='utf-8')
# 类别定义字典进行编码
label_dict = {'business': 0, 'yule': 1, 'it': 2, 'sports': 3, 'auto': 4}

word_encoding = dict()  # {词: 编码}
word_list = []          # 根据len(wordList)作为新加入词的编码
def convert_data():
    i = 0
    tag = 0
    for filename in os.listdir(file_path):
        print(filename)
        # 更新对应文件的类别标签变量tag 
        if filename.find('business') != -1:
            tag = label_dict['business']
        elif filename.find('yule') != -1:
            tag = label_dict['yule']
        elif filename.find('it') != -1:
            tag = label_dict['it']
        elif filename.find('sports') != -1:
            tag = label_dict['sports']
        else:
            tag = label_dict['auto']
 
        i += 1
        rd = random.random()
        # 默认输出为测试集,最终0.8-1部分属于test
        outfile = test_out_file
        if rd < TrainPercent:
            outfile = train_out_file
 
        if i % 100 == 0:
            print('{0} files processed!'.format(i))
 
        # 逐个打开文件,
        infile = open(os.path.join(file_path, filename), 'r', encoding='utf-8')
        outfile.write(str(tag) + ' ')
 
        content = infile.read().strip()
        words = content.replace('n', ' ').split(' ')
        # print(content)
        # print(words)
 
        for word in words:
            if len(word.strip()) < 1:
                continue
            # 当词不在字典中时,将词加入列表中,并将词进行编码
            if word not in word_encoding:
                word_list.append(word)
                word_encoding[word] = len(word_list)
            outfile.write(str(word_encoding[word]) + ' ')
        # print(word_encoding)
 
        outfile.write('#' + filename + 'n')
        infile.close()
 
        print(i, 'files loaded!')
        print('{0} unique words found!'.format(len(word_list)))


if __name__ == '__main__':
    # train和test数据结构:
    # 每行一篇文章 tag + 文章出现的单词编码 + #文章名全称
    convert_data()
    train_out_file.close()
    test_out_file.close()


2.2 加载数据

nb_load_data函数:加载2.1中的训练集数据。

nb_compute_model函数:将统计的频次转化成概率。

nb_savemodel函数:存储模型。


import math

# 训练数据和测试数据路径
train_out_file = 'E:/Pycharm/badou/NaiveBayes/NB_mid_data/train.data'
test_out_file = 'E:/Pycharm/badou/NaiveBayes/NB_mid_data/test.data'
# 模型存储路径
NaiveBayes_modelfile = 'E:/Pycharm/badou/NaiveBayes/NB_mid_data/NB_model'
# 初始化参数
DefaultFreq = 0.1
ClassDefaultProb = {}
ClassFeatDic = {}       # x_j和y_i的矩阵  count
ClassFeatProb = dict()  # 概率
ClassFreq = dict()      # y_i的数组 count
ClassProb = dict()

word_dict = {}  # 词编码

# def Dedup(items):
#     tmp_dic = {}
#     for item in items:
#         if item not in tmp_dic:
#             tmp_dic[item] = True
#     return tmp_dic.keys()


def nb_load_data():
    i = 0
    infile = open(train_out_file, 'r', encoding='utf-8')
 
    # 逐行single line读取
    sline = infile.readline().strip()
    while len(sline) > 0:
        # 如果在不写文件名数据时,这段可以忽略不要
        pos = sline.find('#')
        if pos > 0:
            # 只取标签和单词编码内容
            sline = sline[:pos].strip()
        words = sline.split(' ')
        if len(words) < 1:
            print('Format error!')
        classID = int(words[0])
        if classID not in ClassFeatDic:
            ClassFeatDic[classID] = dict()
            ClassFeatProb[classID] = dict()
            ClassFreq[classID] = 0
        ClassFreq[classID] += 1
 
        words = words[1:]
        # binary distribution,remove duplicate words
        # words = Dedup(words)
 
        # multiple distribution
        for wid in words:
            if len(wid) < 1:
                continue
            word_id = int(wid)
 
            if word_id not in word_dict:
                word_dict[word_id] = 1
 
            # word_id不在矩阵中,初始化为1,在的话频次统计+1
            if word_id not in ClassFeatDic[classID]:
                ClassFeatDic[classID][word_id] = 1
            else:
                ClassFeatDic[classID][word_id] += 1
        i += 1
 
        # 本篇文章读取完毕,读取下一篇文章
        sline = infile.readline().strip()
 
    infile.close()
    print(i, "instances loaded!")
    print(len(ClassFreq), 'classes!', len(word_dict), 'words!')
    print('{class: {feat_encoding: count}}', ClassFeatDic)
    print('{class: {feat_encoding: prob}}', ClassFeatProb)
"""
nb_compute_model函数:将原有count做成概率
"""
def nb_compute_model():
    sum = 0.0  # 统计yi
    for freq in ClassFreq.values():
        sum += freq
    for classid in ClassFreq.keys():
        ClassProb[classid] = float(ClassFreq[classid]) / sum
    
    for classid in ClassFeatDic.keys():
        class_sum = 0.0  # 此类别单词计数
        for word_id in ClassFeatDic[classid].keys():
            class_sum += ClassFeatDic[classid][word_id]
        # 平滑处理 (+1平滑)
        # class_sum是当前这个类别classid中,所有单词出现的总数量
        # len(word_dict)词典大小,表示整个语料中单词的种类数(不一样的词的数量)
        # 分母变大(3,7)=>(0.3, 0.7), (3/12,7/12,2/12)
        # 新的一篇文章中,可能出现语料库中未出现的词
        new_sum = float(class_sum + len(word_dict)) * DefaultFreq
        for wid in ClassFeatDic[classid].keys():
            ClassFeatProb[classid][wid] = float(ClassFeatDic[classid][wid] + DefaultFreq) / new_sum
        ClassDefaultProb[classid] = float(DefaultFreq) / new_sum
    print('{class: {feat_encoding: prob}}:', ClassFeatProb)

  
# 存储模型
def nb_savemodel():
    outfile = open(NaiveBayes_modelfile, 'w', encoding='utf-8')
    for classid in ClassFreq.keys():
        outfile.write(str(classid) + ' ' + str(ClassProb[classid]) + ' ' + str(ClassDefaultProb[classid]))
        outfile.write('n')
    # 每行一篇文章,单词空格分开,每篇文章换行标识
    for classid in ClassFeatDic.keys():
        for word_id in ClassFeatDic[classid].keys():
            outfile.write(str(word_id) + str(ClassFeatDic[classid][word_id]))
            outfile.write(' ')
        outfile.write('n')
    outfile.close()


2.3 加载模型


# 加载模型p(yi)和p(xj|yi)
def nb_loadmodel():
    global word_dict
    word_dict = {}
    global ClassFeatProb
    ClassFeatProb = {}  # p(xj|yi)矩阵
    global ClassDefaultProb
    ClassDefaultProb = {}  # 默认概率
    global ClassProb
    ClassProb = {}      # p(yi)
    
    infile = open(NaiveBayes_modelfile, 'r', encoding='utf-8')
    sline = infile.readline().strip()
    items = sline.split(' ')
    if len(items) < 3 * 5:
        print('Model format error!')
    
    i = 0
    while i < len(items):
        classid = int(items[i])
        ClassFeatProb[classid] = {}
        i += 1
        if i >= len(items):
            print('Model format error!')
            return
        ClassProb[classid] = float(items[i])
        i += 1
        ClassDefaultProb[classid] = float(items[i])
        i += 1
    
    for classid in ClassProb.keys():
        sline = infile.readline().strip()
        items = sline.split(' ')
        i = 0
        while i < len(items):
            wid = int(items[i])
            if wid not in word_dict:
                word_dict[wid] = 1
            i += 1
            ClassFeatProb[classid][wid] = float(items[i])
            i += 1
    infile.close()
    print('{0} classes! {1} words!'.foramt(len(ClassProb), len(word_dict)))


2.4 模型预测&模型评估

nb_predict函数:模型预测。

nb_evaluate函数:模型评估,此处评估指标较单一,可自行尝试多种评估指标。


# 模型预测
def nb_predict():
    global word_dict
    global ClassFeatProb
    global ClassDefaultProb
    global ClassProb
    
    true_label_list = []
    pred_label_list = []
    
    infile = open(test_out_file, 'r', encoding='utf-8')
    sline = infile.readline().strip()
    scoreDic = {}
    iline = 0
    while len(sline)>0:
        iline += 1
        if iline % 10 ==0:
            print('{0} lines finished!'.format(iline))
        position = sline.find('#')
        if position > 0:
            sline = sline[:position].strip()
        words = sline.split()
        classid = int(words[0])
        true_label_list.append(classid)
        words = words[1:]
 
        # 取先验概率
        for classid in ClassProb.keys():
            # 计算这篇文章对应所有类别的概率都要计算
            scoreDic[classid] = math.log(ClassProb[classid])
            
        # 不在字典中的词丢掉
        for word in words:
            if len(word) <1:
                continue
            wid = int(word)
            if wid not in word_dict:
                continue
            for classid in ClassProb.keys():
                # 字典中没有出现过该类别中出现的词时,用该类别的默认概率参与计算
                if wid not in ClassFeatProb[classid]:
                    scoreDic[classid] += math.log(ClassDefaultProb[classid])
                else:
                    scoreDic[classid] += math.log(ClassFeatProb[classid][wid])
        # 概率最大的类别作为预测类别放入pred_label_list中
        maxProb = max(scoreDic.values())
        for classid in scoreDic.keys():
            if scoreDic[classid] == maxProb:
                pred_label_list.append(classid)
        sline = infile.readline().strip()
    infile.close()
    print(len(pred_label_list), len(true_label_list))
    return true_label_list, pred_label_list


# 模型评估
def nb_evaluate(true_list, pred_list):
    accuracy = 0
    i = 0
    while i < len(true_list):
        if pred_list[i] == true_list[i]:
            accuracy += 1
        i += 1
    accuracy = float(accuracy) / float(len(true_list))
    print('accuracy is {0}'.format(accuracy))
    
if __name__ == '__main__':
    nb_load_data()
    nb_compute_model()
    nb_savemodel()
    true_list, pred_list = nb_predict()
    nb_evaluate(true_list, pred_list)


四.适用场景

1. 广告投放,根据不同人群进行定价;

2. 新闻分类,query分类,商品分类,网页分类,垃圾邮件过滤,网页排序......

五、优缺点

优点:1. 性能相当好,速度快,可以避免维度灾难;

2. 支持大规模数据的并行学习,且天然的支持增量学习。

缺点:1. 条件独立性假设有时候并不合理;

2. 无法给出分类概率,难以应用于需要分类概率的场景。

六、补充说明

1. 特征条件独立性假设存在的意义

如果没有特征条件独立性假设,条件概率是所有属性上的联合概率,难以从有限的训练样本直接估计而得。

基于有限训练样本直接估计联合概率,在计算上将遭遇组合爆炸问题,在数据上遭遇样本稀疏问题;属性越多,问题越严重。

2. 频次统计方式

1)多项式,有一个单词在一篇文章中出现10次,这个单词的频次是10。

2)伯努利,有一个单词在一篇文章中出现10次,这个单词的频次是1。

实践中可以两种方式都试试。

3. 拉普拉斯平滑

拉普拉斯平滑避免了因训练样本不充分而导致概率估值为零的问题,修正过程所引入的先验的影响也会逐渐变得可忽略,使得估值渐趋向于实际概率值。

拉普拉斯平滑,在两个方面,一个是先验概率,一个是类条件概率,分子通常+1,先验概率的原始分母+数据集的类别种类数,类条件概率的原始分母+该属性特征可能的取值个数。

4. 模型提升空间

1) 语料大小、根据停用词表去除停用词、切词的大小、根据tf(或idf)进行筛选(剔除高频出现的词,阈值可尝试调整)。

2) 正负样本比例不均衡,对负样本进行重采样,或对负样本进行加权,或调整评估/损失函数。

5. 增量学习&在线学习