Dalle3 对“语义分块”的解读。由作者生成的图像。
使用嵌入和可视化工具将文本分割成有意义的片段本文提供了语义文本切分的解释,这是一种旨在自动将相似的文本片段进行分组的技术,可以作为检索增强生成(RAG)或类似应用的预处理阶段的一部分。我们使用可视化来理解切分过程,并探讨一些涉及聚类和大型语言模型(LLM)标注的扩展。查看完整代码_这里_。
自动从大量文本中检索和总结信息有许多有用的用途。其中最成熟的应用之一是检索增强生成(RAG),它涉及从大型语料库中提取与用户问题相关的文本片段——通常通过语义搜索或其他过滤步骤来实现。然后,这些片段由大型语言模型(LLM)进行解释或总结,以提供高质量且准确的答案。为了使提取的片段尽可能相关,它们需要具有语义连贯性,这意味着每个片段都是关于一个特定的概念,并且自身包含有用的信息。
分块的应用不仅仅局限于RAG。想象一下,我们有一个复杂的文档,比如一本书或一篇期刊文章,想要快速了解其中包含的关键概念。如果文本可以被聚类成语义连贯的组,然后每个聚类以某种方式被总结,这将大大加快获得见解的时间。优秀的BertTopic(参见这篇文章以获取简要概述)可以在这方面提供帮助。
可视化分块也可以很有启发性,无论是作为最终产品还是在开发过程中。人类是视觉学习者,我们的大脑从图表和图像中获取信息的速度远快于从文本流中获取信息。根据我的经验,在不以某种方式可视化分块或阅读所有分块的情况下,很难理解分块算法对文本做了什么——以及什么参数可能是最优的——在处理大型文档时,阅读所有分块是不切实际的。
在本文中,我们将探讨一种将文本分割成语义上有意义的片段的方法,并重点介绍使用图表和图形来理解整个过程。在此过程中,我们将涉及降维和嵌入向量的层次聚类,以及使用大语言模型(LLM)对片段进行总结,以便我们能够快速了解其中包含的信息。我希望这可能会激发正在研究语义分块作为其应用程序潜在工具的任何人产生更多想法。我将在此处使用Python 3.9、LangChain和Seaborn,详细信息请参阅repo。
1. 什么是语义分块?有几种标准的分块类型,如果你想了解更多,我推荐这篇优秀的教程,该教程也为本文提供了灵感。假设我们处理的是英文文本,最简单的分块方式是基于字符的,即选择一个固定的字符窗口,然后将文本分割成该长度的块。可选地,我们可以在块之间添加重叠,以保留它们之间的一些顺序关系。这种分块方法在计算上很简单,但不能保证块在语义上是有意义的,甚至可能不是完整的句子。
递归分块通常更有用,并且被许多应用程序视为首选算法。该过程接受一个分层的分隔符列表(在LangChain中的默认值是 [“\n\n”, “\n”, “ ”, “”]
)和一个目标长度。然后使用分隔符以递归方式分割文本,逐级向下直到每个分块的长度小于或等于目标长度。这样可以更好地保留完整的段落和句子,这很好,因为它使分块更有可能连贯。然而,它不考虑语义:如果一个句子紧跟在上一个句子之后并且恰好位于分块窗口的末尾,这些句子将被分开。
在语义分块中,该方法在LangChain 和 LlamaIndex 中均有实现。分块是基于连续块的嵌入之间的余弦距离进行的。因此,我们首先将文本分成小但连贯的组,可能使用递归分块器。
接下来,我们使用一个经过训练的模型将每个片段向量化,生成有意义的嵌入。通常这采用基于变压器的双编码器的形式(详情和示例请参见SentenceTransformers库),或者使用如OpenAI的端点[text-embeddings-3-small](https://platform.openai.com/docs/guides/embeddings)
,这是我们在这里使用的。最后,我们查看连续片段嵌入之间的余弦距离,并选择距离较大的断点。理想情况下,这有助于创建既连贯又语义上区别的文本组。
最近的一个扩展称为语义双块合并(详情请参阅这篇文章),尝试通过进行第二次遍历并使用一些重组逻辑来扩展这种方法。例如,如果第一次遍历在块1和块2之间插入了一个断点,但块1和块3非常相似,它将创建一个新的包含块1、块2和块3的组。这在块2是例如数学公式或代码块时非常有用。
然而,当涉及到任何形式的语义分块时,一些关键问题仍然存在:在我们设置断点之前,块嵌入之间的距离可以多大?这些块实际上代表了什么?我们是否关心这些问题?这些问题的答案取决于具体的应用和文本内容。
2. 探索断点让我们通过使用语义分块来生成断点的例子来说明。我们将实现我们自己的版本的算法,尽管如上所述,也有现成的实现可用。我们的演示文本在这里:这里,它由三个简短的事实性文章组成,这些文章由GPT-4o撰写并拼接在一起。第一篇文章是关于保护树木的一般重要性,第二篇是关于纳米比亚的历史,第三篇则是更深入地探讨保护树木的医学用途的重要性。主题选择并不重要,但这个语料库是一个有趣的测试案例,因为第一篇和第三篇文章在某种程度上是相似的,但被第二篇截然不同的文章隔开。每篇文章还被分成不同的部分,关注不同的内容。
我们可以使用基本的 RecursiveCharacterTextSplitter
来生成初始的片段。这里最重要的参数是片段大小和分隔符列表,而通常在没有对文本有一定了解的情况下,我们并不知道应该设置为什么值。在这里,我选择了一个相对较小的片段大小,因为我想让初始片段最多只有几句话长。我还选择了分隔符,以避免在句子中间进行分割。
# 本文中提到的文本切分工具包中的工具
from text_chunking.SemanticClusterVisualizer import SemanticClusterVisualizer
# 将你的 OpenAI API 密钥放在包顶层的 .env 文件中
from text_chunking.utils.secrets import load_secrets
# 我们要讨论的示例文本
from text_chunking.datasets.test_text_dataset import TestText
# 基本的文本分割器
from langchain_text_splitters import RecursiveCharacterTextSplitter
import seaborn as sns
splitter = RecursiveCharacterTextSplitter(
chunk_size=250,
chunk_overlap=0,
separators=["\n\n", "\n", "."],
is_separator_regex=False
)
接下来我们可以对文本进行分割。如果生成的任何片段长度小于 min_chunk_len
参数指定的值,那么该片段将被追加到前一个片段的末尾。
original_split_texts = 语义切分器.split_documents(
分割器,
TestText.testing_text,
min_chunk_len=100,
verbose=True
)
### 输出
# 2024-09-14 16:17:55,014 - 使用原始分割器分割文本
# 2024-09-14 16:17:55,014 - 创建了 53 个片段
# 平均长度:178.88679245283018
# 最大长度:245
# 最小长度:103
现在我们可以使用嵌入模型对分块进行嵌入。在 [SemanticClusterVisualizer](https://github.com/rmartinshort/text_chunking/blob/main/text_chunking/SemanticClusterVisualizer.py)
类中,默认情况下我们使用 text-embeddings-3-small
。这将生成一个包含53个向量的列表,每个向量长度为1536。直观地说,这意味着每个分块的语义意义在1536维空间中得到了表示。这对于可视化来说不太理想,这也是为什么我们稍后会转向降维的原因。
original_split_text_embeddings = semantic_chunker.embed_original_document_splits(original_split_texts)
运行语义分块器会生成这样的图形。我们可以将其视为一个时间序列,其中 x 轴表示整个文本中的字符距离,y 轴表示后续分块嵌入之间的余弦距离。断点发生在高于第 95 个百分点的距离值处。
显示由 RecursiveCharacterTextSplitter 生成的连续文本片段之间的余弦距离的图表。我们可以使用这些距离来确定语义分块的断点。图片由作者生成。
这个模式是有道理的,因为我们对文本有所了解——有三个大的主题,每个主题又包含几个不同的部分。除了两个大的峰值之外,其他断点的位置并不明显。
这就是主观性和迭代发挥作用的地方——根据我们的应用需求,我们可能希望有更大或更小的块,并且使用图表来帮助我们的眼睛识别出实际需要阅读的块是很重要的。
有几种方法可以将文本分解为更细粒度的块。第一种方法是降低百分位阈值以创建断点。
由选择较低的百分位数阈值生成的余弦距离数组中的断点。由作者生成的图像。
这创建了4个非常小的片段和8个较大的片段。如果我们看一下前4个片段,例如,这些分割在语义上似乎是合理的,尽管我认为第4个片段有点太长了,因为它包含了第一篇文章中大部分的“经济重要性”、“社会重要性”和“结论”部分。
设置百分位阈值为0.8时生成的前四个语义块。由作者生成的图像。
与其仅仅改变百分位阈值,另一种想法是递归地应用相同的阈值。我们首先在整个文本上创建断点。然后,对于每个新创建的片段,如果该片段的长度超过某个阈值,我们就在该片段内部创建断点。这个过程一直持续到所有片段的长度都低于阈值为止。虽然这种方法在某种程度上具有主观性,但我认为这更接近于人类的做法,即首先识别出非常不同的文本组,然后逐步减小每个组的大小。
它可以使用栈来实现,如下所示。
def get_breakpoints(
embeddings: List[np.ndarray],
start: int = 0,
end: int = None,
threshold: float = 0.95,
) -> np.ndarray:
"""
根据余弦距离阈值识别嵌入中的断点。
Args:
embeddings (List[np.ndarray]): 嵌入列表。
start (int, optional): 处理的起始索引。默认为 0。
end (int, optional): 处理的结束索引。默认为 None。
threshold (float, optional): 确定显著距离变化的百分位数阈值。默认为 0.95。
Returns:
np.ndarray: 发生断点的索引数组。
"""
if end is not None:
embeddings_windowed = embeddings[start:end]
else:
embeddings_windowed = embeddings[start:]
len_embeddings = len(embeddings_windowed)
cdists = np.empty(len_embeddings - 1)
# 获取每个片段与下一个片段之间的余弦距离
for i in range(1, len_embeddings):
cdists[i - 1] = cosine(embeddings_windowed[i], embeddings_windowed[i - 1])
# 获取断点
difference_threshold = np.percentile(cdists, 100 * threshold, axis=0)
difference_exceeding = np.argwhere(cdists >= difference_threshold).ravel()
return difference_exceeding
def build_chunks_stack(
self, length_threshold: int = 20000, cosine_distance_percentile_threshold: float = 0.95
) -> np.ndarray:
"""
根据长度和余弦距离阈值构建文本片段的堆栈。
Args:
length_threshold (int, optional): 文本片段的有效最小长度。默认为 20000。
cosine_distance_percentile_threshold (float, optional): 确定断点的余弦距离百分位数阈值。默认为 0.95。
Returns:
np.ndarray: 表示片段断点的索引数组。
"""
# self.split_texts 是原始拆分的文本
# self.split_text_embeddings 是它们的嵌入
S = [(0, len(self.split_texts))]
all_breakpoints = set()
while S:
# 获取此片段的起始和结束
id_start, id_end = S.pop()
# 获取此片段的断点
updated_breakpoints = self.get_breakpoints(
self.split_text_embeddings,
start=id_start,
end=id_end,
threshold=cosine_distance_percentile_threshold,
)
updated_breakpoints += id_start
# 将更新的断点添加到集合中
updated_breakpoints = np.concatenate(
(np.array([id_start - 1]), updated_breakpoints, np.array([id_end]))
)
# 对于每个更新的断点,将其边界添加到集合中,并将其添加到堆栈中,如果足够长
for index in updated_breakpoints:
text_group = self.split_texts[id_start : index + 1]
if (len(text_group) > 2) and (
self.get_text_length(text_group) >= length_threshold
):
S.append((id_start, index))
id_start = index + 1
all_breakpoints.update(updated_breakpoints)
# 获取所有断点,除了起始和结束(它们将对应于文本拆分的起始和结束)
return np.array(sorted(all_breakpoints))[1:-1]
我们的 length_threshold
选择也是主观的,可以参考图表来确定。在这种情况下,1000 的阈值似乎效果很好。它将文章很好地分成了短而有意义不同的段落。
由递归语义拆分器在原始片段嵌入的余弦距离时间序列上运行后生成的断点。图片由作者生成。
查看与第一篇文章对应的段落,我们发现它们与GPT4-o撰写文章时创建的不同部分紧密对应。显然,在这种特定的文章中,我们只需在 "\n\n"
处进行分割即可完成,但我们希望采用更通用的方法。
由上述递归断点生成方法生成的前六个语义片段。图片由作者生成。
2. 对语义分割进行聚类现在我们已经生成了一些候选的语义片段,看看它们彼此之间有多相似可能会很有用。这将帮助我们了解它们包含的信息。我们将通过嵌入这些语义片段,然后使用UMAP将生成的嵌入维度降低到2D,以便我们可以绘制出来。
UMAP 代表均匀流形近似和投影,是一种强大的、通用的降维技术,可以捕捉非线性关系。UMAP 的工作原理的完整解释可以在这里找到:这里。在这里使用它的目的是在二维图中捕捉嵌入块之间在1536维空间中存在的某些关系。
从 umap 导入 UMAP
维度缩减器 = UMAP(
n_neighbors=5,
n_components=2,
min_dist=0.0,
metric="cosine",
random_state=0
)
减少后的嵌入 = 维度缩减器.fit_transform(语义嵌入)
splits_df = pd.DataFrame(
{
"reduced_embeddings_x": 减少后的嵌入[:, 0],
"reduced_embeddings_y": 减少后的嵌入[:, 1],
"idx": np.arange(len(减少后的嵌入[:, 0])),
}
)
splits_df["chunk_end"] = np.cumsum([len(x) for x in 语义文本组])
ax = splits_df.plot.scatter(
x="reduced_embeddings_x",
y="reduced_embeddings_y",
c="idx",
cmap="viridis"
)
ax.plot(
减少后的嵌入[:, 0],
减少后的嵌入[:, 1],
"r-",
linewidth=0.5,
alpha=0.5,
)
UMAP 对 n_neighbors
参数非常敏感。一般来说,n_neighbors
的值越小,算法越侧重于利用局部结构来学习如何将数据投影到更低的维度。如果将此值设置得太小,可能会导致投影无法很好地捕捉数据的大规模结构,而且随着数据点数量的增加,这个值通常应该相应增大。
下面是我们数据的投影,非常有信息量:很明显,我们有三个相似意义的聚类,第1个和第3个聚类比它们各自与第2个聚类更相似。上方图表中的idx
色条显示了块编号,而红色线条则指示了块的顺序。
上一节生成的语义分割的嵌入的UMAP投影图。idx指的是在遍历文本时生成的块的索引。由作者生成的图像。
关于自动聚类呢?如果我们希望将片段归类到更大的段落或主题中,这在使用混合搜索的RAG应用程序中作为有用的元数据进行过滤时会很有帮助。我们还可以将文本中相隔很远但含义相似的片段进行分组,而这些片段在标准的语义分块(如第1节中所述)中可能不会被归为一类。
在这里可以使用许多聚类方法。HDBSCAN 是一种选择,也是 BERTopic 包推荐的默认方法。然而,在这种情况下,层次聚类似乎更有用,因为它可以让我们了解出现的各个组的相对重要性。要运行层次聚类,我们首先使用 UMAP 减少数据集的维度,将其降到较少的组件。只要 UMAP 运行良好,组件的确切数量不应显著影响生成的聚类。然后我们使用 scipy 的 hierarchy 模块执行聚类,并使用 seaborn 绘制结果。
from scipy.cluster import hierarchy
from scipy.spatial.distance import pdist
from umap import UMAP
import seaborn as sns
# 设置 UMAP
dimension_reducer_clustering = UMAP(
n_neighbors=umap_neighbors,
n_components=n_components_reduced,
min_dist=0.0,
metric="cosine",
random_state=0
)
reduced_embeddings_clustering = dimension_reducer_clustering.fit_transform(
semantic_group_embeddings
)
# 创建层次结构
row_linkage = hierarchy.linkage(
pdist(reduced_embeddings_clustering),
method="average",
optimal_ordering=True,
)
# 绘制热图和树状图
g = sns.clustermap(
pd.DataFrame(reduced_embeddings_clustering),
row_linkage=row_linkage,
row_cluster=True,
col_cluster=False,
annot=True,
linewidth=0.5,
annot_kws={"size": 8, "color": "white"},
cbar_pos=None,
dendrogram_ratio=0.5
)
g.ax_heatmap.set_yticklabels(
g.ax_heatmap.get_yticklabels(), rotation=0, size=8
)
结果也非常有信息量。在这里,n_components_reduced
是 4,所以我们把嵌入的维度降到了 4D,从而形成一个包含 4 个特征的矩阵,每一行代表一个语义片段。层次聚类识别出了两个主要的群体(即树木和纳米比亚),树木中的两个大子群(即医疗用途与其他),以及其他一些可能值得探索的群体。
基于语义块生成的嵌入的UMAP投影进行层次聚类。由作者生成的图像。
注意,BERTopic 使用类似的技术进行主题可视化,这可以被视为对这里展示的内容的一种扩展。
这在我们探索语义分块时有何用处?根据结果,我们可能会选择将一些分块组合在一起。这再次相当主观,可能需要尝试几种不同的分组方式。假设我们查看了层次聚类树状图并决定想要分成8个不同的组。然后我们可以相应地切割层次结构,返回每个组的聚类标签并绘制出来。
cluster_labels = hierarchy.cut_tree(linkage, n_clusters=n_clusters).ravel()
dimension_reducer = UMAP(
n_neighbors=umap_neighbors,
n_components=2,
min_dist=0.0,
metric="cosine",
random_state=0
)
reduced_embeddings = dimension_reducer.fit_transform(semantic_embeddings)
splits_df = pd.DataFrame(
{
"reduced_embeddings_x": reduced_embeddings[:, 0],
"reduced_embeddings_y": reduced_embeddings[:, 1],
"cluster_label": cluster_labels,
}
)
splits_df["chunk_end"] = np.cumsum(
[len(x) for x in semantic_text_groups]
).reshape(-1, 1)
ax = splits_df.plot.scatter(
x="reduced_embeddings_x",
y="reduced_embeddings_y",
c="cluster_label",
cmap="rainbow",
)
ax.plot(
reduced_embeddings[:, 0],
reduced_embeddings[:, 1],
"r-",
linewidth=0.5,
alpha=0.5,
)
下面展示了生成的图表。我们有8个聚类,它们在二维空间中的分布看起来合理。这再次证明了可视化的重要性:根据文本、应用和相关方的不同,合适的聚类数量和分布可能会有所不同,唯一检查算法运行情况的方法就是绘制这样的图表。
用于着色UMAP投影嵌入的语义块的聚类ID。由作者生成的图像。
3. 标记聚类假设经过上述步骤的几次迭代后,我们已经确定了满意的语义分割和聚类。那么接下来的问题就是这些聚类实际上代表什么?显然,我们可以通过阅读文本来找出答案,但对于大型语料库来说这是不切实际的。因此,让我们使用一个大型语言模型(LLM)来帮助我们。具体来说,我们将把每个聚类相关的文本输入到GPT-4o-mini中,让它生成一个摘要。这在LangChain中是一个相对简单的任务,其核心代码如下所示:
import langchain
from langchain.prompts import PromptTemplate
from langchain_core.output_parsers.string import StrOutputParser
from langchain.callbacks import get_openai_callback
from dataclasses import dataclass
@dataclass
class ChunkSummaryPrompt:
system_prompt: str = """
你是一位擅长从文本中提取总结和信息的专家。你将被提供一份文档中的文本片段,你的任务是用少于10个词来总结这个片段中的内容。
首先仔细阅读整个片段,然后认真思考其中的主要内容,最后生成你的总结。
需要总结的片段:{current_chunk}
"""
prompt: langchain.prompts.PromptTemplate = PromptTemplate(
input_variables=["current_chunk"],
template=system_prompt,
)
class ChunkSummarizer(object):
def __init__(self, llm):
self.prompt = ChunkSummaryPrompt()
self.llm = llm
self.chain = self._set_up_chain()
def _set_up_chain(self):
return self.prompt.prompt | self.llm | StrOutputParser()
def run_and_count_tokens(self, input_dict):
with get_openai_callback() as cb:
result = self.chain.invoke(input_dict)
return result, cb
llm_model = "gpt-4o-mini"
llm = ChatOpenAI(model=llm_model, temperature=0, api_key=api_key)
summarizer = ChunkSummarizer(llm)
运行此操作于我们的8个集群,并使用datamapplot绘制结果如下:
由运行 GPT-4o-mini 生成的语义聚类标签。图片由作者生成。
另一种可视化这些组的方法类似于第2节中展示的图表,我们在x轴上绘制累计字符数,并显示各组之间的边界。回想一下,我们有18个语义片段,现在将它们进一步分成了8个聚类。这样绘制可以显示文本的语义内容从开头到结尾是如何变化的,突出相似内容并不总是相邻的事实,并给出片段相对大小的视觉指示。
展示文本按语义聚类分割的图,以及聚类的名称。由作者生成的图像。
用于生成这些图的代码可以在这里找到:here。
4. 在更大的语料库上进行测试到目前为止,我们已经在相对较小的文本上测试了这个工作流程,这只是为了演示。理想情况下,它也应该能在更大的文本集合上发挥作用,而不需要做太多修改。为了测试这一点,让我们尝试在从Project Gutenberg下载的一本书上运行这个工作流程,这里我选择的是《绿野仙踪》。这是一个更具挑战性的任务,因为小说通常不像事实性文章那样有清晰的语义区段。尽管小说通常分为章节,但故事情节可能会以连续或跳跃的方式展开,涉及不同的主题。非常有趣的是,看看语义分块分析是否能从不同作者的作品中学习到一些关于他们写作风格的信息。
步骤 1:嵌入并生成断点 从 text_chunking.SemanticClusterVisualizer 导入 SemanticClusterVisualizer
从 text_chunking.utils.secrets 导入 load_secrets
从 text_chunking.datasets.test_text_dataset 导入 TestText, TestTextNovel
从 langchain_text_splitters 导入 RecursiveCharacterTextSplitter
secrets = load_secrets()
semantic_chunker = SemanticClusterVisualizer(api_key=secrets["OPENAI_API_KEY"])
splitter = RecursiveCharacterTextSplitter(
chunk_size=250,
chunk_overlap=0,
separators=["\n\n", "\n", "."],
is_separator_regex=False
)
original_split_texts = semantic_chunker.split_documents(
splitter,
TestTextNovel.testing_text,
min_chunk_len=100,
verbose=True
)
original_split_text_embeddings = semantic_chunker.embed_original_document_splits(original_split_texts)
breakpoints, semantic_groups = semantic_chunker.generate_breakpoints(
original_split_texts,
original_split_text_embeddings,
length_threshold=10000 # 可能需要一些迭代来找到此参数的良好值
)
这生成了77个大小不一的语义块。通过一些随机检查,我感到相当有信心,它运行得相当好,并且许多块最终在章节边界或附近被划分,这很有道理。
《绿野仙踪》的语义分割。由作者生成的图像。
步骤 2:聚类并生成标签在查看层次聚类树状图后,我决定尝试将聚类数量减少到35个。结果在下面的图表左上角(聚类ID 34)发现了一个离群点,这实际上是一组位于文本末尾的块,包含了一段关于书籍分发条款的详细描述。
《绿野仙踪》语义块的UMAP投影的2D图及其聚类标签。由作者生成图片
下面列出了每个聚类的描述,除了第一个聚类外,其他聚类都很好地概述了小说的主要事件。快速检查每个聚类关联的实际文本后可以确认,这些描述是相当准确的摘要,尽管再次强调,确定聚类边界的划分是非常主观的。
《绿野仙踪》的40个语义簇自动生成的名称。这份列表提供了故事梗概的快速概览。由作者生成的图片。
GPT-4o-mini 将离群点簇标记为“Project Gutenberg 允许免费分发未受保护的作品”。与该标签相关的文本对我们来说并不特别有趣,所以让我们删除它并重新绘制结果。这将使小说中的结构更容易看到。
如果我们对更大的聚类感兴趣会怎样?如果我们关注高层次结构,树状图表明大约有六个语义块的聚类,如下图所示。
搜索高层次结构——如果我们选择从《绿野仙踪》的语义片段中生成6个聚类,这就是我们得到的模式。图片由作者生成。
在这语义空间中,有很多在较远点之间的来回跳跃,表明主题经常突然变化。同样,也很有趣地考虑各个聚类之间的连通性:例如,4 和 5 之间没有任何链接,而 0 和 1 之间则有很多来回跳跃。
我们能否对这些较大的聚类进行总结呢?结果发现,我们的提示似乎不适合处理这种大小的片段,生成的描述要么对聚类的某一部分过于具体(例如聚类0和4),要么过于模糊而无法提供太多帮助。改进提示工程——可能需要多个总结步骤——可能会改善这里的结果。
显示按语义聚类ID分割的文本及上述识别出的六个聚类名称的图表。由作者生成的图像。
尽管名称不太直观,但这个按聚类着色的文本片段图仍然可以作为选择性阅读文本的指南。我们看到这本书开头和结尾都在同一个聚类中,这很可能与多萝西、托托和她们的家的描述有关——并且与《绿野仙踪》作为一段旅程和随后的回归的故事结构相吻合。聚类1主要涉及遇到新角色,这主要发生在书的开头,但也偶尔出现在书的其他部分。聚类2和3主要涉及翡翠城和巫师,而聚类4和5则分别广泛涉及旅行和战斗。
5. 最后的思考感谢您读到最后!在这里,我们深入探讨了语义分块的概念,以及如何通过降维、聚类和可视化来补充这一过程。主要的收获是,在决定最合适的分块方法之前,系统地探索不同分块技术和参数对文本的影响的重要性。我希望这篇文章能激发新的想法,关于如何利用AI和可视化工具来推进语义分块,并快速从大量文本中提取见解。您可以在这里探索完整的代码库:https://github.com/rmartinshort/text_chunking。
共同學習,寫下你的評論
評論加載中...
作者其他優質文章