自然语言处理实战第二版(MEAP)(二)(4)

本文涉及的产品
NLP自然语言处理_基础版,每接口每天50万次
NLP 自学习平台,3个模型定制额度 1个月
NLP自然语言处理_高级版,每接口累计50万次
简介: 自然语言处理实战第二版(MEAP)(二)

自然语言处理实战第二版(MEAP)(二)(3)https://developer.aliyun.com/article/1517851

4.2 挑战:检测毒性

为了看到主题建模的威力,我们将尝试解决一个真实问题:识别维基百科评论中的有毒性。这是当前内容和社交媒体平台面临的常见自然语言处理任务。在本章中,我们将处理一个维基百科讨论评论的数据集,^([7])我们将希望将其分类为两个类别 - 有毒和无毒。首先,让我们加载数据集并查看一下:

第 4.2 节 有毒评论数据集
>>> import pandas as pd
>>> pd.options.display.width = 120  # #1
>>> DATA_DIR = ('https://gitlab.com/tangibleai/nlpia/-/raw/master/'
...             'src/nlpia/data')
>>> url= DATA_DIR + '/toxic_comment_small.csv'
>>>
>>> comments = pd.read_csv(url)
>>> index = ['comment{}{}'.format(i, '!'*j) for (i,j) in
...          zip(range(len(comments)), comments.toxic)
...         ]  # #2
>>> comments = pd.DataFrame(
...     comments.values, columns=comments.columns, index=index)
>>> mask = comments.toxic.astype(bool).values
>>> comments['toxic'] = comments.toxic.astype(int)
>>> len(comments)
5000
>>> comments.toxic.sum()
650
>>> comments.head(6)
                                                        text  toxic
comment0   you have yet to identify where my edits violat...      0
comment1   "\n as i have already said,wp:rfc or wp:ani. (...      0
comment2   your vote on wikiquote simple english when it ...      0
comment3   your stalking of my edits i've opened a thread...      0
comment4!  straight from the smear site itself. the perso...      1
comment5   no, i can't see it either - and i've gone back...      0

所以你有 5,000 条评论,其中 650 条被标记为二进制类别标签“有毒”。

在你深入了解所有复杂的降维技术之前,让我们尝试使用你已经熟悉的消息的向量表示来解决我们的分类问题 - TF-IDF。但是你会选择什么模型来对消息进行分类呢?为了决定,让我们首先看看 TF-IDF 向量。

第 4.3 节 为 SMS 数据集创建 TF-IDF 向量
>>> from sklearn.feature_extraction.text import TfidfVectorizer
>>> import spacy
>>> nlp = spacy.load("en_core_web_sm")
>>>
>>> def spacy_tokenize(sentence):
...    return [token.text for token in nlp(sentence.lower())]
>>>
>>> tfidf_model = TfidfVectorizer(tokenizer=spacy_tokenize)
>>> tfidf_docs = tfidf_model.fit_transform(\
...     raw_documents=comments.text).toarray()
>>> tfidf_docs.shape
(5000, 19169)

spaCy 分词器为您的词汇表提供了 19,169 个单词。您的词汇表中几乎有 4 倍于您的消息的单词数量。而且您的单词数量几乎是有毒评论的 30 倍。因此,您的模型将不会有太多关于表明评论是否有毒的单词的信息。

你在本书中已经至少遇到了一个分类器 - 第二章中的朴素贝叶斯。通常,当您的词汇量远大于数据集中标记示例的数量时,朴素贝叶斯分类器的效果不会很好。所以这次我们需要点不同的东西。

4.2.1 潜在判别分析分类器

在本章中,我们将介绍一种基于称为潜在判别分析(LDA)的算法的分类器。LDA 是您会找到的最简单和最快的分类模型之一,并且它需要的样本比较花哨的算法要少。

LDA 的输入将是一个带有标签的数据 - 因此我们不仅需要表示消息的向量,还需要它们的类别。在这种情况下,我们有两个类别 - 有毒评论和非有毒评论。LDA 算法使用了一些超出本书范围的数学知识,但在两个类别的情况下,其实现非常直观。

本质上,当面临两类问题时,这就是 LDA 算法的工作原理:

  1. 它找到一个线,或者说轴,在您的向量空间中,如果您将空间中的所有向量(数据点)投影到该轴上,两个类别将尽可能地分离。
  2. 它将所有向量投影到那条线上。
  3. 它预测每个向量属于两个类别之一的概率,根据两个类别之间的一个cutoff点。

令人惊讶的是,在大多数情况下,最大化类别分离的线非常接近连接代表每个类别的聚类的两个质心的线。

让我们手动执行这个 LDA 的近似,并看看它在我们的数据集上的表现如何。

>>> mask = comments.toxic.astype(bool).values  # #1
>>> toxic_centroid = tfidf_docs[mask].mean(axis=0)  # #2
>>> nontoxic_centroid = tfidf_docs[~mask].mean(axis=0)  # #3
>>> centroid_axis = toxic_centroid - nontoxic_centroid
>>> toxicity_score = tfidf_docs.dot(centroid_axis)  # #1
>>> toxicity_score.round(3)
array([-0.008, -0.022, -0.014, ..., -0.025, -0.001, -0.022])

特定评论的毒性评分是该评论向量在非有毒评论和非有毒评论之间的线上的投影的长度。您计算这些投影的方法与您对余弦距离所做的计算相同。它是评论向量与从非有毒评论指向有毒评论的向量之间的向量的归一化点积。通过将每个 TF-IDF 向量投影到该线上并使用点积来计算毒性分数。您使用dot()方法一次性进行了这 5000 个点积的“向量化”numpy 操作。与 Python 的for循环相比,这可以加速 100 倍。

在我们的分类中,你只剩下一步了。你需要将我们的分数转换为实际的类预测。理想情况下,你希望你的分数在 0 和 1 之间,就像概率一样。一旦你对分数进行了归一化,你就可以根据一个截止值推断出分类 - 在这里,我们选择了一个简单的 0.5。你可以使用 sklearnMinMaxScaler 来执行归一化:

>>> from sklearn.preprocessing import MinMaxScaler
>>> comments['manual_score'] = MinMaxScaler().fit_transform(\
...     toxicity_score.reshape(-1,1))
>>> comments['manual_predict'] = (comments.manual_score > .5).astype(int)
>>> comments['toxic manual_predict manual_score'.split()].round(2).head(6)
           toxic  manual_predict  manual_score
comment0       0               0          0.41
comment1       0               0          0.27
comment2       0               0          0.35
comment3       0               0          0.47
comment4!      1               0          0.48
comment5       0               0          0.31

看起来不错。前六条消息几乎全部被正确分类了。让我们看看它在其余的训练集上的表现如何。

>>> (1 - (comments.toxic - comments.manual_predict).abs().sum()
...     / len(comments))
0.895...

不错!这个简单的“近似”版本的 LDA 准确地分类了 89.5% 的消息。完整的 LDA 会表现如何?使用 SciKit Learn (sklearn) 来获得最先进的 LDA 实现。

>>> from sklearn import discriminant_analysis
>>> lda_tfidf = discriminant_analysis.LinearDiscriminantAnalysis
>>> lda_tfidf = lda_tfidf.fit(tfidf_docs, comments['toxic'])
>>> comments['tfidf_predict'] = lda_tfidf.predict(tfidf_docs)
>>> float(lda_tfidf.score(tfidf_docs, comments['toxic']))
0.999...

99.9%! 几乎完美的准确率。这意味着你不需要使用更复杂的主题建模算法,比如潜在狄利克雷分配或深度学习吗?这是一个陷阱问题。你可能已经发现了陷阱。这个完美的 99.9% 的结果之所以如此完美,是因为我们没有分离出一个测试集。这个 A+ 分数是在分类器已经“见过”的“问题”上获得的。这就像在学校考试时拿到了和前一天练习的完全相同的问题一样。所以这个模型在恶意评论和垃圾邮件的真实世界中可能表现不佳。

提示

注意你用来训练和进行预测的类方法。sklearn 中的每个模型都有相同的方法:fit()predict()。而且所有的分类器模型甚至都会有一个 predict_proba() 方法,用于给出所有类别的概率分数。这样,当你尝试找到解决机器学习问题的最佳模型算法时,更容易进行不同模型算法的替换。这样你就可以将你的脑力集中在 NLP 工程师的创造性工作上,调整你的模型超参数以在实际世界中发挥作用。

让我们看看我们的分类器在一个更加现实的情况下的表现。你将把你的评论数据集分成两部分 - 训练集和测试集。(你可以想象,在 sklearn 中有一个专门的函数用于此!)然后你将看到分类器在它没有被训练的消息上的表现。

列表 4.4 使用训练-测试拆分的 LDA 模型性能
>>> from sklearn.model_selection import train_test_split
>>> X_train, X_test, y_train, y_test = train_test_split(tfidf_docs,\
...     comments.toxic.values, test_size=0.5, random_state=271828)
>>> lda_tfidf = LDA(n_components=1)
>>> lda = lda_tfidf.fit(X_train, y_train)  # #1
>>> round(float(lda.score(X_train, y_train)), 3)
0.999
>>> round(float(lda.score(X_test, y_test)), 3)
0.554

基于 TF-IDF 的模型的训练集准确率几乎完美。但测试集准确率为 0.55 - 比抛硬币稍微好一点。而测试集准确率才是唯一重要的准确率。这正是主题建模将帮助你的地方。它将允许你从一个小训练集中推广你的模型,使其在使用不同词语组合(但是相似主题)的消息上仍然表现良好。

提示

注意 train_test_split 函数中的 random_state 参数。train_test_split() 函数是随机的。所以每次运行它都会得到不同的结果和不同的准确度值。如果你想要让你的流程可重复,可以查找这些模型和数据集拆分器的 seed 参数。你可以将种子设置为相同的值来获得可再现的结果。

让我们更深入地看一下我们的 LDA 模型的表现,使用一种称为 混淆矩阵 的工具。混淆矩阵将告诉你模型犯错的次数。有两种类型的错误,假阳性 错误和 假阴性 错误。在测试集中标记为有毒的示例上出现的错误称为“假阴性”,因为它们被错误地标记为负面(无毒)并且应该被标记为正面(有毒)。在测试集中标记为非有毒标签上的错误称为“假阳性”,因为它们应该被标记为负面(无毒),但被错误地标记为有毒。下面是使用 sklearn 函数 的方法:

>>> from sklearn.metrics import confusion_matrix
>>> confusion_matrix(y_test, lda.predict(X_test))
array([[1261,  913],
       [ 201,  125]], dtype=int64)

嗯。这里的情况不太清楚。幸运的是,sklearn 考虑到你可能需要一种更直观的方式来向人们展示你的混淆矩阵,并包含了一个专门的函数。让我们试试:

>>> import matplotlib.pyplot as plt
>>> from sklearn.metrics import plot_confusion_matrix
>>> plot_confusion_matrix(lda,X_test, y_test, cmap="Greys",
...                display_labels=['non-toxic', 'toxic'], colorbar=False)
>>> plt.show()

你可以在图 4.2 中看到生成的 matplotlib 图,显示了两个标签(有毒和非有毒)的每个标签的不正确和正确的预测数量。检查这个图表,看看你能否发现你的模型性能有什么问题。

图 4.2 基于 TF-IDF 的分类器的混淆矩阵


首先,在实际上是有毒的测试集中的 326 条评论中,模型只能正确识别出 125 条 - 这是 38.3%。这个指标(我们感兴趣的类别中模型能够识别出多少个实例),称为 召回率,或 敏感度。另一方面,模型标记为有毒的 1038 条评论中,只有 125 条是真正有毒的评论。所以“正面”标签在 12% 的情况下才是正确的。这个指标称为 精度。^([9])

你已经可以看到精度和召回率比模型准确度给我们更多的信息。例如,想象一下,如果你决定使用确定性规则而不是使用机器学习模型,并只将所有评论标记为非有毒。由于我们数据集中约有 13% 的评论实际上是有毒的,所以这个模型的准确度将达到 0.87 - 比你上次训练的 LDA 模型要好得多!但是,它的召回率将为 0 - 在我们的任务中完全没有帮助,即识别有毒消息。

您可能也意识到这两个指标之间存在一种权衡。如果您采用另一种确定性规则,并将所有评论标记为有毒呢?在这种情况下,您的召回率将是完美的,因为您将正确分类所有有毒评论。但是,精确度将会下降,因为大多数被标记为有毒的评论实际上是完全正常的。

根据您的用例,您可能会决定优先考虑另一方面的精确度或召回率。但在很多情况下,您希望它们两者都足够好。

在这种情况下,您可能会使用F[1]分数 - 精确度和召回率的调和平均值。较高的精确度和较高的召回率都会导致较高的 F[1]分数,使得只使用一个指标来评估您的模型更容易。

您可以在附录 D 中了解有关分析分类器性能的更多信息。暂时,在我们继续之前,我们将只记录此模型的 F[1]分数。

超越线性

LDA 在许多情况下都会为您服务。然而,当这些假设不被满足时,它仍然有一些假设将导致分类器性能不佳。例如,LDA 假定所有类别的特征协方差矩阵都相同。这是一个相当强的假设!因此,由此造成的结果是,LDA 只能在类别之间学习线性边界。

如果您需要放松这个假设,您可以使用称为二次判别分析或 QDA 的更一般情况的 LDA。QDA 允许不同类别的不同协方差矩阵,并分别估计每个协方差矩阵。这就是为什么它可以学习二次或曲线边界的原因。这使得它更加灵活,并在某些情况下有助于其表现更好。

减少维度

在我们深入了解 LSA 之前,让我们花点时间了解一下它对我们的数据做了什么概念上的事情。LSA 对主题建模的方法背后的想法是降维。顾名思义,降维是一个过程,在这个过程中,我们找到数据的一个低维表示,保留尽可能多的信息。

让我们审视这个定义并理解它的含义。为了让您有直观的理解,让我们暂时远离自然语言处理,并切换到更直观的例子。首先,什么是数据的低维表示?想象一下将一个三维物体(比如你的沙发)表示为二维空间。例如,如果您在黑暗的房间里用光照射在沙发后面,它在墙上的阴影就是它的二维表示。

我们为什么需要这样的表示?可能有很多原因。也许我们没有能力存储或传输完整的数据。或者我们想要可视化我们的数据以更好地理解它。当我们谈论 LDA 时,你已经看到了可视化数据点并将它们聚类的强大能力。但我们的大脑实际上不能处理超过 2 或 3 个维度 - 当我们处理现实世界的数据,特别是自然语言数据时,我们的数据集可能有数百甚至数千个维度。像 PCA 这样的降维工具在我们想要简化和可视化映射我们的数据时非常有用。

另一个重要原因是我们在第三章中简要提到的维度诅咒。稀疏、多维数据更难处理,而在其上训练的分类器更容易过拟合。数据科学家经常使用的一个经验法则是,每个维度至少应该有 5 条记录。我们已经看到,即使对于小型文本数据集,TF-IDF 矩阵也可能迅速扩展到 10 或 20 万个维度。这也适用于许多其他类型的数据。

从“沙发影子”示例中,你可以看到我们可以构建无限多个相同“原始”数据集的低维表示。但有些表示比其他表示更好。在这种情况下,“更好”是什么意思?当谈到视觉数据时,你可以直观地理解,一个可以让我们识别对象的表示比一个不能的表示更好。例如,让我们拿一个从真实对象的 3D 扫描中获取的点云,并将其投影到一个二维平面上。

您可以在图 4.3 中看到结果。你能猜到那个 3D 对象是什么吗?

图 4.3 从下面看实际对象的点云


继续我们的“影子”类比,想象一下正午的太阳照射在一群人的头顶上。每个人的影子都是一个圆形斑点。我们能用这些斑点来判断谁高谁矮,或者哪些人头发长吗?可能不行。

现在你明白了良好的降维与能够在新表示中区分不同对象和数据点有关。并不是你数据的所有特征或维度对这个区分过程同样重要。因此,可能有一些特征你可以轻松舍弃而不会丢失太多信息。但对于某些特征,丢失它们将严重影响你理解数据的能力。并且因为你在这里处理的是线性代数,你不仅可以选择留下或包括一个维度 - 你还可以将几个维度组合成一个更小的维度集,以更简洁的方式表示我们的数据。让我们看看我们是如何做到的。

4.3.1 进入主成分分析

你现在知道,为了在更少的维度中找到数据的表示,你需要找到一个维度的组合,能够保持你区分数据点的能力。这将使你能够,例如,将它们分成有意义的聚类。继续上面的阴影例子,一个好的“阴影表示”可以让你看到你的阴影的头在哪里,腿在哪里。它通过保持这些对象之间的高度差异来实现,而不是像“中午的太阳表示”那样“压扁”它们到一个点。另一方面,我们身体的“厚度”从顶部到底部大致是均匀的 - 所以当你看到我们的“扁平”阴影表示时,丢弃了那个维度,你不会像丢弃我们的高度那样丢失太多信息。

在数学中,这种差异被方差所代表。当你想一想的时候,更有方差的特征 - 与平均值的偏离更广泛和更频繁 - 对于你来区分数据点更有帮助是有意义的。

但你可以超越单独观察每个特征。更重要的是特征之间的关系如何。在这里,视觉类比可能开始让你失望,因为我们操作的三个维度彼此正交,因此完全不相关。但让我们回想一下你在上一部分看到的主题向量:“动物性”,“宠物性”,“都市性”。如果你检查这三元组中的每两个特征,就会显而易见地发现一些特征之间的联系更紧密。大多数具有“宠物性”质量的词,也具有一些“动物性”的质量。一对特征或者维度的这种性质被称为协方差。它与相关性密切相关,后者仅仅是将每个特征的协方差归一化为这两个特征的差异。特征之间的协方差越高,它们之间的联系就越紧密 - 因此,它们之间的冗余也更多,因为你可以从一个特征推断出另一个特征。这也意味着你可以找到一个单一的维度,能够保持这两个维度中包含的大部分方差。

总结一下,为了减少描述我们的数据的维数而不丢失信息,您需要找到一种表示,最大化其新轴上的方差,同时减少维度之间的依赖性,并消除具有高协方差的维度。 这正是主成分分析(PCA)所做的。 它找到一组最大化方差的维度。 这些维度是正交的(就像物理世界中的x,yz轴),称为主成分 - 因此得名该方法。 PCA 还允许您查看每个维度“负责”的方差有多少,以便您可以选择保留数据集“本质”的最佳主要成分数量。 然后,PCA 将您的数据投影到一组新坐标中。

在我们深入研究 PCA 如何做到这一点之前,让我们看看魔术是如何发挥作用的。 在下面的清单中,您将使用 Scikit-Learn 的 PCA 方法获取上一页上看到的相同的 3D 点云,并找到一组最大化此点云方差的两个维度。

清单 4.5 PCA 魔法
>>> import pandas as pd
>>> from sklearn.decomposition import PCA
>>> import seaborn
>>> from matplotlib import pyplot as plt
>>> DATA_DIR = ('https://gitlab.com/tangibleai/nlpia/'
...             '-/raw/master/src/nlpia/data')
>>> df = pd.read_csv(DATA_DIR + '/pointcloud.csv.gz', index_col=0)
>>> pca = PCA(n_components=2)  # #1
>>> df2d = pd.DataFrame(pca.fit_transform(df), columns=list('xy'))
>>> df2d.plot(kind='scatter', x='x', y='y')
>>> plt.show()

当你将 3D 点(向量)的维数减少到 2D 时,就像是拍摄了那个 3D 点云的照片。 结果可能看起来像图 4.4 的右边或左边的照片,但它永远不会倾斜或扭曲到新的角度。 x 轴(轴 0)将始终沿着点云点的最长轴对齐,在那里点的分布最广泛。 这是因为 PCA 始终找到将最大化方差的维度,并按照方差递减的顺序排列它们。 具有最高方差的方向将成为第一个轴(x)。 第二高方差的维度在 PCA 变换后成为第二维度(y 轴)。 但是这些轴的极性(符号)是任意的。 优化可以自由地围绕 x 轴或 y 轴镜像(翻转)向量(点),或两者兼而有之。

图 4.4 马头对马头的点云颠倒过来


现在我们已经看到了 PCA 是如何工作的^([12]),让我们看看它是如何找到那些允许我们在较少维度中处理数据而不丢失太多信息的主要成分的。

4.3.2 奇异值分解

PCA 的核心是一种称为奇异值分解(SVD)的数学过程^([13])。 SVD 是一种将任何矩阵分解为三个“因子”的算法,可以将这三个矩阵相乘以重新创建原始矩阵。 这类似于为大整数找到确切的三个整数因子。 但是您的因子不是标量整数,而是具有特殊属性的 2D 实数矩阵。

假设我们有一个数据集,由 m 个 n 维点组成,用矩阵 W 表示。在其完整版本中,这就是 W 的 SVD 在数学符号中的样子(假设 m>n):

W[m] [x] [n] = U[m] [x] [m] S[m] [x] [n] V[n] [x] [n]^T

矩阵 U、S 和 V 具有特殊的性质。U 和 V 矩阵是正交的,这意味着如果你将它们乘以它们的转置版本,你将得到一个单位矩阵。而 S 是对角的,意味着它只在对角线上有非零值。

注意这个公式中的等号。它意味着如果你乘以 U、S 和 V,你会得到 完全相同的 W,我们的原始数据集。但是你可以看到我们的矩阵的最小维度仍然是 n。我们不是想要减少维度吗?这就是为什么在这一章中,你将使用 SVD 的版本称为减少,或截断 SVD。这意味着你只需要找到你感兴趣的前 p 个维度。

在这一点上,你可能会说“等等,但我们不能做完整的 SVD 然后只取保留最大方差的维度吗?” 你完全正确,我们可以这样做!然而,使用截断 SVD 还有其他好处。特别是,有几种算法可以快速计算矩阵的截断 SVD 分解,特别是当矩阵是稀疏的时候。稀疏矩阵 是指在其大多数单元格中具有相同值(通常为零或 NaN)的矩阵。NLP 词袋和 TF-IDF 矩阵几乎总是稀疏的,因为大多数文档不包含词汇表中的许多单词。

这就是截断 SVD 的样子:

W[m] [x] [n] ~ U[m] [x] [p] S[p] [x] [p] V[p] [x] [n]^T 在这个公式中,mn 是原始矩阵中的行数和列数,而 p 是您想要保留的维数。例如,在马的例子中,如果我们想要在二维空间中显示马,则 p 将等于二。在下一章中,当您使用 SVD 进行 LSA 时,它将表示您在分析文档时想要使用的主题数。当然,p 需要小于 mn

注意这种情况下的“近似等于”符号 - 因为我们失去了维度,所以当我们乘以我们的因子时,不能期望得到完全相同的矩阵!总会有一些信息丢失。然而,我们所获得的是一种新的表示方式,用比原始表示更少的维度来表示我们的数据。通过我们的马点云,我们现在能够传达其“马”的本质,而无需打印庞大的 3D 图。当 PCA 在现实生活中使用时,它可以将百或千维数据简化为更容易分析、聚类和可视化的短向量。

那么,矩阵 U、S 和 V 有什么用呢?现在,我们将简单地介绍一下它们的作用。在下一章中,我们将深入探讨这些矩阵在 LSA 中的应用。

让我们从V^T开始 - 或者更确切地说,从它的转置版本V开始。V矩阵的列有时被称为主方向,有时被称为主成分。由于本章中使用的 Scikit-Learn 库采用了后一种惯例,我们也将坚持使用这种说法。

你可以将V看作是一个“转换器”工具,用于将你的数据从“旧”空间(在矩阵 W 的“世界”中的表示)映射到新的、低维度的空间。想象一下,我们在我们的 3D 马点云中添加了几个新点,现在想要了解这些新点在我们的 2D 表示中的位置,而不需要重新计算所有点的变换。要将每个新点q映射到其在 2D 图中的位置,你所需要做的就是将其乘以 V:

q̂ = q · V

那么*U · S*的含义是什么呢?通过一些代数技巧,你会发现它实际上是你的数据映射到新空间!基本上,它是你的数据点在新的、更低维度的表示中。

4.4 潜在语义分析

最后,我们可以停止“围绕”,回到主题建模!让我们看看当我们谈论如何在我们的文本数据中找到主题和概念时,你所学到的关于降维、PCA 和 SVD 的一切将开始变得有意义。

让我们从数据集本身开始。你将使用第 4.1 节中用于 LDA 分类器的相同评论语料库,并使用 TF-IDF 将其转换为矩阵。你可能还记得结果被称为术语 - 文档矩阵。这个名字很有用,因为它让你直观地理解了矩阵的行和列包含的内容:行是术语,即你的词汇词;列将是文档。

让我们重新运行列表 4.1 和 4.2 以再次得到我们的 TF-IDF 矩阵。在深入 LSA 之前,我们研究了矩阵的形状:

>>> tfidf_docs.shape
(5000, 19169)

那么这里有什么?一个 19,169 维的数据集,其“空间”由语料库词汇中的术语定义。在这个空间中使用单个向量表示评论是相当麻烦的,因为每个向量中有将近 20,000 个数字 - 比消息本身还要长!而且很难看出消息或其中的句子在概念上是否相似 - 例如,“离开这个页面”和“走开”这样的表达将具有非常低的相似度分数,尽管它们的含义非常接近。因此,在 TF-IDF 矩阵中表示文档的聚类和分类要困难得多。

你还需要注意,你的 5000 条消息中只有 650 条(13%)被标记为有毒。所以你的训练集是不平衡的,约有 8:1 的正常评论和有毒评论(人身攻击、淫秽语言、种族歧视等)。而且你的词汇量很大 - 你的词汇量标记(25172)比你要处理的 4837 条消息(样本)还要多。所以你的词汇表(或词汇)中有很多更多的唯一词,而你的评论数量要少得多,甚至在与有毒消息数量比较时更多。这是一种过拟合的情况。^([15]) 在你的大词汇表中,只有很少的唯一词会被标记为“有毒”词汇在你的数据集中。

过拟合意味着你的词汇表中只会“关键”几个词。所以你的毒性过滤器将依赖于那些毒性词在过滤出来的毒性消息中的位置。如果恶意用户只是使用那些毒性词的同义词,那么他们很容易绕过你的过滤器。如果你的词汇表不包括新的同义词,那么你的过滤器就会误将那些构造巧妙的评论分类为非毒性。

这种过拟合问题是自然语言处理中的固有问题。很难找到一个标记的自然语言数据集,其中包含所有人们可能表达的应该被标记的方式。我们找不到一个“理想”的评论集,其中包含人们说有毒和无毒话的所有不同方式。只有少数几家公司有能力创建这样的数据集。所以我们其他人都需要对过拟合采取“对策”。你必须使用算法,在只有少数几个示例的情况下就能“泛化”得很好。

对抗过拟合的主要措施是将这些数据映射到一个新的、低维空间中。定义这个新空间的是你的语料库以各种方式讨论的加权词汇组合,或者话题。用话题来表示你的消息,而不是具体的词频,会使你的自然语言处理管道更“通用”,并允许我们的垃圾邮件过滤器处理更广泛的消息。这正是 LSA 所做的 - 它找到新的“维度”话题,使方差最大化,使用我们在前一节中发现的 SVD 方法。

这些新话题不一定与我们人类认为的话题相关,比如“宠物”或“历史”。机器不“理解”单词组合的含义,只知道它们在一起。当它经常看到“狗”、“猫”和“爱”这样的词一起出现时,它会把它们放在一个话题中。它不知道这样的话题可能是关于“宠物”的。它可能会在同一个话题中包含很多像“驯养”的词和“野生”的词,它们是彼此的反义词。如果它们在同一份文件中经常出现在一起,LSA 将为它们同时获得高分数。我们人类要看一下哪些词在每个话题中具有较高的权重,并为它们取一个名字。

但是你不必给主题起名字来利用它们。正如你没有分析前几章中你的词袋向量或 TF-IDF 向量中的 1000 多个维度一样,你不必知道你所有主题的 “含义”。你仍然可以使用这些新主题向量进行向量数学运算,就像你使用 TF-IDF 向量一样。你可以将它们相加和相减,并根据它们的 “主题表示” 而不是 “词频表示” 估计文档之间的相似性。而且这些相似性估计将更准确,因为你的新表示实际上考虑了令牌的含义及其与其他令牌的共现。

4.4.1 深入语义分析

但是别光说 LSA 了 - 让我们来写些代码吧!这一次,我们将使用另一个名为TruncatedSVD的 Scikit-Learn 工具,执行 - 有多惊喜 - 我们在上一章中检查过的截断 SVD 方法。我们本可以使用你在上一节看到的PCA模型,但我们选择这种更直接的方法 - 这将使我们更好地理解发生了什么事情 “底层”。此外,TruncatedSVD旨在处理稀疏矩阵,因此它在大多数 TF-IDF 和 BOW 矩阵上表现更好。

我们将从 9232 减少维度到 16 - 后面我们会解释我们选择这个数字的原因。

列表 4.6 使用截断 SVD 进行 LSA
>>> from sklearn.decomposition import TruncatedSVD
>>>
>>> svd = TruncatedSVD(n_components=16, n_iter=100)  # #1
>>> columns = ['topic{}'.format(i) for i in range(svd.n_components)]
>>> svd_topic_vectors = svd.fit_transform(tfidf_docs)  # #2
>>> svd_topic_vectors = pd.DataFrame(svd_topic_vectors, columns=columns,\
...     index=index)
>>> svd_topic_vectors.round(3).head(6)
           topic0  topic1  topic2  ...  topic13  topic14  topic15
comment0    0.121  -0.055   0.036  ...   -0.038    0.089    0.011
comment1    0.215   0.141  -0.006  ...    0.079   -0.016   -0.070
comment2    0.342  -0.200   0.044  ...   -0.138    0.023    0.069
comment3    0.130  -0.074   0.034  ...   -0.060    0.014    0.073
comment4!   0.166  -0.081   0.040  ...   -0.008    0.063   -0.020
comment5    0.256  -0.122  -0.055  ...    0.093   -0.083   -0.074

使用fit-transform方法刚刚生成的是新表示中的文档向量。你不再用 19169 个频率计数来表示你的评论,而是用 16 个。这个矩阵也称为文档-主题矩阵。通过查看列,你可以看到每个主题在每个评论中 “表达” 多少。

我们使用的方法与我们描述的矩阵分解过程有什么关系?你可能已经意识到fit_transform方法返回的正是({U \cdot S}) - 你的 tf-idf 向量投影到新空间。而你的 V 矩阵保存在TruncatedSVD对象的components_变量中。

如果你想探索你的主题,你可以通过检查每个单词或单词组在每个主题中的权重来了解它们 “包含” 多少。

首先让我们为你的转换中的所有维度分配单词。你需要按正确的顺序获取它们,因为你的TFIDFVectorizer将词汇存储为一个字典,将每个术语映射到一个索引号(列号)。

>>> list(tfidf_model.vocabulary_.items())[:5]  # #1
[('you', 18890),
 ('have', 8093),
 ('yet', 18868),
 ('to', 17083),
 ('identify', 8721)]
>>> column_nums, terms = zip(*sorted(zip(tfidf.vocabulary_.values(),
...     tfidf.vocabulary_.keys())))  # #2
>>> terms[:5]
('\n', '\n ', '\n \n', '\n \n ', '\n  ')

现在你可以创建一个漂亮的 Pandas DataFrame,其中包含权重,每一列和每一行的标签都在正确的位置。但是看起来我们的前几个术语只是不同的换行符的组合 - 这并不是很有用!

谁给你提供数据集的人应该更加注意清理它们。让我们使用有用的 Pandas 方法DataFrame.sample()随机查看你的词汇中的一些术语

>>> topic_term_matrix = pd.DataFrame(
...     svd.components_, columns=terms,
...     index=['topic{}'.format(i) for i in range(16)])
>>> pd.options.display.max_columns = 8
>>> topic_term_matrix.sample(5, axis='columns',
...     random_state=271828).head(4)  # #1
...
        littered  unblock.(t•c  orchestra  flanking  civilised
topic0  0.000268      0.000143   0.000630  0.000061   0.000119
topic1  0.000297     -0.000211  -0.000830 -0.000088  -0.000168
topic2 -0.000367      0.000157  -0.001457 -0.000150  -0.000133
topic3  0.000147     -0.000458   0.000804  0.000127   0.000181

这些词都不像是“天生有毒”。让我们看一些我们直觉上认为会出现在“有毒”评论中的词,看看这些词在不同主题中的权重有多大。

>>> pd.options.display.max_columns = 8
>>> toxic_terms = topic_term_matrix[
...     'pathetic crazy stupid idiot lazy hate die kill'.split()
...     ].round(3) * 100  # #1
...
>>> toxic_terms
         pathetic  crazy  stupid  idiot  lazy  hate  die  kill
topic0        0.3    0.1     0.7    0.6   0.1   0.4  0.2   0.2
topic1       -0.2    0.0    -0.1   -0.3  -0.1  -0.4 -0.1   0.1
topic2        0.7    0.1     1.1    1.7  -0.0   0.9  0.6   0.8
topic3       -0.3   -0.0    -0.0    0.0   0.1  -0.0  0.0   0.2
topic4        0.7    0.2     1.2    1.4   0.3   1.7  0.6   0.0
topic5       -0.4   -0.1    -0.3   -1.3  -0.1   0.5 -0.2  -0.2
topic6        0.0    0.1     0.8    1.7  -0.1   0.2  0.8  -0.1
...
>>> toxic_terms.T.sum()
topic0     2.4
topic1    -1.2
topic2     5.0
topic3    -0.2
topic4     5.9
topic5    -1.8
topic6     3.4
topic7    -0.7
topic8     1.0
topic9    -0.1
topic10   -6.6
...

主题 2 和主题 4 似乎更可能包含有毒情绪。而主题 10 则似乎是一个“反有毒”的主题。因此,与毒性相关的词可能对某些主题产生积极影响,对其他主题产生负面影响。没有一个单一明显的有毒主题号。

transform 方法所做的就是将你传递给它的任何内容与 V 矩阵相乘,这个矩阵保存在 components_ 中。你可以查看 TruncatedSVD 的代码来亲眼看看! ^([16])屏幕左上角的链接。

4.4.2 截断 SVD 还是 PCA?

你现在可能会问自己 - 为什么我们在马的例子中使用了 Scikit-Learn 的 PCA 类,但对于评论数据集的主题分析却使用了 TruncatedSVD?难道我们不是说 PCA 基于 SVD 算法吗?

如果你看一下 sklearnPCATruncatedSVD 的实现,你会发现两者之间的大部分代码都是相似的。它们都使用相同的算法来对矩阵进行 SVD 分解。然而,有几个差异可能会使每个模型对某些用例更可取。

最大的区别在于 TruncatedSVD 在分解之前不会居中矩阵,而 PCA 会。这意味着如果你在执行 TruncatedSVD 之前通过减去矩阵的列平均值来居中你的数据,像这样:

>>> tfidf_docs = tfidf_docs - tfidf_docs.mean()

你会得到相同的结果。通过比较对中心化数据的 TruncatedSVD 和 PCA 的结果,自己试试看!

数据被居中是主成分分析(PCA)的某些属性的重要性,你可能还记得,PCA 在自然语言处理之外有很多应用。然而,对于大多数稀疏的 TF-IDF 矩阵来说,居中并不总是有意义的。在大多数情况下,居中会使得一个稀疏矩阵变得稠密,导致模型运行速度变慢,占用更多内存。PCA 经常用于处理稠密矩阵,并且可以计算小矩阵的精确全矩阵奇异值分解(SVD)。相比之下,TruncatedSVD 已经假定输入矩阵是稀疏的,并使用更快的近似随机方法。因此,它比 PCA 更有效地处理您的 TF-IDF 数据。

4.4.3 LSA 在毒性检测中表现如何?

你已经花了足够的时间研究这些主题了 - 现在让我们看看我们的模型如何处理评论的低维表示!你将使用与列表 4.3 中运行的相同代码,但会将其应用于新的 16 维向量。这次,分类将进行得快得多:

>>> X_train_16d, X_test_16d, y_train_16d, y_test_16d = train_test_split(
...     svd_topic_vectors, comments.toxic.values, test_size=0.5,
...     random_state=271828)
>>> lda_lsa = LinearDiscriminantAnalysis(n_components=1)
>>> lda_lsa = lda_lsa.fit(X_train_16d, y_train_16d)
>>> round(float(lda_lsa.score(X_train_16d, y_train_16d)), 3)
0.881
>>> round(float(lda_lsa.score(X_test_16d, y_test_16d)), 3)
0.88

哇,差异如此之大!分类器对 TF-IDF 向量的训练集准确率从 99.9%下降到了 88.1%,但测试集准确率却提高了 33%!这是相当大的进步。

让我们来看看 F1 分数:

>>> from sklearn.metrics import f1_score
>>> f1_score(y_test_16d, lda_lsa.predict(X_test_16d).round(3)
0.342

我们的 F1 分数几乎比 TF-IDF 向量分类时翻了一番!不错。

除非你有完美的记忆力,到现在你一定对滚动或翻页找到之前模型的性能感到很烦恼。当你进行现实的自然语言处理时,你可能会尝试比我们的玩具示例中更多的模型。这就是为什么数据科学家会在超参数表中记录他们的模型参数和性能。

让我们制作自己的超参数表。首先,回想一下在 TF-IDF 向量上运行 LDA 分类器时我们得到的分类性能,并将其保存到我们的表中。

>>> hparam_table = pd.DataFrame()
>>> tfidf_performance = {'classifier': 'LDA',
...                      'features': 'tf-idf (spacy tokenizer)',
...                      'train_accuracy': 0.99 ,
...                      'test_accuracy': 0.554,
...                      'test_precision': 0.383 ,
...                      'test_recall': 0.12,
...                      'test_f1': 0.183}
>>> hparam_table = hparam_table.append(
...     tfidf_performance, ignore_index=True)  # #1

实际上,因为你要提取几个模型的这些分数,所以创建一个执行这项任务的函数是有道理的:

列表 4.7 创建超参数表中记录的函数。
>>> def hparam_rec(model, X_train, y_train, X_test, y_test,
...                model_name, features):
...     return {
...         'classifier': model_name,
...         'features': features,
...         'train_accuracy': float(model.score(X_train, y_train)),
...         'test_accuracy': float(model.score(X_test, y_test)),
...         'test_precision':
...             precision_score(y_test, model.predict(X_test)),
...         'test_recall':
...             recall_score(y_test, model.predict(X_test)),
...         'test_f1': f1_score(y_test, model.predict(X_test))
...         }
>>> lsa_performance = hparam_rec(lda_lsa, X_train_16d, y_train_16d,
...        X_test_16d,y_test_16d, 'LDA', 'LSA (16 components)'))
>>> hparam_table = hparam_table.append(lsa_performance)
>>> hparam_table.T  # #1
                                       0          1
classifier                           LDA        LDA
features        tf-idf (spacy tokenizer)  LSA (16d)
train_accuracy                      0.99     0.8808
test_accuracy                      0.554       0.88
test_precision                     0.383        0.6
test_recall                         0.12   0.239264
test_f1                            0.183   0.342105

你甚至可以进一步将大部分分析包装在一个很好的函数中,这样你就不必再次复制粘贴:

>>> def evaluate_model(X,y, classifier, classifier_name, features):
...     X_train, X_test, y_train, y_test = train_test_split(
...         X, y, test_size=0.5, random_state=271828)
...     classifier = classifier.fit(X_train, y_train)
...     return hparam_rec(classifier, X_train, y_train, X_test,y_test,
...                       classifier_name, features)

4.4.4 降维的其他方法

SVD 是迄今为止最流行的降维数据集的方法,使 LSA 成为你在考虑主题建模时的首选。然而,还有几种其他降维技术可以用来达到相同的目标。并非所有技术都用于自然语言处理,但了解它们也是很好的。我们在这里提到了两种方法- 随机投影非负矩阵分解(NMF)。

随机投影是将高维数据投影到低维空间的方法,以便保留数据点之间的距离。其随机性使得能够在并行计算机上更容易运行。它还允许算法使用更少的内存,因为它不需要像 PCA 那样同时在内存中保存所有数据。并且由于它的计算复杂度较低,随机投影在处理维度非常高的数据集时可以偶尔使用,尤其是在分解速度成为重要因素时。

类似地,NMF 是另一种矩阵因式分解方法,类似于 SVD,但假设数据点和成分都是非负的。它在图像处理和计算机视觉中更常见,但在自然语言处理和主题建模中偶尔也很有用。

在大多数情况下,最好坚持使用 LSA,它在内部使用经过试验的 SVD 算法

自然语言处理实战第二版(MEAP)(二)(5)https://developer.aliyun.com/article/1517859

相关文章
|
18天前
|
机器学习/深度学习 人工智能 自然语言处理
Python自然语言处理实战:文本分类与情感分析
本文探讨了自然语言处理中的文本分类和情感分析技术,阐述了基本概念、流程,并通过Python示例展示了Scikit-learn和transformers库的应用。面对多义性理解等挑战,研究者正探索跨域适应、上下文理解和多模态融合等方法。随着深度学习的发展,这些技术将持续推动人机交互的进步。
20 1
|
20天前
|
自然语言处理 监控 数据挖掘
NLP实战:Python中的情感分析
6月更文挑战第6天
35 2
|
4天前
|
机器学习/深度学习 数据采集 人工智能
Python 高级实战:基于自然语言处理的情感分析系统
**摘要:** 本文介绍了基于Python的情感分析系统,涵盖了从数据准备到模型构建的全过程。首先,讲解了如何安装Python及必需的NLP库,如nltk、sklearn、pandas和matplotlib。接着,通过抓取IMDb电影评论数据并进行预处理,构建情感分析模型。文中使用了VADER库进行基本的情感分类,并展示了如何使用`LogisticRegression`构建机器学习模型以提高分析精度。最后,提到了如何将模型部署为实时Web服务。本文旨在帮助读者提升在NLP和情感分析领域的实践技能。
14 0
|
1月前
|
自然语言处理 API 数据库
自然语言处理实战第二版(MEAP)(六)(5)
自然语言处理实战第二版(MEAP)(六)
30 3
|
1月前
|
机器学习/深度学习 自然语言处理 机器人
自然语言处理实战第二版(MEAP)(六)(4)
自然语言处理实战第二版(MEAP)(六)
27 2
|
1月前
|
机器学习/深度学习 人工智能 自然语言处理
自然语言处理实战第二版(MEAP)(六)(2)
自然语言处理实战第二版(MEAP)(六)
28 2
|
1月前
|
机器学习/深度学习 自然语言处理 机器人
自然语言处理实战第二版(MEAP)(六)(3)
自然语言处理实战第二版(MEAP)(六)
29 1
|
17天前
|
机器学习/深度学习 自然语言处理 PyTorch
【从零开始学习深度学习】48.Pytorch_NLP实战案例:如何使用预训练的词向量模型求近义词和类比词
【从零开始学习深度学习】48.Pytorch_NLP实战案例:如何使用预训练的词向量模型求近义词和类比词
|
20天前
|
机器学习/深度学习 人工智能 自然语言处理
探索未来AI技术的前沿——自然语言处理的发展与应用
本文将深入探讨自然语言处理技术在人工智能领域中的重要性和应用前景。通过分析当前自然语言处理技术的发展趋势和实际应用案例,揭示了其在改善用户体验、提升工作效率以及推动产业创新方面的巨大潜力。
|
21天前
|
自然语言处理 前端开发 Java
探索自然语言生成技术的进展与应用
本文将介绍自然语言生成技术在不同领域的进展和应用。从前端到后端,从Java到Python,从C到PHP,从Go到数据库,我们将深入探讨这些技术的发展趋势和应用场景,并展示它们在实际项目中的价值。