机器学习实现分类模型

在机器学习中一般通过有监督学习实现数据分类。分类问题的应用场景:垃圾邮件过滤,情感分析,微生物种类判别,风险级别预测等。

参考文章:

  1. (相国大人)python 中文文本分类主要就是在这篇文章的基础上进行的实践,受益匪浅,有完整工程代码。
  2. [python] 使用scikit-learn工具计算文本TF-IDF值

 

环境搭建

Linux上搭建机器学习Sklearn环境参考我的另一篇文章:
Linux上python3.5与sklearn环境搭建

线下Daemon环境为:

windows7 64位 Intel(R) Pentium(R) CPU G 2030 @ 3.00Ghz    6G内存

工具:Anaconda(python 3.5),PyCharm,vs code,jupyter Notebook

线上测试环境为:

Red Hat Linux Server 5.8 32位    /    Centos 74 64位

双核Intel(R) Celeron(R) CPU G1620 @ 2.70GHz    4G内存

python 3.5

预处理

数据源

数据源根据线上线下分为2种:

  1. 线下Demon测试时使用的是复旦大学的中文语料库。
  2. 线上使用数据库里获取的数据,对数据库里获取到的一段时间内的数据进行学习。

分词

数据源里的数据是没有分词的原始数据(对于中文语料库来说,既是连续的句子),我们需要将语料库里的句子进行切分获取单词,在单词的基础上,对待分类的文本进行分析。

分词概况

现在的分词手段大致分为:机械分词法基于统计的分词法。目前主流的分词法是基于统计模型的分词法。

主要的统计模型有:N元文法模型(N-gram),隐马尔可夫模型(Hidden Markov Model ,HMM),最大熵模型(ME),条件随机场模型(Conditional Random Fields,CRF)等。想深入了解以上模型原理需要:贝叶斯定理联合概率大数定理马尔科夫状态机动态规划等基础知识。

当然现有分词工具众多,在不了解原理的前提下,不影响使用,这里采用了使用率较高的jieba分词工具:

  • 基于前缀词典实现高效的词图扫描,生成句子中汉字所有可能成词情况所构成的有向无环图 (DAG)
  • 采用了动态规划查找最大概率路径, 找出基于词频的最大切分组合
  • 对于未登录词,采用了基于汉字成词能力的 HMM 模型,使用了 Viterbi 算法

jieba分词&jieba_fast

然而在线上实际运行测试时发现jieba分词的分词速度达不到要求,后来使用了他的另个一版本jieba_fast。使用cpython重写了jieba分词库中计算DAG和HMM中的vitrebi函数,速度得到大幅提升。

jieba分词与jieba_fast的安装,使用与性能参考:

  1. https://github.com/fxsjy/jieba
  2. https://github.com/deepcs233/jieba_fast

jieba分词官方使用示例:

# encoding=utf-8
import jieba

seg_list = jieba.cut("我来到北京清华大学", cut_all=True)
print("Full Mode: " + "/ ".join(seg_list))  # 全模式

seg_list = jieba.cut("我来到北京清华大学", cut_all=False)
print("Default Mode: " + "/ ".join(seg_list))  # 精确模式

seg_list = jieba.cut("他来到了网易杭研大厦")  # 默认是精确模式
print(", ".join(seg_list))

seg_list = jieba.cut_for_search("小明硕士毕业于中国科学院计算所,后在日本京都大学深造")  # 搜索引擎模式
print(", ".join(seg_list))
【全模式】: 我/ 来到/ 北京/ 清华/ 清华大学/ 华大/ 大学

【精确模式】: 我/ 来到/ 北京/ 清华大学

【新词识别】:他, 来到, 了, 网易, 杭研, 大厦    (此处,“杭研”并没有在词典中,但是也被Viterbi算法识别出来了)

【搜索引擎模式】: 小明, 硕士, 毕业, 于, 中国, 科学, 学院, 科学院, 中国科学院, 计算, 计算所, 后, 在, 日本, 京都, 大学, 日本京都大学, 深造

对数据源进行分词处理

对数据源进行分词处理的,主要需要对多分类目录下的大量文件进行遍历处理,对每个文件进行分词处理。

def rawdata_segment(rawdata_path, segment_path):
    '''
    rawdata_path是未分词语料库路径
    segment_path是分词后语料库存储路径
    ./rawdata_path/high_risk/risk1.ini
                            /risk2.ini
                            /risk3.ini
                                .
                                .
                            /riskn.ini
    ./rawdata_path/middle_risk/risk1.ini
                              /risk2.ini
                              /risk3.ini
    ./rawdata_path/low_risk/risk1.ini
                           /risk1.ini
                           /risk1.ini
    '''
    catelist = os.listdir(rawdata_path)  # 获取rawdata_path下的所有子目录,即high_risk/middle_risk/low_risk目录

    '''获取每个目录下所有的文件'''
    for mydir in catelist:
        class_path = rawdata_path + mydir + "/"     # 拼出分类子目录的路径 train_rawdata/high_risk   
        segment_dir = segment_path + mydir + "/"    # 拼出分词后存储的对应目录路径 train_rawdata_segment/high_risk

        if not os.path.exists(segment_dir):     # 是否存在分词目录,如果不存在则创建
            os.makedirs(segment_dir)

        file_list = os.listdir(class_path)      # 获取原始数据中某一类别中的所有文本
        '''遍历类别目录下的所有文件'''
        for file_path in file_list: 
            fullname = class_path + file_path   # 拼出文件名全路径如:train_rawdata/high_risk/risk1.ini
            content = readfile(fullname)        # 读取文件内容
            '''文本预处理,将无关词汇去除,例如多余的回车等等'''
            content = content.replace('\r\n'.encode('utf-8'),''.encode('utf-8')).strip() # 删除换行
            content = content.replace(' '.encode('utf-8'), ''.encode('utf-8')).strip()   # 删除空行、多余的空格
            content_seg = jieba.cut(content, HMM=True)                                   # 为文件内容分词
            savefile(segment_dir + file_path,' '.join(content_seg).encode('utf-8'))      # 将处理后的文件保存到分词后语料目录  
    print("rawdata segment over......")  

#单元测试
if __name__=="__main__":  
    # 对训练集进行分词 
    rawdata_path = "./train_data/"  # 原始训练数据路径
    segment_path = "./train_rawdata_segment/"  # 分词后训练数据路径    
    rawdata_segment(rawdata_path,segment_path)   
    # 对测试集进行分词
    rawdata_path = "./test_data/"   # 原始测试数据路径      
    segment_path = "./test_rawdata_segment/"  # 分词后测试数据路径   
    rawdata_segment(rawdata_path,segment_path) 

词向量空间构造

Bunch数据结构化存储

在我们分词结束后的结果中,我们得到了三种有用的信息:文件名,类别,文件内容,我们需要为每个分词后的数据建立一个关系结构,来将几种信息关联映射起来。

使用了pickle库把对象序列化到文件中,方便别的模块直接使用。

def segdata2Bunch(wordbag_path,seg_path):
    '''
    wordbag_path    Bunch结构存储路径
    seg_path        原始数据分词后的存储路径
    '''
    catelist = os.listdir(seg_path)       # 获取seg_path下的所有子目录,也就是分类类别
    '''创建一个Bunch实例'''  
    bunch = Bunch(target_name=[], label=[], filenames=[], contents=[])  
    bunch.target_name.extend(catelist)    # extend()是python list中的函数,用新的list(addlist)去扩充原来的list 
    
    '''获取每个目录下所有的文件'''  
    for mydir in catelist:   
        class_path = seg_path + mydir + "/"     # 拼出分类子目录的路径     
        file_list = os.listdir(class_path)      # 获取class_path下的所有文件
        '''遍历类别目录下文件'''     
        for file_path in file_list:            
            fullname = class_path + file_path   # 拼出文件名全路径   
            bunch.label.append(mydir)  
            bunch.filenames.append(fullname)
            '''''
            append()是python list中的函数,意思是向原来的list中添加element
            注意与extend()函数的区别
            '''   
            bunch.contents.append(readfile(fullname))   

    with open(wordbag_path, "wb") as file_obj:    # 将bunch存储到wordbag_path路径中
        pickle.dump(bunch, file_obj)  
    print("segment data to Bunch over......") 

# 单元测试
if __name__ == "__main__":
    #对训练集进行Bunch化操作
    wordbag_path = "./train_word_bag/train_set.dat"     #Bunch结构存储文件 
    seg_path = "./train_rawdata_segment/"       #分词后数据存储路径 
    segdata2Bunch(wordbag_path, seg_path)  
  
    #对测试集进行Bunch化操作
    wordbag_path = "/root/Wyxin/test_word_bag/test_set.dat"     #Bunch结构存储文件    
    seg_path = "/root/Wyxin/test_rawdata_segment/"      #分词后数据存储路径 
    segdata2Bunch(wordbag_path, seg_path) 

向量空间模型

TF-IDF

TF-IDF(term frequency–inverse document frequency)是一种用于信息检索与数据挖掘的常用加权技术。

TFIDF的主要思想是:如果某个词或短语在一篇文章中出现的频率TF高,并且在其他文章中很少出现,则认为此词或者短语具有很好的类别区分能力,适合用来分类。(图片来自http://www.ruanyifeng.com/blog/2013/03/tf-idf.html)

TF意思是词频(Term Frequency),表示某个关键词在整篇文章(某类文章)中出现的频率。

IDF意思是逆文本频率指数(Inverse Document Frequency),文本频率是指某个关键词在整个语料所有文章(所有类别)中出现的次数。逆文档频率,它是文本频率的倒数,主要用于降低所有文档中一些常见却对文档影响不大的词语的作用。

分母之所以要加1,是为了避免分母为0。

TF-IDF

Scikit-Learn中TF-IDF权重计算

Scikit-Learn中TF-IDF权重计算方法主要用到两个类:CountVectorizer和TfidfTransformer。

CountVectorizer

CountVectorizer类会将文本中的词语转换为词频矩阵,例如矩阵中包含一个元素a[i][j],它表示j词在i类文本下的词频。它通过fit_transform函数计算各个词语出现的次数,通过get_feature_names()可获取词袋中所有文本的关键字,通过toarray()可看到词频矩阵的结果。

# coding:utf-8
from sklearn.feature_extraction.text import CountVectorizer

#语料
corpus = [
    'This is the first document.',
    'This is the second second document.',
    'And the third one.',
    'Is this the first document?',
]
#将文本中的词语转换为词频矩阵
vectorizer = CountVectorizer()
#计算个词语出现的次数
X = vectorizer.fit_transform(corpus)
#获取词袋中所有文本关键词
word = vectorizer.get_feature_names()
print word
#查看词频结果
print X.toarray()

输出如下所示:

>>> 
[u'and', u'document', u'first', u'is', u'one', u'second', u'the', u'third', u'this']
[[0 1 1 1 0 0 1 0 1]
 [0 1 0 1 0 2 1 0 1]
 [1 0 0 0 1 0 1 1 0]
 [0 1 1 1 0 0 1 0 1]]
>>>

TfidfTransformer

TfidfTransformer用于统计vectorizer中每个词语的TF-IDF值。具体用法如下:

# coding:utf-8
__author__ = "liuxuejiang"
import jieba
import jieba.posseg as pseg
import os
import sys
from sklearn import feature_extraction
from sklearn.feature_extraction.text import TfidfTransformer
from sklearn.feature_extraction.text import CountVectorizer

if __name__ == "__main__":
    corpus=["我 来到 北京 清华大学",#第一类文本切词后的结果,词之间以空格隔开
    "他 来到 了 网易 杭研 大厦",#第二类文本的切词结果
    "小明 硕士 毕业 与 中国 科学院",#第三类文本的切词结果
    "我 爱 北京 天安门"]#第四类文本的切词结果
    vectorizer=CountVectorizer()#该类会将文本中的词语转换为词频矩阵,矩阵元素a[i][j] 表示j词在i类文本下的词频
    transformer=TfidfTransformer()#该类会统计每个词语的tf-idf权值
    tfidf=transformer.fit_transform(vectorizer.fit_transform(corpus))#第一个fit_transform是计算tf-idf,第二个fit_transform是将文本转为词频矩阵
    word=vectorizer.get_feature_names()#获取词袋模型中的所有词语
    weight=tfidf.toarray()#将tf-idf矩阵抽取出来,元素a[i][j]表示j词在i类文本中的tf-idf权重
    for i in range(len(weight)):#打印每类文本的tf-idf词语权重,第一个for遍历所有文本,第二个for便利某一类文本下的词语权重
        print u"-------这里输出第",i,u"类文本的词语tf-idf权重------"
        for j in range(len(word)):
            print word[j],weight[i][j]

输出示例:

-------这里输出第 0 类文本的词语tf-idf权重------           #该类对应的原文本是:"我来到北京清华大学"
中国 0.0
北京 0.52640543361
大厦 0.0
天安门 0.0
小明 0.0
来到 0.52640543361
杭研 0.0
毕业 0.0
清华大学 0.66767854461
硕士 0.0
科学院 0.0
网易 0.0
-------这里输出第 1 类文本的词语tf-idf权重------           #该类对应的原文本是: "他来到了网易杭研大厦"
中国 0.0
北京 0.0
大厦 0.525472749264
天安门 0.0
小明 0.0
来到 0.414288751166
杭研 0.525472749264
毕业 0.0
清华大学 0.0
硕士 0.0
科学院 0.0
网易 0.525472749264
-------这里输出第 2 类文本的词语tf-idf权重------           #该类对应的原文本是: "小明硕士毕业于中国科学院“
中国 0.4472135955
北京 0.0
大厦 0.0
天安门 0.0
小明 0.4472135955
来到 0.0
杭研 0.0
毕业 0.4472135955
清华大学 0.0
硕士 0.4472135955
科学院 0.4472135955
网易 0.0
-------这里输出第 3 类文本的词语tf-idf权重------            #该类对应的原文本是: "我爱北京天安门"
中国 0.0
北京 0.61913029649
大厦 0.0
天安门 0.78528827571
小明 0.0
来到 0.0
杭研 0.0
毕业 0.0
清华大学 0.0
硕士 0.0
科学院 0.0
网易 0.0

构建TF-IDF词向量空间

#创建TF-IDF词向量空间  
def vector_space(stopword_path,bunch_path,space_path,train_tfidf_path=None): 
    
    stpwrdlst = readfile(stopword_path).splitlines()    # 读取停用词 
    bunch = readbunchobj(bunch_path)                    # 导入分词后的词向量bunch对象  
    '''
    创建一个新的Bunch实例词向量空间对象,包含[权重矩阵tdm]与[词汇字典vocabulary]
    词汇字典:{"vocab":number,} {"alarm":0, "warning":1, "piece":2, "forbit":3, "over":4, ......}
    权重矩阵:tdm[label][number]
    number: 0       1       2       3       4
    label1: 0.2     0.05    0.01    0.8     0.2
    label2: 0.01    0.7     0.02    0.1     0.004
    label3: 0.01    0.1     0.06    0.03    0.3
    '''  
    tfidfspace = Bunch(target_name=bunch.target_name, label=bunch.label, filenames=bunch.filenames, tdm=[], vocabulary={})  
    
    if train_tfidf_path is not None:        '''测试数据也映射到相同的词向量空间'''
        trainbunch = readbunchobj(train_tfidf_path)  
        tfidfspace.vocabulary = trainbunch.vocabulary  
        # 将测试集的词汇也映射到相同的词向量空间(训练集的词向量空间)
        vectorizer = TfidfVectorizer(stop_words=stpwrdlst, sublinear_tf=True, max_df=0.5,vocabulary=trainbunch.vocabulary)  
        tfidfspace.tdm = vectorizer.fit_transform(bunch.contents)  
  
    else:       '''训练数据得到词向量空间'''
        vectorizer = TfidfVectorizer(stop_words=stpwrdlst, sublinear_tf=True, max_df=0.5)  
        tfidfspace.tdm = vectorizer.fit_transform(bunch.contents)  
        tfidfspace.vocabulary = vectorizer.vocabulary_ 

    writebunchobj(space_path, tfidfspace)  
    print("structure if-idf vector space over......")  

#单元测试  
if __name__ == '__main__':  
    stopword_path = "./hlt_stop_words.txt"            # 停用词文件
    bunch_path = "./train_word_bag/train_set.dat"     # 训练集Bunch结构存储的路径  
    space_path = "./train_word_bag/tfdifspace.dat"    # 训练集的词向量空间对象序列化文件
    vector_space(stopword_path,bunch_path,space_path)  

    bunch_path = "./test_word_bag/test_set.dat"         # 测试集Bunch结构存储路径
    space_path = "./test_word_bag/testspace.dat"        # 测试集的词向量空间对象序列化文件
    train_tfidf_path="./train_word_bag/tfdifspace.dat"  # 训练集的词向量空间对象序列化文件
    vector_space(stopword_path,bunch_path,space_path,train_tfidf_path)

分类器

sklearn分类模型

决策树,随机森林,GBDT,朴素贝叶斯,KNN,MLP,SVM,逻辑回归

决策树分类器:DecisionTree决策树算法及参数详解+实例+graphviz生成决策树

trainpath = "./train_word_bag/tfdifspace.dat"       # 导入训练集
train_set = readbunchobj(trainpath)  

testpath = "./test/test_word_bag/testspace.dat"     # 导入测试集
test_set = readbunchobj(testpath)  

# 决策树分类模型
clf = DecisionTreeClassifier().fit(train_set.tdm, train_set.label)
#clf = MultinomialNB(alpha=0.001).fit(train_set.tdm, train_set.label)

predicted = clf.predict(test_set.tdm)   # 预测分类结果  

for flabel,file_name,expct_cate in zip(test_set.label,test_set.filenames,predicted):  
    if flabel != expct_cate:  
        print(file_name,": 实际类别:",flabel," -->预测类别:",expct_cate)  

print("train over...model score")  
print(clf.score(test_set.tdm,test_set.label)) 

# 计算分类精度
from sklearn import metrics  
def metrics_result(actual, predict):  
    print('精度:{0:.3f}'.format(metrics.precision_score(actual, predict,average='weighted')))  
    print('召回:{0:0.3f}'.format(metrics.recall_score(actual, predict,average='weighted')))
    print('f1-score:{0:.3f}'.format(metrics.f1_score(actual, predict,average='weighted')))

metrics_result(test_set.label, predicted)

'''保存模型'''
joblib.dump(clf, "./train_model.pkl")

评价

评价标准

举例说明 :
男生 80 人,女生 20 人,共计 100 人。 目标 找出 所有女生
机器学习模型得出结论:男生 50 人,女生 50 人。
实际结果与机器学习结果比较:
50 个女生中, 15 个是女生, 35 个是男生
50 个男生中, 5个是女生, 45 个是男生
TP=15       FP=35         FN=5       TN=45
准确率 (Accuracy) A=(TP+TN)/(FP+FN+TN)=60%
精确率 (Precision) P=TP/(TP+FP)=30%
召回率 (Recall) R=TP/(TP+FN)=75%
F1 -Measure =( 2* P*R)/(P+R) =42.8 %
准确率 (Accuracy)    –给出的结果有多少是正确(对于所有数据 )
精确率 (Precision)   –给出的结果有多少是正确(对于目标找出所有女生 )
召回率 (Recall)         –正确的结果有多少被给出 (对于目标找出所有女生 )
F1 -Measure             –综合指标反映整体(对于目标找出所有女生 )

评价图

各模型准确率与数据量

学习时间与数据量

学习时间与数据量

系统性能指标

CPU与内存使用

其他

高性能方案

最初的使用方案是

  1. 在crontab里配置定时执行任务
  2. 从数据库里读取数据处理并写回数据库

当前的使用方案

嵌入到后台Linux C语言项目中

  1. 启动独立子进程处理机器学习功能模块(守护进程会更好)
  2. 信号处理来关闭后台规则匹配功能

高性能方案

  1. python部分封装功能模块
  2. C语言调用python模块
  3. 可采用多线程C调用python模块,注意多线程python使用,全局解释器锁GILPyGILState_STATE

存在的问题

数据源的准确度

线下数据源与线上数据源差异性巨大,还是需要实验局,大量的数据,真实环境系统性的科学测试。

分词的选择与分词结果

没做分词器的前期调研,没有系统性的测试多种分词器的能力。

对于非自然语言,结构化语句可更多的考虑词法文法分析技术,通过yacc/lex来分词。

分类器模型的选择与调参

多种分类模型没有深入了解,系统性的调参验证,单纯使用最基础的API。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据