处理文本数据(上):词袋
我们讨论过表示数据属性的两种类型的特征:连续特征与分类特征,前者用于描述数量,后者是固定列表中的元素。
第三种类型的特征:文本
- 文本数据通常被表示为由字符组成的字符串。
1、用字符串表示的数据类型
文本通常只是数据集中的字符串,但并非所有的字符串特征都应该被当作文本来处理。
字符串特征有时可以表示分类变量。在查看数据之前,我们无法知道如何处理一个字符串特征。
⭐四种类型的字符串数据:
-
1、分类数据
- 分类数据(categorical data)是来自固定列表的数据。
-
2、可以在语义上映射为类别的自由字符串
- 你向用户提供的不是一个下拉菜单,而是一个文本框,让他们填写自己最喜欢的颜色。
- 许多人的回答可能是像 “黑色” 或 “蓝色” 之类的颜色名称。其他人可能会出现笔误,使用不同的单词拼写(比如 “gray” 和 “grey” ),或使用更加形象的具体名称 (比如 “午夜蓝色”)。
- 可能最好将这种数据编码为分类变量,你可以利用最常见的条目来选择类别,也可以自定义类别,使用户回答对应用有意义。
-
3、结构化字符串数据
- 手动输入值不与固定的类别对应,但仍有一些内在的结构(structure),比如地址、人名或地名、日期、电话号码或其他标识符。
-
4、文本数据
- 例子包括推文、聊天记录和酒店评论,还包括莎士比亚文集、维基百科的内容或古腾堡计划收集的 50 000 本电子书。所有这些集合包含的信息大多是由单词组成的句子。
2、示例应用:电影评论的情感分析
作为本章的一个运行示例,我们将使用由斯坦福研究员 Andrew Maas 收集的 IMDb (Internet Movie Database,互联网电影数据库)网站的电影评论数据集。
数据集链接:http://ai.stanford.edu/~amaas/data/sentiment/
这个数据集包含评论文本,还有一个标签,用于表示该评论是 “正面的”(positive)还是 “负面的” (negative)。
IMDb 网站本身包含从 1 到 10 的打分。为了简化建模,这些评论打分被归纳为一个二分类数据集,评分大于等于 7 的评论被标记为 “正面的”,评分小于等于 4 的评论被标记为 “负面的”,中性评论没有包含在数据集中。
将数据解压之后,数据集包括两个独立文件夹中的文本文件,一个是训练数据,一个是测试数据。每个文件夹又都有两个子文件夹,一个叫作 pos,一个叫作 neg。
pos 文件夹包含所有正面的评论,每条评论都是一个单独的文本文件,neg 文件夹与之类似。scikit-learn 中有一个辅助函数可以加载用这种文件夹结构保存的文件,其中每个子文件夹对应于一个标签,这个函数叫作 load_files。我们首先将 load_files 函数应用于训练数据:
from sklearn.datasets import load_files
from sklearn.model_selection import train_test_split
reviews_train = load_files("../../datasets/aclImdb/train/")
# load_files 返回一个 Bunch 对象,其中包含训练文本和训练标签
#加载数据
text_train,y_train = reviews_train.data,reviews_train.target
#查看数据
print("type of text_train: {}".format(type(text_train)))
print("length of text_train: {}".format(len(text_train)))
print("text_train[6]:\n{}".format(text_train[6]))
'''
```
type of text_train: <class 'list'>
length of text_train: 25000
text_train[6]:
b"This movie has a special way of telling the story, at first i found it rather odd as it jumped through time and I had no idea whats happening.<br /><br />Anyway the story line was although simple, but still very real and touching. You met someone the first time, you fell in love completely, but broke up at last and promoted a deadly agony. Who hasn't go through this? but we will never forget this kind of pain in our life. <br /><br />I would say i am rather touched as two actor has shown great performance in showing the love between the characters. I just wish that the story could be a happy ending."
```
'''
📣
你可以看到,text_train 是一个长度为 25 000 的列表,其中每个元素是包含一条评论的字符串。我们打印出索引编号为 1 的评论。你还可以看到,评论中包含一些 HTML 换行符。虽然这些符号不太可能对机器学习模型产生很大影响,但最好在继续下一步之前清洗数据并删除这种格式:
import numpy as np
text_train = [doc.replace(b"<br />", b" ") for doc in text_train]
#收集数据集时保持正类和反类的平衡,这样所有正面字符串和负面字符串的数量相等:
print("Samples per class (training): {}".format(np.bincount(y_train)))
#我们用同样的方式加载测试数据集:
reviews_test = load_files("../../datasets/aclImdb/test/")
text_test, y_test = reviews_test.data, reviews_test.target
print("Number of documents in test data: {}".format(len(text_test)))
print("Samples per class (test): {}".format(np.bincount(y_test)))
text_test = [doc.replace(b"<br />", b" ") for doc in text_test]
'''
```
Samples per class (training): [12500 12500]
Number of documents in test data: 25000
Samples per class (test): [12500 12500]
```
'''
我们要解决的任务如下:给定一条评论,我们希望根据该评论的文本内容对其分配一个 “正面的” 或 “负面的” 标签。
这是一项标准的二分类任务。
但是,文本数据并不是机器学习模型可以处理的格式。
我们需要将文本的字符串表示转换为数值表示,从而可以对其应用机器学习算法。
3、将文本数据表示为词袋
用于机器学习的文本表示有一种最简单的方法,也是最有效且最常用的方法,就是使用词袋(bag-of-words)表示。
- 使用这种表示方式时,我们舍弃了输入文本中的大部分结构,如章节、段落、句子和格式,只计算语料库中每个单词在每个文本中的出现频次。
- 舍弃结构并仅计算单词出现次数,这会让脑海中出现将文本表示为 “袋” 的画面
对于文档语料库,计算词袋表示包括以下三个步骤。
- (1)分词(tokenization)。
- 将每个文档划分为出现在其中的单词,比如按空格和标点划分。
- (2)构建词表(vocabulary building)。
- 收集一个词表,里面包含出现在任意文档中的所有词, 并对它们进行编号(比如按字母顺序排序)。
- (3)编码(encoding)。
- 对于每个文档,计算词表中每个单词在该文档中的出现频次。
3.1、将词袋应用于玩具数据集
词袋表示是在 CountVectorizer 中实现的,它是一个变换器(transformer)。
我们首先将它应用于一个包含两个样本的玩具数据集,来看一下它的工作原理:
bards_words =["The fool doth think he is wise,",
"but the wise man knows himself to be a fool"]
#导入CountVectorizer并将其实例化
from sklearn.feature_extraction.text import CountVectorizer
vect = CountVectorizer()
vect.fit(bards_words)
#拟合 CountVectorizer 包括训练数据的分词与词表的构建,我们可以通过 vocabulary_ 属性来访问词表:
print("Vocabulary size: {}".format(len(vect.vocabulary_)))
print("Vocabulary content:\n {}".format(vect.vocabulary_))
'''
```
Vocabulary size: 13
Vocabulary content:
{'the': 9, 'fool': 3, 'doth': 2, 'think': 10, 'he': 4, 'is': 6, 'wise': 12, 'but': 1, 'man': 8, 'knows': 7, 'himself': 5, 'to': 11, 'be': 0}
```
'''
#我们可以调用 transform 方法来创建训练数据的词袋表示:
bag_of_words = vect.transform(bards_words)
print("bag_of_words:{}".format(repr(bag_of_words)))
'''
```
bag_of_words:<2x13 sparse matrix of type '<class 'numpy.int64'>'
with 16 stored elements in Compressed Sparse Row format>
```
'''
📣
词袋表示保存在一个 SciPy 稀疏矩阵中,这种数据格式只保存非零元素。
这个矩阵的形状为 2×13,每行对应于两个数据点之一,每个特征对应于词表中的一个单词。
这里使用稀疏矩阵,是因为大多数文档都只包含词表中的一小部分单词,也就是说,特征数组中的大部分元素都为 0。
想想看,与所有英语单词(这是词表的建模对象)相比,一篇电影评论中可能出现多少个不同的单词。
保存所有 0 的代价很高,也浪费内存。
3.2、将词袋应用于电影评论
上一节我们详细介绍了词袋处理过程,下面我们将其应用于电影评论情感分析的任务。
前面我们将 IMDb 评论的训练数据和测试数据加载为字符串列表(text_train 和 text_ test),现在我们将处理它们
vect = CountVectorizer().fit(text_train)
X_train = vect.transform(text_train)
print("X_train:\n{}".format(repr(X_train)))
'''
```
X_train:
<25000x74849 sparse matrix of type '<class 'numpy.int64'>'
with 3431196 stored elements in Compressed Sparse Row format>
```
'''
X_train 是训练数据的词袋表示,其形状为 25 000×74 849,这表示词表中包含 74 849 个 元素。数据同样被保存为 SciPy 稀疏矩阵。我们来更详细地看一下这个词表。访问词表的另一种方法是使用向量器(vectorizer)的 get_feature_name 方法,它将返回一个列表,每个元素对应于一个特征:
feature_names = vect.get_feature_names()
#特征数
print("Number of features: {}".format(len(feature_names)))
#前20个特征
print("First 20 features:\n{}".format(feature_names[:20]))
#中间的特征
print("Features 20010 to 20030:\n{}".format(feature_names[20010:20030]))
#间隔2000打印一个特征
print("Every 2000th feature:\n{}".format(feature_names[::2000]))
'''
```
Number of features: 74849
First 20 features:
['00', '000', '0000000000001', '00001', '00015', '000s', '001', '003830', '006', '007', '0079', '0080', '0083', '0093638', '00am', '00pm', '00s', '01', '01pm', '02']
Features 20010 to 20030:
['dratted', 'draub', 'draught', 'draughts', 'draughtswoman', 'draw', 'drawback', 'drawbacks', 'drawer', 'drawers', 'drawing', 'drawings', 'drawl', 'drawled', 'drawling', 'drawn', 'draws', 'draza', 'dre', 'drea']
Every 2000th feature:
['00', 'aesir', 'aquarian', 'barking', 'blustering', 'bête', 'chicanery', 'condensing', 'cunning', 'detox', 'draper', 'enshrined', 'favorit', 'freezer', 'goldman', 'hasan', 'huitieme', 'intelligible', 'kantrowitz', 'lawful', 'maars', 'megalunged', 'mostey', 'norrland', 'padilla', 'pincher', 'promisingly', 'receptionist', 'rivals', 'schnaas', 'shunning', 'sparse', 'subset', 'temptations', 'treatises', 'unproven', 'walkman', 'xylophonist']
```
'''
词表的前 10 个元素都是数字。
所有这些数字都出现在评论中的某处,因此被提取为单词。
大部分数字都没有一目了然的语义,除了 “007”,在 电影的特定语境中它可能指的是詹姆斯 • 邦德(James Bond)这个角色。
从无意义的 “单词” 中挑出有意义的有时很困难。
进一步观察这个词表,我们发现许多以 “dra” 开头的英语单词。
- 对于 “draught”、“drawback” 和 “drawer”,其单数和复数形式都包含在词表中,并且作为不同的单词。这些单词具有密切相关的语义,将它们作为不同的单词进行计数(对应于不同的特征)可能不太合适。
在尝试改进特征提取之前,我们先通过实际构建一个分类器来得到性能的量化度量。
-
我们将训练标签保存在 y_train 中,训练数据的词袋表示保存在 X_train 中,因此我们可以在这个数据上训练一个分类器。
-
对于这样的高维稀疏数据,类似 LogisticRegression 的线性模型通常效果最好。
from sklearn.model_selection import GridSearchCV #网格搜索 param_grid = {'C': [0.001, 0.01, 0.1, 1, 10]} grid = GridSearchCV(LogisticRegression(), param_grid, cv=5) grid.fit(X_train, y_train) print("Best cross-validation score: {:.2f}".format(grid.best_score_)) print("Best parameters: ", grid.best_params_) #测试集上查看泛化性能 X_test = vect.transform(text_test) print("Test score: {:.2f}".format(grid.score(X_test, y_test))) ''' ``` Best cross-validation score: 0.89 Best parameters: {'C': 0.1} Test score: 0.88 ``` '''