了解如何预测Reddit开采的新闻标题的情绪
在我们之前的文章中,我们介绍了一些情绪分析的基础知识,我们收集并分类政治头条。现在,我们可以使用该数据来训练二元分类器,以预测标题是正还是负。
文章资源
笔记本: GitHub
库:熊猫,numpy,scikit-learn,matplotlib,seaborn,nltk,imblearn
目录
简要介绍分类及我们面临的一些问题
第一个问题:不平衡的数据集
加载数据集
将标题转换为特征
准备训练
平衡数据
朴素贝叶斯
交叉验证
让我们绘制我们的结果
SCIKIT-LEARN中的其他分类算法
集合分类器
最后的话
帮助我们改进这篇文章和系列
分类是基于训练数据集识别新的,看不见的观察的类别的过程,其具有已知的类别。
在我们的例子中,我们的头条新闻是观察,正面/负面情绪是类别。这是一个二元分类问题 - 我们试图预测标题是正面还是负面。
机器学习中最常见的问题之一是使用不平衡的数据集。正如我们将在下面看到的,我们有一个略微不平衡的数据集,其中负数多于正数。
与欺诈检测等问题相比,我们的数据集不是超级不平衡的。有时你会有数据集,其中正类只有训练数据的1%,其余为负数。
我们要小心解释不平衡数据的结果。使用我们的分类器生成分数时,您可能会达到高达90%的准确度,这通常被称为准确性悖论。
我们可能具有90%准确度的原因是由于我们的模型检查数据并决定始终预测为负,从而导致高精度。
有很多方法可以解决这个问题,例如::
在我们的数据集中,我们的正面例子比负面例子少,我们将探索不同的指标并利用称为SMOTE的过采样技术。
让我们建立一些基本的导入:
import math
import random
from collections import defaultdict
from pprint import pprint
# 防止将来/弃用警告显示在输出
import warnings
warnings.filterwarnings(action='ignore')
import seaborn as sns
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
# 设置绘图的全局样式
sns.set_style(style='white')
sns.set_context(context='notebook', font_scale=1.3, rc={'figure.figsize': (16,9)})
这些是整个笔记本中使用的基本导入,通常在每个数据科学项目中导入。当我们使用sklearn和其他库时,将会提出更具体的导入。
首先让我们加载我们在上一篇文章中创建的数据集:
df = pd.read_csv('reddit_headlines_labels.csv', encoding='utf-8')
df.head()
现在我们在数据框中有数据集,让我们删除中性(0)标题标签,这样我们就可以专注于只对正面或负面进行分类:
df = df[df.label != 0]
df.label.value_counts()
-1 758 1 496 Name: label, dtype: int64
我们的数据框现在只包含正面和负面的例子,我们再次确认我们有更多的负面而不是正面。
让我们进入头条新闻的特色化。
为了训练我们的分类器,我们需要将单词的标题转换为数字,因为算法只知道如何使用数字。
要进行这种转换,我们将使用CountVectorizer
sklearn。这是将单词转换为要素的非常简单的类。
与我们手动标记化和小写文本的上一个教程不同,CountVectorizer
将为我们处理此步骤。我们需要做的只是将其作为头条新闻。
让我们使用一个小例子来展示如何将单词向量化为数字:
from sklearn.feature_extraction.text import CountVectorizer
s1 = "Senate panel moving ahead with Mueller bill despite McConnell opposition"
s2 = "Bill protecting Robert Mueller to get vote despite McConnell opposition"
vect = CountVectorizer(binary=True)
X = vect.fit_transform([s1, s2])
X.toarray()
array([[1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1], [0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0]], dtype=int64)
我们在这里做的是关于类似主题的两个标题并将它们矢量化。
vect
使用默认参数设置标记化和小写字。最重要的是,我们设置了binary=True
这样我们得到的输出为0(该句子中不存在单词)或1(该句子中存在单词)。
vect
根据它在你给出的所有文本中看到的所有单词构建词汇表,然后在当前句子中存在该单词时指定0或1。为了更清楚地看到这一点,让我们看一下映射到第一句的特征名称:
list(zip(X.toarray()[0], vect.get_feature_names()))
[(1, 'ahead'), (1, 'bill'), (1, 'despite'), (0, 'get'), (1, 'mcconnell'), (1, 'moving'), (1, 'mueller'), (1, 'opposition'), (1, 'panel'), (0, 'protecting'), (0, 'robert'), (1, 'senate'), (0, 'to'), (0, 'vote'), (1, 'with')]
这是第一句的矢量化映射。你可以看到有一个1映射到'前面'因为'前面'出现了s1
。但是如果我们看一下s2
:
list(zip(X.toarray()[1], vect.get_feature_names()))
[(0, 'ahead'), (1, 'bill'), (1, 'despite'), (1, 'get'), (1, 'mcconnell'), (0, 'moving'), (1, 'mueller'), (1, 'opposition'), (0, 'panel'), (1, 'protecting'), (1, 'robert'), (0, 'senate'), (1, 'to'), (1, 'vote'), (0, 'with')]
因为那个词没有出现在'前方',所以有一个0 s2
。但请注意,每行包含到目前为止看到的每个单词。
当我们将其扩展到数据集中的所有标题时,这个词汇量将会增长很多。像上面打印的那样的每个映射最终将成为矢量化器遇到的所有单词的长度。
现在让我们将矢量化器应用到我们的所有标题中:
vect = CountVectorizer(max_features=1000, binary=True)
X = vect.fit_transform(df.headline)
X.toarray()
array([[0, 0, 0, ..., 0, 1, 0], [0, 0, 0, ..., 0, 0, 0], [0, 0, 0, ..., 0, 0, 0], ..., [0, 0, 0, ..., 0, 0, 0], [0, 0, 0, ..., 0, 0, 0], [0, 0, 0, ..., 0, 0, 0]], dtype=int64)
请注意,矢量图默认情况下将所有内容存储在稀疏数组中,并使用X.toarray()
向我们显示密集版本。稀疏数组的效率要高得多,因为每行中的大多数值都是0.换句话说,大多数标题只有十几个字,每行包含所见过的每个字,稀疏数组只存储非零值索引。
您还会注意到我们有一个新的关键字参数; max_features
。这基本上是按频率排列的要考虑的单词数。所以1000值意味着我们只想看1000个最常见的单词作为特征。
现在我们知道矢量化是如何工作的,让我们在行动中使用它。
在训练,甚至矢量化之前,让我们将数据分成训练和测试集。在对数据进行任何操作之前执行此操作非常重要,因此我们有一个新的测试集。
from sklearn.model_selection import train_test_split
X = df.headline
y = df.label
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
我们的测试尺寸为0.2或20%。这意味着,X_test
和y_test
包含我们的数据,我们保留对测试的20%。
现在让我们只在训练集上拟合矢量化器并执行矢量化。
重申一下,重要的是不要将矢量化器放在所有数据上,因为我们需要一个干净的测试集来评估性能。在一切上拟合矢量化器会导致数据泄漏,导致不可靠的结果,因为矢量化器不应该知道未来的数据。
我们可以适应矢量化器并X_train
一步变换:
from sklearn.feature_extraction.text import CountVectorizer
vect = CountVectorizer(max_features=1000, binary=True)
X_train_vect = vect.fit_transform(X_train)
X_train_vect
现在转换为正确的格式以提供Naive Bayes模型,但我们首先考虑平衡数据。
似乎可能有比正面标题(hmm)更多的负面标题,因此我们有更多的负面标签而不是正面标签。
counts = df.label.value_counts()
print(counts)
print("\nPredicting only -1 = {:.2f}% accuracy".format(counts[-1] / sum(counts) * 100))
-1 758 1 496 Name: label, dtype: int64 Predicting only -1 = 60.45% accuracy
我们可以从上面看到,我们的负数略多于正数,使得我们的数据集略有不平衡。
通过计算我们的模型是否仅选择预测-1,更大的类,我们将获得约60%的准确度。这意味着在我们的二元分类模型中,随机几率为50%,60%的准确度不会告诉我们太多。我们肯定希望看到精度和召回而不是准确性。
我们可以通过使用称为SMOTE 的过采样形式来平衡我们的数据。SMOTE着眼于小班,在我们的案例中肯定,并创建新的综合训练样例。了解更多关于该算法在这里。
注意:我们必须确保我们只对列车数据进行过采样,这样我们就不会将任何信息泄露给测试集。
让我们用imblearn
库来执行SMOTE :
from imblearn.over_sampling import SMOTE
sm = SMOTE()
X_train_res, y_train_res = sm.fit_sample(X_train_vect, y_train)
unique, counts = np.unique(y_train_res, return_counts=True)
print(list(zip(unique, counts)))
[(-1, 601), (1, 601)]
这些班级现在已经为火车组平衡了。我们可以继续训练朴素贝叶斯模型。
对于我们的第一个算法,我们将使用极其快速和多功能的朴素贝叶斯模型。
让我们从sklearn中实例化一个并将其与我们的训练数据相匹配:
from sklearn.naive_bayes import MultinomialNB
nb = MultinomialNB()
nb.fit(X_train_res, y_train_res)
nb.score(X_train_res, y_train_res)
0.9201331114808652
Naive Bayes已经成功完成了我们所有的训练数据,并准备进行预测。你会发现我们得分为92%。这是合适的分数,而不是实际的准确度分数。接下来您将看到我们需要使用我们的测试集来获得准确性的良好估计。
让我们对测试集进行矢量化,然后使用该测试集来预测每个测试标题是正面还是负面。由于我们避免任何数据泄漏,我们只是改造而不是改装。我们也不会使用SMOTE进行过采样。
X_test_vect = vect.transform(X_test)
y_pred = nb.predict(X_test_vect)
y_pred
array([-1, -1, -1, -1, -1, -1, 1, 1, 1, 1, 1, -1, 1, -1, 1, 1, 1, 1, -1, -1, 1, -1, -1, -1, -1, 1, 1, 1, -1, -1, 1, -1, 1, 1, -1, -1, 1, 1, 1, -1, 1, 1, 1, -1, 1, -1, 1, -1, 1, -1, 1, 1, 1, 1, 1, -1, -1, 1, 1, -1, -1, -1, 1, 1, 1, 1, -1, -1, -1, -1, 1, -1, 1, -1, -1, -1, -1, 1, -1, 1, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 1, 1, -1, 1, 1, 1, -1, -1, -1, -1, 1, 1, 1, 1, 1, -1, 1, 1, 1, -1, -1, 1, 1, -1, 1, -1, -1, 1, -1, -1, -1, -1, -1, 1, -1, 1, -1, 1, 1, -1, 1, 1, 1, -1, -1, -1, -1, 1, -1, -1, -1, 1, 1, 1, -1, -1, -1, -1, 1, -1, 1, -1, -1, 1, -1, 1, -1, -1, -1, -1, -1, 1, 1, 1, 1, 1, -1, 1, -1, 1, -1, -1, -1, -1, 1, -1, 1, 1, 1, 1, -1, -1, -1, 1, -1, -1, -1, 1, -1, -1, -1, -1, -1, -1, -1, 1, 1, -1, 1, -1, -1, -1, 1, -1, 1, -1, -1, 1, -1, -1, 1, -1, -1, 1, -1, 1, -1, -1, -1, -1, -1, 1, 1, -1, 1, -1, -1, 1, 1, 1, -1, -1, 1, -1, 1, 1, -1, -1, -1, 1, -1, 1, 1, 1, -1, 1, -1, 1, -1, -1], dtype=int64)
y_pred
现在包含测试集的每一行的预测。使用此预测结果,我们可以将其传递给具有真实标签的sklearn指标,以获得准确度分数,F1分数,并生成混淆矩阵:
from sklearn.metrics import accuracy_score, f1_score, confusion_matrix
print("Accuracy: {:.2f}%".format(accuracy_score(y_test, y_pred) * 100))
print("\nF1 Score: {:.2f}".format(f1_score(y_test, y_pred) * 100))
print("\nCOnfusion Matrix:\n", confusion_matrix(y_test, y_pred))
Accuracy: 74.50% F1 Score: 68.93 COnfusion Matrix: [[116 41] [ 23 71]]
我们可以看到我们的模型已经以75%的准确度预测了标题的情绪,但是看看混淆矩阵,我们可以看到它没有做出那么好的工作分类。
对于混淆矩阵的细分,我们有:
所以我们的分类器正在得到很多负面因素,但是有大量的错误预测。我们将看看我们是否可以使用下面的其他分类器来改进这些指标。
现在让我们使用交叉验证,我们在不同位置对相同数据生成10次不同的训练和测试集。
现在,我们建立了通常80%的数据作为培训,20%作为测试。单个测试集上的预测准确性并没有说明泛化。为了更好地了解我们的分类器的泛化能力,我们可以使用两种不同的技术:
1)K折交叉验证:将这些例子随机分成kk等大小的子集(通常为10)。在kk子集中,单个子样本用于测试模型,剩余的k-1k-1子集用作训练数据。然后将交叉验证技术重复kk次,从而产生这样的过程,其中每个子集仅使用一次作为测试集的一部分。最后,计算kk运行的平均值。这种方法的优点是每个例子都用于训练和测试集。
2)蒙特卡罗交叉验证:随机将数据集拆分为训练和测试数据,运行模型,然后对结果取平均值。该方法的优点是列车/测试分裂的比例不依赖于迭代次数,这对于非常大的数据集是有用的。另一方面,如果您没有经历足够的迭代,则此方法的缺点是可能永远不会在测试子集中选择某些示例,而其他示例可能被选择多次。
有关这两种方法之间差异的更好解释,请查看以下答案:https://stats.stackexchange.com/a/60967
来自sklearn图书馆的相关课程是ShuffleSplit
。这首先执行shuffle,然后将数据拆分为train / test。由于它是一个迭代器,它将执行随机shuffle并为每次迭代分割。这是上面提到的蒙特卡罗方法的一个例子。
通常情况下,我们可以使用sklearn.model_selection.cross_val_score
自动计算每个折叠的分数,但我们将展示手动分割ShuffleSplit
。
此外,如果你熟悉cross_val_score
你,你会注意到它的ShuffleSplit
工作方式不同。所述n_splits
参数ShuffleSplit
是时间随机化数据的数量,然后把它分解80/20,而cv
在参数cross_val_score
是折叠的数量。通过使用n_splits
较大的数据集,我们可以很好地逼近较大数据集的真实性能,但绘制起来更难。
from sklearn.model_selection import ShuffleSplit
X = df.headline
y = df.label
ss = ShuffleSplit(n_splits=10, test_size=0.2)
sm = SMOTE()
accs = []
f1s = []
cms = []
for train_index, test_index in ss.split(X):
X_train, X_test = X.iloc[train_index], X.iloc[test_index]
y_train, y_test = y.iloc[train_index], y.iloc[test_index]
# Fit vectorizer and transform X train, then transform X test
X_train_vect = vect.fit_transform(X_train)
X_test_vect = vect.transform(X_test)
# Oversample
X_train_res, y_train_res = sm.fit_sample(X_train_vect, y_train)
# Fit Naive Bayes on the vectorized X with y train labels,
# then predict new y labels using X test
nb.fit(X_train_res, y_train_res)
y_pred = nb.predict(X_test_vect)
# Determine test set accuracy and f1 score on this fold using the true y labels and predicted y labels
accs.append(accuracy_score(y_test, y_pred))
f1s.append(f1_score(y_test, y_pred))
cms.append(confusion_matrix(y_test, y_pred))
print("\nAverage accuracy across folds: {:.2f}%".format(sum(accs) / len(accs) * 100))
print("\nAverage F1 score across folds: {:.2f}%".format(sum(f1s) / len(f1s) * 100))
print("\nAverage Confusion Matrix across folds: \n {}".format(sum(cms) / len(cms)))
Average accuracy across folds: 72.95% Average F1 score across folds: 66.43% Average Confusion Matrix across folds: [[115.6 39. ] [ 28.9 67.5]]
看起来平均准确度和F1分数都与我们在上面的单个折叠上看到的相似。
fig, (ax1, ax2) = plt.subplots(2, 1, sharex=True, figsize=(16,9))
acc_scores = [round(a * 100, 1) for a in accs]
f1_scores = [round(f * 100, 2) for f in f1s]
x1 = np.arange(len(acc_scores))
x2 = np.arange(len(f1_scores))
ax1.bar(x1, acc_scores)
ax2.bar(x2, f1_scores, color='#559ebf')
# Place values on top of bars
for i, v in enumerate(list(zip(acc_scores, f1_scores))):
ax1.text(i - 0.25, v[0] + 2, str(v[0]) + '%')
ax2.text(i - 0.25, v[1] + 2, str(v[1]))
ax1.set_ylabel('Accuracy (%)')
ax1.set_title('Naive Bayes')
ax1.set_ylim([0, 100])
ax2.set_ylabel('F1 Score')
ax2.set_xlabel('Runs')
ax2.set_ylim([0, 100])
sns.despine(bottom=True, left=True) # Remove the ticks on axes for cleaner presentation
plt.show()
在一些运行之间,F1得分波动超过15个点,这可以用更大的数据集来补救。让我们看看其他算法是如何做的。
正如你所看到的Naive Bayes表现得相当不错,所以让我们试试其他分类器吧。
我们将使用与之前相同的shuffle分割,但现在我们将在每个循环中运行几种类型的模型:
from sklearn.naive_bayes import BernoulliNB
from sklearn.linear_model import LogisticRegression, SGDClassifier
from sklearn.svm import LinearSVC
from sklearn.ensemble import RandomForestClassifier
from sklearn.neural_network import MLPClassifier
X = df.headline
y = df.label
cv = ShuffleSplit(n_splits=20, test_size=0.2)
models = [
MultinomialNB(),
BernoulliNB(),
LogisticRegression(),
SGDClassifier(),
LinearSVC(),
RandomForestClassifier(),
MLPClassifier()
]
sm = SMOTE()
# Init a dictionary for storing results of each run for each model
results = {
model.__class__.__name__: {
'accuracy': [],
'f1_score': [],
'confusion_matrix': []
} for model in models
}
for train_index, test_index in cv.split(X):
X_train, X_test = X.iloc[train_index], X.iloc[test_index]
y_train, y_test = y.iloc[train_index], y.iloc[test_index]
X_train_vect = vect.fit_transform(X_train)
X_test_vect = vect.transform(X_test)
X_train_res, y_train_res = sm.fit_sample(X_train_vect, y_train)
for model in models:
model.fit(X_train_res, y_train_res)
y_pred = model.predict(X_test_vect)
acc = accuracy_score(y_test, y_pred)
f1 = f1_score(y_test, y_pred)
cm = confusion_matrix(y_test, y_pred)
results[model.__class__.__name__]['accuracy'].append(acc)
results[model.__class__.__name__]['f1_score'].append(f1)
results[model.__class__.__name__]['confusion_matrix'].append(cm)
我们现在为每个模型存储了一堆准确度分数,f1分数和混淆矩阵。让我们一起平均这些以获得模型和折叠的平均分数:
for model, d in results.items():
avg_acc = sum(d['accuracy']) / len(d['accuracy']) * 100
avg_f1 = sum(d['f1_score']) / len(d['f1_score']) * 100
avg_cm = sum(d['confusion_matrix']) / len(d['confusion_matrix'])
slashes = '-' * 30
s = f"""{model}\n{slashes}
Avg. Accuracy: {avg_acc:.2f}%
Avg. F1 Score: {avg_f1:.2f}
Avg. Confusion Matrix:
\n{avg_cm}
"""
print(s)
MultinomialNB ------------------------------ Avg. Accuracy: 74.70% Avg. F1 Score: 69.63 Avg. Confusion Matrix: [[114.05 36.4 ] [ 27.1 73.45]] BernoulliNB ------------------------------ Avg. Accuracy: 75.32% Avg. F1 Score: 67.96 Avg. Confusion Matrix: [[122.75 27.7 ] [ 34.25 66.3 ]] LogisticRegression ------------------------------ Avg. Accuracy: 74.80% Avg. F1 Score: 68.31 Avg. Confusion Matrix: [[119.2 31.25] [ 32. 68.55]] SGDClassifier ------------------------------ Avg. Accuracy: 71.75% Avg. F1 Score: 65.31 Avg. Confusion Matrix: [[112.6 37.85] [ 33.05 67.5 ]] LinearSVC ------------------------------ Avg. Accuracy: 73.01% Avg. F1 Score: 66.61 Avg. Confusion Matrix: [[115.55 34.9 ] [ 32.85 67.7 ]] RandomForestClassifier ------------------------------ Avg. Accuracy: 69.64% Avg. F1 Score: 52.74 Avg. Confusion Matrix: [[132. 18.45] [ 57.75 42.8 ]] MLPClassifier ------------------------------ Avg. Accuracy: 74.14% Avg. F1 Score: 67.43 Avg. Confusion Matrix: [[118.75 31.7 ] [ 33.2 67.35]]
我们得到了一些相当不错的结果,但总的来说,我们需要更多的数据来确定哪一个表现最好。
由于我们仅在大约300个示例的测试集大小上运行度量标准,因此精度的0.5%差异意味着只有大约2个示例与其他模型正确分类。如果我们的测试集合为10,000,那么准确度的0.5%差异将等于50个正确分类的标题,这更令人放心。
随机森林和多项式朴素贝叶斯之间的区别非常明显,但多项式和伯努利朴素贝叶斯之间的区别并非如此。为了进一步比较这两者,我们需要更多数据。
让我们看看合奏是否可以带来更好的效果。
在我们单独评估每个分类器之后,让我们看看集成是否有助于改进我们的指标。
我们将使用VotingClassifier
默认为多数规则投票的sklearn 。
from sklearn.ensemble import VotingClassifier
X = df.headline
y = df.label
cv = ShuffleSplit(n_splits=10, test_size=0.2)
models = [
MultinomialNB(),
BernoulliNB(),
LogisticRegression(),
SGDClassifier(),
LinearSVC(),
RandomForestClassifier(),
MLPClassifier()
]
m_names = [m.__class__.__name__ for m in models]
models = list(zip(m_names, models))
vc = VotingClassifier(estimators=models)
sm = SMOTE()
# No need for dictionary now
accs = []
f1s = []
cms = []
for train_index, test_index in cv.split(X):
X_train, X_test = X.iloc[train_index], X.iloc[test_index]
y_train, y_test = y.iloc[train_index], y.iloc[test_index]
X_train_vect = vect.fit_transform(X_train)
X_test_vect = vect.transform(X_test)
X_train_res, y_train_res = sm.fit_sample(X_train_vect, y_train)
vc.fit(X_train_res, y_train_res)
y_pred = vc.predict(X_test_vect)
accs.append(accuracy_score(y_test, y_pred))
f1s.append(f1_score(y_test, y_pred))
cms.append(confusion_matrix(y_test, y_pred))
print("Voting Classifier")
print("-" * 30)
print("Avg. Accuracy: {:.2f}%".format(sum(accs) / len(accs) * 100))
print("Avg. F1 Score: {:.2f}".format(sum(f1s) / len(f1s) * 100))
print("Confusion Matrix:\n", sum(cms) / len(cms))
Voting Classifier ------------------------------ Avg. Accuracy: 75.78% Avg. F1 Score: 68.51 Confusion Matrix: [[123.7 28.7] [ 32.1 66.5]]
尽管我们的大多数分类器都表现出色,但它与我们从Multinomial Naive Bayes得到的结果没有多大差别,这可能是令人惊讶的。肯定将一堆混合在一起会产生更好的结果,但这种性能差异的缺乏证明仍有许多领域需要探索。例如:
到目前为止我们已经
不幸的是,没有明显的获胜模式。有一对我们已经看到它肯定表现不佳,但有一些徘徊在相同的准确性。此外,混淆矩阵显示大约一半的正面标题被错误分类,因此还有很多工作要做。
既然您已经了解了这个管道的工作原理,那么代码和建模的架构还有很大的改进空间。我鼓励您在提供的笔记本中尝试所有这些。看看你可以利用什么其他的subreddits情绪,如股票,公司,产品等..有很多有价值的数据!
如果您对将本文和系列文章扩展到某些探索领域感兴趣,请在下面发表评论,我们会将其添加到内容管道中。
谢谢阅读!
原文:
https://www.learndatasci.com/tutorials/predicting-reddit-news-sentiment-naive-bayes-text-classifiers/