ed459d787b1fd7cdacaf2ce2c6c94cab.png

一个课程实践项目,当时感觉难点主要是一百万条新闻数据的获取,我一部分用了清华新闻文本数据集,一部分自己爬取,最后组合成了一个一百万条新闻文本的数据集,一共十个分类。清华大学中文文本分类数据集链接

THUCTC: 一个高效的中文文本分类工具​thuctc.thunlp.org

这里只用了机器学习(朴素贝叶斯,SVM)做了文本分类,没有使用深度学习方法。

文本分类流程:分词->去停用词->获取词典(降维)->求TF-IDF特征->分类

1.分词

采用jieba分词,示例:

import jieba.posseg as pseg
s = '北京邮电大学,简称北邮,是中华人民共和国教育部直属、工业和信息化部共建的全国重点大学'
seg_content = pseg.cut(s)
str_c = ''
for word, flag in seg_content:
    str_c = str_c + word + '/' + flag + ' '
ptint(str_c)

78d51454d5230f00b898d6722b822605.png

选取其中所有的名词来代表一条文本(简单起见,因为名词在文本类别中起到主要作用)。去停用词,即去掉一些与类别无关的一些通用词。

2.获取词典(降维)

在一百万条文本数据集中,最后分词得到的名词有几百万个,它们之中有很多与本类别无关,最后的文本特征不需要这么高的维度。可以用卡方检验的方法找出每个类别的“关键词”,从而达到降维的目的。当然还有别的方法,比如互信息,我这里使用的卡方检验。

卡方检验:每个词对于每个类别,使用四表格的形式,计算该词对于该类是否有较大的影响,公式:

例如四表格:

5ba7c52834a11bbd9cfa77231d06c034.png

A表示包含“算法”一词且属于计算机类的文本数,B表示包含“算法”一词且不属于计算机类的文本数。与计算机类别相关度高的词,所求的卡方值会偏高,反之卡方值很低。卡方值越高,代表该词对该类别的的影响越大。因此,我们求出词典中所有词相对于每一类的卡方值,就可以知道与每一类别相关度最高的词。

import json
import codecs
import re
from math import log2
import time
def chi_square(N_10, N_11, N_00, N_01):
    """
    卡方计算
    :param N_10:
    :param N_11:
    :param N_00:
    :param N_01:
    :return: 词项t卡方值
    """
    fenzi = (N_11 + N_10 + N_01 + N_00)*(N_11*N_00-N_10*N_01)*(N_11*N_00-N_10*N_01)
    fenmu = (N_11+N_01)*(N_11+N_10)*(N_10+N_00)*(N_01+N_00)
    if fenmu == 0:
        return 0
    return fenzi*1.0/fenmu

def selectFeatures(documents, category_name, top_k, select_type="chi"):
    """
    特征抽取
    :param documents: 预处理后的文档集
    :param category_name: 类目名称
    :param top_k:  返回的最佳特征数量
    :param select_type: 特征选择的方法,可取值chi,mi,freq,默认为chi
    :return:  最佳特征词序列
    """
    L = []
    # 互信息和卡方特征抽取方法
    if select_type == "chi" or select_type == "mi":
        for t in vocabulary:
            print("hans:",t)
            start_time = time.time()
            N_11 = 0
            N_10 = 0
            N_01 = 0
            N_00 = 0
            N = 0
            for label, word_set in documents:
                if (t in word_set) and (category_name == label):
                    N_11 += 1
                elif (t in word_set) and (category_name != label):
                    N_10 += 1
                elif (t not in word_set) and (category_name == label):
                    N_01 += 1
                elif (t not in word_set) and (category_name != label):
                    N_00 += 1
                else:
                    print("N error")
                    exit(1)

            if N_00 == 0 or N_01 == 0 or N_10 == 0 or N_11 == 0:
                continue
            # 卡方计算
            A_tc = chi_square(N_10, N_11, N_00, N_01)
            L.append((t, A_tc))
            end_time = time.time()
            print("time:",end_time-start_time)
    return sorted(L, key=lambda x:x[1], reverse=True)[:top_k]

vocabulary = set()
def main():
    # 读取文档集(需要根据具体类目名称修改)
    category_name_li = ["entertainment", "military", "sports",
                        "education", "finance", "politics","stock",
                        "energy","home","social","tech"]
    # 获取文本(根目录需要根据具体类目名称修改)
    all_text = getDocuments(category_name_li)
    print("all_text len = ", len(all_text))
    # 读取词汇表
    getVocabulary(category_name_li)
    print("vocabulary len = ", len(vocabulary))
    # 获取特征词表
    print("="*20, 'n', "  卡方特征选择  n", "="*20)
    feature_select_type = "chi"
    for category_name in category_name_li:
        # 特征抽取,最后一个参数可选值 "chi"卡方
        feature_li = selectFeatures(all_text, category_name, 1000, feature_select_type)
        print(category_name)
        f = open("count/"+category_name+".txt",'w')
        for t, i_uc in feature_li:
            print("%st%.3f" % (t, i_uc))
            f.write(t+'t'+str(i_uc)+'n')
        f.close()

if __name__ == "__main__":
    main()

这里我选取了每一类的top1000卡方值。但是实际的特征并不是每一类1000个词,其实1000还挺多的,还可以再缩小范围。

3.求TF-IDF向量

接下来,根据我们卡方得到的词典,求出这个在这个词典上每条文本的TF-IDF表示。TF-IDF分为两部分:

TF: 词频。TF的形式化描述,对于在某一文本

里的词
,它的词频可表示为:

, 其中
表示
在文本
中的出现次数,分母表示文本
所有词出现的次数总和。

IDF:逆文档率。IDF形式化描述为:

, |D| 是语料库中所有文档总数,分母是包含词语
的所有文档数。

sklearn库里已经写好了TF-IDF的计算接口: train_list和test_list每个item的多个词表示一条文本;self.vocabulary是一个key为词,value为词下标的dict。

from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer, TfidfTransformer
tfidf_vec = TfidfVectorizer(vocabulary=self.vocabulary)
tfidf_matrix_train = tfidf_vec.fit_transform(train_list) #train_list=['名词1 名词2 ...','名词1 名词2 ...',...]
tfidf_matrix_test = tfidf_vec.fit_transform(test_list) ##test_list=['名词1 名词2 ...','名词1 名词2 ...',...]
print(tfidf_matrix_train.shape)
print(tfidf_matrix_test.shape)

# output:
# (400000, 864)
# (600000, 864)

tfidf_matrix_train,tfidf_matrix_test 分别是训练集和测试集的TF-IDF向量,shape为(400000, 864),(600000,864),维度为864,训练集和测试集文本数分别为400000和600000

我们希望把这个tfidf向量保存下来,因为不想每次训练测试的时候都计算一次tfidf,这个过程有点耗时,不仅TfidfVectorizer计算慢(数据量太大),加载源数据也非常慢,需要好几分钟。

TfidfVectorizer返回的是一个稀疏矩阵,即tfidf_matrix_train和tfidf_matrix_test是两个稀疏矩阵,是scipy里实现的csr_matrix。下面的代码实现了csr_matrix的保存和加载,整个过程非非常快只要两三秒,保存的npz文件也只有几十兆。

from scipy import sparse
sparse.save_npz("train.npz",tfidf_matrix_train)
sparse.save_npz("test.npz",tfidf_matrix_test)
train_data = sparse.load_npz("train.npz")
test_data = sparse.load_npz("test.npz")

在处理数据的过程中,多次用到crs_matrix,比如在做交叉验证的时候,需要调换训练集和测试集的TF-IDF向量,所以这里简单总结一下crs_matrix用法

from scipy.sparse import csr_matrix
row = [0,0,1,3]                                        # 行下标
col = [1,2,2,0]                                        # 列下标
data = [1.0,2.0,3.0,4.0]                               # value
tfidf = csr_matrix((data, (row, col)), shape=(4,4))    # 构造函数
print(tfidf)
print(tfidf.toarray())                                 # 稀疏矩阵转m*n矩阵

''' output:
  (0, 1)        1.0
  (0, 2)        2.0
  (1, 2)        3.0
  (3, 0)        4.0

[[0. 1. 2. 0.]
 [0. 0. 3. 0.]
 [0. 0. 0. 0.]
 [4. 0. 0. 0.]]
'''


d_coo = tfidf.getrow(0).tocoo()                       # 得到稀疏矩阵第0行并转换成COOrdinate格式
print(d_coo.col)
print(d_coo.data)

'''output:
[1 2]

[1. 2.]
'''

5.分类

朴素贝叶斯:

IDFfrom sklearn.naive_bayes import MultinomialNB
import numpy as np
from sklearn.metrics import confusion_matrix
class Eval:
    def __init__(self,pred,gt,cfg):
        self.pred = np.array(pred)
        self.gt = np.array(gt)
        assert len(self.pred.shape)==1, len(self.gt.shape)==1

    '''
    准确率(accuracy) = 预测对的/所有 = (TP+TN)/(TP+FN+FP+TN)
    '''
    def get_acc(self):
        return len(np.where(self.pred==self.gt)[0])/len(self.gt)

    '''
    精确率是针对我们预测结果而言的,它表示的是预测为正的样本中有多少是真正的正样本。那么预测为正就有两种    可能了,一种就是把正类预测为正类(TP),另一种就是把负类预测为正类(FP)
    精确率(precision) = TP/(TP+FP)
    '''
    def get_precision(self):
        precision = []
        u, indices = np.unique(self.pred, return_inverse=True)
        for i in range(len(u)):
            TP_FP = np.where(indices==i)[0]
            TP = set(np.where(self.gt==u[i])[0]).intersection(set(TP_FP))
            precision.append(len(TP)/len(TP_FP))
        return precision

    '''
    召回率是针对我们原来的样本而言的,它表示的是样本中的正例有多少被预测正确了。那也有两种可能,一种是把    原来的正类预测成正类(TP),另一种就是把原来的正类预测为负类(FN)。
    召回率(recall) = TP/(TP+FN)
    '''
    def get_recall(self):
        recall = []
        u, indices = np.unique(self.gt, return_inverse=True)
        for i in range(len(u)):
            TP_FN = np.where(indices==i)[0]
            TP = set(np.where(self.pred==u[i])[0]).intersection(set(TP_FN))
            recall.append(len(TP)/len(TP_FN))
        return recall

    '''
    生成混淆矩阵
    '''
    def get_matrix(self):
    	cm = confusion_matrix(self.gt,self.pred)
    	return cm

clf = MultinomialNB()
print("training...")
clf.fit(X_train, y_train)              # X_train:TF-IDF;y_train: label_list
print("predict start!")
y_predict = clf.predict(X_test)
evalution = Eval(y_predict, y_test, args.cfg)
print("Acc:",evalution.get_acc())
print("Precision:",evalution.get_precision())
print("Recall:",evalution.get_recall())
print("Confusion matrix:")
print(evalution.get_matrix())

ae898e021ddb5c826a8765a5c08ff511.png

SVM:

clf=LinearSVC()
print("SVM training")
clf.fit(X_train,y_train)                      # X_train:TF-IDF;y_train: label_list
print("predict start!")
y_predict = clf.predict(X_test)
evalution = Eval(y_predict, y_test, args.cfg)
print("Acc:", evalution.get_acc())
print("Precision:", evalution.get_precision())
print("Recall:", evalution.get_recall())
print("Confusion matrix:", evalution.get_matrix())

f3bcafe875545874112b9a8f46ce443d.png
Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐