chatpdf-minimal-demo

项目名称中虽然带有 'pdf' 字样,但实际与 PDF 无关。它含有一个 Web 前端,允许用户将文章输入其中,之后通过调用 Python 脚本进行 Embeddings,之后用户可以对内容提问。相当于省去了 PDF 转文字的过程,对转文字后续流程的核心原理,进行演示。

py/process-article.py

实现对 PDF 进行 Embeddings。核心方法有两个:

首先将文章按段落进行切分(通过 \n)。

嵌入过程 get_embedding

get_embedding 方法用于对段落进行 Embeddings

def get_embedding(text: str, 
				  model: str=EMBEDDING_MODEL) -> list[float]:
	# embedding 缓存目录
	folder = 'embeddings/cache/'
	Path(folder).mkdir(parents=True, exist_ok=True)

	# 根据 text 的 md5 唯一标识
	tmpfile = folder+hashlib.md5(
		text.encode('utf-8')).hexdigest()+".json"

	# 命中缓存则直接返回
	if os.path.isfile(tmpfile):
		with open(tmpfile , 'r', encoding='UTF-8') as f:
			return json.load(f)

	# 调用 API 进行 Embedding
	result = openai.Embedding.create(
		model=model,
		input=text)

	# 保存 Embedding 结果
	with open(tmpfile, 'w',encoding='utf-8') as handle2:
		json.dump(result["data"][0]["embedding"], 
				  handle2, 
				  ensure_ascii=False, 
				  indent=4)

	# 同时返回
	return result["data"][0]["embedding"]

嵌入过程

对段落内容调用 Embedding API 后,通过嵌入模型的处理,会得到一个向量。

不同的嵌入模型被训练执行不同的功能,有的模型专门用于捕捉句子或段落的核心语义,有的模型专门用于捕获单词或短语的语义,可以看到它们的语境与粒度是不同的。

OpenAI 文档中对 text-embedding-ada-002 的介绍如下(参考非官方的中文文档):

嵌入(Embedding)是文本的数字表示,可用于衡量两段文本之间的相关性。我们的第二代嵌入模型 text-embedding-ada-002 旨在以一小部分成本取代之前的 16 种第一代嵌入(Embedding)模型。嵌入(Embedding)可用于搜索、聚类、推荐、异常检测和分类任务。

如何理解“两段文本之间的相关性”?

“两段文本之间的相关性”是指衡量两段文本在语义、主题或内容上的相似度或关联程度。

  1. 语义层面的相关性:如果两段文本在意义上相似或几乎相同,即使它们的词汇和语法结构不同,也可以认为它们具有高度的相关性。例如,“狗是人类的好朋友”和“人们常把狗视为忠实的伙伴”在语义上具有高度相关性。

  2. 主题层面的相关性:如果两段文本讨论的主题或核心概念相同或相似,那么它们也可以认为具有相关性。例如,一段文本讨论健康饮食的重要性,另一段文本讨论特定的健康食谱,尽管它们的焦点不同,但仍然具有主题相关性。

  3. 内容层面的相关性:这涉及到两段文本的具体内容和信息是否相似或一致。例如,两个关于特定历史事件的不同报道可能在内容上具有高度相关性。

"text-embedding-ada-002"模型能够在语义、主题和内容三个层面上衡量文本之间的相关性。

对超长段落进行嵌入,是否会丢失信息?

由于嵌入向量的维数是固定的,所以当输入文本的信息量超过向量能够承载的信息时,就会发生信息损失。这是一种必然的数学现象,因为尝试将更高维度的信息压缩到较低维度的空间中。

计算余弦相似度 vector_similarity

每个段落通过嵌入,得到一个嵌入向量。同时提问在嵌入后,也是嵌入向量,两者通过计算余弦相似度即可获得相关性。如果相关性高,则说明这段话与问题相关,如果相关,则需要汇集起来,作为供 GPT API 参考的资料。

def vector_similarity(x: list[float], y: list[float]) -> float:
	return np.dot(np.array(x), np.array(y))

因为OpenAI嵌入向量已经被标准化为长度为1,所以余弦相似度等同于点积

提问过程

提问嵌入

提问过程,首先对问题进行嵌入:

query_embedding = get_embedding(query)

余弦相似度

之后,将问题与各个段落分别计算计算余弦相似度,并按照从高到低排列:

document_similarities = sorted([
	(vector_similarity(query_embedding, doc_embedding), doc_index) \
		for doc_index, doc_embedding in enumerate(embeddings)
], reverse=True, key=lambda x: x[0])

其中:doc_index 用于记录嵌入向量对应的是文章中的第几段。

构建 Context

该项目中所谓 Context,指的是引用的相关段落数目。在一定 token 约束下(通过常量控制),按照相似度从高到低,将相关内容的文本,汇集起来,最终作为 Prompt 的一部分,传入 OpenAPI 的 GPT API。

相关段落汇聚过程如下:

# 上一届中的段落余弦相似度排序结果
ordered_candidates = order_document_sections_by_query_similarity(
						question,
						embeddings)
ctx = u""
for candi in ordered_candidates:
	# candi[1] 是 doc_id,sources 索引得到对应段落原文,加入 ctx
	next = ctx + u"\n" + sources[candi[1]]
	# 如果 Token 是否常量,则终止
	if len(next)>CONTEXT_TOKEN_LIMIT:
		break
	ctx = next
if len(ctx) == 0:
  return u""  

这里对 token 数量的比较是不准确的。因为一个单词、一个汉字可能会对应于多个 Token(参见词嵌入)。这里的比较只能是一个粗略估计。另外 CONTEXT_TOKEN_LIMIT 初始值是 1500,这是参考段落的长度上限。

构建 Prompt

最终构建如下 Prompt:

prompt = u"".join([
	u"Answer the question base on the context, answer in the same language of question\n\n"
	u"Context:" + ctx + u"\n\n"
	u"Question:" + question + u"\n\n"
	u"Answer:"
])

最后调用 API 完成提问过程:

completion = openai.ChatCompletion.create(
					model="gpt-3.5-turbo", 
					messages=[{"role": "user", "content":prompt}])

return [prompt, completion.choices[0].message.content]

文章内容提问

在项目中,还包含有一个很有趣的功能(get3questions),他的功能是针对文章的内容,提出多个问题。这是如何实现的呢?

首先,对文章还是按照逐个段落进行 embeddings。

之后,对嵌入向量进行 KMeans 聚类,由 get_topic_num 方法根据文章长度判断分为几类。值得注意的是,这里将每一个聚类,称为主题(Topic),也就是说,通过聚类将文章分为了多个主题。

换句话说,每个主题都对应文章中的一个或多个段落。将这些主题下的段落汇聚起来,作为这一个主题的 context。

然后,针对每个主题,分别构建 Prompt:

"Suggest a simple, clear, single, short question base on the context, answer in the same language of context\n\nContext:"+ctx+u"\n\nAnswer with the language used in context, the question is:"

将得到一个对应于该主题的,简单、清晰、单一、简短的问题。

将各个主题调用 Prompt 结果收集起来,最终实现了:这篇文章回答了那些问题?

网络资源

postor/chatpdf-minimal-demo: chatpdf 的最小实现,和文章对话 | mvp of chatpdf


本文作者:Maeiee

本文链接:chatpdf-minimal-demo

版权声明:如无特别声明,本文即为原创文章,版权归 Maeiee 所有,未经允许不得转载!


喜欢我文章的朋友请随缘打赏,鼓励我创作更多更好的作品!