source: python machine learning 3rd
词袋模型是NLP的基础,理解掌握了词袋模型的处理方法和步骤是NLP必备的敲门砖。本文将着重介绍梳理使用python对词袋模型的构造和处理过程。
什么是词袋模型?简而言之,词袋模型就是我们使用量化模拟的方式来表达文本的一种方式。
具体而言,词袋模型的构造思路如下:
由于每个文章中的关键词都不一样,因此词汇表中很多特征维度中的值都是0,因此我们称词袋模型的特征维度是稀疏的
词袋模型内心是严重排斥不干净的文本的,如果你想运用词袋模型进行NLP分析,首先你得把文本清理干净
清理文本的途径当然是多种多样的,且对不同种类的文本需要清理的对象不同,下面就简单援引一种较好用的常规方法
首先看这段文本:
‘is seven.
Title (Brazil): Not Available ’
html标签,标点符号,表情符号啥都有,必须多次使用正则表达式来处理,直到文本干净为止:
import re
def preprocessor(text):
# 去除html标签
text = re.sub('<[^>]*>', '', text)
# 简化复杂的emoji符号,如:-) --> :)
emoticons = re.findall('(?::|;|=)(?:-)?(?:\)|\(|D|P)',
text)
# 标点符号肯定也不能有
text = (re.sub('[\W]+', ' ', text.lower()) +
' '.join(emoticons).replace('-', ''))
return text
没错,正则表达式很重要(但事实上是不推荐使用正则表达式去除html标签的)
对于大小写的处理,还是要以具体情况为准。很多情况下,大小写不那么重要,同样使用正则表达式进行替换即可,如果你能够看懂之前代码中的正则表达式,那么这肯定也不是问题
典型的需要保留大小写区别的情况就比如学术文章的处理,此时大写字母可能会代表专门的术语而不是平常的词汇
词袋模型同样不欢迎停用词(比如like,is,has),因为它们对文本分类问题几乎没有帮助,当然,如果你的NLP问题不涉及文本分类,你可以选择不对停用词进行删除(即使是分类问题,也需要具体问题具体分析,比如说小学生的文章中停用词的使用频率会高于专业作家,此时停用词对于分析文章水平是有一定意义的)
直接上代码:
import nltk
# 下载一次后以后就不用下载了
nltk.download('stopwords')
from nltk.corpus import stopwords
stop = stopwords.words('english')
print([w for w in tokenizer_porter('a runner likes running and runs a lot')[-10:]
if w not in stop])
['runner', 'like', 'run', 'run', 'lot']
其中tokenizer_porter是之后文本特征化中的内容,负责将文本分割成单个字符然后存储在数组上,后文中我们会详细介绍
使用python可以很简单就完成这个步骤:
import numpy as np
from sklearn.feature_extraction.text import CountVectorizer
count = CountVectorizer()
docs = np.array([
'The sun is shining',
'The weather is sweet',
'The sun is shining, the weather is sweet, and one and one is two'])
bag = count.fit_transform(docs)
使用sklearn中地CountVectorizer可以一步到位地帮我们完成转换,稍加注意我们此时需要将文本内容存储到numpy一维数组中
查看获得的词汇表:
>>>print(count.vocabulary_)
{'and': 0,
'two': 7,
'shining': 3,
'one': 2,
'sun': 4,
'weather': 8,
'the': 6,
'sweet': 5,
'is': 1}
最后,再次一步到位使用toarray方法完成特征维度的构建:
>>> print(bag.toarray())
[[0 1 0 1 1 0 1 0 0]
[0 1 0 0 0 1 1 0 1]
[2 3 2 1 1 1 2 1 1]]
这三组特征维度分别为分别属于我们的原始文本
得到了特征维度之后,我们引入一个新的概念 - 原始词频: t f ( t , d ) tf(t, d) tf(t,d),来表达特征维度中的数字(即特征值),t=term(词汇),d=document(文本)。原始词频即文本d中出现某个特定词汇t的次数
目前而言,原始词频的排布顺序一般是按照字母表先后顺序来排布的。换言之,我们构造出的特征维度坐标从左向右的顺序符合词汇表中的词汇字母表顺序
N-gram表示我们的每个表征包含的词汇数量,示例中我们的表征很明显都是单个词汇,顾名思义称之为 1-gram 或者 unigram
不同的文本类型,构造特征维度使用的N-gram不同,比如说对于垃圾邮件分类问题,N一般取3-4能够获得最好的效果
全称:term frequency-inverse document frequency,即原始词频和逆文本频率指数的结合使用:
t f − i d f ( t , d ) = t f ( t , d ) × i d f ( t , d ) t f-i d f(t, d)=t f(t, d) \times i d f(t, d) tf−idf(t,d)=tf(t,d)×idf(t,d)
有了原始词频,为什么我们还需要逆文本频率指数?因为原始词频可以帮助我们使用量化的特征维度来表示文本,但是完全无法体现出词汇在文本中的特殊性,而词汇的特殊性对于文本的分类是相当重要的
逆文本频率指数的表示如下:
i d f ( t , d ) = log n d 1 + d f ( d , t ) i d f(t, d)=\log \frac{\mathrm{n}_{\mathrm{d}}}{1+d f(d, t)} idf(t,d)=log1+df(d,t)nd
其中df(d,t)为出现词汇(t)的文本(d)的数量,解释背后的数学原理在我的能力之外
from sklearn.feature_extraction.text import TfidfTransformer
tfidf = TfidfTransformer(use_idf=True,
norm='l2',
smooth_idf=True)
print(tfidf.fit_transform(count.fit_transform(docs))
.toarray())
同样使用一个简单的sklearn方法,我们可以快速地构造TF-IDF特征维度。其中可能让人感到疑惑地是smooth_idf参数。设置其为True后idf方程图形会更加平滑(因为方程从log1=0开始,如果是log=0 --> 无穷会使曲线很陡峭),其改造idf方程为:
idf ( t , d ) = log 1 + n d 1 + d f ( d , t ) \text {idf}(t, d)=\log \frac{1+\mathrm{n}_{\mathrm{d}}}{1+d f(d, t)} idf(t,d)=log1+df(d,t)1+nd验证以下,不难计算得: idf ( "is", d 3 ) = log 1 + 3 1 + 3 = 0 \text {idf}\left(\text { "is", } d_{3}\right)=\log \frac{1+3}{1+3}=0 idf( "is", d3)=log1+31+3=0可见is在文本没有特殊性
然而,0得出现不能够总是让人感到快乐的,0不能够帮助文本分类不代表它的频率就对文本一点意义都没有,因此,sklearn中的实际TF-IDF方程对光滑的idf还做了+1处理,如下:
t f − i d f ( t , d ) = t f ( t , d ) × ( i d f ( t , d ) + 1 ) t f-i d f(t, d)=t f(t, d) \times(i d f(t, d)+1) tf−idf(t,d)=tf(t,d)×(idf(t,d)+1)
最后设置了norm参数进行‘l2’归一化使得所有特征值落到0,1之间
终于,分析完了构建特征维度需要的步骤,在这里我们终于将对文本的分类问题进行探讨。
思考一个问题,获得特征维度的你如何对原始文本进行分析分类?这是一个有难度的问题。但是如果我们将原始文本也像特征维度一样用词汇来表示,那么问题会简单得多。
如果你已经将文本清理的很干净了,那么使用split方法就能够很好地将大块的文本特征化,空间之间即是一个单词。随后,我们还需要进行词干提取(word stemming):
from nltk.stem.porter import PorterStemmer
porter = PorterStemmer()
# 1. 简单的split方法将清理完毕的文本分割好
def tokenizer(text):
return text.split()
# 2. 使用专业的stemmer将分割后的文本进行词根提取
def tokenizer_porter(text):
return [porter.stem(word) for word in text.split()]
-->文本分割['runners', 'like', 'running', 'and', 'thus', 'they', 'run']
-->词根提取 ['run', 'like', 'run', 'and', 'thu', 'they', 'run']
关于词干提取,了解以下几点能够帮助你更好地运用它:
porter stemming方法是最古老,最简单的词干提取算法,使用其它的流行算法如Snowball stemmer以及Lancaster stemmer一般都有更好的效果,如果要求不苛刻,当然porter stemming就够用了 查看其它词干提取方法
由于词干提取有时候会给出一些莫名其妙的词语(比如thu),你可以使用*词形化(lemmatization)*来避免这个问题。然而,词形化费时费力,而且实践中几乎没有什么优势
OK,接下来再使用特征维度对文本进行分类分析就会简单得多了
from sklearn.pipeline import Pipeline
from sklearn.linear_model import LogisticRegression
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.model_selection import GridSearchCV
tfidf = TfidfVectorizer(strip_accents=None,
lowercase=False,
preprocessor=None)
param_grid = [{'vect__ngram_range': [(1, 1)],
'vect__stop_words': [stop, None],
'vect__tokenizer': [tokenizer, tokenizer_porter],
'clf__penalty': ['l1', 'l2'],
'clf__C': [1.0, 10.0, 100.0]},
{'vect__ngram_range': [(1, 1)],
'vect__stop_words': [stop, None],
'vect__tokenizer': [tokenizer, tokenizer_porter],
'vect__use_idf':[False],
'vect__norm':[None],
'clf__penalty': ['l1', 'l2'],
'clf__C': [1.0, 10.0, 100.0]},
]
lr_tfidf = Pipeline([('vect', tfidf),
('clf', LogisticRegression(random_state=0, solver='liblinear'))])
gs_lr_tfidf = GridSearchCV(lr_tfidf, param_grid,
scoring='accuracy',
cv=5,
verbose=2,
n_jobs=-1)