自然语言处理实战第二版(MEAP)(二)(4)https://developer.aliyun.com/article/1517855
4.5 潜在狄利克雷分配(LDiA)
在本章的大部分时间里,你已经学习了关于潜在语义分析以及使用 Scikit-Learn 将单词和短语的潜在含义表示为向量的各种方法。LSA 应该是大多数主题建模、语义搜索或基于内容的推荐引擎的首选^([18])。它的数学是简单而高效的,并且它产生的线性转换可以应用到新的自然语言批次中,而无需训练,且准确度损失很小。在这里,你将了解一个更复杂的算法,潜在狄利克雷分配,或称为"LDiA"以区别于 LDA,即线性判别分析。在某些情况下,LDiA 将会给你略微更好的结果。
LDiA 做了很多与使用 LSA(和底层的 SVD)创建主题模型相似的事情,但与 LSA 不同的是,LDiA 假设单词频率呈狄利克雷分布。它在将单词分配给主题的统计学方面比 LSA 的线性数学更精确。
LDiA 创建了一个语义向量空间模型(类似于你的主题向量),使用的方法类似于本章早些时候的思维实验中你的大脑是如何工作的。在你的思维实验中,你根据它们在同一篇文档中出现的频率手动将单词分配给主题。然后,文档的主题混合可以通过每个主题中单词混合来确定,这些单词被分配到哪个主题。这使得 LDiA 主题模型更容易理解,因为分配给主题的单词和分配给文档的主题倾向于比 LSA 更有意义。
LDiA 假设每个文档都是一些任意数量的主题(线性组合)的混合,你在开始训练 LDiA 模型时选择这些主题。LDiA 还假设每个主题可以由单词(术语频率)的分布表示。每个文档中这些主题的概率或权重,以及单词被分配给主题的概率,都假设从一个狄利克雷概率分布开始(如果你记得你的统计学,这是先验)。这就是算法得到它名字的地方。
4.5.1 LDiA 的概念
LDiA 方法是在 2000 年由英国的遗传学家们开发的,以帮助他们从基因序列中“推断人群结构”^([19])。斯坦福大学的研究人员(包括安德鲁·吴)于 2003 年将该方法推广应用于 NLP^([20])。但不要被提出这种方法的大人物吓到。我们很快就会用几行 Python 解释它的要点。你只需要理解足够多,以便对它正在做的事情有所感觉(直觉),这样你就知道你可以在管道中使用它做什么。
Blei 和 Ng 通过颠覆您的思维实验的想法提出了这个想法。 他们想象一台机器,除了掷骰子(生成随机数)之外无能为力,可以写出您想要分析的语料库中的文档。 由于您只处理词袋,他们取消了将这些单词组合在一起以产生意义的部分,以编写真实文档的部分。 他们只是模拟了成为每个文档 BOW 一部分的单词混合的统计数据。
他们想象了一台机器,只需做出两个选择,就可以开始生成特定文档的单词混合。 他们想象,文档生成器随机选择这些单词,具有某种可能的选择概率分布,就像选择骰子的面数和要添加到一起以创建 D&D 角色表的骰子的组合一样。 您的文档“角色表”只需要两次掷骰子。 但是骰子很大,而且有几个,关于如何将它们组合在一起以产生您想要的不同值的所需概率的复杂规则。 您希望特定的概率分布适用于单词数量和主题数量,以便它与人类分析的真实文档中的这些值的分布相匹配。
两次掷骰子代表:
- 用于文档生成的单词数量(Poisson 分布)
- 用于文档混合的主题数量(Dirichlet 分布)
一旦它有了这两个数字,困难的部分就开始了,选择文档的单词。 想象的 BOW 生成机器在那些主题上迭代,并随机选择适合该主题的单词,直到它达到了在第 1 步中决定文档应该包含的单词数量。 决定这些单词对主题的概率-单词对每个主题的适当性-是困难的部分。 但是一旦确定了这一点,您的“机器人”只需从术语-主题概率矩阵中查找每个主题的单词的概率。 如果您忘记了该矩阵的外观,请回顾一下本章早些时候的简单示例。
因此,这台机器所需的一切就是用于 Poisson 分布(在第 1 步的骰子投掷中)的单个参数,该参数告诉它应该是什么“平均”文档长度,以及另外几个参数来定义设置主题数量的 Dirichlet 分布。 然后,您的文档生成算法需要一个术语-主题矩阵,其中包含其喜欢使用的所有单词和主题,即其词汇表。 它还需要一种它喜欢“谈论”的主题混合。
现在我们将文档生成(写作)问题反过来,回到你最初的问题,即从现有文档中估计主题和单词。您需要测量或计算前两步的有关单词和主题的参数。然后,您需要从一组文档中计算出术语-主题矩阵。这就是 LDiA 的作用。
Blei 和 Ng 意识到,可以通过分析语料库中文档的统计数据来确定步骤 1 和步骤 2 的参数。例如,针对步骤 1,他们可以计算他们语料库中所有文档的单词(或 n-grams)袋子中的平均数量,类似于这样:
>>> total_corpus_len = 0 >>> for document_text in comments.text: ... total_corpus_len += len(spacy_tokenize(document_text)) >>> mean_document_len = total_corpus_len / len(sms) >>> round(mean_document_len, 2) 21.35
或者,使用 sum 函数:
>>> sum([len(spacy_tokenize(t)) for t in comments.text] ... ) * 1\. / len(comments.text) 21.35
请注意,您应直接从 BOW 中计算此统计数据。您需要确保计算您文档中的标记化和向量化单词。在计算您的唯一术语之前,请确保您已经应用了任何停用词过滤或其他标准化。这样,您的计数将包括您的 BOW 向量词汇表中的所有单词(您正在计算的全部 n-grams),但仅包括您的 BOWs 使用的单词(例如不包括停用词)。与将 TF-IDF 矩阵作为输入的 LSA 不同,此 LDiA 算法依赖于词袋向量空间模型。
对于 LDiA 模型的第二个需要指定的参数——主题数——有点棘手。在分配单词到这些主题之后,您才能直接测量特定文档集中的主题数。与 k-means 和 KNN 等其他聚类算法一样,您必须事先告诉它 k。您可以猜测主题数(类似于 k-means 中的 k,即“簇”的数量),然后检查它是否适用于文档集。告诉 LDiA 要查找多少个主题后,它将找到每个主题中要放入的单词组合,以优化其目标函数。
您可以通过调整“超参数”(主题数 k)来优化此参数,直到适合您的应用程序为止。如果您可以衡量 LDiA 语言模型在表示文档含义方面的质量的某些方面,则可以自动化此优化。您可以使用一些分类或回归问题(如情感分析、文档关键字标记或主题分析)中 LDiA 模型的执行情况作为此优化的 “成本函数”。您只需要一些标记过的文档来测试您的主题模型或分类器。
4.5.2 评论的 LDiA 主题模型
LDiA 产生的主题更易于人理解和“解释”。这是因为经常一起出现的单词被分配到相同的主题,而人们期望是这种情况。LSA 尝试保持原本分开的事物的分散程度,而 LDiA 则试图保持原本在一起的事物的接近程度。
这听起来可能是相同的事情,但它并不是。数学优化不同。你的优化器有不同的目标函数,因此它将达到不同的目标。为了保持高维向量在低维空间中靠得很近,LDiA 必须以非线性的方式扭曲和变形空间(和向量)。这是一个难以想象的事情,除非你在 3D 物体上做了这个操作,并在 2D 中取“投影”的结果向量。
让我们看看如何将一个标记为垃圾邮件的几千个评论的数据集应用于此。首先计算 TF-IDF 向量,然后为每个短信信息(文档)计算一些主题向量。与先前一样,我们假设使用仅 16 个主题(组件)来分类信息的垃圾邮件。保持主题数量(维度)较低可以有助于减少过拟合。^([23])
LDiA 使用的是原始 BOW 计数向量而不是归一化的 TF-IDF 向量。你已经在第三章中完成了这个过程:
>>> from sklearn.feature_extraction.text import CountVectorizer >>> >>> counter = CountVectorizer(tokenizer=spacy_tokenize) >>> bow_docs = pd.DataFrame(counter.fit_transform( raw_documents=comments.text)\ ... .toarray(), index=index) >>> column_nums, terms = zip(*sorted(zip(counter.vocabulary_.values(), ... counter.vocabulary_.keys()))) >>> bow_docs.columns = terms
让我们仔细检查一下第一个带有“comment0”标签的评论的计数是否正确:
>>> comments.loc['comment0'].text 'you have yet to identify where my edits violated policy. 4 july 2005 02:58 (utc)' >>> bow_docs.loc['comment0'][bow_docs.loc['comment0'] > 0].head() 1 ( 1 ) 1 . 1 02:58 1 Name: comment0, dtype: int64
我们将在计数向量矩阵上应用潜在狄利克雷分配,方式与我们在 TF-IDF 矩阵上应用 LSA 相同:
>>> from sklearn.decomposition import LatentDirichletAllocation as LDiA >>> ldia = LDiA(n_components=16, learning_method='batch') >>> ldia = ldia.fit(bow_docs) # #1 >>> ldia.components_.shape (16, 19169)
因此,你的模型已经将 19,169 个单词(术语)分配到了 16 个主题(组件)。让我们看一下前几个单词及其分配情况。请记住,你的计数和主题与我们不同。LDiA 是一种依赖于随机数生成器进行某些关于将单词分配给主题的统计决策的随机算法。因此,每次运行sklearn.LatentDirichletAllocation
(或任何 LDiA 算法)时,除非你将随机种子设置为固定值,否则你将得到不同的结果。
>>> pd.set_option('display.width', 75) >>> term_topic_matrix = pd.DataFrame(ldia.components_, index=terms,\ ... columns=columns) # #1 >>> term_topic_matrix.round(2).head(3) topic0 topic1 ... topic14 topic15 a 21.853 0.063 ... 0.063 922.515 aaaaaaaaaahhhhhhhhhhhhhh 0.063 0.063 ... 0.063 0.063 aalst 0.063 0.063 ... 0.063 0.063 aap 0.063 0.063 ... 2.062 0.062
看起来 LDiA 主题向量中的值的分布比 LSA 主题向量中的值要高得多-有很多接近零的值,但也有一些非常大的值。让我们做与 LSA 进行主题建模时所做的相同技巧。我们可以查看典型的“有毒”词语,并查看它们在每个主题中的显著程度。
>>> toxic_terms= components.loc['pathetic crazy stupid lazy idiot hate die kill'.split()].round(2) >>> toxic_terms topic0 topic1 topic2 ... topic13 topic14 topic15 pathetic 1.06 0.06 32.35 ... 0.06 0.06 9.47 crazy 0.06 0.06 3.82 ... 1.17 0.06 0.06 stupid 0.98 0.06 4.58 ... 8.29 0.06 35.80 lazy 0.06 0.06 1.34 ... 0.06 0.06 3.97 idiot 0.06 0.06 6.31 ... 0.06 1.11 9.91 hate 0.06 0.06 0.06 ... 0.06 480.06 0.06 die 0.06 0.06 26.17 ... 0.06 0.06 0.06 kill 0.06 4.06 0.06 ... 0.06 0.06 0.06
这与我们有毒术语的 LSA 表示非常不同!似乎某些术语在某些主题中具有高的主题词权重,但在其他主题中没有。topic0
和topic1
对有毒术语似乎非常“冷淡”,而topic2
和topic15
至少对 4 或 5 个有毒术语具有很大的主题词权重。而topic14
对词语hate
的权重非常高!
让我们看看这个主题中的其他高分词。正如你之前看到的,因为我们没有对数据集进行任何预处理,很多术语并不是很有趣。让我们关注单词,而且长度大于 3 个字母的术语-这将消除很多停用词。
>>> non_trivial_terms = [term for term in components.index if term.isalpha() and len(term)>3] components.topic14.loc[non_trivial_terms].sort_values(ascending=False)[:10] hate 480.062500 killed 14.032799 explosion 7.062500 witch 7.033359 june 6.676174 wicked 5.062500 dead 3.920518 years 3.596520 wake 3.062500 arrived 3.062500
看起来主题中的许多词之间有语义关系。像"killed"和"hate",或者"wicked"和"witch"这样的词,似乎属于"toxic"领域。您可以看到词语分配到主题的方式是可以理解或推理的,即使只是一个快速的看一眼。
在您拟合分类器之前,您需要计算所有文档(评论)的这些 LDiA 主题向量。让我们看看它们与相同文档的 LSA 产生的主题向量有什么不同。
>>> ldia16_topic_vectors = ldia.transform(bow_docs) >>> ldia16_topic_vectors = pd.DataFrame(ldia16_topic_vectors,\ ... index=index, columns=columns) >>> ldia16_topic_vectors.round(2).head() topic0 topic1 topic2 ... topic13 topic14 topic15 comment0 0.0 0.0 0.00 ... 0.00 0.0 0.0 comment1 0.0 0.0 0.28 ... 0.00 0.0 0.0 comment2 0.0 0.0 0.00 ... 0.00 0.0 0.0 comment3 0.0 0.0 0.00 ... 0.95 0.0 0.0 comment4! 0.0 0.0 0.07 ... 0.00 0.0 0.0
您可以看到这些主题更清晰地分开了。在您将主题分配给消息时有很多零。这是 LDiA 主题更容易向同事解释的一点,这样他们就可以基于您的 NLP 管道结果做出商业决策。
那么 LDiA 主题对人类来说效果很好,但是对机器呢?你的 LDA 分类器在这些主题下会有怎样的表现?
4.5.3 使用 LDiA 检测毒性
让我们看看这些 LDiA 主题在预测一些有用的东西,比如评论毒性方面有多好。您将再次使用 LDiA 主题向量来训练一个 LDA 模型(就像您两次使用 TF-IDF 向量和 LSA 主题向量那样)。由于您在列表 4.5 中定义的方便函数,您只需要几行代码来评估您的模型:
>>> model_ldia16 = LinearDiscriminantAnalysis() >>> ldia16_performance=evaluate_model(ldia16_topic_vectors, comments.toxic,model_ldia16, 'LDA', 'LDIA (16 components)') >>> hparam_table = hparam_table.append(ldia16_performance, ... ignore_index = True) >>> hparam_table.T 0 1 2 classifier LDA LDA LDA features tf-idf (spacy tokenizer) LSA (16d) LDIA (16d) train_accuracy 0.99 0.8808 0.8688 test_accuracy 0.554 0.88 0.8616 test_precision 0.383 0.6 0.388889 test_recall 0.12 0.239264 0.107362 test_f1 0.183 0.342105 0.168269
看起来,在 16 个主题 LDIA 向量上的分类性能比没有主题建模的原始 TF-IDF 向量要差。这是否意味着在这种情况下 LDiA 是无用的?让我们不要太早放弃它,试着增加主题数量。
4.5.4 更公平的比较:32 个 LDiA 主题
让我们再试一次,用更多的维度,更多的主题。也许 LDiA 不像 LSA 那样有效,所以它需要更多的主题来分配词。让我们试试 32 个主题(组件)。
>>> ldia32 = LDiA(n_components=32, learning_method='batch') >>> ldia32 = ldia32.fit(bow_docs) >>> model_ldia32 = LinearDiscriminantAnalysis() >>> ldia32_performance =evaluate_model(ldia32_topic_vectors, ... comments.toxic, model_ldia32, 'LDA', 'LDIA (32d)') >>> hparam_table = hparam_table.append(ldia32_performance, ... ignore_index = True) >>> hparam_table.T 0 1 2 3 classifier LDA LDA LDA LDA features tf-idf (spacy tokenizer) LSA (16d) LDIA (16d) LDIA (32d) train_accuracy 0.99 0.8808 0.8688 0.8776 test_accuracy 0.554 0.88 0.8616 0.8796 test_precision 0.383 0.6 0.388889 0.619048 test_recall 0.12 0.239264 0.107362 0.199387 test_f1 0.183 0.342105 0.168269 0.301624
很好!增加 LDIA 的维度几乎使模型的精确度和召回率翻了一番,我们的 F1 得分看起来好多了。更多的主题使 LDIA 在主题上更加精确,并且,至少对于这个数据集来说,产生了更好地线性分离主题的主题。但是这些向量表示的性能仍然不及 LSA。所以 LSA 让你的评论主题向量更有效地分散,允许在用超平面分隔类别时有更大的间隙。
随意探索 Scikit-Learn 和gensim
中提供的狄利克雷分配模型的源代码。它们具有与 LSA(sklearn.TruncatedSVD
和gensim.LsiModel
)类似的 API。我们将在后面的章节中讨论总结时向您展示一个示例应用程序。找到可解释的主题,比如用于总结的主题,是 LDiA 擅长的。而且它在创建用于线性分类的主题方面也不错。
提示
您之前看到了如何从文档页面浏览所有“sklearn”的源代码。但是,您甚至可以通过 Python 控制台更简单地执行此操作。您可以在任何 Python 模块上找到__file__
属性中的源代码路径,例如sklearn.__file__
。在ipython
(jupyter console
)中,您可以使用??
查看任何函数、类或对象的源代码,例如LDA??
:
>>> import sklearn >>> sklearn.__file__ '/Users/hobs/anaconda3/envs/conda_env_nlpia/lib/python3.6/site-packages/skl earn/__init__.py' >>> from sklearn.discriminant_analysis\ ... import LinearDiscriminantAnalysis as LDA >>> LDA?? Init signature: LDA(solver='svd', shrinkage=None, priors=None, n_components =None, store_covariance=False, tol=0.0001) Source: class LinearDiscriminantAnalysis(BaseEstimator, LinearClassifierMixin, TransformerMixin): """Linear Discriminant Analysis A classifier with a linear decision boundary, generated by fitting class conditional densities to the data and using Bayes' rule. The model fits a Gaussian density to each class, assuming that all classes share the same covariance matrix.""" ...
这对于函数和类的扩展不起作用,其源代码隐藏在编译后的 C++ 模块中。
4.6 距离和相似度
我们需要重新审视第二章和第三章中谈到的那些相似性分数,以确保您的新主题向量空间与它们配合良好。请记住,您可以使用相似性分数(和距离)来根据您用于表示它们的向量的相似性(或距离)来判断两个文档的相似程度或相距多远。
您可以使用相似性分数(和距离)来查看您的 LSA 主题模型与第三章中更高维度的 TF-IDF 模型的一致性。您将看到您的模型在消除了许多包含在更高维度词袋中的信息后保留了多少距离。您可以检查主题向量之间的距离以及是否这是文档主题之间距离的良好表示。您希望检查意思相似的文档是否在您的新主题向量空间中彼此靠近。
LSA 保留了较大的距离,但并不总是保留较近的距离(您的文档之间关系的细微“结构”)。底层的 SVD 算法旨在最大化新主题向量空间中所有文档之间的方差。
特征向量(词向量、主题向量、文档上下文向量等)之间的距离驱动着 NLP 流水线或任何机器学习流水线的性能。那么,在高维空间中测量距离的选择有哪些呢?对于特定的 NLP 问题,应该选择哪些呢?其中一些常用的例子可能在几何课程或线性代数中很熟悉,但许多其他例子可能对您来说是新的:
- 欧几里得距离或笛卡尔距离,或均方根误差(RMSE):2-范数或 L[2]
- 平方欧几里得距离,平方和距离(SSD):L[2]²
- 余弦距离或角距离或投影距离:归一化点积
- 闵可夫斯基距离:p-范数或 L[p]
- 分数距离,分数范数:p-范数或 L[p],其中
0 < p < 1
- 城市街区距离,曼哈顿距离或出租车距离,绝对距离之和(SAD):1-范数或 L[1]
- Jaccard 距离,逆集相似度
- 马氏距离
- Levenshtein 或编辑距离
计算距离的多种方法表明了它的重要性。除了 Scikit-Learn 中的成对距离实现之外,还有许多其他方法在数学专业中被使用,如拓扑学、统计学和工程学。^([24]) 供参考,以下是在sklearn.metrics
模块中可以计算距离的所有方法:^([25])
列出 4.8 sklearn
中可用的成对距离
'cityblock', 'cosine', 'euclidean', 'l1', 'l2', 'manhattan', 'braycurtis', 'canberra', 'chebyshev', 'correlation', 'dice', 'hamming', 'jaccard', 'kulsinski', 'mahalanobis', 'matching', 'minkowski', 'rogerstanimoto', 'russellrao', 'seuclidean', 'sokalmichener', 'sokalsneath', 'sqeuclidean', 'yule'
距离度量通常是从相似度度量(分数)计算的,反之亦然,以便距离与相似度分数成反比。相似度分数的设计范围在 0 到 1 之间。典型的转换公式如下:
>>> similarity = 1\. / (1\. + distance) >>> distance = (1\. / similarity) - 1.
但是对于范围在 0 到 1 之间的距离和相似度分数,例如概率,更常见的是使用以下公式:
>>> similarity = 1\. - distance >>> distance = 1\. - similarity
余弦距离具有其自己的值范围约定。两个向量之间的角距离通常被计算为两个向量之间可能的最大角度分离的一部分,即pi
弧度或 180 度。^([26]) 因此,余弦相似度和距离是彼此的倒数:
>>> import math >>> angular_distance = math.acos(cosine_similarity) / math.pi >>> distance = 1\. / similarity - 1. >>> similarity = 1\. - distance
为什么我们要花这么多时间谈论距离呢?在本书的最后一节中,我们将讨论语义搜索。语义搜索的理念是找到与您的搜索查询具有最高语义相似性或最低语义距离的文档。在我们的语义搜索应用中,我们将使用余弦相似度 - 但正如您在最后两页中所看到的,有多种方法来衡量文档的相似程度。
4.7 带反馈的引导
所有先前的语义分析方法都未考虑文档之间相似性的信息。我们创建了一个适用于一般规则的最佳主题。我们对这些模型进行了无监督学习以提取特征(主题),没有关于主题向量应该彼此有多“接近”的数据。我们不允许任何关于主题向量最终位置或它们彼此之间的关系的“反馈”。
舵机或“学习距离度量”^([27])是降维和特征提取的最新进展。通过调整报告给聚类和嵌入算法的距离分数,您可以“引导”您的向量,使它们最小化某些成本函数。通过这种方式,您可以强制您的向量专注于您感兴趣的信息内容的某些方面。
在关于 LSA 的先前章节中,您忽略了关于您的文档的所有元信息。例如,对于您忽略了消息发送者的评论。这是主题相似性的一个很好的指标,可以用来通知您的主题向量转换(LSA)。
在 Talentpair,我们尝试使用每个文档的主题向量之间的余弦距离将简历与工作描述相匹配。这样做效果还不错。但我们很快发现,当我们开始根据候选人和负责帮助他们找工作的客户经理的反馈来“引导”我们的主题向量时,我们得到了更好的结果。与“好配对”的向量比其他配对的向量更加接近。
一种做法是计算你两个质心之间的平均差异(就像你为 LDA 做的那样),并将这种“偏差”的一部分添加到所有简历或工作描述向量中。这样做应该可以消除简历和工作描述之间的平均主题向量差异。工作描述中可能会出现的主题,如午餐时的生啤可能永远不会出现在简历中。类似地,一些简历中可能会出现奇特的爱好,如水下雕塑,但从不会出现在工作描述中。引导你的主题向量可以帮助你将它们聚焦在你感兴趣建模的主题上。
4.8 主题向量功效
借助主题向量,你可以比较单词、文档、语句和语料库的含义。你可以找到相似文档和语句的“聚类”。你不再只根据单词的使用情况来比较文档之间的距离。你不再局限于基于词语选择或词汇的关键字搜索和相关性排名。你现在可以找到与你的查询相关的文档,而不仅仅是与单词统计本身匹配的文档。
这被称为“语义搜索”,不要与“语义网”混淆。^([28]) 当强大的搜索引擎给你提供一些不包含查询中很多单词的文档时,这就是语义搜索,但这些文档正是你正在寻找的内容。这些先进的搜索引擎使用 LSA 主题向量来区分“The Cheese Shop”的Python
包与佛罗里达宠物店水族馆中的一只蟒蛇,同时还能识别其与“Ruby gem”的相似性。^([29])
语义搜索为您提供了一种查找和生成有意义的文本的工具。但是我们的大脑不擅长处理高维对象、向量、超平面、超球面和超立方体。我们作为开发者和机器学习工程师的直觉在三个以上的维度上崩溃。
例如,在 Google 地图上进行 2D 向量查询,比如您的纬度/经度位置,您可以很快找到附近的所有咖啡店而无需进行太多的搜索。您可以使用肉眼或代码进行扫描,沿着搜索外螺旋向外扩展。或者,您可以使用代码创建越来越大的边界框,检查每个边界框上的经度和纬度是否在某个范围内,这仅用于比较操作,并应该找到附近的所有东西。
然而,用超平面和超立方体作为搜索的边界来分割高维向量空间(超空间)是不切实际的,在许多情况下是不可能的。
正如 Geoffry Hinton 所说:“在一个 14 维空间中处理超平面,将一个 3D 空间可视化,然后对自己说 14。”如果你年轻且容易受影响时读过 Abbott 1884 年的《Flatland》,你可能能比这种手势更好理解。“Flatland”中,你用了很多二维可视化来帮助你探索单词在超空间中在你的三维世界中留下的影子。如果你急于查看它们,请跳到显示单词向量的“散点矩阵”部分。你可能还想回顾一下上一章中的三维词袋向量,并尝试想象一下,如果你再增加一个词汇来创建一个四维的语义世界,那些点会是什么样子。
如果你在深思四维空间的事情,必须要记住,你试图理解的复杂性爆炸比从二维到三维的复杂性增长要大,而且是指数级别大于从数字的一维世界到三角形、正方形和圆形的二维世界的复杂性增长。
4.8.1 语义搜索
当你根据文档中包含的单词或部分单词搜索文档时,这被称为全文搜索。这就是搜索引擎的工作原理。它们将文档分成可以使用反向索引索引的块(通常是单词),就像你在教科书后面找到的索引一样。处理拼写错误和打字错误需要大量的簿记和猜测,但效果还不错。^([30])
语义搜索是全文搜索,它考虑了查询中的单词和被搜索的文档的含义。在本章中,你学会了两种方法——LSA 和 LDiA——来计算捕捉单词和文档语义(意义)的主题向量。潜在语义分析首先被称为潜在语义索引的原因之一是因为它承诺以数字值的索引(如 BOW 和 TF-IDF 表)来帮助语义搜索。语义搜索是信息检索领域的下一个重大突破。
但与 BOW 和 TF-IDF 表不同,语义向量表不能使用传统的倒排索引技术轻松离散化和索引。传统的索引方法适用于二进制单词出现向量、离散向量(BOW 向量)、稀疏连续向量(TF-IDF 向量)和低维连续向量(3D GIS 数据)。但高维连续向量,如 LSA 或 LDiA 的主题向量,是一个挑战。倒排索引适用于离散向量或二进制向量,例如二进制或整数词-文档向量表,因为索引只需要为每个非零离散维度维护一个条目。该维度的值在引用的向量或文档中存在或不存在。由于 TF-IDF 向量是稀疏的,大多数为零,您不需要为大多数文档的大多数维度在索引中添加条目。
LSA(和 LDiA)产生高维、连续且密集的主题向量(零很少)。语义分析算法不会产生可扩展搜索的高效索引。事实上,你在前一节谈到的维度诅咒使得精确索引成为不可能。潜在语义索引的“索引”部分是一种希望,而不是现实,因此 LSI 术语是一个误称。也许这就是为什么 LSA 已成为描述产生主题向量的语义分析算法的更流行方式。
解决高维向量挑战的一种方法是使用局部敏感哈希(LSH)对其进行索引。局部敏感哈希就像一个邮政编码,指定了一个超空间区域,以便稍后可以轻松找到。而且像常规哈希一样,它是离散的,仅取决于向量中的值。但即使如此,一旦超过约 12 个维度,这也不会完美地工作。在图 4.6 中,每行代表一个主题向量大小(维度),从 2 维开始,一直到 16 维,就像您之前用于短信垃圾邮件问题的向量一样。
图 4.5 语义搜索准确性在约 12-D 处下降
表格显示了如果您使用局部敏感哈希对大量语义向量进行索引,您的搜索结果将有多好。一旦您的向量超过 16 维,您将很难返回任何好的 2 个搜索结果。
那么,如何在 100 维向量上进行语义搜索而不使用索引呢?现在你知道如何使用 LSA 将查询字符串转换为主题向量。你也知道如何使用余弦相似度分数(标量乘积、内积或点积)来比较两个向量的相似性,以找到最接近的匹配项。要找到精确的语义匹配项,你需要找到与特定查询(搜索)主题向量最接近的所有文档主题向量(在专业术语中,它被称为穷举搜索)。但是如果你有n个文档,你必须对你的查询主题向量进行n次比较。这是很多点积。
你可以使用矩阵乘法在numpy
中对操作进行向量化,但这并不会减少操作次数,只会使其快 100 倍。^([33]) 从根本上讲,精确的语义搜索仍然需要对每个查询进行O(N)次乘法和加法运算。因此,它的规模只会随着语料库的大小呈线性增长。这对于大型语料库,比如谷歌搜索或者维基百科语义搜索来说是行不通的。
关键是要接受“足够好”的结果,而不是为我们的高维向量追求完美的索引或 LSH 算法。现在有几种开源实现了一些高效准确的近似最近邻算法,它们使用 LSH 来有效地实现语义搜索。我们将在第十章中进一步讨论它们。
从技术上讲,这些索引或哈希解决方案不能保证您将为您的语义搜索查询找到所有最佳匹配项。但是,如果你愿意放弃一点精度,它们可以几乎与 TF-IDF 向量或词袋向量上的传统反向索引一样快地为你提供一个良好的近似匹配项列表。^([34])
4.9 为你的机器人配备语义搜索
让我们利用你在主题建模方面新获得的知识来改进你在上一章中开始构建的机器人。我们将专注于相同的任务 - 问答。
我们的代码实际上会与第三章中的你的代码非常相似。我们仍然会使用向量表示来找到数据集中最相似的问题。但是这次,我们的表示将更接近于表示这些问题的含义。
首先,让我们像上一章那样加载问题和答案数据
>>> REPO_URL = 'https://gitlab.com/tangibleai/qary/-/raw/master' >>> FAQ_DIR = 'src/qary/data/faq' >>> FAQ_FILENAME = 'short-faqs.csv' >>> DS_FAQ_URL = '/'.join([REPO_URL, FAQ_DIR, FAQ_FILENAME]) >>> df = pd.read_csv(DS_FAQ_URL)
下一步是将问题和我们的查询都表示为向量。这就是我们需要添加一些内容来使我们的表示具有语义的地方。因为我们的问题数据集很小,所以我们不需要应用 LSH 或任何其他索引算法。
>>> vectorizer = TfidfVectorizer() >>> vectorizer.fit(df['question']) >>> tfidfvectors = vectorizer.transform(df['question']) >>> svd = TruncatedSVD(n_components=16, n_iterations=100) >>> tfidfvectors_16d = svd.fit_transform(tfidfvectors) >>> >>> def bot_reply(question): ... question_tfidf = vectorizer.transform([question]).todense() ... question_16d = svd.transform(question_tfidf) ... idx = question_16d.dot(tfidfvectors_16d.T).argmax() ... print( ... f"Your question:\n {question}\n\n" ... f"Most similar FAQ question:\n {df['question'][idx]}\n\n" ... f"Answer to that FAQ question:\n {df['answer'][idx]}\n\n" ... )
让我们对我们的模型进行健全性检查,确保它仍然能够回答简单的问题:
>>> bot_reply("What's overfitting a model?") Your question: What's overfitting a model? Most similar FAQ question: What is overfitting? Answer to that FAQ question: When your test set accuracy is significantly lower than your training set accuracy.
现在,让我们给我们的模型一个更难的问题 - 就像我们之前的模型处理不好的那个问题一样。它能做得更好吗?
>>> bot_reply("How do I decrease overfitting for Logistic Regression?") Your question: How do I decrease overfitting for Logistic Regression? Most similar FAQ question: How to reduce overfitting and improve test set accuracy for a LogisticRegression model? Answer to that FAQ question: Decrease the C value, this increases the regularization strength.
哇!看起来我们的新版本机器人能够“意识到”'decrease’和’reduce’有相似的含义。不仅如此,它还能“理解”'Logistic Regression’和“LogisticRegression”非常接近 - 对于我们的 TF-IDF 模型来说,这样简单的步骤几乎是不可能的。
看起来我们正在接近建立一个真正健壮的问答系统。在下一章中,我们将看到如何做得比主题建模更好!
4.10 接下来是什么?
在接下来的章节中,您将学习如何微调主题向量的概念,以便与单词相关联的向量更加精确和有用。为此,我们首先开始学习神经网络。这将提高您的管道从短文本甚至孤立单词中提取含义的能力。
4.11 自我测试
- 为了更高效地使用 LDiA 进行主题建模,您会使用哪些预处理技术?LSA 呢?
- 您能想到一个数据集/问题,TF-IDF 表现比 LSA 更好吗?相反呢?
- 我们提到过过滤停用词作为 LDiA 的预处理过程。在什么情况下,这种过滤会有益处?
- 语义搜索的主要挑战是,密集的 LSA 主题向量无法逆向索引。你能解释为什么吗?
4.12 总结
- 您可以通过分析数据集中术语的共现来推导您的单词和文档的含义。
- SVD 可用于语义分析,将 TF-IDF 和 BOW 向量分解和转换为主题向量。
- 超参数表可用于比较不同管道和模型的性能。
- 当您需要进行可解释的主题分析时,请使用 LDiA。
- 无论您如何创建主题向量,都可以利用语义搜索来查找基于其含义的文档。
在这一章关于主题分析中,我们使用术语“主题向量”,在第六章关于 Word2vec 中,我们使用术语“词向量”。像 Jurafsky 和 Martin 的《NLP 圣经》(web.stanford.edu/~jurafsky/slp3/ed3book.pdf#chapter.15:
)这样的正式 NLP 文本使用“主题向量”。其他人,比如《语义向量编码和相似性搜索》的作者(arxiv.org/pdf/1706.00957.pdf:
),则使用“语义向量”一词。
短语还是词元化都会去除或改变单词的词尾和前缀,即单词的最后几个字符。编辑距离计算更适合识别拼写相似(或拼写错误)的单词。
我喜欢用 Google Ngram Viewer 可视化趋势,比如这个:(mng.bz/ZoyA
)。
斯坦福的 Doug Lenat 正在尝试将常识编码到算法中。请参阅《Wired Magazine》文章《Doug Lenat’s Artificial Intelligence Common Sense Engine》(www.wired.com/2016/03/doug-lenat-artificial-intelligence-common-sense-engine
)。
[5] 语素 是一个单词的最小有意义的部分。参见维基百科上的“语素”文章(en.wikipedia.org/wiki/Morpheme
)。
[6] 主题模型的维基百科页面有一个视频,展示了 LSA 背后的直觉。mng.bz/VRYW
[7] 这个数据集的较大版本是 2017 年 Kaggle 竞赛的基础(www.kaggle.com/c/jigsaw-toxic-comment-classification-challenge
),由 Jigsaw 在 CC0 许可下发布。
[8] 簇的质心是一个点,其坐标是该簇中所有点的坐标的平均值。
[9] 要对精确率和召回率有更直观的了解,可以参考维基百科的文章(en.wikipedia.org/wiki/Precision_and_recall
),其中有一些良好的可视化。
[10] 您可以阅读关于在某些情况下不使用 F [1]分数以及替代指标的维基百科文章:en.wikipedia.org/wiki/F-score
[11] 您可以在 Scikit-Learn 文档中看到两个估算器的视觉示例:scikit-learn.org/dev/modules/lda_qda.html
[12] 要更深入地了解降维,可以查看 Hussein Abdullatif 的这篇四部曲文章系列:mng.bz/RlRv
[13] 实际上有两种主要的 PCA 执行方法;您可以查看 PCA 的维基百科文章(en.wikipedia.org/wiki/Principal_component_analysis#Singular_value_decomposition
),了解另一种方法以及这两种方法基本上产生几乎相同的结果。
[14] 要了解Full SVD 及其其他应用,可以阅读维基百科上的文章:en.wikipedia.org/wiki/Singular_value_decomposition
[15] 查看名为“过拟合 - 维基百科”的网页(en.wikipedia.org/wiki/Overfitting
)。
[16] 您可以点击查看任何 Scikit-Learn 函数的代码 [source
[17] 您可以深入研究 PCA 的数学原理:en.wikipedia.org/wiki/Principal_component_analysis
[18] Sonia Bergamaschi 和 Laura Po 在 2015 年对基于内容的电影推荐算法进行了比较,发现 LSA 的准确率大约是 LDiA 的两倍。详见 Sonia Bergamaschi 和 Laura Po 的论文“Comparing LDA and LSA Topic Models for Content-Based Movie Recommendation Systems”(www.dbgroup.unimo.it/~po/pubs/LNBI_2015.pdf
)。
[19] “Jonathan K. Pritchard, Matthew Stephens, Peter Donnelly, 使用多位点基因型数据推断人口结构” www.genetics.org/content/155/2/945
[20] 参见标题为"Latent Dirichlet Allocation"的 PDF(David M. Blei、Andrew Y. Ng 和 Michael I. Jordan)( www.jmlr.org/papers/volume3/blei03a/blei03a.pdf
)。
[21] 你可以在原论文《Online Learning for Latent Dirichlet Allocation》(Matthew D. Hoffman、David M. Blei 和 Francis Bach)中了解更多关于 LDiA 目标函数的详细信息。原论文链接在这里( www.di.ens.fr/%7Efbach/mdhnips2010.pdf
)。
[22] Blei 和 Ng 使用的符号是theta,而不是k。
[23] 如果你想了解过拟合为什么是一件坏事以及泛化是如何帮助的更多信息,请参见附录 D。
[24] 有关更多距离度量,请参见 Math.NET Numerics( numerics.mathdotnet.com/Distance.html
)。
[25] 参见 sklearn.metrics 的文档( scikit-learn.org/stable/modules/generated/sklearn.metrics.DistanceMetric.html
)。
[26] 参见标题为"Cosine similarity - Wikipedia"的网页( en.wikipedia.org/wiki/Cosine_similarity
)。
[27] 参见标题为"eccv spgraph"的网页( users.cecs.anu.edu.au/~sgould/papers/eccv14-spgraph.pdf
)。
[28] 语义网是在 HTML 文档中使用标签来结构化自然语言文本的实践,以便标签的层次结构和内容提供有关网页元素(文本、图片、视频)间关系(连接的网络)的信息。
[29] Ruby 是一种编程语言,其包被称为gems
。
[30] PostgreSQL 中的全文检索通常基于字符的trigrams
,以处理拼写错误和无法解析为单词的文本。
[31] 对高维数据进行聚类等效于使用边界框离散化或索引化高维数据,这在维基百科文章"Clustering high dimensional data"中有描述( en.wikipedia.org/wiki/Clustering_high-dimensional_data
)。
[32] 参见标题为"Inverted index - Wikipedia"的网页( en.wikipedia.org/wiki/Inverted_index
)。
[33] 将你的 Python 代码向量化,特别是用于成对距离计算的双重嵌套for
循环,可以将代码的速度加快近 100 倍。参见 Hacker Noon 文章"Vectorizing the Loops with Numpy"( hackernoon.com/speeding-up-your-code-2-vectorizing-the-loops-with-numpy-e380e939bed3
)。
[34] 如果你想了解更快的找到高维向量最近邻居的方法,请查看附录 F,或者直接使用 Spotify 的annoy
包来索引你的主题向量。