《Hands-On Large Language Models》第5章 文本聚类和主题建模

\(\text{5}\) 文本聚类和主题建模

Text Clustering and Topic Modeling

尽管在过去几年中,监督式技术(例如分类)在业界占据了主导地位,但无监督式技术(例如文本聚类 (\(\text{Text clustering}\)))的潜力不容低估

文本聚类旨在根据文本的语义内容、含义和关系相似的文本进行分组。如图 \(\text{5}-1\) 所示,由此产生的语义相似文档的簇\(\text{clusters}\))不仅有助于对大量非结构化文本进行高效分类,还能实现快速的探索性数据分析

F5.1

语言模型的最新发展使得文本的上下文和语义表征成为可能,从而增强了文本聚类的有效性。语言不仅仅是词袋\(\text{bag of words}\)),最近的语言模型已证明非常有能力捕捉这一概念不受监督约束的文本聚类,允许创造性的解决方案和多样的应用,例如发现异常值加速标记查找错误标记的数据

文本聚类也进入了主题建模\(\text{topic modeling}\))的领域,我们在其中想要发现大量文本数据中出现的(抽象)主题。如图 \(\text{5}-2\) 所示,我们通常使用关键词或关键短语来描述一个主题,理想情况下,它会有一个单一的总括标签

F5.1

在本章中,我们将首先探讨如何使用嵌入模型进行聚类,然后转向一种受文本聚类启发的主题建模方法,即 \(\text{BERTopic}\)

文本聚类和主题建模在本书中占有重要地位,因为它们探索了结合各种不同语言模型的创造性方式。我们将探索如何结合仅编码器(嵌入)、仅解码器(生成)甚至经典方法(词袋)可以产生惊人的新技术和管线

\(\text{ArXiv}\) 文章:计算与语言

ArXiv’s Articles: Computation and Language

在本章中,我们将对 \(\text{ArXiv}\) 文章运行聚类和主题建模算法。\(\text{ArXiv}\) (https://arxiv.org/)是一个开放获取的学术文章平台,主要涉及计算机科学、数学和物理领域。为了与本书的主题保持一致,我们将探索计算与语言领域的文章。该数据集包含 \(\text{1991}\) 年到 \(\text{2024}\) 年间来自 \(\text{ArXiv}\)\(\text{cs.CL}\)(计算与语言)部分\(\text{44,949}\) 篇摘要。

我们加载数据,并为每篇文章的摘要、标题和年份创建单独的变量:

1
2
3
4
5
6
# Load data from Hugging Face
from datasets import load_dataset
dataset = load_dataset("maartengr/arxiv_nlp")["train"]
# Extract metadata
abstracts = dataset["Abstracts"]
titles = dataset["Titles"]

文本聚类的通用管线

A Common Pipeline for Text Clustering

文本聚类允许您发现可能熟悉或不熟悉的数据模式。它有助于对任务(例如分类任务)及其复杂性获得直观的理解。因此,文本聚类不仅仅是一种探索性数据分析的快速方法

虽然文本聚类有许多方法,从基于图的神经网络基于中心点的聚类技术,但一个日益流行的通用管线涉及三个步骤和算法

  1. 使用嵌入模型将输入文档转换为嵌入。
  2. 使用降维模型减少嵌入的维度。
  3. 使用聚类模型找到语义相似文档的群组。

嵌入文档

Embedding Documents

第一步是将我们的文本数据转换为嵌入,如图 \(\text{5}-3\) 所示。回顾前几章,嵌入文本的数值表征,旨在捕捉其含义

F5.1

选择针对语义相似性任务进行优化的嵌入模型对于聚类尤其重要,因为我们试图找到语义相似文档的群组。幸运的是,在撰写本文时,大多数嵌入模型都专注于这一点:语义相似性

正如我们在上一章中所做的那样,我们将使用 \(\text{MTEB}\) 排行榜来选择一个嵌入模型。我们需要一个在聚类任务上得分不错但又足够小以至于能快速运行的嵌入模型。我们不使用上一章中使用的 \(\text{sentence}-\text{transformers/all}-\text{mpnet}-\text{base}-\text{v}2\) 模型,而是改用 \(\text{thenlper/gte}-\text{small}\) 模型。这是一个更新的模型,在聚类任务上表现优于前一个模型,并且由于其体积小推理速度甚至更快。不过,请随意尝试自发布以来更新的模型!

1
2
3
4
from sentence_transformers import SentenceTransformer
# Create an embedding for each abstract
embedding_model = SentenceTransformer("thenlper/gte-small")
embeddings = embedding_model.encode(abstracts, show_progress_bar=True)

让我们检查每个文档嵌入包含多少个值:

1
2
# Check the dimensions of the resulting embeddings
embeddings.shape
1
(44949, 384)

每个嵌入有 \(\text{384}\) 个值,它们共同代表了文档的语义表征。您可以将这些嵌入视为我们想要聚类的特征

减少嵌入的维度

Reducing the Dimensionality of Embeddings

在对嵌入进行聚类之前,我们首先需要考虑它们的高维度。随着维度数量的增加每个维度内可能值的数量会呈指数级增长在每个维度内找到所有子空间变得越来越复杂

因此,高维数据可能会给许多聚类技术带来麻烦,因为它使得识别有意义的簇变得更加困难。相反,我们可以利用降维\(\text{dimensionality reduction}\))。如图 \(\text{5}-4\) 所示,这项技术允许我们缩小维度空间的大小,并用更少的维度来表示相同的数据降维技术旨在通过寻找低维表征保留高维数据的全局结构

F5.1

请注意,这是一种压缩技术,底层算法并非随意移除维度。因此,我们聚类管线中的第二步是降维,如图 \(\text{5}-5\) 所示,以帮助聚类模型创建有意义的簇

F5.1

著名的降维方法有主成分分析\(\text{Principal Component Analysis, PCA}\))和均匀流形近似与投影\(\text{Uniform Manifold Approximation and Projection, UMAP}\))。对于这个管线,我们选择 \(\text{UMAP}\),因为它往往比 \(\text{PCA}\) 更好地处理非线性关系和结构

💡 然而,降维技术并非完美无缺。它们不能将高维数据完美地捕获到低维表征中。在这个过程中信息总是会丢失。在减少维度保留尽可能多的信息之间存在一种平衡

为了执行降维,我们需要实例化我们的 \(\text{UMAP}\),并将生成的嵌入传递给它:

1
2
3
4
5
6
from umap import UMAP
# We reduce the input embeddings from 384 dimensions to 5 dimensions
umap_model = UMAP(
n_components=5, min_dist=0.0, metric='cosine', random_state=42
)
reduced_embeddings = umap_model.fit_transform(embeddings)

我们可以使用 \(\text{n\_components}\) 参数来决定低维空间的形状,即 \(\text{5}\) 个维度。通常,\(\text{5}\)\(\text{10}\) 之间的值能很好地捕获高维全局结构。

\(\text{min\_dist}\) 参数是嵌入点之间的最小距离。我们将此设置为 \(\text{0}\),因为这通常会导致更紧密的簇。我们将 \(\text{metric}\) 设置为 \(\text{cosine}\),因为基于欧几里得的方法在处理高维数据时存在问题。

请注意,在 \(\text{UMAP}\) 中设置 \(\text{random\_state}\) 将使结果在不同会话中可重现,但会禁用并行性,因此会减慢训练速度

聚类降维后的嵌入

Cluster the Reduced Embeddings

第三步是对降维后的嵌入进行聚类,如图 \(\text{5}-6\) 所示。

F5.1

尽管常见的选择是像 \(\text{k}-\text{means}\) 这样的基于中心点的算法(它要求事先设定要生成的簇的数量),但我们事先不知道簇的数量。相反,基于密度的算法可以自由计算簇的数量,并且不会强制所有数据点都属于一个簇,如图 \(\text{5}-7\) 所示。

F5.1

一种常见的基于密度的模型是 \(\text{HDBSCAN}\)基于密度的带噪声应用的层次聚类)。\(\text{HDBSCAN}\) 是一种称为 \(\text{DBSCAN}\) 的聚类算法的层次变体,它允许在不必明确指定簇数量的情况下找到密集(微观)簇。作为一种基于密度的方法\(\text{HDBSCAN}\) 还可以检测数据中的异常值\(\text{outliers}\)),即不属于任何簇的数据点。这些异常值不会被分配或强制属于任何簇。换句话说,它们会被忽略。由于 \(\text{ArXiv}\) 文章可能包含一些小众论文,使用一个可以检测异常值的模型可能会有所帮助。

与之前的软件包一样,使用 \(\text{HDBSCAN}\) 非常简单。我们只需要实例化模型并将我们的降维后的嵌入传递给它:

1
2
3
4
5
6
7
8
from hdbscan import HDBSCAN
# We fit the model and extract the clusters
hdbscan_model = HDBSCAN(
min_cluster_size=50, metric="euclidean", cluster_selection_method="eom"
).fit(reduced_embeddings)
clusters = hdbscan_model.labels_
# How many clusters did we generate?
len(set(clusters))
1
156

使用 \(\text{HDBSCAN}\),我们在数据集中生成了 \(\text{156}\) 个簇。要创建更多簇,我们将需要减小 \(\text{min\_cluster\_size}\) 的值,因为它代表了一个簇可以采用的最小规模

检查聚类

Inspecting the Clusters

现在我们已经生成了\(\text{clusters}\)),我们可以手动检查每个簇并探索分配给它的文档,以了解其内容。例如,让我们从\(\text{0}\)中随机抽取一些文档:

1
2
3
4
5
import numpy as np
# Print first three documents in cluster 0
cluster = 0
for index in np.where(clusters==cluster)[0][:3]:
print(abstracts[index][:300] + "... \n")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
This works aims to design a statistical machine translation from English text
to American Sign Language (ASL). The system is based on Moses tool with some
modifications and the results are synthesized through a 3D avatar for
interpretation. First, we translate the input text to gloss, a written fo...

Researches on signed languages still strongly dissociate lin- guistic issues
related on phonological and phonetic aspects, and gesture studies for
recognition and synthesis purposes. This paper focuses on the imbrication of
motion and meaning for the analysis, synthesis and evaluation of sign lang...

Modern computational linguistic software cannot produce important aspects of
sign language translation. Using some researches we deduce that the majority of
automatic sign language translation systems ignore many aspects when they
generate animation; therefore the interpretation lost the truth inf...

从这些文档来看,这个簇中的文档主要关于从英语文本到美国手语 (\(\text{ASL}\)) 的翻译,很有趣!

我们可以更进一步,尝试可视化我们的结果,而不是手动浏览所有文档。为此,我们需要将文档嵌入降维到二维,因为这样我们就可以在 \(\text{x}/\text{y}\) 平面上绘制文档:

1
2
3
4
5
6
7
8
9
10
11
12
import pandas as pd
# Reduce 384-dimensional embeddings to two dimensions for easier visualization
reduced_embeddings = UMAP(
n_components=2, min_dist=0.0, metric="cosine", random_state=42
).fit_transform(embeddings)
# Create dataframe
df = pd.DataFrame(reduced_embeddings, columns=["x", "y"])
df["title"] = titles
df["cluster"] = [str(c) for c in clusters]
# Select outliers and non-outliers (clusters)
to_plot = df.loc[df.cluster != "-1", :]
outliers = df.loc[df.cluster == "-1", :]

我们还分别为\(\text{clusters\_df}\))和异常值\(\text{outliers\_df}\))创建了一个数据框,因为我们通常希望专注于簇并突出显示它们

💡 用于可视化目的的任何降维技术都会造成信息损失。它仅仅是对我们原始嵌入外观的近似。虽然它具有信息性,但它可能会将簇推得更近或拉得更远,与它们的实际距离不符。因此,人工评估(即我们自己检查簇)是聚类分析的关键组成部分

为了生成一个静态图,我们将使用著名的绘图库 \(\text{matplotlib}\)

1
2
3
4
5
6
7
8
import matplotlib.pyplot as plt
# Plot outliers and non-outliers separately
plt.scatter(outliers_df.x, outliers_df.y, alpha=0.05, s=2, c="grey")
plt.scatter(
clusters_df.x, clusters_df.y, c=clusters_df.cluster.astype(int),
alpha=0.6, s=2, cmap="tab20b"
)
plt.axis("off")

正如我们在图 \(\text{5}-8\) 中所看到的,它往往能很好地捕捉主要的簇。 请注意点的簇是如何以相同的颜色着色的,这表明 \(\text{HDBSCAN}\) 将它们归为一组。由于我们有大量的簇,绘图库会在簇之间循环使用颜色,所以不要认为所有的绿点都是一个簇,例如。

F5.1

这在视觉上很吸引人,但尚不能让我们看到簇内部正在发生什么。相反,我们可以通过从文本聚类转向主题建模来扩展这种可视化。

从文本聚类到主题建模

From Text Clustering to Topic Modeling

文本聚类在大规模文档集合中寻找结构的强大工具。在我们前面的示例中,我们可以手动检查每个簇,并根据其文档集合来识别它们。例如,我们探讨了一个包含手语相关文档的簇。我们可以说,该簇的主题是“手语”

这种在文本数据集合中寻找主题或潜在主题的想法通常被称为主题建模\(\text{Topic Modeling}\))。如图 \(\text{5}-9\) 所示,传统上,它涉及寻找一组最能代表和捕捉主题含义的关键词或短语

F5.1

这些技术没有将主题标记为“手语”,而是使用关键词,例如“\(\text{sign}\)”(手语)、“\(\text{language}\)”(语言)和“\(\text{translation}\)”(翻译)来描述主题。因此,这并没有给主题一个单一的标签,而是要求用户通过这些关键词来理解主题的含义

经典方法,如潜在狄利克雷分配\(\text{latent Dirichlet allocation, LDA}\)),假设每个主题都由语料库词汇表中单词的概率分布来表征。如图 \(\text{5}-10\) 所示,它演示了词汇表中的每个单词如何根据其与每个主题的相关性进行评分。

F5.1

这些方法通常使用词袋\(\text{bag}-\text{of}-\text{words}\))技术作为文本数据的主要特征,这没有考虑到单词和短语的上下文或含义。相比之下,我们的文本聚类示例确实考虑了这两点,因为它依赖于基于 \(\text{Transformer}\) 的嵌入,这些嵌入通过注意力机制针对语义相似性和上下文含义进行了优化。

在本节中,我们将通过一个高度模块化的文本聚类和主题建模框架,即 \(\text{BERTopic}\),将文本聚类扩展到主题建模领域

\(\text{BERTopic}\):模块化主题建模框架

BERTopic: A Modular Topic Modeling Framework

\(\text{BERTopic}\) 是一种主题建模技术,它利用语义相似文本的簇来提取各种类型的主题表征。其底层算法可以分为两个步骤

首先,如图 \(\text{5}-11\) 所示,我们遵循与文本聚类示例中相同的程序。我们嵌入文档减少它们的维度,最后聚类降维后的嵌入,以创建语义相似文档的群组

F5.1

其次,它通过利用一种经典方法——即词袋\(\text{bag}-\text{of}-\text{words}\)),来对语料库词汇表中的单词进行建模。正如我们在\(\text{1}\)中简要讨论并如图 \(\text{5}-12\) 所示,词袋的作用正如其名称所暗示计算每个单词在文档中出现的次数。由此产生的表征可用于提取文档中最常出现的单词

F5.1

然而,这里有两个注意事项。首先,这是一个文档级别的表征,而我们感兴趣的是簇级别的视角。为了解决这个问题,单词的频率是在整个簇内计算的,而不仅仅是在单个文档中,如图 \(\text{5}-13\) 所示。

F5.1

其次,像“\(\text{the}\)”和“\(\text{I}\)”这样的停用词\(\text{stop words}\))往往频繁出现在文档中,但对实际文档提供的意义很少\(\text{BERTopic}\) 使用术语频率-逆文档频率\(\text{term frequency–inverse document frequency, TF}-\text{IDF}\))的基于类别的变体(\(\text{c}-\text{TF}-\text{IDF}\)),以便赋予对簇更有意义的单词更高的权重,并降低在所有簇中都使用的单词的权重

词袋中的每个单词\(\text{c}-\text{TF}-\text{IDF}\) 中的 \(\text{c}-\text{TF}\) 部分)都乘以每个单词的 \(\text{IDF}\)。如图 \(\text{5}-14\) 所示,\(\text{IDF}\) 值是通过取所有簇中所有单词的平均频率除以每个单词的总频率对数来计算的。

F5.1

结果是每个单词的权重(“\(\text{IDF}\)”),我们可以用它乘以它们的频率(“\(\text{c}-\text{TF}\)”)来获得加权值(“\(\text{c}-\text{TF}-\text{IDF}\)”)。

如图 \(\text{5}-15\) 所示,过程的第二部分允许我们像以前一样生成单词上的分布。我们可以使用 \(\text{scikit}-\text{learn}\)\(\text{CountVectorizer}\) 来生成词袋(或术语频率)表征。在这里,每个簇被视为一个主题,它对语料库的词汇表有一个特定的排名

F5.1

将这两步(聚类表征主题)结合起来,就形成了 \(\text{BERTopic}\) 的完整管线,如图 \(\text{5}-16\) 所示。通过这个管线,我们可以聚类语义相似的文档,并从这些簇中生成由多个关键词代表的主题一个单词在一个主题中的权重越高,它就越能代表该主题

F5.1

这种管线的一个主要优势在于,聚类主题表征这两个步骤在很大程度上相互独立。例如,对于 \(\text{c}-\text{TF}-\text{IDF}\),我们不依赖于用于文档聚类的模型。这使得整个管线中的每个组件都具有显著的模块化。正如我们将在本章后面探讨的那样,这也是微调主题表征的一个绝佳起点。

如图 \(\text{5}-17\) 所示,虽然 \(\text{sentence}-\text{transformers}\) 被用作默认的嵌入模型,但我们可以将其替换为任何其他嵌入技术。同样的道理也适用于所有其他步骤。如果你不希望使用 \(\text{HDBSCAN}\) 生成异常值,你可以改用 \(\text{k}-\text{means}\)

F5.1

你可以将这种模块化视为使用乐高积木进行构建;管线的每个部分都可以被另一个类似算法完全替换。通过这种模块化,新发布的模型可以被整合到其架构中。随着语言 \(\text{AI}\) 领域的发展,\(\text{BERTopic}\) 也在不断成长!

\(\text{BERTopic}\) 的模块化

\(\text{BERTopic}\)模块化还有另一个优势:它允许使用相同的基本模型应用于和适应不同的用例。例如,\(\text{BERTopic}\) 支持各种算法变体

  • 引导式主题建模(\(\text{Guided topic modeling}\)
  • (半)监督式主题建模(\((\text{Semi})-\text{supervised topic modeling}\)
  • 分层主题建模(\(\text{Hierarchical topic modeling}\)
  • 动态主题建模(\(\text{Dynamic topic modeling}\)
  • 多模态主题建模(\(\text{Multimodal topic modeling}\)
  • 多视角主题建模(\(\text{Multi}-\text{aspect topic modeling}\)
  • 在线和增量主题建模(\(\text{Online and incremental topic modeling}\)
  • 零样本主题建模(\(\text{Zero}-\text{shot topic modeling}\)
  • 等等。

这种模块化算法灵活性是作者旨在让 \(\text{BERTopic}\) 成为主题建模的一站式商店的基础。您可以在文档代码库中找到其功能的完整概述。

要使用我们的 \(\text{ArXiv}\) 数据集运行 \(\text{BERTopic}\),我们可以使用我们先前定义的模型和嵌入(尽管这不是强制性的):

1
2
3
4
5
6
7
8
from bertopic import BERTopic
# Train our model with our previously defined models
topic_model = BERTopic(
embedding_model=embedding_model,
umap_model=umap_model,
hdbscan_model=hdbscan_model,
verbose=True
).fit(abstracts, embeddings)

让我们从探索创建的主题开始。\(\text{get\_topic\_info()}\) 方法有助于快速获取我们发现的主题的描述

1
topic_model.get_topic_info()

F5.1

这些主题中的每一个都由几个关键词代表,这些关键词在 \(\text{Name}\) 栏位中用 \(\_\) 连接起来。这个 \(\text{Name}\) 栏位使我们能够快速了解主题的内容,因为它显示了最能代表该主题的四个关键词

您可能还注意到,第一个主题被标记为 \(-1\)。该主题包含所有无法归入任何主题的文档,并被视为异常值\(\text{outliers}\))。这是聚类算法 \(\text{HDBSCAN}\) 的结果,它不强制所有点都必须聚类。要移除异常值,我们可以使用像 \(\text{k}-\text{means}\) 这样的非异常值算法,或者使用 \(\text{BERTopic}\)\(\text{reduce\_outliers()}\) 函数将异常值重新分配给主题。

我们可以使用 \(\text{get\_topic}\) 函数检查单个主题,并探索哪些关键词最能代表它们。例如,主题 \(\text{0}\) 包含以下关键词:

1
2
3
4
5
6
7
8
9
10
11
topic_model.get_topic(0)
[('speech', 0.028177697715245358),
('asr', 0.018971184497453525),
('recognition', 0.013457745472471012),
('end', 0.00980445092749381),
('acoustic', 0.009452082794507863),
('speaker', 0.0068822647060204885),
('audio', 0.006807649923681604),
('the', 0.0063343444687017645),
('error', 0.006320144717019838),
('automatic', 0.006290216996043161)]

例如,主题 \(\text{0}\) 包含关键词 “\(\text{speech}\)”、“\(\text{asr}\)”和 “\(\text{recognition}\)”。根据这些关键词,该主题似乎是关于自动语音识别\(\text{ASR}\))的。

我们可以使用 \(\text{find\_topics()}\) 函数根据搜索词搜索特定主题。让我们搜索一个关于主题建模的主题:

1
2
3
topic_model.find_topics("topic modeling")
([22, -1, 1, 47, 32],
[0.95456535, 0.91173744, 0.9074769, 0.9067007, 0.90510106])

这表明主题 \(\text{22}\) 与我们的搜索词有相对较高的相似度\(\text{0.95}\))。如果我们接着检查该主题,我们可以看到它确实是关于主题建模的:

1
2
3
4
5
6
7
8
9
10
11
topic_model.get_topic(22)
[('topic', 0.06634619076655907),
('topics', 0.035308535091932707),
('lda', 0.016386314730705634),
('latent', 0.013372311924864435),
('document', 0.012973600191120576),
('documents', 0.012383715497143821),
('modeling', 0.011978375291037142),
('dirichlet', 0.010078277589545706),
('word', 0.008505619415413312),
('allocation', 0.007930890698168108)]

虽然我们知道这个主题是关于主题建模的,但让我们看看 \(\text{BERTopic}\) 摘要是否也分配给了这个主题:

1
2
3
topic_model.topics_[titles.index("BERTopic: Neural topic modeling with a class-
based TF-IDF procedure")]
22

确实如此!这些功能使我们能够快速找到我们感兴趣的主题

💡 \(\text{BERTopic}\) 的模块化为您提供了很多选择,这可能会让人感到不知所措。为此,作者创建了一份最佳实践指南(https://maartengr.github.io/BERTopic/getting_started/best_practices/best_practices.html),其中介绍了加速训练、改进表征常见实践

为了让主题探索更容易一些,我们可以回顾我们的文本聚类示例。在那里,我们创建了一个静态可视化来查看所创建主题的总体结构。使用 \(\text{BERTopic}\),我们可以创建一个交互式变体,使我们能够快速探索存在哪些主题以及它们包含哪些文档

这样做要求我们使用我们用 \(\text{UMAP}\) 创建的二维嵌入 \(\text{reduced\_embeddings}\)。此外,当我们悬停在文档上时,我们将显示标题而不是摘要,以便快速了解主题中的文档:

1
2
3
4
5
6
7
8
9
# Visualize topics and documents
fig = topic_model.visualize_documents(
titles,
reduced_embeddings=reduced_embeddings,
width=1200,
hide_annotations=True
)
# Update fonts of legend for easier visualization
fig.update_layout(font=dict(size=16))

正如我们在图 \(\text{5}-18\) 中所看到的,这个交互式图表可以让我们快速了解所创建的主题。您可以放大查看单个文档,或双击右侧的主题以仅查看该主题

F5.1

\(\text{BERTopic}\) 中有多种可视化选项。有三种值得探索,以便了解主题之间的关系

1
2
3
4
5
6
# Visualize barchart with ranked keywords
topic_model.visualize_barchart()
# Visualize relationships between topics
topic_model.visualize_heatmap(n_clusters=30)
# Visualize the potential hierarchical structure of topics
topic_model.visualize_hierarchy()

添加一个特殊的乐高积木(主题表征增强)

Adding a Special Lego Block

到目前为止我们探索的 \(\text{BERTopic}\) 管线,尽管速度快且模块化,但有一个缺点:它仍然通过词袋来表示主题,而没有考虑到语义结构

解决方案是利用词袋表征的优势(即生成有意义表征的速度)。我们可以使用这第一个有意义的表征,并使用更强大但更慢的技术(如嵌入模型)对其进行调整。如图 \(\text{5}-19\) 所示,我们可以对初始的词语分布进行重新排序,以改进生成的表征。请注意,这种对初始结果集进行重新排序的想法是神经搜索中的主要方法,我们将在\(\text{8}\)中介绍这个主题。

F5.1

因此,我们可以设计一个新的乐高积木,如图 \(\text{5}-20\) 所示,它接受这个第一个主题表征输出一个改进的表征

F5.1

\(\text{BERTopic}\) 中,此类重新排序模型被称为表征模型\(\text{representation models}\))。这种方法的一个主要优势是主题表征的优化只需要对主题的数量执行相应的次数。例如,如果我们有数百万文档和一百个主题,表征模块只需要对每个主题应用一次,而不是对每个文档应用一次

如图 \(\text{5}-21\) 所示,\(\text{BERTopic}\) 已经设计了各种表征模块,允许您微调表征表征模块甚至可以多次堆叠,使用不同的方法来微调表征。

F5.1

在我们探索如何使用这些表征模块之前,我们首先需要做两件事。

首先,我们将保存我们原始的主题表征,以便更容易地与使用和不使用表征模型的版本进行比较

1
2
3
# Save original representations
from copy import deepcopy
original_topics = deepcopy(topic_model.topic_representations_)

其次,让我们创建一个简短的包装函数,我们可以用它来快速可视化主题词的差异,以便与使用和不使用表征模型进行比较:

1
2
3
4
5
6
7
8
9
def topic_differences(model, original_topics, nr_topics=5):
"""Show the differences in topic representations between two models """
df = pd.DataFrame(columns=["Topic", "Original", "Updated"])
for topic in range(nr_topics):
# Extract top 5 words per topic per model
og_words = " | ".join(list(zip(*original_topics[topic]))[0][:5])
new_words = " | ".join(list(zip(*model.get_topic(topic)))[0][:5])
df.loc[len(df)] = [topic, og_words, new_words]
return df

我们将探索的第一个表征模块\(\text{KeyBERTInspired}\)。正如您可能猜到的,\(\text{KeyBERTInspired}\) 是一种受关键词提取软件包 \(\text{KeyBERT}\) 启发的方法\(\text{KeyBERT}\) 通过余弦相似度比较单词和文档嵌入来提取文本中的关键词

\(\text{BERTopic}\) 采用了类似的方法。\(\text{KeyBERTInspired}\) 使用 \(\text{c}-\text{TF}-\text{IDF}\),通过计算文档的 \(\text{c}-\text{TF}-\text{IDF}\)与其对应主题的 \(\text{c}-\text{TF}-\text{IDF}\)之间的相似性,来提取每个主题最具代表性的文档。如图 \(\text{5}-22\) 所示,它计算每个主题的平均文档嵌入,并将其与候选关键词的嵌入进行比较,以重新排序关键词

F5.1

由于 \(\text{BERTopic}\) 的模块化特性,我们可以使用 \(\text{KeyBERTInspired}\)更新我们最初的主题表征,而无需执行降维和聚类步骤

1
2
3
4
5
6
from bertopic.representation import KeyBERTInspired
# Update our topic representations using KeyBERTInspired
representation_model = KeyBERTInspired()
topic_model.update_topics(abstracts, representation_model=representation_model)
# Show topic differences
topic_differences(topic_model, original_topics)

F5.1

更新后的模型显示,与原始模型相比,主题更易于阅读。它也展示了使用基于嵌入的技术缺点。原始模型中的词语,例如 \(\text{nmt}\)(主题 \(\text{3}\),代表神经机器翻译),被移除了,因为模型无法正确地表示这个实体。对于领域专家来说,这些缩写信息量非常大

使用 \(\text{c}-\text{TF}-\text{IDF}\) 和前面展示的 \(\text{KeyBERTInspired}\) 技术,我们仍然在生成的主题表征中存在显著的冗余。例如,在一个主题表征中同时拥有“\(\text{summaries}\)”和“\(\text{summary}\)”这两个词会引入冗余,因为它们非常相似

我们可以使用最大边缘相关性\(\text{Maximal Marginal Relevance, MMR}\))来使我们的主题表征多样化。该算法试图找到一组彼此多样化仍与被比较的文档相关联的关键词。它通过嵌入一组候选关键词迭代计算要添加的下一个最佳关键词来实现这一点。这样做需要设置一个多样性参数,该参数指示关键词需要的多样化程度

\(\text{BERTopic}\) 中,我们使用 \(\text{MMR}\) 将一组初始关键词(假设 \(\text{30}\) 个)缩小为一组更小但更多样化的关键词(假设 \(\text{10}\) 个)。它过滤掉冗余词,只保留那些对主题表征有新贡献的词语。

这样做相当简单:

1
2
3
4
5
6
from bertopic.representation import MaximalMarginalRelevance
# Update our topic representations to MaximalMarginalRelevance
representation_model = MaximalMarginalRelevance(diversity=0.2)
topic_model.update_topics(abstracts, representation_model=representation_model)
# Show topic differences
topic_differences(topic_model, original_topics)

F5.1

由此产生的主题在其表征中表现出更高的多样性。例如,主题 \(\text{4}\) 只显示一个“\(\text{summary}\)”类的词语,而是添加了其他可能对整体表征贡献更大的词语。

💡 \(\text{KeyBERTInspired}\)\(\text{MMR}\) 都是改进第一组主题表征的绝佳技术。\(\text{KeyBERTInspired}\) 尤其倾向于移除几乎所有停用词,因为它侧重于单词和文档之间的语义关系

文本生成乐高积木

The Text Generation Lego Block

在我们前面的示例中,\(\text{BERTopic}\) 中的表征模块一直充当重新排序模块。然而,正如我们在上一章中已经探讨过的,生成模型在各种任务中具有巨大的潜力。

我们可以通过遵循重新排序过程的一部分,在 \(\text{BERTopic}\) 中非常高效地使用生成模型。我们不使用生成模型来识别所有文档的主题(这可能涉及数百万文档),而是使用模型来为我们的主题生成一个标签。如图 \(\text{5}-23\) 所示,我们不生成或重新排序关键词,而是要求模型根据先前生成的关键词一小部分代表性文档生成一个简短的标签

F5.1

所展示的提示包含两个组成部分。

首先,使用 \(\text{[DOCUMENTS]}\) 标签插入的文档最能代表该主题的一小部分文档(通常是四个)。系统会选择\(\text{c}-\text{TF}-\text{IDF}\)主题 \(\text{c}-\text{TF}-\text{IDF}\)具有最高余弦相似度的文档。

其次,构成主题的关键词也会被传递给提示,并使用 \(\text{[KEYWORDS]}\) 标签引用。这些关键词可以由 \(\text{c}-\text{TF}-\text{IDF}\) 或我们迄今为止讨论的任何其他表征生成。

因此,我们只需要对每个主题(可能多达数百个)使用一次生成模型,而不是对每个文档(可能多达数百万个)使用一次。我们可以选择许多生成模型,包括开源专有模型。让我们从我们在上一章中探讨过的模型 \(\text{Flan}-\text{T5}\) 模型开始。

我们创建一个与该模型配合良好的提示,并通过 \(\text{representation\_model}\) 参数\(\text{BERTopic}\) 中使用它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from transformers import pipeline
from bertopic.representation import TextGeneration
prompt = """I have a topic that contains the following documents:
[DOCUMENTS]
The topic is described by the following keywords: '[KEYWORDS]'.
Based on the documents and keywords, what is this topic about?"""
# Update our topic representations using Flan-T5
generator = pipeline("text2text-generation", model="google/flan-t5-small")
representation_model = TextGeneration(

)
topic_model.update_topics(abstracts, representation_model=representation_model)
# Show topic differences
topic_differences(topic_model, original_topics)

F5.1

其中一些标签,例如“\(\text{Summarization}\)”(摘要),在与原始表征进行比较时似乎是合乎逻辑的。然而,其他一些标签,例如“\(\text{Science/Tech}\)”(科学/技术),看起来相当宽泛没有公正地体现原始主题。让我们转而探索 \(\text{OpenAI}\)\(\text{GPT}-\text{3.5}\) 模型的表现,考虑到该模型不仅更大,而且预期具有更强的语言能力

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import openai
from bertopic.representation import OpenAI
prompt =
"""
I have a topic that contains the following documents:
[DOCUMENTS]
The topic is described by the following keywords: [KEYWORDS]
Based on the information above, extract a short topic label in the following
format:
topic: <short topic label>
"""
# Update our topic representations using GPT-3.5
client = openai.OpenAI(api_key="YOUR_KEY_HERE")
representation_model = OpenAI(
client, model="gpt-3.5-turbo", exponential_backoff=True, chat=True,
prompt=prompt
)
topic_model.update_topics(abstracts, representation_model=representation_model)
# Show topic differences
topic_differences(topic_model, original_topics)

F5.1

生成的标签相当令人印象深刻!我们甚至没有使用 \(\text{GPT}-\text{4}\),但生成的标签似乎比我们上一个例子更具信息量。请注意,\(\text{BERTopic}\) 并不局限于只使用 \(\text{OpenAI}\) 的服务,它也支持本地后端

💡 尽管看起来我们不再需要关键词了,但它们仍然代表着输入文档没有模型是完美的,通常建议生成多个主题表征\(\text{BERTopic}\) 允许所有主题不同的表征来表示。例如,您可以同时使用 \(\text{KeyBERTInspired}\)\(\text{MMR}\)\(\text{GPT}-\text{3.5}\),以获得关于同一主题的不同视角

有了这些 \(\text{GPT}-\text{3.5}\) 生成的标签,我们可以使用 \(\text{datamapplot}\) 软件包创建精美的插图(图 \(\text{5}-24\)):

F5.1

1
2
3
4
5
6
7
8
9
10
# Visualize topics and documents
fig = topic_model.visualize_document_datamap(
titles,
topics=list(range(20)),
reduced_embeddings=reduced_embeddings,
width=1200,
label_font_size=11,
label_wrap_width=20,
use_medoids=True,
)

总结

在本章中,我们探讨了大型语言模型\(\text{LLM}\)),无论是生成式还是表征式的,如何应用于无监督学习领域。尽管近年来像分类这样的监督方法非常流行,但文本聚类无监督方法因其无需事先标记即可根据语义内容对文本进行分组的能力而具有巨大的潜力

我们介绍了一个聚类文本文档的通用管线,它首先将输入文本转换为数值表征,我们称之为嵌入。然后,对这些嵌入应用降维\(\text{dimensionality reduction}\)),以简化高维数据,从而获得更好的聚类结果。最后,将聚类算法应用于降维后的嵌入,对输入文本进行聚类。手动检查这些簇帮助我们理解它们包含哪些文档以及如何解释这些簇。

为了摆脱这种手动检查,我们探索了 \(\text{BERTopic}\) 如何通过一种自动表征簇的方法来扩展文本聚类管线。这种方法通常被称为主题建模\(\text{topic modeling}\)),它试图揭示大量文档中的主题\(\text{BERTopic}\) 通过词袋方法(并辅以 \(\text{c}-\text{TF}-\text{IDF}\) 进行增强)来生成这些主题表征,该方法根据单词的簇相关性和在所有簇中的频率加权单词

\(\text{BERTopic}\) 的一个主要优势是其模块化特性。在 \(\text{BERTopic}\) 中,您可以选择管线中的任何模型,这允许对主题进行额外的表征,从而为同一主题创建多个视角。我们探讨了最大边缘相关性\(\text{MMR}\))和 \(\text{KeyBERTInspired}\) 作为微调 \(\text{c}-\text{TF}-\text{IDF}\) 生成的主题表征的方法。此外,我们使用了与上一章相同的生成式 \(\text{LLM}\)\(\text{Flan}-\text{T5}\)\(\text{GPT}-\text{3.5}\))来生成高度可解释的标签,从而进一步提高主题的可解释性

在下一章中,我们将转移焦点,探索一种改进生成模型输出的常用方法,即提示工程\(\text{prompt engineering}\))。

书籍各章的机翻md文件:
《Hands-On Large Language Models》目录及前言
《Hands-On Large Language Models》第1章 大型语言模型简介
《Hands-On Large Language Models》第2章 词元与嵌入
《Hands-On Large Language Models》第3章 深入了解大型语言模型
《Hands-On Large Language Models》第4章 文本分类
《Hands-On Large Language Models》第5章 文本聚类和主题建模
《Hands-On Large Language Models》第6章 提示工程
《Hands-On Large Language Models》第7章 高级文本生成技术与工具
《Hands-On Large Language Models》第8章 语义搜索与检索增强生成
《Hands-On Large Language Models》第9章 多模态大型语言模型
《Hands-On Large Language Models》第10章 创建文本嵌入模型
《Hands-On Large Language Models》第11章 微调用于分类的表征模型
《Hands-On Large Language Models》第12章 生成模型的微调