想了解更多AIGC的内容,请访问:

51CTO AI.x社区

https://www.51cto.com/aigc/

背景

当今社会,人们使用大量数据训练包含数百万和数十亿模型参数的大型语言模型(LLM),目标是生成文本,如文本完成、文本摘要、语言翻译和回答问题。虽然LLM都是从给定的训练数据源开始来开发知识库本身,但是其中总有一个截止训练日期的问题,这导致了LLM不会知道在以后的新日期内的任何新生成的数据。

例如,训练OpenAI的GPT-3.5-turbo-instruct LLM的截止日期为2021年9月(参考:https://platform.openai.com/docs/models/gpt-3-5-turbo);因此,GPT-3.5-turbo-instruct LLM可能无法准确回答与2022年、2023年或2024年中发生的事件有关的问题。这种不是LLM的原始训练数据的部分数据被称为外部数据。

正是在这种情况下出现了检索增强生成(RAG)技术,这种技术能够通过从授权的外部来源中检索与输入提示相关的适当信息,并能够增强输入,从而使LLM能够生成准确和相关的响应。实际上,RAG形成了LLM和外部数据之间的一个网关。这种增强消除了对LLM模型进行再训练或进一步微调的需要。

LLM的典型实现方案

LLM是自回归的,基于标记为标记序列的输入提示,从而生成新的标记。下一个最佳标记的生成是基于概率的,并且可以表示如下:

P( Yn∣X0, X1, … Xn-1, θ )

本质上,新生成的第n个标记Yn的概率取决于n-1个先前标记序列X和学习的模型参数θ的出现概率。这里应该注意的是,标记化的输入序列X在生成下一个标记中起着至关重要的作用。此外,自注意机制补充了有效的自回归,其中序列中的每个输入标记通过关注和权衡序列中其他标记的重要性来计算其表示。

序列中的标记之间的这种复杂关系和依赖性也使LLM能够破译与输入序列中的标记“很好地结合”的最可能的次优标记。LLM将新的标记附加到先前的标记以形成新的输入序列,并重复自回归过程,直到满足完成条件,例如达到最大标记计数。

这种自关注驱动的自回归意味着,LLM主要依赖于输入序列来生成次优标记。只要输入序列有助于通过自我关注来确定下一个最佳标记,LLM就会继续处于“良性”循环中,从而产生连贯、可理解和相关的输出。相反,如果提示输入无助于确定下一个最佳标记,则LLM将开始依赖于模型参数。在这种情况下,如果模型已被训练为包含足够的输入提示上下文的“知识”,则该模型可能成功生成下一个最佳标记。相反,如果提示输入与LLM从未训练过的“外部数据”有关,则模型可能会进入“恶性循环”,导致产生不连贯、不可理解且可能不相关的输出。

当前人们已经研究出多种技术来解决这个问题。提示工程就是其中之一,其目标是通过调整提示来增强上下文,从而使LLM能够生成相关输出,从而解决“缺失的上下文”。RAG则是另一种技术,其目标是通过以自动化的方式从外部数据源检索与输入提示相关的最合适的信息并增强提示,来专门解决“由于外部数据而丢失的上下文”。

RAG面临的挑战

RAG的主要职责是从外部数据源(如信息数据库、API和维基百科等其他文档库)中搜索和检索与输入提示上下文相关的数据。一个简单的关键词搜索并不能解决这个问题。相反,RAG需要一个语义搜索。为了便于语义搜索,从外部来源检索的文本信息被转换为数字表示或向量,通常称为文本嵌入,并存储在向量数据库中。已经存在多种模型或算法,用于从文本创建这些嵌入。首先,提示被转换为其向量表示,以搜索和检索最匹配的外部数据向量。然后,计算提示向量和先前存储的外部数据向量之间的向量相似性(或向量距离)。使用阈值对最相似或最接近的向量进行排序和过滤,并检索它们对应的文本信息以增强提示的上下文。下面的概念图展示了启用RAG的不同组件之间的典型交互:

实现RAG的主要系统组件交互的概念视图(作者本人图片)

RAG面临的挑战是,进行向量驱动的语义搜索并不简单,需要大量的计算资源,因为它涉及到针对数据库中潜在的大量向量计算向量相似性或距离。对于每个输入提示,从庞大的向量数据库中计算每个存储向量的相似性或距离指标将变得不可行。此外,语义匹配质量越低,LLM的生成输出质量就越低。因此,找到一种有效地进行语义搜索的方法变得至关重要。

解决方案

当前,可以采用几种算法解决方案来进行高效的语义搜索。这些算法的典型思路是,将外部数据向量分组或聚类为最近邻居,并通过映射到这样的聚类来对它们进行索引。大多数向量数据库都提供这种索引作为内置功能。在语义搜索期间,首先针对输入提示向量来评估匹配的聚类。对于每个评估的簇,都会选择索引向量。然后计算输入提示向量和所选向量之间的相似性。这里的期望是,找到“最近的邻居”作为中间步骤,可以显著减少相似性计算的数量。最后,检索与通过阈值滤波的最相似或最接近的向量相对应的文本信息。诸如k-最近邻、半径球-R、位置敏感哈希、DBSCAN聚类、类树层次结构和类图层次结构之类的算法通常由向量数据库实现,以便于语义搜索。

当然,不存在一刀切的解决方案,因为不同的算法族在内存效率、计算效率、延迟、准确性、向量维度、数据集大小等方面有不同的权衡。例如,聚类方法通过缩小语义搜索的向量空间来提高速度,而类树或类图方法则提高了低维向量数据的准确性。

自组织映射

自组织映射(SOM)是芬兰神经网络专家Teuvo Kohonen在20世纪80年代开发的一种基于神经网络的降维算法。它通常用于将高维特征向量减少为低维(通常是二维)特征向量。SOM背后的核心思想是,将高维数据向量表示为低维空间中的特定节点,同时保留向量在原始空间中的拓扑结构。低维空间中的节点数(SOM节点)是固定的(超参数)。通过多个训练时期来评估SOM节点的确切位置。迭代训练的目标是调整低维空间中SOM节点的位置,以便将它们映射到高维特征空间中最近的相邻向量。换言之,目标是将高维空间中的最近邻向量映射到也是低维空间中最近邻的SOM节点。

RAG的SOM

在这篇文章中,我想分享我用SOM作为一种可能的算法来推动RAG的语义搜索的实验笔记和有关发现。与其他算法相比,SOM可能更为理想一些,这基于以下三个关键原因:

  • 向量的高维度可能会成为大多数其他算法的瓶颈,如树和图,即所谓的维度诅咒。相反,SOM是为降维而构建的;因此,它可以有效地应用于高维和低维场景。
  • SOM对可能渗入原始高维向量空间的随机变化不太敏感,从而导致噪声。其他算法可能对这种噪声敏感,影响它们将高维向量聚类或分组为最近邻的方式。由于SOM在低维向量空间中使用中间SOM节点,这些节点被评估为来自高维空间的映射向量的局部平均值,因此它有效地减少了噪声。
  • 外部数据集的大尺寸可能会约束其他算法来创建语义向量空间,这可能会影响语义匹配的延迟和准确性。另一方面,SOM可以处理海量数据集,因为低维空间中的SOM节点数量可以通过与底层数据集大小成比例的超参数进行微调。虽然使用大型数据集训练SOM可能需要更长的时间,但一旦训练完成,查询时间映射仍然更快。

对此,我展示了一个简单的例子。在这个例子中,使用SOM进行RAG的语义搜索,以使用OpenAI的GPT-3.5-turbo-instruct LLM来增加问答的上下文。使用OpenAI的GPT-3.5-turbo-instruct LLM的主要原因是,训练OpenAI的GPT-3.5-turbo-instruct LLM的截止日期是2021年9月(参考:https://platform.openai.com/docs/models/gpt-3-5-turbo);因此,GPT-3.5-turbo-instruct LLM可能无法准确回答2022年、2023年或2024年发生的事件相关的问题。因此,关于2022年、2023年或2024年发生的事件的信息可以成为OpenAI的GPT-3.5-turbo-instruct LLM的“外部数据”。我使用维基百科API作为这种“外部数据”的来源来获取事件的信息。以下展示我用来开发和训练示例的步骤,以及示例代码。

步骤1:基于PyTorch的Kohonen的SOM实现

示例中,我利用PyTorch张量来表示向量,并使用PyTorch实现了Kohonen的SOM。该算法使用了一个二维晶格,其大小变为超参数。该算法的数学方面是从精心设计的角度得出的,并在以下文章中进行了清晰的解释:

下面的代码片段显示了Kohonen的SOM的Python类。完整的代码可在GitHub(https://github.com/kbmurali/som-driven-qa-rag/blob/main/kohonen_som.py)获得。值得注意的是,这个实现是独立的,所以它可以在RAG示例之外使用。

class KohonenSOM():
"""
该代码是基于以下文章开发的:
http://www.ai-junkie.com/ann/som/som1.html

向量和矩阵运算是使用PyTorch张量进行的。
"""
def __init__( ... )
...
def find_topk_best_matching_units( self, data_points : torch.Tensor, topk : int = 1 ) -> List[ List[ int ] ] :
if len( data_points.size() ) == 1:
#batching 
data_points = data_points.view( 1, data_points.shape[0] )

topk = int( topk )

distances = self.dist_evaluator( data_points, self.lattice_node_weights )

topk_best_matching_unit_indexes = torch.topk( distances, topk, dim=1, largest=False ).indices
topk_best_matching_units = []

for i in range( data_points.shape[0] ):
best_matching_unit_indexes = topk_best_matching_unit_indexes[i]
best_matching_units = [ self.lattice_coordinates[ bmu_index.item() ].tolist() for bmu_index in best_matching_unit_indexes ]
topk_best_matching_units.append( best_matching_units )

return topk_best_matching_units