python机器学习16:文本数据处理

1.文本数据的特征提取、中文分词及词袋模型

本节我们一起学习如何对文本数据进行特征提取,如何对中文分词处理,以及如何使用词袋模型将文本特征转化为数组的形式,以便将文本转化为机器可以“看懂”的数字形式。

1.1使用CountVectorizer对文本进行特征提取

在前面的章节,我们用来展示的数据特征大致可以分为两种:一种是用来表示数值的连续特征;另一种是表示样本所在分类的类型特征。而在自然语言处理的领域中,我们会接触到的第三种数据类型--文本数据。举个例子,假如我们想知道用户对某个商品的评价是“好”还是“差”,就需要使用用户评价的内容文本对模型进行训练。例如,用户评论说“刚买的手机总是死机,太糟糕了!” 或者“新买的衣服很漂亮,老公很喜欢。”这就需要我们提取出两个不同评论中的关键特征,并进行标注用于训练机器学习模型。
文本数据在计算机中往往被存储为字符串类型(String),在不同的场景中,文本数据的长度差异会非常大,这也使得文本数据的处理方式与数值型数据的处理方式完全不同。而中文的处理尤其困难,因为在一个句子当中,中文的词与词之间没有边界,也就是说,中文不像英语那样,在每个词之间有空格作为分界线,这就要求我们在处理中文文本的时候,需要先进行分词处理。
例如这句英语:“The quick brown fox jumps over a lazy dog”,翻成中文是“那只敏捷的棕色狐狸跳过了一直懒惰的狗”。这两句话在处理中非常不同,我们来看下面的代码:

#导入向量化工具
from sklearn.feature_extraction.text import CountVectorizer
vect = CountVectorizer()
#使用vector拟合文本数据
en = ['The quick brown fox jumps over a lazy dog']
vect.fit(en)
#打印结果
print('单词数:{}'.format(len(vect.vocabulary_)))
print('分词:{}'.format(vect.vocabulary_))

运行代码,得到如下图所示的结果:


使用CountVector拟合数据的结果

结果分析:可能读者朋友们对这个结果会感觉到有点奇怪,明明这句话当中有9个单次,为什么程序告诉我们单次数是8个呢?我们来检查一下分词的结果,原来程序没有将冠词“a”统计进来。因为“a”只有一个字母,所以程序没有把她作为一个单次。
下面来看中文的情况,输入代码如下:

#使用中文分词作为实验
cn = ['那只敏捷的棕色狐狸跳过了一只懒惰的狗']
#拟合中文文本数据
vect.fit(cn)
#打印结果
print('单词数:{}'.format(len(vect.vocabulary_)))
print('分词:{}'.format(vect.vocabulary_))

运行代码,得到如下图的结果:


对中文文本进行向量化的结果

结果分析:可以看到,程序无法对中文语句进行分词,它把整句话当成了一个词,因为中文与英语不同,英语的词与词之间有空格作为天然的分割词,而中文却没有。在这种情况下,我们就需要使用专门的工具来对中文进行分词。目前市面上有几款用于中文分词的工具,使用较多的工具之一是“结巴分词”,下面我们以“结巴分词”为例,介绍一下中文分词的分词方法。

1.2使用分词工具对中文文本进行分词

我们使用“结巴分词”来对上文中的中文语句进行分词,输入代码如下:

import jieba
cn = jieba.cut('那只敏捷的棕色狐狸跳过了一只懒惰的狗')
cn = [' '.join(cn)]
print(cn)

运行代码,得到如下图所示的结果:


结巴分词对中文文本分词的结果

借助“结巴分词”,我们把这句中文语句进行了分词操作,并在每个单词之间插入空格作为分界线。下面我们重新使用CountVectorizer对其进行特征抽取,输入代码如下:

#使用CountVectorizer对中文文本进行向量化
vect.fit(cn)
print('单词数:{}'.format(len(vect.vocabulary_)))
print('分词:{}'.format(vect.vocabulary_))

运行代码,得到如下图所示的结果:


使用CountVectorizer提取出的特征

结果分析:经过了分词工具的处理,我们看到CountVectorizer已经可以从中文文本中提取出若干个整型数值,并且生成了一个字典。
接下来,我们要将使用这个字典将文本的特征表达出来,以便可以用来训练模型。

1.3使用词袋模型将文本数据转为数组

在上面的实验中,CountVectorizer给每个词编码为一个从0到5的整型数。经过这样的处理后,我们便可以用一个稀疏矩阵(sparse matrix)对这个文本数据进行表示了。
输入代码如下:

#定义词袋模型
bag_of_words = vect.transform(cn)
#打印词袋模型中的数据特征
print('转化为词袋的特征:{}'.format(repr(bag_of_words)))

运行代码,可以得到如下图所示的结果:


转为词袋模型的特征

结果分析:从结果中可以看到,原来的那句话被转化为一个1行6列的稀疏矩阵,类型为64位整型数值,其中有6个元素。
下面我们看看6个元素都是什么,输入代码如下:

#打印词袋密度的表达
print('词袋的密度表达:{}'.format(bag_of_words.toarray()))

运行代码,会得到如下图所示的结果:


词袋的密度表达

结果分析:可能看到结果会让人有点费解。它的意思是,在这一句话中,我们通过分词工具拆分出来的6个单词在这句话中出现的次数是1次;第二个元素1,代表这句话中,“懒惰”这个词出现的次数也是1.
为了更加容易理解,我们试着换一句话来看看结果有什么不同,例如,“懒惰的狐狸不如敏捷的狐狸敏捷,敏捷的狐狸不如懒惰的狐狸懒惰”。输入代码:

cn1 = jieba.cut('懒惰的狐狸不如敏捷的狐狸敏捷,敏捷的狐狸不如懒惰的狐狸懒惰')
print(type(cn))
cn2 = [' '.join(cn1)]
print(cn2)

上面这段代码主要是使用“结巴分词”将我们编造的这段话进行分词,运行代码,得到如下图所示的结果:


对新的文本进行分词处理的结果

接下来,我们再使用CountVectorizer将这句文本进行转化,输入代码如下:

#导入向量化工具
from sklearn.feature_extraction.text import CountVectorizer
#建立词袋模型
new_bag = vect.transform(cn2)
print('转为词袋的特征:{}'.format(repr(new_bag)))
print('词袋的密度表达:{}'.format(new_bag.toarray()))

运行代码,得到如下图所示的结果:


新文本的词袋和密度表达

结果分析:我么发现,同样还是1行6列的,不过存储的元素只有3个,而数组[[0 3 3 0 4 0]]的意思是,“一只”这个词出现的次数是0,而“懒惰”这个词出现了3次,“敏捷”这个词出现了3次,“棕色”这个词出现了0次,“狐狸”这个词出现了4次,“跳过”这个词出现了0次。
上面这种用数组表示一句话中,单词出现次数的方法,被称为“词袋模型”(bag-of-words)。这种方法是忽略了一个文本中的词序和语法,仅仅将它看作一个词的集合。这种方法对于自然语言进行了简化,以便于机器可以读取并且进行模型的训练。但是词袋模型也具有一定的局限性,下面我们继续介绍对于文本类型数据的进一步优化处理。

2.对文本数据进一步优化处理

本节,我们将和大家一起学习如何使用n_Gram算法来改善词袋模型,以及如何使用tf-idf算法对文本数据进行处理,和如何删除文本数据中的停用词。

2.1使用n-Gram改善词袋模型

虽然用词袋模型可以简化自然语言,利于机器学习算法建模,但是它的劣势也是很明显-----由于词袋模型把句子看成单词的简单集合,那么单词出现的顺序就会被无视,这样一来可能会导致包含同样单词,但是顺序不一样的两句话在机器学习看来成了完全一样的意思。
比如:“道士看见和尚亲吻了尼姑的嘴唇”,我们用词袋模型将这句话的特征进行提取:

import jieba
joke = jieba.cut('道士看见和尚亲吻了尼姑的嘴唇')
joke = [' '.join(joke)]
vect.fit(joke)
joke_feature = vect.transform(joke)
print(joke_feature.toarray())

这里我们首先用“结巴分词”对这句话进行了分词,然后使用CountVectorizer将其表达为数组
运行代码,结果如下:
[[1 1 1 1 1 1]]
接下来,我们把这句话的顺序打乱,变成“尼姑看见道士的嘴唇亲吻了和尚”,再看看结果会有什么不同,输入代码如下:

joke2 = jieba.cut('尼姑看见道士的嘴唇亲吻了和尚')
joke2 = [' '.join(joke2)]
#进行特征提取
joke2_feature = vect.transform(joke2)
print('特征表达:{}'.format(joke2_feature.toarray()))

运行代码,得到如下结果:
特征表达:[[1 1 1 1 1 1]]
结果分析:和上一个代码的结果进行对比的话,发现两个结果完全一样!也就是说,这两句意思完全不同的话,对于机器来讲,意思是一模一样!
要解决这个问题,我么可以对CountVectorizer中的ngram_range参数进行调节。这里我们先介绍一下,n_Gram是大词汇连续文本或语音识别中常用的一种语言模型,它是利用上下文相邻词的搭配信息来进行文本数据转换的,其中n代表一个整型数值,例如n等于2的时候,模型称为bi-Gram,意思是会对相邻的两个单词进行搭配;而n等于3时,模型称为tri-Gram,也就是会对相邻的3个单词进行配对。下面我们来演示如何在CountVectorizer中调节n-Gram函数,来进行词袋模型的优化,输入代码如下:

#修改CountVectorizer的ngram参数
vect = CountVectorizer(ngram_range=(2,2))
#重新进行文本数据的特征提取
cv = vect.fit(joke)
joke_feature = cv.transform(joke)
print('特征表达:{}'.format(joke_feature.toarray()))
print('分词:{}'.format(vect.vocabulary_))

这里,我们将CountVectorizer的ngram_range参数调节为(2,2),意思是进行组合的单词数量的下限是2,上限也是2.也就是说,我们限制CountVectorizer将句子中相邻的两个单词进行组合,运行代码,得到如下图所示的结果:


调整CountVectorizer的ngram参数后的数据处理结果

现在我们再来试试另外一句“尼姑看见道士的嘴唇亲吻了和尚”,看看转化的特征是否有了变化,输入代码如下:

#调整文本顺序
joke2 = jieba.cut('尼姑看见道士的嘴唇亲吻了和尚')
#插入空格
joke2 = [' '.join(joke2)]
#提取文本特征
joke2_feature = vect.transform(joke2)
#特征表达
print(joke2_feature.toarray())

运行代码,得到如下图所示的结果:


调整顺序后的文本数据特征表达

结果分析:现在我们看到,在调整了CountVectorizer的ngram_range参数之后,机器不再认为这两句是同一个意思了。而除了使用n-Gram模型对文本特征提取进行优化之外,在scikit-lean中,还可以使用另外一种tf-idf模型来进行文本特征提取的类,称为TfidfVector。

2.2使用tf-idf模型对文本数据进行处理

tf-idf全称为“term frequency-inverse document frequency”,一般翻译为“词频-逆向文件频率”。它是一种用来评估某个词对于一个语料库中某一份文件的重要程度,如果某个词在某个文件中出现的次数非常高,但在其他文件中出现的次数很少,那么tf-idf就会认为这个词能够很好地将文件进行区分,重要程度就会较高,反之则认为该单词的重要程度较低。下面我们看一下tf-idf的公式。
首先是计算tf值的公式:
\frac{n_{i,j}}{\sum_{k}n_{k,j}}
式中:n_{i,j}表示某个词在语料库中某个文件内出现的次数;{\sum_{k}n_{k,j}}表示的是该文件中所有单词出现的次数之和。
而在scikit-lean中,idf的计算公式如下:
idf = log(\frac{N+1}{N_w+1})+1
式中:N代表的是语料库中文件的总数;N_w代表语料库中包含上述单词的文件数量。
那么最终计算tf-idf值的公式就是:
tf-idf = tf* idf

注意

读者朋友可能会在其他地方看到和此处不太一样的公式,不要觉得奇怪,这是因为tf-idf的计算公式本身就有很多种变体
在scikit-lean当中,有两个类使用了tf-idf方法,其中一个是TfidfTransformer,它用来将CountVectorizer从文本中提取的特征矩阵进行转化;另一个是TfidfVectorizer,它和CountVectorizer的用法是相同的--简单理解的话,它相当于把CountVectorizer和TfidfTransformer所做的工作整合在了一起。
为了进一步介绍TfidfVectorizer的用法,已经它和CountVectorizer的区别,我们下面使用一个相对复杂的数据集,也是一个非常经典的用于进行自然语言处理的案例,就是IMDB电影评论数据集。这个数据集是由斯坦福大学的研究人员创建的,包括100000条IMDB网站用户对于不同电影的评论,每条评论被标注为“正面”(positive)或者“负面”(negtive)两种类型。如果用户在IMDB网站上给某个电影的评分大于或等于6,那么他的评论将被标注为“正面”,否则被标注为“负面”。
值得称赞的是,创建者已经将数据集拆分成了训练集和测试集,分别有25000条数据,并且放在了不同的文件夹中,正面评论放在“pos”文件夹中,而负面评论放在了“neg”文件夹中,还有5000条没有进行分类的数据集,可以供我们进行无监督学习的实验。可以在http://ai.stanford.edu/~amaas/data/setiment/中下载这个数据集来进行实验。
接下来,载入IMDB电影评论数据集,来看看它的结构,输入命令:

!tree C:\\Users\\1003342\\Desktop\\study\\20190528_sklearn\\datasets\\aclImdb

运行,得到如下图所示的树状文件夹列表:


影评数据文件结构

结果分析:
从结果中,可以看出IMDB影评数据集解压后是存放在一个名叫aclImdb文件夹中,训练集和测试集分别保存在名为“train”和“test”子文件夹中,每个子文件夹下还有存放正面评论的“pos”文件夹和“neg”文件夹,而“train”文件夹下还有一个“unsump”的子文件夹,存放的是不含分类标注的用于进行无监督学习的数据。
为了能够减低数据载入的时间,我们从train和test文件夹中各抽取50个正面评论和50个负面评论,保存在新的文件夹中。
输入代码如下:

#导入文件载入工具
#导入文件载入工具
from sklearn.datasets import load_files
#定义训练数据集
train_set = load_files('C:\\Users\\1003342\\Desktop\\study\\20190528_sklearn\\datasets\\aclImdb\\train')
X_train,y_train = train_set.data,train_set.target
#打印训练数据集文件数量
print('训练集文件数量:{}'.format(len(X_train)))
#随便抽取一条影评打印出来
print('随便抽一个看看:{}'.format(X_train[22]))

运行代码,得到如图所示的结果:


训练集文件数量和某条影评内容

结果分析:由于我们各从pos文件夹中的正面评论和neg文件夹的负面评论中抽取了50个样本,因此整个训练集中有100个样本。通过打印第22个样本,我们看到这段影评的内容还是相当丰富的,但大家会发现在评论正文中,有很多<br />的符号,这是在网页中用来分行的符号。为了不让它影响机器学习的模型,我们把它用空格来替换掉,输入代码如下:

#将文本中的<br/>全部去掉
X_train = [doc.replace(b'<br />',b' ') for doc in X_train]

运行这行代码之后,再打印同一条影评的话,你就会发现<br />全部被空格替换掉了。
我们再次载入测试集,输入代码如下:

#载入测试集
test = load_files('C:\\Users\\1003342\\Desktop\\study\\20190528_sklearn\\datasets\\aclImdb\\test')
X_test,y_test = test.data,test.target
len(X_test)

运行代码,发现程序返回的测试集样本数100,说明测试集加载成功。同时也把测试集中的
去掉:

#将文本中的<br/>全部去掉
X_test = [doc.replace(b'<br />',b' ') for doc in X_test]

下一步使用CountVectorizer进行特征提取:

#转化为向量
from sklearn.feature_extraction.text import CountVectorizer
#使用CountVectorizer拟合训练数据
vect = CountVectorizer().fit(X_train)
#将文本转化为向量
X_train_vect = vect.transform(X_train)
#特征数量
print('训练集样本特征数量:{}'.format(len(vect.get_feature_names())))
#打印最后10个训练集样本特征
print('最后10个训练集样本特征:{}'.format(vect.get_feature_names()[-10:]))

运行代码,得到如下图所示的结果:


训练集的特征数量和最后10个特征

结果分析:结果看到,训练集又4000个特征。
下面使用有监督学习算法进行交叉验证评分,看看模型是否能较好地拟合训练集数据:

##导入线性SVC分类模型
from sklearn.svm import LinearSVC
# #导入交叉验证工具
from sklearn.model_selection import cross_val_score
#使用交叉验证对模型进行评分
scores = cross_val_score(LinearSVC(),X_train_vect,y_train)
#打印交叉验证平均分
print('模型平均分:{:.3f}'.format(scores.mean()))

我们使用LinearSVC算法来进行建模,运行代码,得到如下图所示的结果:


模型平均分

结果分析:从结果中可以看到模型的平均分是0.778,虽然不是很低,但仍有些差强人意,下一步试试泛化到测试集:

#把测试数据集转化为向量
X_test_vect = vect.transform(X_test)
clf = LinearSVC().fit(X_train_vect,y_train)
print('测试集得分:{}'.format(clf.score(X_test_vect,y_test)))

运行代码,得到结果如下:
测试集得分:0.58
结果分析:0.58分并不是很理想,所以再尝试用tf-idf算法来处理一下数据试试:

#导入tfidf转化工具
from sklearn.feature_extraction.text import TfidfTransformer
#使用tfidf工具转化训练集和测试集
tfidf = TfidfTransformer(smooth_idf = False)
tfidf.fit(X_train_vect)
X_train_tfidf = tfidf.transform(X_train_vect)
X_test_tfidf = tfidf.transform(X_test_vect)
print('未经tfidf处理的特征:{}'.format(X_train_vect[:5,:5].toarray()))
print('经过tfidf处理的特征:{}'.format(X_train_tfidf[:5,:5].toarray()))

运行代码,得到如下图所示的结果:


tf-idf处理前后的特征对比

结果分析:我们打印了前5个样本的前5个特征。从结果可以看到,在未经TfidfTransform处理前,CountVectorizer只是计算某个词在该样本中某个特征出现的次数,而tf-idf计算的是词频乘以逆向文档频率,所以是一个浮点数。
下面试验一下经过处理后的数据集训练模型评分:

#重新训练线性svc模型
clf = LinearSVC().fit(X_train_tfidf,y_train)
#使用新数据集进行交叉验证
scores2 = cross_val_score(LinearSVC(),X_train_tfidf,y_train)
#打印交新的分数进行对比
print('经过tf-idf处理的训练集交叉验证得分:{:.3f}'.format(scores2.mean()))
print('经过tf-idf处理的测试集得分:{:.3f}'.format(clf.score(X_test_tfidf,y_test)))

运行代码,得到如下结果:


经过tf-idf处理后模型的得分

结果分析:看起来模型的表现并没有得到提升。接下来继续尝试对模型改进——去掉文本中的“停用词”:

# #导人Tfidf模型
from sklearn.feature_extraction.text import TfidfVectorizer
#激活英语停用词参数
tfidf = TfidfVectorizer(smooth_idf = False,stop_words='english')
#拟合训练数据集
tfidf.fit(X_train)
#将拟合好的训练数据集转为向量
X_train_tfidf = tfidf.transform(X_train)
#使用交叉验证进行评分
scores3 = cross_val_score(LinearSVC(),X_train_tfidf,y_train)
clf.fit(X_train_tfidf,y_train)
#将测试数据集转化为向量
X_test_tfidf = tfidf.transform(X_test)
print('去掉停用词后的训练集交叉验证平均分:{:.3f}'.format(scores3.mean()))
print('去掉停用词后的测试集模型得分:{:.3f}'.format(clf.score(X_test_tfidf,y_test)))

运行代码,得到结果如下:


去掉停用词之后的交叉验证分数和测试集分数

结果分析:从结果中看到,去掉停用词之后,模型的得分有了显著提高。这说明去掉停用词确实可以让机器学习模型更好地拟合文本数据。并且能够有效提高模型的泛化能力。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,458评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,454评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,171评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,062评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,440评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,661评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,906评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,609评论 0 200
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,379评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,600评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,085评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,409评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,072评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,088评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,860评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,704评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,608评论 2 270

推荐阅读更多精彩内容