跳至主要内容

使用 Scikit-Learn 侦测假新闻

这篇 scikit-learn 教程将带您使用贝叶斯模型构建一个假新闻分类器。
· 15分钟 阅读

使用 AI 探索

datacamp graphic

侦测所谓的“假新闻”并不容易。首先,得先定义什么是假新闻——如今这甚至已成为一种政治表态。即便您找到了或达成了一定定义共识,也必须收集并正确标注真假新闻(最好是围绕相似主题,以便更清晰地区分)。完成收集后,还需要找到有用的特征来判别真假新闻。

想更深入了解这一问题空间,建议阅读 Miguel Martinez-Alvarez 的文章《How can Machine Learning and AI Help Solve the Fake News Problem》。

在读到 Miguel 的这篇洞见之作的同时,我还看到一篇开放数据科学的文章,探讨了如何使用贝叶斯模型构建成功的假新闻检测器。作者甚至创建了一个包含真假新闻标注样本的数据集仓库。我很好奇自己是否能轻松复现结果,并进一步弄清模型到底学到了什么。

在本教程中,您将跟随我最初的一些探索,一起看看能否构建一个有效的假新闻检测器!

提示:如果您想进一步学习自然语言处理(NLP)基础,建议参加Python 自然语言处理基础课程。

数据探索

首先,您应当快速浏览数据,感受其内容。为此,可以使用 Pandas DataFrame,查看其形状、前几行,并进行必要的转换。

# Import `fake_or_real_news.csv` 
df = __.________("https://s3.amazonaws.com/assets.datacamp.com/blog_assets/fake_or_real_news.csv")
    
# Inspect shape of `df` 
df._____

# Print first lines of `df`
df.______
# Set index
df = df.set_index("Unnamed: 0") 

# Print first lines of `df` 
df.______

提取训练数据

现在 DataFrame 已经更接近所需的样子,您需要分离标签并建立训练集与测试集。

在这个笔记本中,我决定聚焦使用较长的文章正文。因为我知道会用词袋(bag-of-words)与词频-逆文档频(TF-IDF)来抽取特征,所以这看起来是个不错的选择。使用更长的文本,有望为真假新闻数据带来更有区分度的词与特征。

#Set `y` 
y = df._____ 
 
# Drop the `label` column 
df.____("label", axis=1) 
 
# Make training and test sets 
X_train, X_test, y_train, y_test = ________________(df['text'], y, test_size=0.33, random_state=53)

构建向量化器分类器

现在您已经有了训练与测试数据,可以开始构建分类器了。为了了解文章中的词与标记是否对真假新闻的判断有显著影响,先从使用 CountVectorizer 与 TfidfVectorizer 开始。

您会看到示例中为 TF-IDF 向量化器 tfidf_vectorizer 通过 max_df 参数设置了最大阈值 .7。这会移除出现在超过 70% 文章中的词。此外,内置的 stop_words 参数会在向量化前移除英文停用词。

还有更多可用参数,您可在 scikit-learn 文档中阅读相关说明:TfidfVectorizer 与 CountVectorizer。

# Initialize the `count_vectorizer` 
count_vectorizer = _______________(stop_words='english') 

# Fit and transform the training data 
count_train = count_vectorizer._____________(X_train) 

# Transform the test set
count_test = count_vectorizer._________(X_test)
# Initialize the `tfidf_vectorizer`
tfidf_vectorizer = _______________(stop_words='english', max_df=0.7) 

# Fit and transform the training data 
tfidf_train = tfidf_vectorizer._____________(X_train) 

# Transform the test set 
tfidf_test = tfidf_vectorizer._________(X_test)

现在已有向量,接着可以查看存储在 count_vectorizer 与 tfidf_vectorizer 中的向量特征。

有没有明显的问题?(有!)

很明显,当前数据集中包含注释、计量单位或其他无意义的词,还有多语种文章。通常,您应投入更多时间进行预处理并去除噪声,但由于本教程仅作为一个小型概念验证,我们将看看模型能否克服噪声,尽管存在这些问题仍能正确分类。

# Get the feature names of `tfidf_vectorizer` 
print(tfidf_vectorizer.___________()[-10:])

# Get the feature names of `count_vectorizer` 
print(count_vectorizer.___________()[:10])

插曲:Count 与 TF-IDF 特征

我很好奇 count 与 TF-IDF 向量化器是否提取了不同的标记。为了查看并比较特征,您可以把向量信息还原回 DataFrame,以便进行简单的 Python 比较。

如您运行下列单元所见,两种向量化器提取了相同的标记,但显然权重不同。很可能通过调整 TF-IDF 向量化器的 max_df 与 min_df 会改变结果,从而让二者的特征产生差异。

In [15]:
 count_df = pd.DataFrame(count_train.A, columns=count_vectorizer.get_feature_names())
In [16]:
tfidf_df = pd.DataFrame(tfidf_train.A, columns=tfidf_vectorizer.get_feature_names())
In [17]:
difference = set(count_df.columns) - set(tfidf_df.columns)difference
Out[17]:
set()
In [18]:
print(count_df.equals(tfidf_df))
In [19]:
count_df.head()
Out[19]:
 000000000000000310000350000600010001pt000ft000km...حلبعربيعنلممامحاولاتمنهذاوالمرضىยงade
00000000000...0000000000
10000000000...0000000000
20000000000...0000000000
30000000000...0000000000
40000000000...0000000000

5 rows × 56922 columns

In [20]:
tfidf_df.head()
Out[20]:
 000000000000000310000350000600010001pt000ft000km...حلبعربيعنلممامحاولاتمنهذاوالمرضىยงade
00.00.00.00.00.00.00.00.00.00.0...0.00.00.00.00.00.00.00.00.00.0
10.00.00.00.00.00.00.00.00.00.0...0.00.00.00.00.00.00.00.00.00.0
20.00.00.00.00.00.00.00.00.00.0...0.00.00.00.00.00.00.00.00.00.0
30.00.00.00.00.00.00.00.00.00.0...0.00.00.00.00.00.00.00.00.00.0
40.00.00.00.00.00.00.00.00.00.0...0.00.00.00.00.00.00.00.00.00.0

5 rows × 56922 columns

比较模型

现在是训练与测试模型的时候了。

这里,我们从 NLP 常客 MultinomialNB 开始。您可以用它来比较 TF-IDF 与词袋。我直觉认为词袋(即 CountVectorizer)在该模型下表现更好。(想了解多项分布以及其为何更适用于整数的原因,请参阅这份宾大统计课程的简明解释)。

我个人觉得混淆矩阵更易于比较与阅读,于是参考了 scikit-learn 文档构建了易读的混淆矩阵(感谢开源!)。混淆矩阵在主对角线(从左上到右下)显示预测正确的标签。其他单元格显示预测错误的标签,通常被称为假阳性或假阴性。根据您的问题场景,其中一项可能更重要。例如,对于假新闻问题,我们是否更在意不要把真实新闻误判为假新闻?如果是,最终可能需要对准确率进行加权,以更好地反映这一关注点。

除了混淆矩阵,scikit-learn 还提供了多种可视化和比较模型的方式。一种常见方法是使用 ROC 曲线。在 scikit-learn 的 metrics 模块中还有许多其他评估模型的方法。

In [21]:
def plot_confusion_matrix(cm, classes,                          normalize=False,                          title='Confusion matrix',                          cmap=plt.cm.Blues):    """    See full source and example:     http://scikit-learn.org/stable/auto_examples/model_selection/plot_confusion_matrix.html        This function prints and plots the confusion matrix.    Normalization can be applied by setting `normalize=True`.    """    plt.imshow(cm, interpolation='nearest', cmap=cmap)    plt.title(title)    plt.colorbar()    tick_marks = np.arange(len(classes))    plt.xticks(tick_marks, classes, rotation=45)    plt.yticks(tick_marks, classes)    if normalize:        cm = cm.astype('float') / cm.sum(axis=1)[:, np.newaxis]        print("Normalized confusion matrix")    else:        print('Confusion matrix, without normalization')    thresh = cm.max() / 2.    for i, j in itertools.product(range(cm.shape[0]), range(cm.shape[1])):        plt.text(j, i, cm[i, j],                 horizontalalignment="center",                 color="white" if cm[i, j] > thresh else "black")    plt.tight_layout()    plt.ylabel('True label')    plt.xlabel('Predicted label')
In [22]:
clf = MultinomialNB()
In [23]:
 clf.fit(tfidf_train, y_train)pred = clf.predict(tfidf_test)score = metrics.accuracy_score(y_test, pred)print("accuracy:   %0.3f" % score)cm = metrics.confusion_matrix(y_test, pred, labels=['FAKE', 'REAL'])plot_confusion_matrix(cm, classes=['FAKE', 'REAL'])
accuracy:   0.857Confusion matrix, without normalization
 
confusion matrix 1
In [24]:
clf = MultinomialNB() 
In [25]:
clf.fit(count_train, y_train)pred = clf.predict(count_test)score = metrics.accuracy_score(y_test, pred)print("accuracy:   %0.3f" % score)cm = metrics.confusion_matrix(y_test, pred, labels=['FAKE', 'REAL'])plot_confusion_matrix(cm, classes=['FAKE', 'REAL'])
accuracy:   0.893Confusion matrix, without normalization
 
confusion matrix 2
 

果然,在完全未调参的情况下,词频向量化后的训练集 count_train 明显优于 TF-IDF 向量!

测试线性模型

有很多精彩的文章讲述线性模型与 TF-IDF 向量化器的良好配合(可参阅用于分类的 word2vec、scikit-learn 文本分析中的 SVM 参考等)。

那么,您就该用 SVM,对吗?

我最近看了Victor Lavrenko 关于文本分类的讲座,他将被动-进取(Passive Aggressive)分类器与线性 SVM 用于文本分类进行了比较。我们将用该数据集来测试这种方法(它具有显著的速度优势,但学习是一次性的也有劣势)。

In [26]:
linear_clf = PassiveAggressiveClassifier(n_iter=50)
In [27]:
 linear_clf.fit(tfidf_train, y_train)pred = linear_clf.predict(tfidf_test)score = metrics.accuracy_score(y_test, pred)print("accuracy:   %0.3f" % score)cm = metrics.confusion_matrix(y_test, pred, labels=['FAKE', 'REAL'])plot_confusion_matrix(cm, classes=['FAKE', 'REAL'])
accuracy:   0.936Confusion matrix, without normalization
confusion matrix 3

哇!

令人印象深刻。混淆矩阵看起来有所不同,模型对假新闻的分类稍有提升。我们可以测试一下为 MultinomialNB 调整 alpha 值能否得到可比结果。您还可以使用网格搜索进行参数调优,做更全面的探索。

In [28]:
clf = MultinomialNB(alpha=0.1)
In [29]:
 last_score = 0for alpha in np.arange(0,1,.1):    nb_classifier = MultinomialNB(alpha=alpha)    nb_classifier.fit(tfidf_train, y_train)    pred = nb_classifier.predict(tfidf_test)    score = metrics.accuracy_score(y_test, pred)    if score > last_score:        clf = nb_classifier    print("Alpha: {:.2f} Score: {:.5f}".format(alpha, score))
Alpha: 0.00 Score: 0.61502Alpha: 0.10 Score: 0.89766Alpha: 0.20 Score: 0.89383Alpha: 0.30 Score: 0.89000Alpha: 0.40 Score: 0.88570Alpha: 0.50 Score: 0.88427Alpha: 0.60 Score: 0.87470Alpha: 0.70 Score: 0.87040Alpha: 0.80 Score: 0.86609Alpha: 0.90 Score: 0.85892

还差点意思……此时,或许可以对所有分类器进行参数调优,或看看其他scikit-learn 的贝叶斯分类器。您也可以用支持向量机(SVM)测试一下,看看是否优于被动-进取分类器。

不过我更好奇的是,被动-进取模型到底学到了什么。所以我们继续做内省。

模型内省

所以,假新闻问题解决了,对吗?在我的数据集上达到了 93% 的准确率,那就可以打烊回家了。

当然并非如此。考虑到我们在特征中看到的大量噪声,我对这些结果持保留态度。StackOverflow 上有一篇很棒的写作,提供了一个非常实用的函数,用来寻找对标签影响最大的向量。它只适用于二分类(两个类别的分类器),但对您来说正合适,因为只有 FAKE 与 REAL 两个标签。

使用当前表现最好的分类器与 TF-IDF 向量数据集(tfidf_vectorizer)和被动-进取分类器(linear_clf),来查看假新闻与真新闻的前 30 个向量:

In [30]:
FAKE -4.86382369883 2016FAKE -4.13847157932 hillaryFAKE -3.98994974843 octoberFAKE -3.10552662226 shareFAKE -2.99713810694 novemberFAKE -2.9150746075 articleFAKE -2.54532100449 printFAKE -2.47115243995 advertisementFAKE -2.35915304509 sourceFAKE -2.31585837413 emailFAKE -2.27985826579 electionFAKE -2.2736680857 octFAKE -2.25253568246 warFAKE -2.19663276969 mosulFAKE -2.17921304122 podestaFAKE -1.99361009573 novFAKE -1.98662624907 comFAKE -1.9452527887 establishmentFAKE -1.86869495684 corporateFAKE -1.84166664376 wikileaksFAKE -1.7936566878 26FAKE -1.75686475396 donaldFAKE -1.74951154055 snipFAKE -1.73298170472 mainstreamFAKE -1.71365596627 ukFAKE -1.70917804969 ayotteFAKE -1.70781651904 entireFAKE -1.68272667818 jewishFAKE -1.65334397724 youtubeFAKE -1.6241703128 pipelineREAL 4.78064061698 saidREAL 2.68703967567 tuesdayREAL 2.48309800829 gopREAL 2.45710670245 islamicREAL 2.44326123901 saysREAL 2.29424417889 cruzREAL 2.29144842597 marriageREAL 2.20500735471 candidatesREAL 2.19136552672 conservativeREAL 2.18030834903 mondayREAL 2.05688105375 attacksREAL 2.03476457362 rushREAL 1.9954523319 continueREAL 1.97002430576 fridayREAL 1.95034103105 conventionREAL 1.94620720989 senREAL 1.91185661202 jobsREAL 1.87501303774 debateREAL 1.84059602241 presumptiveREAL 1.80111133252 sayREAL 1.80027216061 sundayREAL 1.79650823765 marchREAL 1.79229792108 parisREAL 1.74587899553 securityREAL 1.69585506276 conservativesREAL 1.68860503431 recountsREAL 1.67424302821 dealREAL 1.67343398121 campaignREAL 1.66148582079 foxREAL 1.61425630518 attack

您也可以用几行非常直接的 Python 代码来实现:将系数与特征打包到一起,然后查看列表的顶部与底部。

In [31]:
feature_names = tfidf_vectorizer.get_feature_names()
In [32]:
### Most realsorted(zip(clf.coef_[0], feature_names), reverse=True)[:20]
Out[32]:
[(-6.2573612147015822, 'trump'), (-6.4944530943126777, 'said'), (-6.6539784739838845, 'clinton'), (-7.0379446628670728, 'obama'), (-7.1465399833812278, 'sanders'), (-7.2153760086475112, 'president'), (-7.2665628057416169, 'campaign'), (-7.2875931446681514, 'republican'), (-7.3411184585990643, 'state'), (-7.3413571102479054, 'cruz'), (-7.3783124419854254, 'party'), (-7.4468806724578904, 'new'), (-7.4762888011545883, 'people'), (-7.547225599514773, 'percent'), (-7.5553074094582335, 'bush'), (-7.5801506339098932, 'republicans'), (-7.5855405012652435, 'house'), (-7.6344781725203141, 'voters'), (-7.6484824436952987, 'rubio'), (-7.6734836186463795, 'states')]
In [33]:
### Most fakesorted(zip(clf.coef_[0], feature_names))[:20]
Out[33]:
[(-11.349866225220305, '0000'), (-11.349866225220305, '000035'), (-11.349866225220305, '0001'), (-11.349866225220305, '0001pt'), (-11.349866225220305, '000km'), (-11.349866225220305, '0011'), (-11.349866225220305, '006s'), (-11.349866225220305, '007'), (-11.349866225220305, '007s'), (-11.349866225220305, '008s'), (-11.349866225220305, '0099'), (-11.349866225220305, '00am'), (-11.349866225220305, '00p'), (-11.349866225220305, '00pm'), (-11.349866225220305, '014'), (-11.349866225220305, '015'), (-11.349866225220305, '018'), (-11.349866225220305, '01am'), (-11.349866225220305, '020'), (-11.349866225220305, '023')]

很明显,在“假”类的顶级特征中,有一些词语可能传达出政治立场与来源(比如 corporate 与 establishment)。

另外,真实新闻数据更常用 “to say” 的各种形式,这很可能是因为在报纸与大多数新闻出版物中,来源会被直接引用(“德国总理默克尔表示……”)。

若要从当前分类器中导出完整列表并查看每个标记(或在分类器之间轻松比较标记),可以像下面这样导出。

In [34]:
tokens_with_weights = sorted(list(zip(feature_names, clf.coef_[0])))

插曲:HashingVectorizer

文本分类中有时还会使用另一种向量化器:HashingVectorizer。HashingVectorizer 更省内存、速度更快(因为是稀疏的并使用哈希而非标记),但更难以做内省。如果您感兴趣,可在 scikit-learn 文档中了解使用 HashingVectorizer 的利弊。

您可以尝试一下,并将其结果与其他向量化器比较。它的表现相当不错,使用 MultinomialNB 时优于 TF-IDF 向量化器(这在一定程度上意料之中,原因与 CountVectorizer 表现更好的原因类似),但仍不及结合被动-进取线性算法的 TF-IDF 向量化器。

In [35]:
hash_vectorizer = HashingVectorizer(stop_words='english', non_negative=True)hash_train = hash_vectorizer.fit_transform(X_train)hash_test = hash_vectorizer.transform(X_test)
In [36]:
clf = MultinomialNB(alpha=.01)
In [37]:
clf.fit(hash_train, y_train)pred = clf.predict(hash_test)score = metrics.accuracy_score(y_test, pred)print("accuracy:   %0.3f" % score)cm = metrics.confusion_matrix(y_test, pred, labels=['FAKE', 'REAL'])plot_confusion_matrix(cm, classes=['FAKE', 'REAL'])
accuracy:   0.902Confusion matrix, without normalization
 
confusion matrix 4
In [38]:
clf = PassiveAggressiveClassifier(n_iter=50)
In [39]:
clf.fit(hash_train, y_train)pred = clf.predict(hash_test)score = metrics.accuracy_score(y_test, pred)print("accuracy:   %0.3f" % score)cm = metrics.confusion_matrix(y_test, pred, labels=['FAKE', 'REAL'])plot_confusion_matrix(cm, classes=['FAKE', 'REAL'])
accuracy:   0.921Confusion matrix, without normalization
confusion matrix 5

结论

那么,您的假新闻分类器实验成功了吗?当然没有。

但您是否玩转了一个新数据集,测试了几种 NLP 分类模型,并对它们的效果进行了内省?是的。

正如一开始所预期的,试图用简单的词袋或 TF-IDF 向量来定义假新闻是过度简化的做法。尤其是在一个包含大量噪声标记的多语种数据集中。如果您没有看看模型到底学到了什么,可能会误以为它学到了某些有意义的东西。所以,记住:务必尽可能对模型进行内省!

如果您发现了我可能遗漏的其他数据趋势,我很感兴趣。我将会在我的博客跟进发布一篇文章,讨论不同分类器在重要特征方面的比较。如果您花时间做了研究并发现了有趣的内容,欢迎在评论区分享,也可以随时在 Twitter 上联系我(我在 @kjam)。

希望您在与我一起探索这个新的 NLP 数据集时玩得开心!

查看 DataCamp 的Python 机器学习:Scikit-Learn 教程。

主题