跳至主要内容

使用机器学习检测真实与虚假酒店评论

在本教程中,您将使用机器学习算法在 Python 中实现一个真实场景的问题。您将学习如何在 Python 中读取多个文本文件、提取标签、使用数据框等!
· 11分钟 阅读

使用 AI 探索

在本教程中,您将以文本数据集 Deceptive Opinion Spam Corpus 为例进行练习。

简而言之,今天的教程将涵盖以下主题:

  • 首先,将简要介绍酒店评论数据集,然后导入所有所需的模块,
  • 接着理解数据结构:从数据路径中抓取所有文本文件,提取其中的标签,创建标签与评论的数据框,最后合并这两个数据框,
  • 然后进行数据预处理:使用自然语言工具包库从酒店评论中移除停用词,并为数据集中每个词提取词性。
  • 完成预处理后开始准备模型训练:将数据划分为训练集和测试集,了解 Tfidf,向量化训练与测试数据,实施支持向量机模型,同时保存数据对应的 Tfidf 向量器与模型,并将它们重新加载。
  • 最后,在测试数据上进行预测:绘制混淆矩阵、准确率与分类报告来检验模型效果;您还将通过采用不同的随机种子来划分训练和测试数据以测试模型;最后,使用两条线上 Yelp 评论对模型进行测试。

理解 Deceptive Opinion Spam 数据集

在继续加载数据之前,先了解一下您究竟要处理的是什么!Deceptive opinion spam 数据集是一个包含 20 家芝加哥酒店真实与虚假评论的语料库。数据按评论的情感在两篇论文中进行了描述。具体而言,文章在 1 中讨论了正向情感评论,在 2 中讨论了负向情感评论,欢迎查阅这些论文以获得更深入的了解。

该语料库包含:

  • 来自 TripAdvisor 的 400 条真实的正面评论
  • 来自 Mechanical Turk 的 400 条虚假的正面评论
  • 来自 Expedia、Hotels.com、Orbitz、Priceline、TripAdvisor 和 Yelp 的 400 条真实的负面评论
  • 来自 Mechanical Turk 的 400 条虚假的负面评论

总计有 1600 条评论,您的任务是使用机器学习算法对真实与虚假的酒店评论进行分类。

那么,我们开始吧!

导入所有所需模块

首先导入所有需要的模块,如 os、pandas、nltk、regex,最重要的是 Sklearn,因为在今天的教程中,您将使用 Sklearn 库提供的机器学习算法 支持向量机!

您将需要 os 来遍历存放文本文件的文件夹及其子文件夹,fnmatch 可用于筛选出仅包含文本的文件并忽略文件夹中其他内容;pandas 可帮助您将数据组织为行和列,便于进行各种数据处理。Regex 根据您指定的模式来提取数据,这里也会用到!Nltk 是一个自然语言工具包,可帮助您移除不希望模型学习的停用词。最后也是最重要的,您将导入 Sklearn,它提供了一系列库,您将用它们来处理数据、向量化数据、利用机器学习算法学习数据点之间的边界,并最终绘制准确率。

import os
import fnmatch
from textblob import TextBlob
import pandas as pd
from sklearn.feature_extraction.text import TfidfVectorizer
from nltk.corpus import stopwords
from nltk import pos_tag,pos_tag_sents
import regex as re
import operator
from sklearn.svm import SVC, LinearSVC
from sklearn.metrics import classification_report, accuracy_score, confusion_matrix
from sklearn_cross_validation import train_test_split  
from sklearn import metrics
from sklearn import svm
from sklearn.grid_search import GridSearchCV
import pickle
from nltk.corpus import stopwords

从路径中抓取所有文本文件,提取标签并创建标签数据框

structure of the data
图:数据结构


导入库之后,下一步是提取数据的标签。标签将帮助模型区分评论,判断某条评论属于真实类还是虚假类。

您将首先遍历所有文本文件,获取所有文本文件的绝对路径,然后据此提取相应的标签。

path = 'op_spam_train/'

label = []

configfiles = [os.path.join(subdir,f)
for subdir, dirs, files in os.walk(path)
    for f in fnmatch.filter(files, '*.txt')]

应该会有 1600 个路径,每个路径代表一个新的文本文件。我们快速验证一下!

len(configfiles)
1600

我们也打印一个文本文件路径看一下。

configfiles[1]
'op_spam_train/negative_polarity/deceptive_from_MTurk/fold4/d_swissotel_14.txt'

从上述输出可见,为了提取标签,我们需要某种过滤方式。为此,您将使用 Regex(正则表达式)。

for f in configfiles:
    c = re.search('(trut|deceptiv)\w',f)
    label.append(c.group())

一旦将标签提取到名为 label 的列表中,就来把它转换为一个数据框。

labels = pd.DataFrame(label, columns = ['Labels'])

打印 labels 数据框的前五行。

labels.head(5)
 Labels
0deceptive
1deceptive
2deceptive
3deceptive
4deceptive

抓取所有评论并追加到列表中

在提取完所有标签之后,该从文本文件中提取评论了!

review = []
directory =os.path.join("op_spam_train")
for subdir,dirs ,files in os.walk(directory):
   # print (subdir)
    for file in files:
        if fnmatch.filter(files, '*.txt'):
            f=open(os.path.join(subdir, file),'r')
            a = f.read()
            review.append(a)

与之前一样,您现在将把 review 列表构造成一个数据框。

reviews = pd.DataFrame(review, columns = ['HotelReviews'])

打印 reviews 数据框的前五行。

reviews.head(5)
 HotelReviews
0Grant it, this hotel seems very nice, but I wa...
1I recently stayed at the Swissotel Chicago wit...
2I was sorely disappointed with the Sheraton Ch...
3My family and I stayed at the Sheraton Chicago...
4I recently stayed at the Homewood Suites by Hi...

很好!到目前为止都非常直观。是不是?

现在我们来合并 labels 与 reviews 两个数据框!

合并评论数据框与标签数据框

result = pd.merge(reviews, labels,right_index=True,left_index = True)


result['HotelReviews'] = result['HotelReviews'].map(lambda x: x.lower())

合并后,您将得到一个名为 result 的新数据框。我们也打印几行看看。

result.head()
 HotelReviewsLabels
0grant it, this hotel seems very nice, but i wa...deceptive
1i recently stayed at the swissotel chicago wit...deceptive
2i was sorely disappointed with the sheraton ch...deceptive
3my family and i stayed at the sheraton chicago...deceptive
4i recently stayed at the homewood suites by hi...deceptive

从 Hotel Reviews 列中移除停用词

处理文本数据时,移除停用词至关重要。停用词并不具有意义,无法为模型学习数据模式提供有用信息。

您将在数据框中创建一个名为 review_without_stopwords 的新列。lambda 函数将处理 HotelReviews 列的所有行。您将使用列表推导式,只将不在 stop 变量中的词存入新列。

import ntlk
nltk.download('stopwords')

注意:要执行下面的单元,您可能需要先在终端中运行以上代码行。

stop = stopwords.words('english')

result['review_without_stopwords'] = result['HotelReviews'].apply(lambda x: ' '.join([word for word in x.split() if word not in (stop)]))

我们快速打印一下去除停用词后的数据框!

result.head()
 HotelReviewsLabelsreview_without_stopwords
0grant it, this hotel seems very nice, but i wa...deceptivegrant it, hotel seems nice, pleased stay here....
1i recently stayed at the swissotel chicago wit...deceptiverecently stayed swissotel chicago husband two ...
2i was sorely disappointed with the sheraton ch...deceptivesorely disappointed sheraton chicago. outside ...
3my family and i stayed at the sheraton chicago...deceptivefamily stayed sheraton chicago hotel towers be...
4i recently stayed at the homewood suites by hi...deceptiverecently stayed homewood suites hilton chicago...

从酒店评论中提取词性并作为模型的特征输入

根据 1 的观点,真实与虚假观点分别可能被划分为信息型与想象型体裁。两者在分布上有明显差异:前者通常包含更多名词、形容词、介词、限定词以及并列连词,而后者则包含更多动词、副词、代词与前限定词。然而,虚假观点含有更多最高级并不意外,因为虚假写作(但不一定是一般的想象型写作)往往使用夸张的语言。

因此,我们现在为数据框每一行中的每个词提取其对应的词性,然后将其作为特征向量输入模型。

对于 词性 标注,您将使用 TextBlob,这是一个用于处理文本数据的 Python 库。它提供了简单的 API 来完成常见的自然语言处理(NLP)任务,如词性标注、名词短语抽取、情感分析、分类、翻译等。

注意:要运行 pos 函数的单元,您可能需要在终端窗口中运行以下代码行。

import nltk
nltk.download('punkt')
nltk.download('averaged_perceptron_tagger')
def pos(review_without_stopwords):
    return TextBlob(review_without_stopwords).tags
os = result.review_without_stopwords.apply(pos)
os1 = pd.DataFrame(os)

os1 数据框的每一行都包含带有各自词性的词列表。您无法直接向量化要输入模型的列表,因此需要将这些列表行转换为字符串。

os1.head()
 review_without_stopwords
0[(grant, NN), (it, PRP), (hotel, NN), (seems, ...
1[(recently, RB), (stayed, VBN), (swissotel, NN...
2[(sorely, RB), (disappointed, JJ), (sheraton, ...
3[(family, NN), (stayed, VBD), (sheraton, JJ), ...
4[(recently, RB), (stayed, VBN), (homewood, NN)...

将每一行转换为字符串,其中每个词与其对应的 词性 用斜杠连接,词与词之间用一个空格分隔。

os1['pos'] = os1['review_without_stopwords'].map(lambda x:" ".join(["".join(x) for x in x ]) )

最后,将 pos 列与主数据框 result 合并,并打印前几行!

result = result = pd.merge(result, os1,right_index=True,left_index = True)
result.head()
first few rows of pos column and the main result df

训练阶段

将数据划分为两部分:80% 训练集与 20% 测试集

提取出特征后,现在是将数据划分为训练与测试的时候了。在本教程中,您将把 80% 的数据划分为训练集,剩余 20% 作为测试集。

review_train, review_test, label_train, label_test = train_test_split(result['pos'],result['Labels'], test_size=0.2,random_state=13)

使用 TfidfVectorizer 向量化训练与测试数据

让我们详细了解一下 TfidfVectorizer

Tfidf 包含两部分:

    • 词频(Tf):某个词在单个文档中出现的次数。为了更好地理解,我们以词 “the” 为例。这个词很常见,会在所有文档中高频出现。但仔细想想,“the” 并不会提供关于文档的额外信息。相反,如果我们谈到 “messi”,就提供了上下文。我们可以较有把握地说文档可能与足球有关。因此我们需要以某种方式降低 “the” 的权重。您可能会说可以对它们各自的频率取倒数,以便给 “messi” 更高的权重。接近了,但有个问题。文档中可能存在非常罕见的词。例如:“floccinaucinihilipilification”。这是一个真实的单词,意思是:“将某事物评估为毫无价值的行为或习惯。”如果对此词使用倒数,数值会接近 1,但仍然无法说明上下文。所以(用数字举例)tf(“the”)= 100,tf(“messi”)= 5,tf(“floccinaucinihilipilification”)= 1
  • 逆文档频率(idf):通过增加这一项来解决上述频繁与罕见词的问题。它是什么?计算方式是对 {语料库中文档总数 / 含有该词的文档数} 取对数。对于 “the”,该比值接近 1,因此其对数接近 0。idf(“the”)= 0;idf(“messi”)= log(10/3) = 0.52(因为语料是关于足球的);idf(“floccinaucinihilipilification”)= log(10/1) = 1
  • 最后一步:我们希望给 “messi” 的权重大于另外两个词。因此将两项相乘:tfidf = tf x idf。“the” = 100 x 0 = 0,“floccinaucinihilipilification” = 1 x 1 = 1,“messi” = 5 x 0.52 = 2.6

这种加权技术在分类时能帮助机器学习(ML)模型,因为我们明确指出哪些词的权重更高/更低。

现在您已经了解了它,让我们将其应用到训练与测试数据上。

tf_vect = TfidfVectorizer(lowercase = True, use_idf=True, smooth_idf=True, sublinear_tf=False)

X_train_tf = tf_vect.fit_transform(review_train)
X_test_tf = tf_vect.transform(review_test)

实现模型

现在,您将实现一个名为 支持向量机(SVM) 的机器学习模型。如需了解其原理,可参考 此链接。

为了为您的 ML 算法选择最佳超参数,您将使用 GridSearchCV。它会基于训练数据和标签,从您指定的参数列表中找出最佳超参数取值。您将为 C 与 gamma 各选择五个不同的值,并基于数据得到最佳组合。

def svc_param_selection(X, y, nfolds):
    Cs = [0.001, 0.01, 0.1, 1, 10]
    gammas = [0.001, 0.01, 0.1, 1]
    param_grid = {'C': Cs, 'gamma' : gammas}
    grid_search = GridSearchCV(svm.SVC(kernel='linear'), param_grid, cv=nfolds)
    grid_search.fit(X, y)
    return grid_search.best_params_
svc_param_selection(X_train_tf,label_train,5)
{'C': 10, 'gamma': 0.001}
clf = svm.SVC(C=10,gamma=0.001,kernel='linear')
clf.fit(X_train_tf,label_train)
pred = clf.predict(X_test_tf)

保存 Tfidf 向量器与 ML 模型

让我们将刚训练好的模型和 Tfidf 向量器使用一开始导入的 pickle 库保存下来,这样日后即可直接加载数据、向量化并用该 ML 模型进行预测。

with open('vectorizer.pickle', 'wb') as fin:
    pickle.dump(tf_vect, fin)
with open('mlmodel.pickle','wb') as f:
    pickle.dump(clf,f)

加载 Tfidf 向量器与 ML 模型

pkl = open('mlmodel.pickle', 'rb')
clf = pickle.load(pkl)   
vec = open('vectorizer.pickle', 'rb')
tf_vect = pickle.load(vec)
/home/naveksha/.local/lib/python3.5/site-packages/sklearn/base.py:311: UserWarning: Trying to unpickle estimator SVC from version pre-0.18 when using version 0.19.1. This might lead to breaking code or invalid results. Use at your own risk.
  UserWarning)
/home/naveksha/.local/lib/python3.5/site-packages/sklearn/base.py:311: UserWarning: Trying to unpickle estimator TfidfTransformer from version pre-0.18 when using version 0.19.1. This might lead to breaking code or invalid results. Use at your own risk.
  UserWarning)
/home/naveksha/.local/lib/python3.5/site-packages/sklearn/base.py:311: UserWarning: Trying to unpickle estimator TfidfVectorizer from version pre-0.18 when using version 0.19.1. This might lead to breaking code or invalid results. Use at your own risk.
  UserWarning)

在测试数据上进行预测

X_test_tf = tf_vect.transform(review_test)
pred = clf.predict(X_test_tf)

绘制混淆矩阵、准确率与分类报告以分析模型表现

print(metrics.accuracy_score(label_test, pred))
0.96875
print (confusion_matrix(label_test, pred))
[[149   6]
 [  4 161]]

上述混淆矩阵为 2 x 2,其中第一行第一个元素为真正例(TP),第一行第二个元素为假负例(FN),第二行第一个元素为假正例(FP),第二行第二个元素为真负例(TN)。

从该混淆矩阵可得,320 个测试样本中仅有 10 个被误分类。

print (classification_report(label_test, pred))
             precision    recall  f1-score   support

  deceptive       0.97      0.96      0.97       155
      truth       0.96      0.98      0.97       165

avg / total       0.97      0.97      0.97       320

使用不同随机状态测试模型

  • 随机状态 1
review_train, review_test, label_train, label_test = train_test_split(result['pos'],result['Labels'], test_size=0.2,random_state=1)
X_test_tf = tf_vect.transform(review_test)
pred = clf.predict(X_test_tf)
print(metrics.accuracy_score(label_test, pred))
0.978125
print (confusion_matrix(label_test, pred))
[[146   5]
 [  2 167]]
print (classification_report(label_test, pred))
             precision    recall  f1-score   support

  deceptive       0.99      0.97      0.98       151
      truth       0.97      0.99      0.98       169

avg / total       0.98      0.98      0.98       320
  • 随机状态 10
review_train, review_test, label_train, label_test = train_test_split(result['pos'],result['Labels'], test_size=0.2,random_state=10)
X_test_tf = tf_vect.transform(review_test)
pred = clf.predict(X_test_tf)
print(metrics.accuracy_score(label_test, pred))
0.98125
print (confusion_matrix(label_test, pred))
[[155   5]
 [  1 159]]
print (classification_report(label_test, pred))
             precision    recall  f1-score   support

  deceptive       0.99      0.97      0.98       160
      truth       0.97      0.99      0.98       160

avg / total       0.98      0.98      0.98       320
  • 随机状态 42
review_train, review_test, label_train, label_test = train_test_split(result['pos'],result['Labels'], test_size=0.2,random_state=42)
X_test_tf = tf_vect.transform(review_test)
pred = clf.predict(X_test_tf)
print(metrics.accuracy_score(label_test, pred))
0.984375
print (confusion_matrix(label_test, pred))
[[165   3]
 [  2 150]]
print (classification_report(label_test, pred))
             precision    recall  f1-score   support

  deceptive       0.99      0.98      0.99       168
      truth       0.98      0.99      0.98       152

avg / total       0.98      0.98      0.98       320

从以上预测可以看到,模型表现相当出色,且并未过拟合,因为您多次通过不同的划分方式对模型进行了测试。

用两条来自 Yelp 的评论测试模型

def test_string(s):
    X_test_tf = tf_vect.transform([s])
    y_predict = clf.predict(X_test_tf)
    return y_predict
test_string("The hotel was good.The room had a 27-inch Samsung led tv, a microwave.The room had a double bed")
array(['truth'], dtype=object)
test_string("My family and I are huge fans of this place. The staff is super nice, and the food is great. The chicken is very good, and the garlic sauce is perfect. Ice cream topped with fruit is delicious too. Highly recommended!")
array(['deceptive'], dtype=object)

模型对上述两条评论的预测是正确的。第一条为真实评论,第二条为虚假评论!

注意

当在不同配置的处理系统上实现时,模型性能会发生变化,这是一个不寻常的现象。如果社区能够帮助定位这个问题,将会对包括本文作者在内的很多人有所帮助。这里的社区,指的是会阅读本教程的各位读者!

学习愉快!

希望本教程对您有所帮助,并为您的技能增添价值。

如果您想进一步学习机器学习,可以参加 DataCamp 的 Building Chatbots in Python 课程。

主题