NLP电影评论情感分析(基础篇)
Posted 山顶夕景
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了NLP电影评论情感分析(基础篇)相关的知识,希望对你有一定的参考价值。
学习总结
(1)spacy3.0和2.0的区别:https://spacy.io/usage/v3
(2)Spacy可以GPU训练模型,也可以和huggingface结合使用transformer模型。
(3)本篇特别注意spacy的tokenizer的操作,官网教程。
文章目录
导言
除了典型的两种数据属性:
连续特征:描述数量
分类特征:固定列表中的元素
还有第三类型:文本。ex:在客户服务中,判断一条消息是投诉还是咨询, 通过判断主题和内容,推出客户的目的,然后给转发给相关部门。
一、用字符串表示的数据类型
四种类型的字符串数据:
- 分类数据:如调查人们最喜欢的颜色,数据集有红色、绿色、蓝色等8个取值。
- 可以在语义上映射为类比的自由字符串:如颜色调查问卷中让人们自己填写喜欢的颜色,有的人写“我弟弟房间的橙色”,这样就不容易和橙色自动对应。
- 结构化字符串数据:地址、人名或者地名、日期、电话号码,处理方法依赖于上下文和具体领域。
- 文本数据:单词组成的句子。下面都用英语。
语料库corpus:数据集。
文档document:每个由单个文本表示的数据点。
二、电影数据集IMDb
IMDb(Internet Movie Database,互联网电影数据集)数据集:去http://ai.stanford.edu/~amaas/data/sentiment/aclImdb_v1.tar.gz下载包
数据集简介:IMDb网站本身是有1到10分的电影打分,为了简化,这些评分整为二分类,即大于等于7位正面,小于等于4位负面的,中性评论不在数据集中。
文件结构如下,neg即negative,pos即positive:
aclImdb_v1/aclImdb
├── test
│ ├── neg
│ └── pos
└── train
├── neg
├── pos
└── unsup
可以通过sklearn的load_files
函数加载这种文件夹结构:
from sklearn.datasets import load_files
reviews_train = load_files("aclImdb_v1/aclImdb/train/")
# load_files returns a bunch, containing training texts and training labels
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]))
结果如下,text_train
是一个列表,并且元素个数为25000,比如下标为6的电影评论句子如下所示,可以看到句子是有<br /><br />
换行符等符号的(最好进行数据清洗,删除这类格式)。
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 = [doc.replace(b"<br />", b" ") for doc in text_train]
text_train 的元素类型与你所使用的 Python 版本有关。在 Python 3 中,它们是 bytes 类
型,是表示字符串数据的二进制编码。在 Python 2 中,text_train 包含的是字符串。
推荐阅读 Python 2(https://docs.python.
org/2/howto/unicode.html) 和 Python 3(https://docs.python.org/3/howto/unicode.html)的文
档中关于字符串和 Unicode 的内容。
收集数据集时保持正面字符串和负面字符串的平衡:
print("Samples per class (training): ".format(np.bincount(y_train)))
# Samples per class (training): [12500 12500]
用同样操作加载并处理,测试数据集:
reviews_test = load_files("aclImdb_v1/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]
打印出:
Number of documents in test data: 25000
Samples per class (test): [12500 12500]
三、文本字符串的数值化
3.1将文本数据表示为词袋
即只计算语料库中每个单词在文本中的出现频次。
计算词袋表示包括以下三个步骤。
- 分词(tokenization)。将每个文档划分为出现在其中的单词 [ 称为词例(token)],比如
按空格和标点划分。 - 构建词表(vocabulary building)。收集一个词表,里面包含出现在任意文档中的所有词,
并对它们进行编号(比如按字母顺序排序)。 - 编码(encoding)。对于每个文档,计算词表中每个单词在该文档中的出现频次。
下图对字符串This is how you get ants
的处理:
输出:包含每个文档中单词计数的一个向量。
得到词表中的每个单词,他们在每个文档中的出现次数。
3.2 将词袋应用于玩具数据集
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)
print("Vocabulary size: ".format(len(vect.vocabulary_)))
print("Vocabulary content:\\n ".format(vect.vocabulary_))
通过CountVectorizer
对训练数据的分词和词表的构建,然后通过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
词表有13个单词,从be到wise,通过transform
创建训练数据的词袋表示(没转换前是sparse
格式),注意这里的词袋表示会保存在一个SciPy
稀疏矩阵中,所以可以通过toarray
方法将其转为稠密的numpy
数组:
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>
print("Dense representation of bag_of_words:\\n".format(
bag_of_words.toarray()))
使用toarray
转为稠密向量:
Dense representation of bag_of_words:
[[0 0 1 1 1 0 1 0 0 1 1 0 1]
[1 1 0 1 0 1 0 1 1 1 0 1 1]]
3.3 将词袋应用于电影评论
(1)加载为字符串列表
# 将训练数据和测试数据加载为字符串列表
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>
"""
# 使用get_feature_name方法,返回一个列表,每个元素对应一个特征
feature_names = vect.get_feature_names()
print("Number of features: ".format(len(feature_names)), '\\n')
print("First 20 features:\\n".format(feature_names[:20]), '\\n')
print("Features 20010 to 20030:\\n".format(feature_names[20010:20030]), '\\n')
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']
"""
(1)有些元素都是数字,从无意义的单词中找出意义有时很难;
(2)有些类似词语,如draught、drawback和drawer,其单数复数都在词表中,其实不应该作为不同单词。
(2)交叉验证
对于高维系数数据,LogisticRegression
线性模型是最简单的。先用交叉验证对LogisticRegression
线性模型评估。
似乎这样做违背以前的交叉验证与预处理规则。
CountVectorizer
的默认设置实际上不会收集任何统计信息,所以我们的结果是有效的。对于应用而言,从一开始就使用 Pipeline 是更好的选择,我们后面再这么做。
from sklearn.model_selection import cross_val_score
from sklearn.linear_model import LogisticRegression
scores = cross_val_score(LogisticRegression(), X_train, y_train, cv=5)
print("Mean cross-validation accuracy: :.2f".format(np.mean(scores)))
"""
Mean cross-validation accuracy: 0.88
"""
交叉验证平均分数为88%,处理二分类任务合理;又LogisticRegression
线性模型有一个正则化参数C,可以通过交叉验证调节C:
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_)
通过交叉验证发现C=0.1能得到交叉验证分数为89%
Best cross-validation score: 0.89
Best parameters: 'C': 0.1
X_test = vect.transform(text_test)
print("Test score: :.2f".format(grid.score(X_test, y_test)))
# Test score: 0.88
(3)用正则表达式提取词例
默认使用的正则表达式是 “\\b\\w\\w+\\b
”。
含义:找到所有包含至少两个字母或数字(\\w)且被词边界(\\b)分隔的字符序列。它不会匹配只有一个字母的单词,还会将类似“doesn’t”或“bit.ly”之类的缩写分开,但它会将“h8ter”匹配为一个单词。
然后,CountVectorizer
将所有单词转换为小写字母,这样“soon”“Soon”和“sOon”都对应于同一个词例(因此也对应于同一个特征)。这一简单机制在实践中的效果很好,但正如前面所见,我们得到了许多不包含信息量的特征(比如数字)。减少这种特征的一种方法是,仅使用至少在 2 个文档(或者至少 5 个,等等)中出现过的词例。仅在一个文档中出现的词例不太可能出现在测试集中,因此没什么用。我们可以用 min_df
参数来设置词例至少需要在多少个文档中出现过。
vect = CountVectorizer(min_df=5).fit(text_train)
X_train = vect.transform(text_train)
print("X_train with min_df: ".format(repr(X_train)))
"""
X_train with min_df: <25000x27271 sparse matrix of type '<class 'numpy.int64'>'
with 3354014 stored elements in Compressed Sparse Row format>
"""
通过上面的设置min_df = 5
,即要求每个词例至少在5个文档上出现过,可以将特征数减少到27271
个(只有原始特征的三分之一左右)。再看看一些词例:
feature_names = vect.get_feature_names()
print("First 50 features:\\n".format(feature_names[:50]))
print("Features 20010 to 20030:\\n".format(feature_names[20010:20030]))
print("Every 700th feature:\\n".format(feature_names[::700]))
First 50 features:
['00', '000', '007', '00s', '01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '100', '1000', '100th', '101', '102', '103', '104', '105', '107', '108', '10s', '10th', '11', '110', '112', '116', '117', '11th', '12', '120', '12th', '13', '135', '13th', '14', '140', '14th', '15', '150', '15th', '16', '160', '1600', '16mm', '16s', '16th']
Features 20010 to 20030:
['repentance', 'repercussions', 'repertoire', 'repetition', 'repetitions', 'repetitious', 'repetitive', 'rephrase', 'replace', 'replaced', 'replacement', 'replaces', 'replacing', 'replay', 'replayable', 'replayed', 'replaying', 'replays', 'replete', 'replica']
Every 700th feature:
['00', 'affections', 'appropriately', 'barbra', 'blurbs', 'butchered', 'cheese', 'commitment', 'courts', 'deconstructed', 'disgraceful', 'dvds', 'eschews', 'fell', 'freezer', 'goriest', 'hauser', 'hungary', 'insinuate', 'juggle', 'leering', 'maelstrom', 'messiah', 'music', 'occasional', 'parking', 'pleasantville', 'pronunciation', 'recipient', 'reviews', 'sas', 'shea', 'sneers', 'steiger', 'swastika', 'thrusting', 'tvs', 'vampyre', 'westerns']
上面结果发现,数字减少了,生僻词和拼写错误也少了,再次运行网格搜索(如下),虽然结果还是89%,但很多时候,我们减少需要处理的特征数量,可以提高模型的可解释性。
grid = GridSearchCV(LogisticRegression(), param_grid, cv=5)
grid.fit(X_train, y_train)
print("Best cross-validation score: :.2f".format(grid.best_score_))
# Best cross-validation score: 0.89
如果一个文档中包含训练数据中没有包含的单词,并对其调用
CountVectorizer
的transform
方法,那么这些单词将被忽略,因为它们没有包含在字典中。这对分类来说不是一个问题,因为从不在训练数据中的单词中学不到任何内容。但对于某些应用而言(比如垃圾邮件检测),添加一个特征来表示特定文档中有多少个所谓“词表外”单词可能会有所帮助。为了实现这一点,你需要设置min_df
,否则这个特征在训练期间永远不会被用到。
(4)删除停用词
查看停用词:
from sklearn.feature_extraction.text import ENGLISH_STOP_WORDS
print("Number of stop words: ".format(len(ENGLISH_STOP_WORDS)))
print("Every 10th stopword:\\n".format(list(ENGLISH_STOP_WORDS)[::10]))
Number of stop words: 318
Every 10th stopword:
['cannot', 'across', 'describe', 'even', 'how', 'because', 'nothing', 'every', 'up', 'either', 'thru', 'call', 'ourselves', 'why', 'becomes', 'seemed', 'thereby', 'etc', 'whereby', 'than', 'yours', 'everyone', 'eg', 'these', 'by', 'mostly', 'always', 'during', 'among', 'to', 'alone', 'it']
我们对自己的数据删除停用词:
# Specifying stop_words="english" uses the built-in list.
# We could also augment it and pass our own.
vect = CountVectorizer(min_df=5, stop_words="english").fit(text_train)
X_train = vect.transform(text_train)
print("X_train with stop words:\\n".format(repr(X_train)))
删除后发现特征数量减少为26966
,即去掉了300多个停用词。
X_train with stop words:
<25000x26966 sparse matrix of type '<class 'numpy.int64'>'
with 2149958 stored elements in Compressed Sparse Row format>
最后还是一样使用网格搜索:
grid = GridSearchCV(LogisticRegression(), param_grid, cv=5)
grid.fit(X_train, y_train)
print("Best cross-validation score: :.2f".format(grid.best_score_))
# Best cross-validation score: 0.88
使用停用词后的网格搜索性能略有下降——不至于担心,但鉴于从 27 000 多个特征中删
除 305 个不太可能对性能或可解释性造成很大影响,所以使用这个列表似乎是不值得的。
固定的列表主要对小型数据集很有帮助,这些数据集可能没有包含足够的信息,模型从数
据本身无法判断出哪些单词是停用词。
练习:通过设置CountVectorizer
的 max_df
选项来舍弃出现最频繁的单词,并查看它对特征数量和性能有什么影响。
四、用 tf-idf 缩放数据
刚才的方法是丢弃不重要的特征,还有一种方法是按照预计的特征信息量来缩放特征。如tf-idf:在很多文档都有出现的词语,给予的权重不高,但对特定文档高频词,权重高。
特征缩放有几种变体:https://en.wikipedia.org/wiki/Tf-idf
单词w在文档d中的ti-idf分数,计算公式:
tfidf
(
w
,
d
)
=
tf
log
(
N
+
1
N
w
+
1
)
+
1
\\operatornametfidf(w, d)=\\operatornametf \\log \\left(\\fracN+1N_w+1\\right)+1
tfidf(w,d)=tflog(Nw+1N+1)+1
- N是训练集中的文档数量
- N w N_w Nw是训练集中出现单词w的文档数量
- tf(词频)是单词w在查询文档d中出现的次数
- 两个类在计算tf-idf表示后还应用了L2范数。它们将每个文档的表示缩放到欧几里得范数为 1。利用这种缩放方法,文档长度(单词数量)不会改变向量化表示。
因为tf-idf利用了训练数据的统计学属性,所以可以使用管道,确保网格搜索的结果有效。
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.pipeline import make_pipeline
pipe = make_pipeline(TfidfVectorizer(min_df=5, norm=None),
LogisticRegression())
param_grid = 'logisticregression__C': [0.001, 0.01, 0.1, 1, 10]
grid = GridSearchCV(pipe, param_grid, cv=5)
grid.fit(text_train, y_train)
print("Best cross-validation score: :.2f".format(grid.best_score_))
# Best cross-validation score: 0.89
上面的结果是有提高的,Best cross-validation score: 0.89。
(1)还可以查看 tf-idf 找到的最重要的单词。
(2)tf-idf 缩放的目的是找到能够区分文档的单词,但它完全是一种无监督技术。因此,这里的“重要”不一定与我们感兴趣的“正面评论”和“负面评论”标签相关。
首先,我们从管道中提取 TfidfVectorizer:
vectorizer = grid.best_estimator_.named_steps["tfidfvectorizer"]
# 变换训练集
X_train = vectorizer.transform(text_train)
# 找到数据集中每个特征的最大值
max_value = X_train.max(axis=0).toarray().ravel()
sorted_by_tfidf = max_value.argsort()
# 获取特征名称
feature_names = np.array(vectorizer.get_feature_names())
print("Features with lowest tfidf:\\n".format(
feature_names[sorted_by_tfidf[:20]]))
print("Features with highest tfidf: \\n".format(
feature_names[sorted_by_tfidf[-20:]]))
Features with lowest tfidf:
['poignant' 'disagree' 'instantly' 'importantly' 'lacked' 'occurred'
'currently' 'altogether' 'nearby' 'undoubtedly' 'directs' 'fond'
'stinker' 'avoided' 'emphasis' 'commented' 'disappoint' 'realizing'
'downhill' 'inane']
Features with highest tfidf:
['coop' 'homer' 'dillinger' 'hackenstein' 'gadget' 'taker' 'macarthur'
'vargas' 'jesse' 'basket' 'dominick' 'the' 'victor' 'bridget' 'victoria'
'khouri' 'zizek' 'rob' 'timon' 'titanic']
tf-idf 较小的特征要么是在许多文档里都很常用,要么就是很少使用,且仅出现在非常长
的文档中。有趣的是,许多 tf-idf 较大的特征实际上对应的是特定的演出或电影。这些术
语仅出现在这些特定演出或电影的评论中,但往往在这些评论中多次出现。
例如,对于"pokemon"、“smallville” 和 “doodlebops” 是显而易见的,但这里的 “scanners” 实际上指的也是电影标题。这些单词不太可能有助于我们的情感分类任务(除非有些电影的评价可能普遍偏正面或偏负面),但肯定包含了关于评论的大量具体信息。
我们还可以找到逆向文档频率较低的单词,即出现次数很多,因此被认为不那么重要的单
词。训练集的逆向文档频率值被保存在 idf_
属性中:
sorted_by_idf = np.argsort(vectorizer.idf_)
print("Features with lowest idf:\\n".format(
feature_names[sorted_by_idf[:100]]))
Features with lowest idf:
['the' 'and' 'of' 'to' 'this' 'is' 'it' 'in' 'that' 'but' 'for' 'with'
'was' 'as' 'on' 'movie' 'not' 'have' 'one' 'be' 'film' 'are' 'you' 'all'
'at' 'an' 'by' 'so' 'from' 'like' 'who' 'they' 'there' 'if' 'his' 'out'
'just' 'about' 'he' 'or' 'has' 'what' 'some' 'good' 英文电影评论情感分析