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

本文详细介绍了如何处理文本数据,包括使用CountVectorizer进行特征提取,使用分词工具处理中文文本,以及应用词袋模型将文本数据转换为数组形式。还探讨了n-Gram和tf-idf模型对文本数据的优化处理,强调了去除停用词的重要性。
摘要由CSDN通过智能技术生成

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值的公式:
n i , j ∑ k n k , j \frac{n_{i,j}}{\sum_{k}n_{k,j}} knk,jni,j
式中: n i , j n_{i,j} ni,j表示某个词在语料库中某个文件内出现的次数; ∑ k n k , j {\sum_{k}n_{k,j}} knk,j表示的是该文件中所有单词出现的次数之和。
而在scikit-lean中,idf的计算公式如下:
i d f = l o g ( N + 1 N w + 1 ) + 1 idf = log(\frac{N+1}{N_w+1})+1 idf=log(Nw+1N+1)+1
式中:N代表的是语料库中文件的总数; N w N_w Nw代表语料库中包含上述单词的文件数量。
那么最终计算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/>全部去掉
X_train = [doc.replace(b'<br />',b' ') for doc in X_train]

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

#载入测试集
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)))

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

  • 10
    点赞
  • 54
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

曾牛

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值