搜狗新闻语料文本分类实践

**

搜狗新闻语料文本分类实践

**


本文作为曾经在实验室工作的少许经验,记录当初对文本分类方面的部分实践过程。文本语料来自搜狗语料库中的新闻语料,文章采用scikit-learn、gensim和jieba库提供的函数实现各类文本处理过程,最终在LR模型下取得97%~98%的分类正确率。

本文中语料来源于搜狗语料库,文章会详细的讲述对该文本库处理时需要的步骤和踩过的坑。要注意本文使用的是python 3.7版本,所有的字符编码均为UTF-8。实验前需要下载一些库,都使用pip直接安装即可。

pip install jieba
pip install gensim

另外scikit- learn的安装可以参考其他博文,这里推荐直接安装 anaconda3 ,里面自带python3环境和各种机器学习库,方便今后学习使用,而且今后也可以使用conda安装其他库。

首先对搜狗语料库的样例文件进行分析。搜狗语料库由搜狗实验室提供,我们使用搜狗新闻语料库,下载地址在:http://www.sogou.com/labs/resource/cs.php。分析语料格式时先下载迷你版分析。
这里写图片描述
具体实践时可以使用完整版或者精简版。下载后看到搜狗新闻语料的格式为:

<doc>
<url>http://gongyi.sohu.com/20120706/n347457739.shtml</url>
<docno>98590b972ad2f0ea-34913306c0bb3300</docno>
<contenttitle>深圳地铁将设立VIP头等车厢 买双倍票可享坐票</contenttitle>
<content>南都讯 记者刘凡 周昌和 任笑一 继推出日票后,深圳今后将设地铁VIP头等车厢,设坐票制。昨日,《南都METRO》创刊仪式暨2012年深港地铁圈高峰论坛上透露,在未来的11号线上将增加特色服务,满足不同消费层次的乘客的不同需求,如特设行李架的车厢和买双倍票可有座位坐的VIP车厢等。论坛上,深圳市政府副秘书长、轨道交通建设办公室主任赵鹏林透露,地铁未来的方向将分等级,满足不同层次的人的需求,提供不同层次的有针对的服务。其中包括一些档次稍微高一些的服务。“我们要让公共交通也能满足档次稍高一些的服务”。比如,尝试有座位的地铁票服务。尤其是一些远道而来的乘客,通过提供坐票服务,让乘坐地铁也能享受到非常舒适的体验。他说,这种坐票的服务有望在地铁3期上实行,将加挂2节车厢以实施花钱可买座位的服务。“我们希望轨道交通和家里开的车一样,分很多种。”赵鹏林说,比如有些地铁是“观光线”,不仅沿途的风光非常好,还能凭一张票无数次上下,如同旅游时提供的“通票服务”。再比如,设立可以放大件行李的车厢,今后通过设专门可放大件行李的座位,避免像现在放行李不太方便的现象。“未来地铁初步不仅在干线上铺设,还会在支线、城际线上去建设。”“觉得如果车费不太贵的话,还是愿意考虑的。”昨日市民黄小姐表示,尤其是从老街到机场这一段,老街站每次上下客都很多人,而如果赶上上下班高峰期,特别拥挤,要一路从老街站站到机场,40、50分钟还是挺吃力的,宁愿多花点钱也能稍微舒适一点。但是白领林先生则表示,自己每天上下班都要坐地铁,出双倍车资买坐票费用有点高。</content>
</doc>

我们做文本分类时需要样本和标签,样本数据来源在这里可以是新闻标题+新闻内容,本文中做简化仅使用新闻内容,而该新闻的类别标签则可以来源于该新闻的URL,即:gongyi.sohu.com,从该URL可以看出该新闻属于类别《公益》。这些工作在我们使用完整版时通过代码编写正则表达式来完成(和写爬虫时提取网页内容差不多)。
然后下载完整版预料,完整版文件组织形式:
这里写图片描述
于是样本提取的代码逻辑很清晰了,先逐个读入这些txt文件内容,然后正则表达匹配出URL(新闻类别)和content(新闻内容),然后根据URL将content存入不同文件夹/文件中。正则匹配表达式如下:

<url>(.*?)</url> # 匹配URL
<content>(.*?)</content>  # 匹配content

具体代码如下

SamplesGen.py

# -*- coding: utf-8 -*-
'''
该脚本用于将搜狗语料库新闻语料
转化为按照URL作为类别名、
content作为内容的txt文件存储
'''
import os
import re

'''生成原始语料文件夹下文件列表'''
def listdir(path, list_name):
    for file in os.listdir(path):
        file_path = os.path.join(path, file)
        if os.path.isdir(file_path):
                listdir(file_path, list_name)
        else:
                list_name.append(file_path)

'''字符数小于这个数目的content将不被保存'''
threh = 30
'''获取所有语料'''
list_name = []
listdir('SogouCS.reduced/',list_name)

'''对每个语料'''
for path in list_name:
    print(path)
    file = open(path, 'rb').read().decode("utf8")

    '''
    正则匹配出url和content
    '''
    patternURL = re.compile(r'<url>(.*?)</url>', re.S)
    patternCtt = re.compile(r'<content>(.*?)</content>', re.S)

    classes = patternURL.findall(file)
    contents = patternCtt.findall(file)

    '''
    # 把所有内容小于30字符的文本全部过滤掉
    '''
    for i in range(contents.__len__())[::-1]:
        if len(contents[i]) < threh:
            contents.pop(i)
            classes.pop(i)

    '''
    把URL进一步提取出来,只提取出一级url作为类别
    '''
    for i in range(classes.__len__()):
        patternClass = re.compile(r'http://(.*?)/',re.S)
        classi = patternClass.findall(classes[i])
        classes[i] = classi[0]

    '''
    按照RUL作为类别保存到samples文件夹中
    '''
    for i in range(classes.__len__()):
        file = 'samples/' + classes[i] + '.txt'
        f = open(file,'a+',encoding='utf-8')
        f.write(contents[i]+'\n')   #加\n换行显示

使用该脚本时注意点:

  1. 该代码运行时仅能 decode UTF-8字符集,而搜狗语料库上下载的语料用记事本打开可以看出是ASCII编码方式。这里推荐可以用软件将多个语料文件转换编码为UTF-8方便今后处理。
  2. 语料库文件打开后可以看到许多内容不全、内容过少甚至为空的语料,这部分语料通过一个threshold来控制,本文中采用30,即新闻内容少于50个字符时整个新闻内容和URL都不被记录为样本。
  3. 另外内容不全的样本被剔除时不可简单地在for循环中使用pop()方法,因为 for i in range(len(vec)) 方法中使用pop方法时会导致数组 vec 的维度被减小,而 for 循环又不知道这个减小的事件,因此for还会尝试对len(vec) - 1等下标的访问,而此时该下标的数组元素已经不在,就会报出越界错误(别问我怎么知道的,写代码不小心真的要付出很大代价才能debug出来)。本文中采用for i in range(contents.len())[::-1]:从右向左取数即可。

处理后的语料被按照类别保存为txt文档:
这里写图片描述

样本已经处理完毕,接下来需要使用这些样本进行分类器的训练。实验时仅使用两个类别先搭建整体代码框架,这份代码可以稍加修改后直接改成多个类别。

(1)从samples文件夹中读取所有样本文件

这个没什么好说的,还是utf-8,直接贴代码。这里我用一个脚本保存所有想要使用的函数。
textProcess.py (辅助函数保存脚本)

'''生成原始语料文件夹下文件列表'''
def listdir(path, list_name):
    for file in os.listdir(path):
        file_path = os.path.join(path, file)
        if os.path.isdir(file_path):
                listdir(file_path, list_name)
        else:
                list_name.append(file_path)

list_name = []
listdir('samples/', list_name)
for path in list_name[0:2]:
        print(path)

        file = open(path, 'rb').read().decode('utf-8').split('\n')
        class_count = 0
        for text in file:
        ...

(2)分词
本文处理的语料均为中文,因此处理中文分词需要使用结巴分词。结巴分词的原理大致是:先使用正则表达式粗略划分,然后基于trie树高速扫描,将每个句子构造有向无环图,使用动态规划查找最大概率路径,基于词频寻找最佳切分方案,最后对于未登录的单词(词表里没有的词语),采用HMM模型,维特比算法划分。分词代码:

            content = text
            # 分词
            word_list = list(jieba.cut(content, cut_all=False))

(3)去停用词
中文中很多词语虽然在文章中大量出现,但对文章分类并没有什么实际意义。比如“只”、“的”、“应该”这样的词语,对它们的计算既浪费空间时间也可能影响最终分类结果。因此需要先建立一个词表,将样本语料分词后出现在该词表中的单词去掉。停用词表按行存储一个单词:
这里写图片描述
去停用词代码:

def get_stop_words():
    path = "stop_words"
    file = open(path, 'rb').read().decode('utf-8').split('\r\n')
    return set(file)


def rm_stop_words(word_list):
    word_list = list(word_list)
    stop_words = get_stop_words()
    # 这个很重要,注意每次pop之后总长度是变化的
    for i in range(word_list.__len__())[::-1]:
        # 去停用词
        if word_list[i] in stop_words:
            word_list.pop(i)
        #  去数字
        elif word_list[i].isdigit():
            word_list.pop(i)
    return word_list

此外,如果统计词频的时候一个单词出现的次数过少,也不用统计这个词

def rm_word_freq_so_little(dictionary, freq_thred):
    small_freq_ids = [tokenid for tokenid, docfreq in dictionary.dfs.items() if docfreq < freq_thred ]
    dictionary.filter_tokens(small_freq_ids)
    dictionary.compactify()

这些函数都被写在textProcess.py辅助函数脚本中

(4)统计词频,生成词袋

对每个文章:

       for text in file:

            # 打标签
            class_count = class_count + 1

            content = text
            # 分词
            word_list = list(jieba.cut(content, cut_all=False))
            # 去停用词
            word_list = rm_stop_words(word_list)

            dictionary.add_documents([word_list])

            '''
            转化成词袋
            gensim包中的dic实际相当于一个map
            doc2bow方法,对没有出现过的词语,在dic中增加该词语
            如果dic中有该词语,则将该词语序号放到当前word_bow中并且统计该序号单词在该文本中出现了几次
            '''
            word_bow = dictionary.doc2bow(word_list)
            bow.append(word_bow)

这里用gensim提供的 dictionary ,它相当于一个map,每个单词如果出现在里面,那么就会在当前文章向量中记录该单词在词典中的序号和在该文章中的频率。生成词向量仅需要调用dictionary.doc2bow()方法即可生成。注意这里保存的是稀疏矩阵。具体格式为:

  1. 单个词向量:( 5 , 2 )
    5是该单词在dictionary中的序号为5,2是在这篇文章中出现了两次。
  2. 一篇文章矩阵: [ (5 ,2) , (3 , 1) ]
    在该文章中出现了5号单词两次,3号单词1次。

(5)生成TF-IDF矩阵
TF-IDF相比One-hot编码方式更加合理,它是根据该单词在当前文章中出现的频率和该单词在所有语料中出现的频率评估一个单词的重要性,当一个单词在这篇文章中出现的次数很多的时候,这个词语更加重要;但如果它在所有文章中出现的次数都很多,那么它就显得不那么重要。统计TF-IDF可以使用gensim提供的方法。

    # 去除过少单词 ps:可能导致维数不同
    rm_word_freq_so_little(dictionary,freq_thred)

    dictionary.save('dicsave.dict')
    corpora.MmCorpus.serialize('bowsave.mm', bow)

    tfidf_model = models.TfidfModel(corpus=bow,dictionary=dictionary)

    with open('tfidf_model.pkl', 'wb') as f2:
        pickle.dump(tfidf_model, f2)
    '''训练tf-idf模型'''
    corpus_tfidf = [tfidf_model[doc] for doc in bow]

    '''将gensim格式稀疏矩阵转换成可以输入scikit-learn模型格式矩阵'''
    data = []
    rows = []
    cols = []
    line_count = 0
    for line in corpus_tfidf:
        for elem in line:
            rows.append(line_count)
            cols.append(elem[0])
            data.append(elem[1])
        line_count += 1

    print(line_count)
    tfidf_matrix = csr_matrix((data,(rows,cols))).toarray()

    count = 0
    for ele in tfidf_matrix:
        # print(ele)
        # print(count)
        count = count + 1

注意gensim生成的数据格式和scikit-learn中模型所能接受的数据格式不同,因此需要使用csr_matrix方法将格式转换成[[0, 0, 0.1], [0.2, 0, 0]] 这样的格式。

代码运行过程中对所有生成的模型等进行保存,方便今后训练和测试时调用。可以使用pickle 进行保存,也可以使用gensim自带的方式序列化保存:

    dictionary.save('dicsave.dict')
    corpora.MmCorpus.serialize('bowsave.mm', bow)

(6)输入模型训练/预测

进行训练前需要生成样本的标签值,可以先统计各个类别样本的数目,如样本0数目100,样本1数目100,则生成
[ 0 , 0, 0, …, 0, 1 , 1 , … , 1]样本向量,其中0有100个,1有100个。

    # cut label 1 mil label 0
    '''生成labels'''
    labels = np.zeros(sum(labels_count) + 1)
    for i in range(labels_count[0]):
        labels[i] = 1

然后将样本划分为训练集和测试集,划分比例为8:2或7:3。有两种划分代码,一种自己写:

    '''分割训练集和测试集'''
    rarray=np.random.random(size=line_count)
    x_train = []
    y_train = []
    x_test = []
    y_test = []

    for i in range(line_count-1):
        if rarray[i]<0.8:
            x_train.append(tfidf_matrix[i,:])
            y_train.append(labels[i])
        else:
            x_test.append(tfidf_matrix[i,:])
            y_test.append(labels[i])

或者调用scikit-learn自带的划分函数:

x_train,x_test,y_train,y_test = train_test_split(tfidf_matrix,labels,test_size=0.3,random_state=0)

不知道为什么用这个自带的方法有时候会报memery error,而用自己写的方式就不会报错。

接下来直接输入model即可。这里先采用scikit-learn中的LR模型,logistic-regression 是一种线性模型:
H(x) = w x + b,计算结果输入softmax函数,将H映射到 0-1 的区间作为概率预测, 可以采用梯度下降算法进行模型训练。
调用模型的代码:

    '''LR模型分类训练'''
    classifier=LogisticRegression()

    classifier.fit(x_train, y_train)
    #
    # with open('LR_model.pkl', 'wb') as f:
    #     pickle.dump(classifier, f)

    print(classification_report(y_test,classifier.predict(x_test)))

最终分类效果为:
这里写图片描述
这里贴上所有代码:

textClsfy.py:

'''
本文档负责实际读取语料库文件
训练LR模型
过程中保存词典、语料和训练后的模型
'''
import numpy as np
from sklearn.linear_model.logistic import  *
from gensim import corpora, models, similarities
import jieba
from sklearn.model_selection import train_test_split
import pickle
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
from learnTextClsf.textProcess import *
from scipy.sparse import csr_matrix
from sklearn.metrics import classification_report

if __name__ == '__main__':
    freq_thred = 10        # 当一个单词在所有语料中出现次数小于这个阈值,那么该词语不应被计入词典中

    # 字典
    dictionary = corpora.Dictionary()
    # 词袋
    bow  = []

    labels_count = []
    list_name = []
    listdir('samples/', list_name)
    count = 0

    for path in list_name[0:2]:
        print(path)

        file = open(path, 'rb').read().decode('utf-8').split('\n')
        class_count = 0
        for text in file:

            # 打标签
            class_count = class_count + 1

            content = text
            # 分词
            word_list = list(jieba.cut(content, cut_all=False))
            # 去停用词
            word_list = rm_stop_words(word_list)

            dictionary.add_documents([word_list])

            '''
            转化成词袋
            gensim包中的dic实际相当于一个map
            doc2bow方法,对没有出现过的词语,在dic中增加该词语
            如果dic中有该词语,则将该词语序号放到当前word_bow中并且统计该序号单词在该文本中出现了几次
            '''
            word_bow = dictionary.doc2bow(word_list)
            bow.append(word_bow)

        labels_count.append(class_count-1)


    # with open('dictionary.pkl', 'wb') as f1:
    #     pickle.dump(dictionary, f1)

    # 去除过少单词 ps:可能导致维数不同
    rm_word_freq_so_little(dictionary,freq_thred)

    # dictionary.save('dicsave.dict')
    # corpora.MmCorpus.serialize('bowsave.mm', bow)

    tfidf_model = models.TfidfModel(corpus=bow,dictionary=dictionary)

    # with open('tfidf_model.pkl', 'wb') as f2:
    #     pickle.dump(tfidf_model, f2)
    '''训练tf-idf模型'''
    corpus_tfidf = [tfidf_model[doc] for doc in bow]

    '''将gensim格式稀疏矩阵转换成可以输入scikit-learn模型格式矩阵'''
    data = []
    rows = []
    cols = []
    line_count = 0
    for line in corpus_tfidf:
        for elem in line:
            rows.append(line_count)
            cols.append(elem[0])
            data.append(elem[1])
        line_count += 1

    print(line_count)
    tfidf_matrix = csr_matrix((data,(rows,cols))).toarray()

    count = 0
    for ele in tfidf_matrix:
        # print(ele)
        # print(count)
        count = count + 1

    # cut label 1 mil label 0
    '''生成labels'''
    labels = np.zeros(sum(labels_count) + 1)
    for i in range(labels_count[0]):
        labels[i] = 1

    '''分割训练集和测试集'''
    rarray=np.random.random(size=line_count)
    x_train = []
    y_train = []
    x_test = []
    y_test = []

    for i in range(line_count-1):
        if rarray[i]<0.8:
            x_train.append(tfidf_matrix[i,:])
            y_train.append(labels[i])
        else:
            x_test.append(tfidf_matrix[i,:])
            y_test.append(labels[i])

    # x_train,x_test,y_train,y_test = train_test_split(tfidf_matrix,labels,test_size=0.3,random_state=0)

    '''LR模型分类训练'''
    classifier=LogisticRegression()

    classifier.fit(x_train, y_train)
    #
    # with open('LR_model.pkl', 'wb') as f:
    #     pickle.dump(classifier, f)

    print(classification_report(y_test,classifier.predict(x_test)))
  • 25
    点赞
  • 134
    收藏
    觉得还不错? 一键收藏
  • 21
    评论
评论 21
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值