最近在做一个简单的小项目,基于SVM的专利文本分类。这里记录一下整个过程。

文本分类主要分为四个步骤:

  1. 文本特征提取
  2. 文本特征表示
  3. 归一化处理
  4. 文本分类

1. 特征提取

特征提取的步骤可以总结为:

  1. 对全部训练文档进行分词,由这些词作为向量的维数来表示文本;
  2. 统计每一类内文档所有出现的词语及其频率,然后过滤,剔除停用词和单字词;
  3. 统计每一类内出现词语的总词频,并取其中的若干个频率最高的词汇作为这一类别的特征词集;
  4. 去除每一类别中都出现的词,合并所有类别的特征词集,形成总特征词集。最后所得到的特征词集就是我们用到的特征集合,再用该集合去筛选测试集中的特征。

关于Python的文本处理,用得最多的是以下两个库:

  • NLTK(自然语言工具包):用于诸如分词,词法去除,词干提取,解析,POS标记等任务。该库具有用于几乎所有NLP任务的工具。

  • spaCy:是NLTK的主要竞争对手。这两个库可用于相同的任务。

NLTK更具学术性,你可以使用它尝试不同的算法,将它们组合起来,等等。spaCy则为每个问题提供了一种即用的解决方案。你不必考虑哪种方法更好,spaCy的作者已经考虑了这一点。并且spaCy速度非常快(比NLTK快几倍),所以这里我们选择spaCy来进行文本特征提取。

spaCy安装

spaCy的github地址:https://github.com/explosion/spaCy

使用pip可以直接安装spaCy。

pip install -U spacy

spaCy的模型又称为管道(Pipline),管道由多个不同的模块组成,每个模块有不同的功能,如分词、词性标注等等,用户可以自定义管道中的模块以实现自定义功能。

pipeline-fde48da9b43661abcdf62ab70a546d71
pipeline-fde48da9b43661abcdf62ab70a546d71

通过命令可以下载模型:

python -m spacy download en_core_web_lg

en_core_web_lg是模型的名称,可以到该链接搜索模型。 lg是该模型的大小,还有mdlgtrf选项。smtrf管道中不包含静态词向量。因为我们打算直接使用spaCy的词向量来进行训练,所以这里选择lg大小的模型。

由于在国内,可能会有下载慢的问题,可以到github搜索模型,然后使用pip install some_model.whl手动安装

接下来简单测试一下spaCy的功能:

import spacy

nlp = spacy.load("en_core_web_sm")
doc = nlp("Apple is looking at buying U.K. startup for $1 billion")

for token in doc:
print(token.text, token.lemma_, token.pos_, token.tag_, token.dep_,
token.shape_, token.is_alpha, token.is_stop)

2. 特征表示

由于计算机无法直接处理如语音,文字和图像等非结构数据,故我们需要将文本转化为计算机能够识别的结构化数据。这一过程称为文本特征表示,即将自然语言转化为向量的过程。

2.1 词的表示

将自然语言的词转化为向量的过程就叫词嵌入(Word Embedding)。根据向量的不同形式,又可分为词的离散表示和分布式表示。

离散表示(Discrete Representation)

把每个词表示为一个长向量。这个向量的维度是词表大小,向量中只有一个维度的值为1,其余维度为0,这个维度就代表了当前的词。然而离散表示不能表示出单词之间的关系,因为任意两个单词之间是两两正交的,而且面临着数据稀疏性和维度灾难的问题。但也有一个好处,就是在高维空间中,很多应用任务线性可分。离散表示的例子包括独热编码,词袋模型、。

分布式表示(Distribution Representation)

离散表示每个单词彼此无关这个特点明显不符合我们的现实情况。我们知道大量的单词都是有相关性的。分布式表示就是将词转化成一种分布式表示,将词表示成一个定长的连续的稠密向量。我们更希望用诸如“语义”,“复数”,“时态”等维度去描述一个单词。每一个维度不再是0或1,而是连续的实数,表示不同的程度。

分布式表示优点:

  • 词之间存在相似关系,存在“距离”概念,这对很多自然语言处理的任务非常有帮助。
  • 词向量能够包含更多信息,并且每一维都有特定的含义。

2.2 词袋模型(Bag Of Words,BOW)

该模型忽略掉文本的语法和语序等要素,将其仅仅看作是若干个词汇的集合,文档中每个单词的出现都是独立的。它不考虑句子中单词的顺序,只考虑词表(Vocabulary)中单词在这个句子中的出现次数。向量中每个位置的值为该编码对应的词在这段话中出现的次数。

与词袋模型非常类似的一个模型是词集模型(Set Of Words,SOW),和词袋模型唯一的不同是它仅仅考虑词是否在文本中出现,而不考虑词频。也就是一个词在文本在文本中出现1次和多次特征处理是一样的。在大多数时候,我们使用词袋模型。

大部分的文本都只会使用词汇表中的很少一部分的词,因此我们的词向量中会有大量的0。也就是说词向量是稀疏的。在实际应用中一般使用稀疏矩阵来存储。

2.3 TF-IDF

TF-IDF (Term Frequency-Inverse Document Frequency,词频一逆文件频率)是一种用来评估某个词语对于某篇文章重要程度的统计方法,其核心思想是,如果某个词语在一篇文章中出现的频率较高,且在其他文章中鲜少出现,则判定该词语能较好地代表当前文章: TF(词频)指某一个词语在当前文章中出现的次数,由于同一个词语在不同长度的文章中出现的次数不一样,且文章越长,出现的频率可能就越高,故还需要对词频进行归一化(一般是词频除以文章总词数),以防止它偏向长的文件。计算公式如下所示:

分子为词语在文档中出现的次数,分母为文档中所有词语出现次数的总和。

IDF(逆文档频率)是指某个关键词在整个语料所有文章中出现的次数。逆文档频率是文档频率的倒数,主要用于降低所有文章中都很常见却对当前文章影响不大的词语的作用。IDF反应了一个词在所有文章中出现的频率,如果一个词在很多的文章中出现,那么它的IDF值应该低,而反过来如果一个词在比较少的文章中出现,那么它的IDF值应该高。其计算公式如下: 其中代表语料库中文本的总数(样本数),而代表语料库中有多少文本包含词。 为防止该词语在语料库中不存在,即分母为,使用作为分母。

使用sklearn可以很方便地计算TF-IDF,代码如下。

from sklearn.feature_extraction.text import TfidfTransformer
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.pipeline import Pipeline

def getTfidfVector(corpus):

vocabulary = [
'cache', 'coherence', 'coherency', 'cosistency', 'consistent',
'bus', 'snooping', 'sniffing', 'snoopy', 'filter',
'data', 'uniformity', 'simultaneous', 'storing',
]

pipe = Pipeline(
[
('count', CountVectorizer(vocabulary=vocabulary, lowercase=False)),
('tfid', TfidfTransformer())
]
).fit(corpus)

count_arr = np.array(pipe['count'].transform(corpus).toarray())
tfid_arr = pipe['tfid'].transform(count_arr).toarray()

return tfid_arr

2.4 Word2Vec

基于分布式表示的思想,我们构建一个密集向量(Dense Vector)来表征一个词汇。使用向量点积表征词汇含义相似度,这就是Word2Vec方法的基础。Word2Vec是一种用来产生词向量的浅层神经网络,于2013年Google团队提出,核心思想是通过词的上下文得到词的向量化表示,共有skip-grams和CBOW两种模型。

CBOW模型先获取某个词的上下文,然后利用上下文推测出这个特定词,即将某一个特征词的上下文相关的词对应的词向量作为输入,并通过评估概率找出概率最大特定词输出。

Skip-gram则是通过某个特定词来预测出可能出现在其上下文的词,即输入是特定的一个词的词向量,而输出是特定词对应的上下文词向量。

故由上可知,同one-hot编码相比,Wrod2vec不仅在一定程度上解决了向量空间过大且稀疏的问题,同时还保留词与词之间的关系,从而能有效提升分类的精确率。

3. 分类

sklearn提供了封装好的svm和knn分类器,开箱即用。将数据集处理好后直接输入分类器中训练即可。

from sklearn import svm
from sklearn.neighbors import KNeighborsClassifier

def train_svm(train_data, train_label, test_data, test_label):

clf = svm.SVC(kernel='poly')
clf.fit(train_data, train_label)
print("训练集准确率:", clf.score(train_data, train_label))
print("测试集准确率:", clf.score(test_data, test_label))
print(clf.predict(test_data).tolist())
print(test_label)

def train_knn(train_data, train_label, test_data, test_label):
clf = KNeighborsClassifier(n_neighbors=3)
clf.fit(train_data, train_label)
print("训练集准确率:", clf.score(train_data, train_label))
print("测试集准确率:", clf.score(test_data, test_label))
print(clf.predict(test_data).tolist())
print(test_label)

这里词向量表示采用TF-IDF,126个样本点的训练输出结果如下:

训练集准确率: 0.8888888888888888
测试集准确率: 0.7619047619047619
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
(1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0)
训练集准确率: 0.9753086419753086
测试集准确率: 0.7142857142857143
[1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1]
(1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0)

结果不算很好,主要是由于目前的数据集比较少,而且统计学习模型本身也有一定的局限。

213个样本点的结果如下:

model 准确率 精确率 召回率 score
svm