Python之ML–情感分析
深入研究自然语言处理(natual language processing,NLP)领域的一个分支–情感分析(sentiment analysis)
主要知识点如下:
- 清洗和准备数据
- 基于文本文档构建特征向量
- 训练机器学习模型用于区分电影的正面与负面评论
- 使用out-of-core学习处理大规模文本数据集
一.获取IMDb电影评论数据集
情感分析,有时也称为观点挖掘(opinion mining),是NLP领域一个非常流行的分支;它分析的是文档的情感倾向(polarity).情感分析的一个常见任务就是根据作者对某一主题所表达的观点或是情感来对文档进行分类
我们使用互联网电影数据库(Internet Movie Database,IMDb)数据集来进行测试.此数据集包含50000个关于电影的正面或负面的评论,正面的意思是影片在IMDb数据集中的评分高于6星,而负面的意思是影片的评分低于5星.我们可以通过链接:http://ai.stanford.edu/~amaas/data/sentiment/,下载电影评论数据集
aclImdb.zip
在成功提取数据集后,我们现在着手将从压缩文件中得到的各文本文档组合为一个CSV文件.把电影的评论读取到pandas的DataFrame对象中.为了实现对处理过程的可视化,同时能够预测剩余处理时间,我们将用到pyprind包
import pyprind
import pandas as pd
import os
pbar=pyprind.ProgBar(50000)
labels={'pos':1,'neg':0}
df=pd.DataFrame()
for s in ('test','train'):
for l in ('pos','neg'):
path="./data/aclImdb/%s/%s"%(s,l)
for file in os.listdir(path):
with open(os.path.join(path,file),'rb') as infile:
txt=infile.read()
df=df.append([[txt,labels[l]]],ignore_index=True)
pbar.update()
df.columns=['review','sentiment']
0% [##############################] 100% | ETA: 00:00:00
Total time elapsed: 00:03:46
执行上述代码,我们首先初始化了一个包含50000次迭代的进度条对象pbar,这也是我们准备读取的文档的数量.使用嵌套的for循环,我们迭代读取aclImdb目录下的train和test两个子目录,以及pos和neg二级子目录下的文本文件,并将其附加到DataFrame对象df中,同时加入的还有文档对应的整数型类标(1代表正面,0代表负面)
由于集成处理过后数据集中的类标是经过排序的,我们现在将使用np.random子模块下的permutation函数对DataFrame对象进行重排
import numpy as np
np.random.seed(0)
df=df.reindex(np.random.permutation(df.index))
df.to_csv('./data/movie_data.csv',index=False)
读取并输出前5个样本摘要,以此来快速确认数据以按正确格式存储
df=pd.read_csv('./data/movie_data.csv')
df.head(5)
review | sentiment | |
---|---|---|
0 | b’In 1974, the teenager Martha Moxley (Maggie … | 1 |
1 | b"OK… so… I really like Kris Kristofferson… | 0 |
2 | b’SPOILER Do not read this, if you think… | 0 |
3 | b’hi for all the people who have seen this won… | 1 |
4 | b’I recently bought the DVD, forgetting just h… | 0 |
二.词袋模型简介
我们将介绍词袋模型(bag-of-words model),它将文本以数值特征向量的形式来表示.词袋模型的理念很简单,可描述如下:
- 我们在整个文档集上为每个词汇创建了唯一的标记,例如单词
- 我们为每个文档构建一个特征向量,其中包含每个单词在此文档中出现的次数
1.将单词转换为特征向量
根据每个文档中的单词数量构建词袋模型,我们可以使用scikit-learn中的CountVectorizer类.CountVectorizer以文本数据数组作为输入,其中文本数据可以是个文档或仅仅是个句子,返回的就是我们所要构建额词袋模型
import numpy as np
from sklearn.feature_extraction.text import CountVectorizer
count=CountVectorizer()
docs=np.array([
'The sum is shining',
'The weather is sweet',
'The sum is shining and the weather is sweet'
])
bag=count.fit_transform(docs)
通过调用CountVectorizer的fit_transform方法,我们构建了词袋模型的词汇库,并将下面三个句子转换成为稀疏的特征向量:
1.The sun is shining
2.The weather is sweet
3.The sum is shining and the weather is sweet
我们将相关词汇的内容显示出来,以更好地理解相关概念:
print(count.vocabulary_)
{'the': 5, 'sum': 3, 'is': 1, 'shining': 2, 'weather': 6, 'sweet': 4, 'and': 0}
由运行结果可见,词汇以python字典的格式存储,将单个的单词映射为一个整数索引.下面我们来看一个之前创建的特征向量
print(bag.toarray())
[[0 1 1 1 0 1 0]
[0 1 0 0 1 1 1]
[1 2 1 1 1 2 1]]
特征向量中的每个索引位置与通过CountVectorizer得到的词汇表中存储的整数值对应.出现在特征向量中的值也称作原始词频(raw term frequency):tf(t,d)–词汇t在文档d中出现的次数
2.通过词频-逆文档频率计算单词关联度
词频-逆文档频率(term frequency-inverse document frequency,tf-idf)用于解决特征向量中单词频繁出现的问题.
tf-idf可以定义为词频与逆文档频率的乘积:
其中,tf(t,d)是我们前面介绍的词频,而逆文档频率idf(t,d)可通过如下公式计算:
这里的n为文档的总数,df(d,t)为包含词汇t的文档d的数量.请注意:分母中加入常数1是可选的,对于没有出现在任何训练样本中的词汇,它能保证分母不为零;取对数是为了保证文档中出现频率较低的词汇不会被赋予过大的权重
scikit-learn实现了另外一个转换器:TfidfTransformer,它以CountVectorizer的原始词频作为输入,并将其转换为tf-idf
from sklearn.feature_extraction.text import TfidfTransformer
tfidf=TfidfTransformer()
np.set_printoptions(precision=2)
print(tfidf.fit_transform(count.fit_transform(docs)).toarray())
[[0. 0.43 0.56 0.56 0. 0.43 0. ]
[0. 0.43 0. 0. 0.56 0.43 0.56]
[0.4 0.48 0.31 0.31 0.31 0.48 0.31]]
3.清洗文本数据
通过去除所有不需要的字符对文本数据进行清洗,为了说明此步骤的重要性,我们先展示一下经过重排后数据集中第一个文档的最后50个字符
df.loc[0,'review'][-50:]
"s seven.
Title (Brazil): Not Available'"
import re
def preprocessor(text):
text = re.sub('<[^>]*>', '', text)
emoticons = re.findall('(?::|;|=)(?:-)?(?:\)|\(|D|P)', text)
text = re.sub('[\W]+', ' ', text.lower()) +\
' '.join(emoticons).replace('-', '')
return text
通过代码中的第一个正则表达式<[^>]*>,我们视图移除电影评论中所有的HTML标记.通过[\W]+删除文本中所有的非单词字符,将单词转换为小写字母,最后将emiticons中临时存储的表情符号追加在经过处理的文档字符串后
preprocessor(df.loc[0,'review'][-50:])
's seven title brazil not available '
preprocessor("This :) is :( a test :-)!")
'this is a test :) :( :)'
现在通过preprocessor函数移除DataFrame中所有的电影评论信息:
df['review']=df['review'].apply(preprocessor)
4.标记文档
标记(tokenize)文档的一种常用方法就是通过文档的空白字符将其拆分为单独的单词
def tokenizer(text):
return text.split()
tokenizer('runners like running and thus they run')
['runners', 'like', 'running', 'and', 'thus', 'they', 'run']
在对文本进行标记的过程中,另外一种有用的技术就是词干提取(word stemming),这是一个提取单词原形的过程,那样我们就可以将一个单词映射到其对应的词干上.python自然语言工具包NLTK实现了Porter Stemming算法
from nltk.stem.porter import PorterStemmer
porter=PorterStemmer()
def tokenizer_porter(text):
return [porter.stem(word) for word in text.split()]
tokenizer_porter('runners like running and thus they run')
['runner', 'like', 'run', 'and', 'thu', 'they', 'run']
我们可通过调用nltk.download函数得到NLTK库提供的停用词,并使用其中的127个停用词对电影评论数据进行停用词移除处理
import nltk
nltk.download('stopwords')
[nltk_data] Downloading package stopwords to
[nltk_data] C:\Users\lenovo\AppData\Roaming\nltk_data...
[nltk_data] Package stopwords is already up-to-date!
True
from nltk.corpus import stopwords
stop=stopwords.words('english')
[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']
三.训练用于文档分类的逻辑斯谛回归模型
我们将训练一个逻辑斯谛回归模型以将电影评论划分为正面评价或负面评价.首先,将清洗过的文本文档对象DataFrame划分为25000个训练文档和25000个测试文档
X_train=df.loc[:25000,'review'].values
y_train=df.loc[:25000,'sentiment'].values
X_test=df.loc[25000:,'review'].values
y_test=df.loc[25000:,'sentiment'].values
接下来我们将使用GridSearchCV对象,并使用5折分层交叉验证找到逻辑斯谛回归模型最佳的参数组合:
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))
])
gs_lr_tfidf=GridSearchCV(lr_tfidf,param_grid,scoring='accuracy',cv=5,verbose=1,n_jobs=-1)
gs_lr_tfidf.fit(X_train,y_train)
Fitting 5 folds for each of 48 candidates, totalling 240 fits
在网格搜索结束后,我们可以输出最佳的参数集:
print('Best parameter set:%s'%gs_lr_tfidf.best_params_)
Best parameter set: {'vect__tokenizer': , 'clf__C': 10.0,
'vect__stop_words': None, 'clf__penalty': 'l2', 'vect__ngram_range': (1, 1)}
由此可见,网格搜索返回的最佳参数设置集合为:使用不含有停用词的常规标记(token)生成器,同时在逻辑斯谛回归中使用tf-idf,其中逻辑斯谛回归分类器使用L2正则化,正则化强度C=10.0
输出训练集上5折交叉验证的准确率得分,以及在测试数据集上的分类准确率
print('CV Accuracy:%.3f'%gs_lr_tfidf.best_score_)
clf=gs_lr_tfidf.best_estimator_
print('Test Accuracy:%.3f'%clf.score(X_test,y_test))
CV Accuracy: 0.897
Test Accuracy: 0.899
结果表明,我们机器学习模型对电影评论是正面评价还是反面评价的分类准确率为90%
备注:朴素贝叶斯分类器(Naive Bayes Classifier)是迄今为止执行文本分类十分流行的一种分类器,特别是用于垃圾邮件过滤.朴素贝叶斯分类器易于实现,计算性能高效,相当于其他算法,它在小数据集上的表现异常出色
链接
四.使用大数据–在线算法与外存学习
我们将使用scikit-learn中SGDClassifier的partial_fit函数来读取本地存储设备,并且使用小型子批次(minibatches)文档来训练一个逻辑斯谛回归模型
首先,我们定义一个tokenizer函数来清理movie_data.csv文件中未经处理的文本数据
import numpy as np
import re
from nltk.corpus import stopwords
stop=stopwords.words('english')
def tokenizer(text):
text = re.sub('<[^>]*>', '', text)
emoticons = re.findall('(?::|;|=)(?:-)?(?:\)|\(|D|P)', text.lower())
text = re.sub('[\W]+', ' ', text.lower()) +\
' '.join(emoticons).replace('-', '')
tokenized = [w for w in text.split() if w not in stop]
return tokenized
接下来我们定义一个生成器函数:stream_docs,它每次读取且返回一个文档的内容
def stream_docs(path):
with open(path, 'r', encoding='utf-8') as csv:
next(csv) # skip header
for line in csv:
text, label = line[:-3], int(line[-2])
yield text, label
为了验证stream_docs函数是否能正常工作,我们来读取一下movie_data-csv文件的第一个文档,它应返回一个包含评论信息和对应类标的元组
next(stream_docs(path='./data/movie_data.csv'))
('"b\'In 1974, the teenager Martha Moxley (Maggie Grace) moves to the high-class area of Belle Haven, Greenwich, Connecticut. On the Mischief Night, eve of Halloween, she was murdered in the backyard of her house and her murder remained unsolved. Twenty-two years later, the writer Mark Fuhrman (Christopher Meloni), who is a former LA detective that has fallen in disgrace for perjury in O.J. Simpson trial and moved to Idaho, decides to investigate the case with his partner Stephen Weeks (Andrew Mitchell) with the purpose of writing a book. The locals squirm and do not welcome them, but with the support of the retired detective Steve Carroll (Robert Forster) that was in charge of the investigation in the 70\\\'s, they discover the criminal and a net of power and money to cover the murder.
""Murder in Greenwich"" is a good TV movie, with the true story of a murder of a fifteen years old girl that was committed by a wealthy teenager whose mother was a Kennedy. The powerful and rich family used their influence to cover the murder for more than twenty years. However, a snoopy detective and convicted perjurer in disgrace was able to disclose how the hideous crime was committed. The screenplay shows the investigation of Mark and the last days of Martha in parallel, but there is a lack of the emotion in the dramatization. My vote is seven.
Title (Brazil): Not Available\'"',
1)
定义一个get_minibatch函数,它以stream_docs函数得到的文档数据流作为输入,并通过参数size返回指定数量的文档内容:
def get_minibatch(doc_stream, size):
docs, y = [], []
try:
for _ in range(size):
text, label = next(doc_stream)
docs.append(text)
y.append(label)
except StopIteration:
return None, None
return docs, y
不幸的是,由于需要将所有的词汇加载到内存中,我们无法通过CountVectorizer来使用外存学习方法.另外,TfidfVectorizer需要将所有训练数据集中的特征向量加载到内存以计算逆文档频率.不过,scikit-learn提供了另外一个处理文本信息的向量处理器:HashingVectorizer.它独立于数据的,其哈希算法使用了Austin Appleby提出的32位MurmurHash3算法(https://sites.google.com/site/murmurhash)
from sklearn.feature_extraction.text import HashingVectorizer
from sklearn.linear_model import SGDClassifier
vect = HashingVectorizer(decode_error='ignore',
n_features=2**21,
preprocessor=None,
tokenizer=tokenizer)
clf = SGDClassifier(loss='log', random_state=1, n_iter=1)
doc_stream = stream_docs(path='./data/movie_data.csv')
设置好所有的辅助函数后,我们可以通过下面代码使用外存学习:
import pyprind
pbar = pyprind.ProgBar(45)
classes = np.array([0, 1])
for _ in range(45):
X_train, y_train = get_minibatch(doc_stream, size=1000)
if not X_train:
break
X_train = vect.transform(X_train)
clf.partial_fit(X_train, y_train, classes=classes)
pbar.update()
0% [# ] 100% | ETA: 00:00:36
0% [## ] 100% | ETA: 00:00:32
0% [### ] 100% | ETA: 00:00:30
0% [#### ] 100% | ETA: 00:00:28
0% [##### ] 100% | ETA: 00:00:26
0% [###### ] 100% | ETA: 00:00:25
0% [####### ] 100% | ETA: 00:00:23
0% [######## ] 100% | ETA: 00:00:22
0% [######### ] 100% | ETA: 00:00:21
0% [########## ] 100% | ETA: 00:00:19
0% [########### ] 100% | ETA: 00:00:18
0% [############ ] 100% | ETA: 00:00:18
0% [############# ] 100% | ETA: 00:00:16
0% [############## ] 100% | ETA: 00:00:15
0% [############### ] 100% | ETA: 00:00:14
0% [################ ] 100% | ETA: 00:00:13
0% [################# ] 100% | ETA: 00:00:12
0% [################### ] 100% | ETA: 00:00:10
0% [#################### ] 100% | ETA: 00:00:10E
0% [##################### ] 100% | ETA: 00:00:08
0% [###################### ] 100% | ETA: 00:00:07
0% [####################### ] 100% | ETA: 00:00:06
0% [######################## ] 100% | ETA: 00:00:05
0% [######################### ] 100% | ETA: 00:00:04
0% [########################## ] 100% | ETA: 00:00:03
0% [########################### ] 100% | ETA: 00:00:02
0% [############################ ] 100% | ETA: 00:00:01
0% [############################# ] 100% | ETA: 00:00:00
0% [##############################] 100% | ETA: 00:00:00
Total time elapsed: 00:00:29
为了估计学习算法的进度,将进度条对象设定为45次迭代,在接下来的for循环中,我们在45个文档的子批次上进行迭代,每个子批次包含1000个文档
完成了增量学习后,我们将使用剩余的5000个文档来评估模型的性能:
X_test, y_test = get_minibatch(doc_stream, size=5000)
X_test = vect.transform(X_test)
print('Accuracy: %.3f' % clf.score(X_test, y_test))
Accuracy: 0.866
可以看到,模型的准确率约为87%,略微低于我们使用网格搜索进行超参调优得到的模型.不过外存学习的存储效率很高,用了不到一分钟就完成了计算