文本分类

    由于之前在做文本分类的有关项目,这儿对文本分类的基本流程稍作总结。之前的项目是将十六万篇从WOS上下载的题录数据(包括AU、TI、ID、AB、SO、WC等字段),由于从WOS上下载数据时尽量保证了查全率,而查准率不太高,所以需要再从中挑出的确和研究主题相关的那部分题录数据。所以利用人工标注的数据当做训练集,这里的标注是指根据作者、主题、摘要等字段进行人工判断某一篇题录数据是否属于要研究的主题明确相关,所以这是个二分类问题。从这儿可以看出,其和垃圾邮件分类的基本形式差不多,自然而然就会想到文本分类的基本流程。所以下面介绍文本分类的几步操作,先说明一下,以下是对英文文本进行分类,某些地方不适合中文,但是基本流程类似(汉语难点在于分词操作)。
    文本分类有基于规则的分类和基于机器学习算法的分类两种。基于规则的大概可以理解为,通过该行业的专家制定一套规则,比如选取某些单词共现或频率高于阈值作为一个判断的依据,举个例子,比如某篇文献的题名出现了“soil”、“water”、“pollution”这三个单词,那么可以以60\%的概率断定这是一篇归为水污染主题的文章。这种基于规则的文本分类需要大量的专业知识和专业术语储备,并且费时费力。而基于机器学习的文本分类算法的主要思路则是将文本以某种方式转换为数值,然后利用朴素贝叶斯、支持向量机、决策树、集成学习、神经网络等算法进行训练。然而基于机器学习进行文本分类也是需要训练集作为支撑的,这就引发了文本分类的瓶颈--标注瓶颈问题,即需要专家事先进行人工标注数据集,这无疑也是非常耗时耗力的。当然标注瓶颈问题可以由半监督学习(Semi-supervised Learning)解决。除此之外,文本分类的另一难点就是文本到数值的转换过程和特征的提取了。
    文本分类首先要解决的是文本表示的问题,在信息检索领域,文本表示有布尔逻辑模型、向量空间模型、概率模型、潜在语义索引等表示方法。这里先给出几个名词:词(term)、文档(document)、文档集(document set)、词汇表(vocabulary)、文档向量、文档向量矩阵。词是经过分词、预处理后得到的文档最小单位,比如“dog”、“machin”,注意这里的“machin”为“machine”进行提取词干(stemming)预处理后得到的
结果;文档是一段文本或由几个字段里的文本拼凑在一起的文本进过预处理后得到的一个单词列表,比如[“cat”,“is”,“lovely”,...];文档集是所有文档的集合,可以看作是二维的矩阵;词汇表是从文档集中获取所有不重复的词构成的一个列表;文档向量是利用某种规则将文档转换为一个和词汇表一样大小的数值向量;文档向量矩阵是文档集里每一篇文档转换为文档向量之后得到的矩阵。这里个人称为文档向量和文档向量矩阵,或许有不准确之处,欢迎批评指正。下面据此,先简单介绍一下信息检索领域的几个模型:
    布尔逻辑模型: 词在文档里面出现则在构建文档向量时记为1,否则记为0。这样得到的文档向量的每一个分量全是0,1。比如词汇表为[“cat”,“dog”,“machin”,“learn”,“hard”],文档为[“machin”,“cat”],那么其转换的文档向量为[1,0,1,0,0]。在进行检索时,比如检索内容为“cat AND dog”,那么将文档向量代入得到“1 AND 0”,结果为False,所以此文档不匹配。由此可以看出布尔逻辑模型操作起来简单,但是效果不理想。
    向量空间模型(Vector Space Model): 相比于布尔逻辑模型,在构建文档向量时不仅仅只是判断其是否出现,而是利用了词在文档中出现的频次(词文档频率)、词逆向文档频率法(TF-IDF)等构建文档向量。在进行检索时,也不仅仅只是布尔操作,而是进行计算文档相似度,比如余弦相似度等。
    概率模型: 概率模型引入了贝叶斯分类,在检索时实质上是二元分类,相关或是不相关。但实际操作也没有分类的过程,而是对相关条件概率和非相关条件概率的比值进行排序,选择其排序前K个。
    潜在语义索引(Latent Semantic Indexing): 实质上是利用了奇异值分解对文档向量矩阵进行分解,选择其最大的几个奇异值对应的U和V进行分解,可以达到减少计算量和存储、降低噪声干扰等效果。关于奇异值分解,详细推导过程参见矩阵分解(一):奇异值分解
    上面简单介绍了信息检索领域的文本表示方法。在文本分类中常用的就是词集模型(set-of-words)和词袋模型(bag-of-words),词集模型可以简单理解为布尔逻辑模型里面的文档向量表示方法,出现则为1,不出现则为0,不考虑频次等信息;词袋模型就相当于向量空间模型的文档向量表示法,最简单的就是统计词在文档中出现的频次。
    接下来便是文本分类的操作了,主要分为预处理、构建文档向量矩阵、特征选择、训练测试四个部分。
    预处理: 预处理操作包括分词、去除低频词、去除停用词(stop words)、提取词干(stemming)。在英文里面,分词主要是利用空格进行切分,同时还可考虑以分号、连字符等进行切分,当然也可以利用正则表达式进行匹配。去除低频词主要是为了提高分类效果、减少稀疏性、降低噪声影响,其主要思想是去除那些在所有文档出现频次小于某个阈值的单词,出现频次太小的单词对分类贡献不大,如不去除则会导致很多文档向量的分量为0,
另外还可以去除拼写错误的单词,降低噪声。去除停用词,即去除掉“is”、“he”等对分类无意义的词,停用词表可由网站http://www.ranks.nl/resources/stopwords.html获得;提取词干就是将“cat”,“cats”都统一变为“cat”等操作,降低数据之间的相关性。
    构建文档向量矩阵: 进过预处理操作,就得到了文档和文档集,将文档集里所有文档取并集,得到词汇表,也就是初步的特征集。根据特征集进行构建文档向量矩阵,如果只是简单采用词频法,则对每一篇文档,遍历词汇表,将词在文档中出现的频次作为文档向量对应分量的值即可。如果采用TF-IDF,其计算思想主要是:对于第i篇文档,如果词j在这个文档中出现的频次高,那么赋予其较大的权值,如果其在大部分文档中都出现了,那么其携带的分类信息就少了,那么赋予其较小的权值。用词在该文档中出现的次数即词频当作tf,用总的文档数量除以
包含该词的文档数量,再取对数,当作idf。文档i的文档向量的第j个分量w计算公式如下:

tfidf.png


    特征选择:构建文档向量矩阵的同时也得到了词汇表,即特征集,很多时候,文档数量即样本数目很少,但是特征数量却很多。虽然经过了去除停用词、去除低频、提取词干等操作,但是仍然会有很多词汇被选入特征。特征选择是机器学习里面对预测结果影响很大的一块内容,有时通过优化算法、调参得到的模型改善却不如进行特征选择带来的改进大,由此也导致了特征选择是比较难以处理的一块内容。可以说,深度学习网络在全连接层之前进行的卷积、池化等操作就是特征提取的过程。这里,特征选择和特征提取并不是相同的概念,但二者目的相同,都是为了减少特征维数,避免维数灾难(Curse of Dimensionality)。文本特征选择的方法主要有:对于单个特征采用信息增益、ROC曲线、互信息、期望交叉熵、统计检验等,对于多个特征的选择包括顺序前向选择、顺序后向选择、浮动搜索技术等。
        信息增益:记D位标签列,对每个特征A基于以下公式进行计算,最后对每个特征的信息增益进行排序可得到前K个特征:

infogain.png


        ROC曲线:对每个特征A和标签D,对特征A取值的范围进行取阈值,然后以此阈值分类,计算分类结果的召回率和假阳率,得到ROC曲线上的一个点,逐步将所有的阈值得到的点进行计算得到AUC。最后对AUC进行排序,得到特征的重要性排序。
        统计检验:主要是利用卡方检验、t检验等操作判断类的可分性等。
        顺序前向选择:这是对特征子集的选取,假设现在先选择了k个特征,下面对剩下的特征进行遍历,将特征加入k个特征构成k+1维的特征集。选择使得评价函数最优的那个特征作为第k+1个特征,这里的评价函数主要包括filter和wrapper两种(目前尚未接触过这部分内容,这里先省略对其的叙述)。逐步选择,直到选择K个特征。
    其余特征选择算法不再详细叙述。特征提取算法主要包括主成分分析,即利用PCA进行降维等。
    训练测试:当文档向量矩阵构建完成以及特征提取也完成之后,就可以利用算法进行训练测试了,贝叶斯分类器、支持向量机、随机森林、集成学习算法等都是文本分类领域适用的算法,个人项目中支持向量机结果最好,随机森林次之。
    最后就是编程实现了,还好sklearn包提供了很多便捷的工具可以很快搭建出来一个小型的文本分类器。
    先贴出来代码,代码文件见附件text.py(打包在text.zip),版本是python3.5,环境是Anaconda,需要调用的包有sklearn.feature_extraction.text里的CountVectorizer、TfidfVectorizer、numpy、nltk.stem。其中nltk.stem是进行词干提取的包,在anaconda里面没有集成,下载官网为http://www.nltk.org/install.html。官网上说在windows下安装需要Python3.5并且只适用于32位,还有的博客说不能直接用pip install安装,但是实际上,在anaconda命令行下直接conda install nltk即可成功。
    程序测试用数据为['This is a book about machine learning.',  'It\'s a cat classified by manual labeling.',  'A newspaper about machine learning.',  'Cats are lovely,while dogs are not.',  'Reading books about machine learning,with a cat at hand.'],这是一个包括五篇文献的文档集。
    关键代码展示如下:其具体内容和其余实验代码以及输出结果可参考博客代码内容(代码展示效果不好)或附件text.py。
    # 综合了去除低频词、去除停用词、词干提取、tfidf的文档向量构建函数,只需传入原始文档即可
    def tfidf_stem_stop_words_vector(text_data):
    # 利用sklearn提供的TfidfVectorizer构造词向量提取器,这个类在上述函数中用到
    class StemmedTfidfVectorizer(TfidfVectorizer):
   
代码文件text.py内容:   
#coding: UTF-8
#encoding: UTF-8
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.feature_extraction.text import TfidfVectorizer
import numpy as np
import nltk.stem as ns


def get_simple_data():
    text_data = ['This is a book about machine learning.',
                 'It\'s a cat classified by manual labeling.',
                 'A newspaper about machine learning.',
                 'Cats are lovely,while dogs are not.',
                 'Reading books about machine learning,with a cat at hand.']
    return text_data
# 先不做任何处理,只是简单对文档进行分词
def simple_text_preprocess(text_data):
    vectorizer = CountVectorizer(min_df=1)                 # min_df代指单词在所有文档中出现的频率小于min_df的都被过滤掉;如果是分数,则按频率计算
    print(vectorizer)
    words_vector = vectorizer.fit_transform(text_data)     # 在文本集中进行提取词向量
    words_feature_names = vectorizer.get_feature_names()   # 得到选取的特征
    print('feature_names:',words_feature_names)
    print('words_vector:',words_vector.toarray())          # 输出词向量,不用toarray()则输出稀疏矩阵;有toarray()则输出完整矩阵
    
    new_text = ['Machine learning is hard to learn for a cat.',
                 'Cat is lazy.']
    new_words_vector = vectorizer.transform(new_text)      # 对新的文本数据进行转换
    print('new_text_words_vector:',new_words_vector.toarray())
# 去除停用词进行分词
def stop_words_text_preprocess(text_data):    
    # 去除停用词之后再进行以便操作
    vectorizer = CountVectorizer(min_df=1,stop_words='english')   # 简单加入"stop_words"即可
    words_vector = vectorizer.fit_transform(text_data)     # 在文本集中进行提取词向量
    words_feature_names = vectorizer.get_feature_names()   # 得到选取的特征
    print('remove_stopwords,feature_names:',words_feature_names)
    print('remove_stopwords,words_vector:',words_vector.toarray())          # 输出词向量,不用toarray()则输出稀疏矩阵;有toarray()则输出完整矩阵
    
    new_text = ['Machine learning is hard to learn for a cat.',
                 'Cat is lazy.']
    new_words_vector = vectorizer.transform(new_text)      # 对新的文本数据进行转换
    print('remove_stopwords,new_text_words_vector:',new_words_vector.toarray())
# 再加上提取词干
def stem_stop_text_preprocess(text_data):
    # stemming提取词干
    st = ns.SnowballStemmer('english')    # 构造提取器
    print('imaging->',st.stem('imaging'),'machine->',st.stem('machine'),'learning->',st.stem('learning'))
    
    # 根据自己构造的StemmedCountVectorizer进行对文本集提取词干
    vectorizer = StemmedCountVectorizer(min_df=1,stop_words='english')
    words_vector = vectorizer.fit_transform(text_data)     # 在文本集中进行提取词向量
    words_feature_names = vectorizer.get_feature_names()   # 得到选取的特征
    print('remove_stopwords,feature_names:',words_feature_names)
    print('remove_stopwords,words_vector:',words_vector)          # 输出词向量,不用toarray()则输出稀疏矩阵;有toarray()则输出完整矩阵
                                
    new_text = ['Machine learning is hard to learn for a cat.',
                 'Cat is lazy.']
    new_words_vector = vectorizer.transform(new_text)      # 对新的文本数据进行转换
    print('remove_stopwords,new_text_words_vector:',new_words_vector)
# 去除停用词的词向量提取器
class StemmedCountVectorizer(CountVectorizer):
    def build_analyzer(self):
        english_stemmer = ns.SnowballStemmer('english')
        analyzer = super(StemmedCountVectorizer,self).build_analyzer()
        return lambda doc: (english_stemmer.stem(w) for w in analyzer(doc))

# 自己实现TFIDF,输入参数为某个要求tfidf的单词,其所在的文档,所有文档集合
def tfidf(term,doc,corpus):
    tf = doc.count(term)/len(doc)                    # 文档频率,单词在其所属文档中出现的频率
    num_docs_with_term = len([d for d in corpus if term in d]) # 包含这个单词的文档总数
    idf = np.log(len(corpus)/num_docs_with_term)     # 文档逆频率,总的文档数除以包含这个单词的文档总数,再取对数
    return tf*idf     
# 测试自己实现的tfidf
def test_tfidf():
    doc = ['cat','cat','dog','dog','machin','learn']
    corpus = [['cat','machin','learn'],
              ['machin','learn'],
              ['cat','learn'],
              ['dog','machin','learn'],
              ['cat','cat','dog','dog','machin','learn']]
    for term in set(doc):
        print('term,tfidf:',term,tfidf(term,doc,corpus))
# 再加入tfidf进行处理
def tfidf_stem_stop_words_vector(text_data):
    # stemming提取词干
    st = ns.SnowballStemmer('english')    # 构造提取器
    print('imaging->',st.stem('imaging'),'machine->',st.stem('machine'),'learning->',st.stem('learning'))
    
    # 根据自己构造的StemmedTfidfVectorizer构造tfidf词向量
    vectorizer = StemmedTfidfVectorizer(min_df=1,stop_words='english',decode_error='ignore') # 忽略编码错误
    words_vector = vectorizer.fit_transform(text_data)     # 在文本集中进行提取词向量
    words_feature_names = vectorizer.get_feature_names()   # 得到选取的特征
    print('remove_stopwords,feature_names:',words_feature_names)
    print('remove_stopwords,words_vector:',words_vector)          # 输出词向量,不用toarray()则输出稀疏矩阵;有toarray()则输出完整矩阵
                                
    new_text = ['Machine learning is hard to learn for a cat.',
                 'Cat is lazy.']
    new_words_vector = vectorizer.transform(new_text)      # 对新的文本数据进行转换
    print('remove_stopwords,new_text_words_vector:',new_words_vector)
# 利用sklearn提供的TfidfVectorizer构造词向量提取器
class StemmedTfidfVectorizer(TfidfVectorizer):
    def build_analyzer(self):
        english_stemmer = ns.SnowballStemmer('english')
        analyzer = super(TfidfVectorizer,self).build_analyzer()
        return lambda doc: (english_stemmer.stem(w) for w in analyzer(doc))

# 简单计算欧氏距离
def dist_raw(v1,v2):
    delta = v1 - v2
    return np.linalg.norm(delta.toarray())


if __name__ == "__main__":
    text_data_1 = get_simple_data()
    #simple_text_preprocess(text_data_1)
    #stop_words_text_preprocess(text_data_1)
    #stem_stop_text_preprocess(text_data_1)
    #test_tfidf()
    tfidf_stem_stop_words_vector(text_data_1)
  
    代码文件到此结束,下面是实例输出结果:
'''
    text_preprocess(text_data_1)输出结果:
    
    CountVectorizer的详细用法:
        analyzer是表示基于word进行分词、并且以token_pattern为正则进行分词这里的u'(?u)\\b\\w\\w+\\b'
        stop_words指是否使用停用词
        lowercase是先将所有变成小写
        
    CountVectorizer(analyzer=u'word', binary=False, decode_error=u'strict',
        dtype=<type 'numpy.int64'>, encoding=u'utf-8', input=u'content',
        lowercase=True, max_df=1.0, max_features=None, min_df=1,
        ngram_range=(1, 1), preprocessor=None, stop_words=None,
        strip_accents=None, token_pattern=u'(?u)\\b\\w\\w+\\b',
        tokenizer=None, vocabulary=None)
    ('feature_names:', [u'about', u'are', u'at', u'book', u'books', u'by', u'cat', u'cats', u'classified', u'dogs', u'hand', u'is', u'it', u'labeling', u'learning', u'lovely', u'machine', u'manual', u'newspaper', u'not', u'reading', u'this', u'while', u'with'])
    ('words_vector:', array([[1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1,
        0, 0],
       [0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0,
        0, 0],
       [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0,
        0, 0],
       [0, 2, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0,
        1, 0],
       [1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0,
        0, 1]], dtype=int64))
    ('new_text_words_vector:', array([[0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0,
        0, 0],
       [0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0]], dtype=int64))
    
    stop_words_text_preprocess(text_data_1)输出结果:
    ('remove_stopwords,feature_names:', [u'book', u'books', u'cat', u'cats', u'classified', u'dogs', u'hand', u'labeling', u'learning', u'lovely', u'machine', u'manual', u'newspaper', u'reading'])
    ('remove_stopwords,words_vector:', array([[1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0],
       [0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0],
       [0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0],
       [0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0],
       [0, 1, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1]], dtype=int64))
    ('remove_stopwords,new_text_words_vector:', array([[0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0],
       [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=int64))
    
    stem_stop_text_preprocess(text_data_1)输出结果:
    imaging-> imag machine-> machin learning-> learn
    remove_stopwords,feature_names: ['book', 'cat', 'classifi', 'dog', 'hand', 'label', 'learn', 'love', 'machin', 'manual', 'newspap', 'read']
    remove_stopwords,words_vector:   (0, 6) 1
      (0, 8)        1
      (0, 0)        1
      (1, 5)        1
      (1, 9)        1
      (1, 2)        1
      (1, 1)        1
      (2, 10)       1
      (2, 6)        1
      (2, 8)        1
      (3, 3)        1
      (3, 7)        1
      (3, 1)        1
      (4, 4)        1
      (4, 11)       1
      (4, 1)        1
      (4, 6)        1
      (4, 8)        1
      (4, 0)        1
    remove_stopwords,new_text_words_vector:   (0, 1)        1
      (0, 6)        2
      (0, 8)        1
      (1, 1)        1
    
    text_tfidf()输出结果:
    term,tfidf: learn 0.0
    term,tfidf: machin 0.0371905918857
    term,tfidf: cat 0.170275207922
    term,tfidf: dog 0.305430243958
    
    tfidf_stem_stop_words_vector(text_data_1)输出结果:
    imaging-> imag machine-> machin learning-> learn
    remove_stopwords,feature_names: ['book', 'cat', 'classifi', 'dog', 'hand', 'label', 'learn', 'love', 'machin', 'manual', 'newspap', 'read']
    remove_stopwords,words_vector:   (0, 0) 0.648462625687
      (0, 8)        0.538282557346
      (0, 6)        0.538282557346
      (1, 1)        0.36063832635
      (1, 2)        0.538497910106
      (1, 9)        0.538497910106
      (1, 5)        0.538497910106
      (2, 8)        0.486240416592
      (2, 6)        0.486240416592
      (2, 10)       0.726044430146
      (3, 1)        0.427992922683
      (3, 7)        0.639070441396
      (3, 3)        0.639070441396
      (4, 0)        0.40357561577
      (4, 8)        0.335004217566
      (4, 6)        0.335004217566
      (4, 1)        0.335004217566
      (4, 11)       0.500221573402
      (4, 4)        0.500221573402
    remove_stopwords,new_text_words_vector:   (0, 8)        0.408248290464
      (0, 6)        0.816496580928
      (0, 1)        0.408248290464
      (1, 1)        1.0
'''

 

0 个评论

要回复文章请先登录注册