我的学习笔记

土猛的员外

谈谈RAG存在的一些问题和避免方式

关注“土猛的员外”公众号的朋友应该都知道我最近写了不少检索增强生成(RAG)的文章,远比大语言模型(LLM)要多。原因很简单,我觉得对于绝大多数的公司和开发者来说这是很务实的做法,那种需要太多显卡(Money)的事情本身就是一种门槛。所以我觉得去研究和实践基于LLM的应用更实际,而RAG就是非常好的一个方向,这对于很多企业来说是刚需。

1

RAG是各方面综合之后的最优解。

但是就像前面我说的,RAG入门很简单,但是要能让客户买单却很难,我已经看到好几个失败案例了…

下面我们来看看会有哪些方面会引起RAG的失败(下面的举例并不完全,还有一些本次来不及写了)。

1.分块(Chunking)策略和Top-k算法

一个成熟的RAG应该支持灵活的分块,并且可以添加一点重叠以防止信息丢失。一般来说,分块过程忽略了文本的内容,这就产生了问题。块的理想内容应该围绕单个主题保持一致,以便Embedding模型更好地工作。他们不应该从一个话题跳到另一个话题,他们不应该改变场景。比如长篇大论的论文和140字上限的微博内容的分析就会需要适配不同的分块策略,用固定的、不适合的分块策略会造成相关度下降

除了chunk,我们需要考虑参数top_k的影响。RAG系统使用top_k来选择得分达到多少的文本chunk才能送到LLM里面进行生成(Gen)操作。在大多数设计中,top_k是一个固定的数字。因此,如果块大小太小或块中的信息不够密集,我们可能无法从向量数据库中提取所有必要的信息。

对于熟悉机器学习模型调优的人来说会对chunk_sizetop_k非常敏感,为了确保RAG系统以最佳状态运行,需要对块大小和top_k进行调优,以确保它们是最合适的。机器学习里面参数调优的古老智慧仍然适用,唯一的区别是它们的调优成本更高。

2.世界知识缺失

考虑这样一个场景:我们正在构建一个《西游记》的问答系统。我们已经把所有的《西游记》的故事导入到一个向量数据库中。现在,我们问它:人有几个头?

最有可能的是,系统会回答3个,因为里面提到了哪吒有“三头六臂”,也有可能会说很多个,因为孙悟空在车迟国的时候砍了很多次头。而问题的关键是小说里面不会正儿八经地去描述人有多少个头,所以RAG的数据有可能会和真实世界知识脱离。

“今天的AI和机器学习真的很糟糕。人类有常识,而机器没有。”

——Yann LeCun

因此,当我们开发RAG系统时,不要让大语言模型已经知道解决方案的想法欺骗了您。他们没有。

3.多跳问题

让我们考虑另一个场景:我们建立了一个基于社交媒体的RAG系统。那么我们的问题是:谁知道埃隆·马斯克?然后,系统将遍历向量数据库,提取埃隆·马斯克的联系人列表。由于chunk大小和top_k的限制,我们可以预期列表是不完整的;然而,从功能上讲,它是有效的。

现在,如果我们重新思考这个问题:除了艾梅柏·希尔德,谁能把约翰尼·德普介绍给伊隆·马斯克?单次信息检索无法回答这类问题。这种类型的问题被称为多跳问答。解决这个问题的一个方法是:

  1. 找回埃隆·马斯克的所有联系人
  2. 找回约翰尼·德普的所有联系人
  3. 看看这两个结果之间是否有交集,除了艾梅柏·希尔德
  4. 如果有交集,返回结果,或者将埃隆·马斯克和约翰尼·德普的联系方式扩展到他们朋友的联系方式并再次检查。

有几种架构来适应这种复杂的算法,其中一个使用像ReACT这样复杂的prompt工程,另一个使用外部图形数据库来辅助推理。我们只需要知道这是RAG系统的限制之一。

4.信息丢失

如果我们看一下RAG系统中的流程链:

  1. 将文本分块(chunking)并生成块(chunk)的Embedding
  2. 通过语义相似度搜索检索数据块
  3. 根据top-k块的文本生成响应

我们会看到所有的过程都是有信息损失的,这意味着不能保证所有的信息都能保存在结果中。如上所述,由于分块大小的选择和Embedding模型的效用,分块和Embedding是有损耗的;由于我们使用的top_k限制和相似函数,检索过程不可能完美;由于内容长度的限制和生成式大语言模型的能力,响应生成过程并不完善。

如果我们把所有的限制放在一起,重新考虑一些公司即将推出的基于RAG的企业搜索,我真的很好奇它们能比传统的全文搜索引擎好多少。记住,传统的搜索引擎是很难被击败的。不久前,微软E5是第一个超越流行搜索算法BM25的LLM。

我的意思是,搜索引擎和LLM的结合是可行的,然而,简单的RAG很难比搜索引擎表现得更好。

我们的一些突破

确实,RAG是很难的,我前面的文章就说过一些注意点,包括数据处理、内容提取、分块策略、embedding模型选择等等,可以参考之前的文章《大模型主流应用RAG的介绍——从架构到技术细节》。下面是这篇文章里面的截图:

2

RAG应用中需要注意的关键点

最近我和小明对RAG做了一些广泛的测试,我在这里可以做一些简单的曝光。

a.通篇理解之后的检索生成

3

对于三网隔离的理解,原文中是需要多张PPT一起理解才能得到“三网融合”这个答案的,因为直接的答案这页PPT显示的是“三张网”。

b.多张PPT整合能力

4

对于为什么要用(爱快的优势),和连锁机构面临的挑战其实是两个PPT中的内容,我们的RAG是可以自动进行理解与整合的。

结论

RAG作为一种简单而强大的LLM应用程序设计模式,有其优点和缺点。我们确实需要彻底了解该技术才能对我们的设计充满信心。我个人的看法是,尽管所有关于LLM和惊人突破的炒作,大语言模型应该被视为企业AI架构的重要组成部分。它们不应该是主要框架本身。

LLM有限的权力是我担心的一个问题,可解释性是另一个问题。所有的大语言模型都像黑匣子一样工作。人们不知道自己是如何储存知识的,也不知道自己是如何推理的。对于我们自己玩玩的应用程序来说这不是一个主要问题,但在企业应用中却很关键。我们可以看到,越来越多的监管规则被发布,以确保AI不会造成伤害,我们必须提高基于LLM的应用在自己的适配方向上实现真正的价值。

在未来的研究中,我将探索如何将LLM与其他外部知识库(如图形数据库)相结合,以实现更难以实现的目标。


我们的创业项目已经上线!!!

TorchV AI,帮助企业快速进入AI时代!

具体详情,请点击官网咨询


最新内容,关注“土猛的员外”公众号

PostgreSQL——让关系型、向量和时间序列数据三位一体

尝试PostgreSQL的原因

最近两周我和小明在研究基于大模型的检索应用RAG,小明在实操层面有一些显著的突破,RAG性能应该已经超过了绝大部分目前开源市场上的同类产品,已经可以让我们有一些小小的满意了。但是优化创新的路依然可谓道阻且长,接下来我们准备在数据库上做一些新的尝试。

基于大模型的RAG应用,我们用到的数据库类型还是挺多的,主要有以下三种:

  • 向量(Vector)数据库:大模型和深度学习的都知道,这波AI浪潮中向量是基础,是对现实时间的语义级别的表示。区别于之前的关键词搜索的字面量搜索,向量搜索(相似度)可以认为是一种懂思想的搜索;
  • 关系型数据库(RDB):这是最传统和广泛的数据库,比如传统的PostgreSQL、MySQL、Oracle数据库等,即使在现在,关系型数据库依然是当今绝大多数应用系统运行的架构基础;
  • 时间序列数据库:时序数据库在元数据过滤中发挥了重大作用,它是一种记录事件和发生时间的数据库,对于时间序列的搜索速度非常快。在RAG应用中,如果行业知识文件被切分出几万个,那么使用时间过滤就会非常重要,比如我们只需要检索2023年3月份的合同文件,那么就可以用时序数据将目标chunk从几万个里面先挑出来,再进行向量计算。

目前我们用的数据看包括MySQL、elasticsearch和redis等,因为说整体的程序体量会变得非常大,结构松散,部署难度激增。而且对于后续的内部API管理也会感觉到非常杂乱。所以我们尝试使用PostgreSQL及其扩展,这样就可以在一个数据库上搞定了。

PostgreSQL介绍

对于熟悉PostgreSQL的朋友可以不用看这一节,前面的MySQL和PostgreSQL大战(口水战)我相信也会让很多人对PostgreSQL有一些关注。我不站任何一边,因为两个数据库在不同的应用中都在被使用。

PostgreSQL是世界上最受欢迎的数据库,它有一个很好的优点:它已经在生产中使用了30多年,强大而可靠,并且它有一个丰富的工具、驱动程序和连接器生态系统。之前除了关系型数据库,我们还用过PostGIS做地图导航应用,2018年转载过PostgreSQL的时序数据库Timescaledb的中文手册。面对AI,PostgreSQL其实已经有一个向量扩展——pgvector。虽然pgvector是一个很棒的扩展(它的所有功能都是作为Timescale vector的一部分提供的),但它只是为PostgreSQL上的AI应用程序开发人员提供生产级体验的一块拼图。在向企业技术人员调研中,发现pgvector需要增强的地方还有很多。

1

下面我们来看看PostgreSQL在向量领域的一些优劣势。

专用Vector数据库的问题

像Pinecone、Weaviate、Qdrant和Zilliz这样的向量数据库受益于人们对AI应用的兴趣激增,它们专门用于大规模存储和查询矢量数据,具有独特的功能,如近似最近邻(ANN)搜索和混合搜索的索引。但随着开发人员开始在他们的AI应用程序中使用它们,使用这些数据库构建的显著缺点变得清晰起来:

  • 操作复杂性:仅为向量数据持续维护单独的数据库增加了另一层操作开销,要求团队跨多个系统复制、同步和跟踪数据。更不用说备份、高可用性和监控了。
  • 学习曲线:工程团队浪费时间学习新的查询语言、系统内部、api和优化技术。
  • 可靠性:从头开始构建一个健壮的数据库是一个巨大的挑战,而且在生产环境中尤其注重健壮。大多数小众向量数据库都是未经证实的新兴技术,长期的稳定性和可靠性值得怀疑。

用我们采访的一位开发者的话来说:

“与几乎任何其他矢量存储相比,Postgres更适合生产,更可配置,并且在操作上更透明。”- LegalTech创业公司软件工程师

使用基于PostgreSQL的Timescale Vector的优势

借助PostgreSQL在关系型数据库方面的企业级应用优势,再加上Timescale Vector的向量特性,以及时间序列数据的结合,在AI应用中使用PostgreSQL是非常划算的,特别是在RAG等检索应用中。对于关系型数据库特性和时间序列数据特性我就不在这里介绍了,下面我们看看Timescale Vector的优势:

  • 对数百万个向量的更快的相似性搜索:由于引入了一种受DiskANN算法启发的新搜索索引,Timescale Vector在99%的召回率下实现了比专用数据库快3倍的搜索速度,并且在100万个OpenAI Embeddings(1536维)数据集上比全部现有的PostgreSQL搜索索引高出39.39%到1590.33%。此外,与pgvector相比,启用产品量化可以节省10倍的索引空间。Timescale Vector还提供pgvector的Hierarchical Navigable Small Worlds (HNSW,分层导航)和 Inverted File Flat(IVFFlat,倒置文件平面)索引算法。
  • Timescale Vector优化了基于时间的向量搜索查询:利用Timescale的超级表的自动基于时间的分区和索引,有效地找到最近的Embeddings,通过时间范围或文档存在年份约束向量搜索,并轻松存储和检索大型语言模型(LLM)响应和聊天历史。基于时间的语义搜索还使您能够使用检索增强生成(Retrieval Augmented Generation, RAG)和基于时间的上下文检索,从而为用户提供更有用的LLM响应。
  • 简化的AI基础设施堆栈:通过将向量Embeddings关系型数据时间序列数据组合在一个PostgreSQL数据库中,Timescale vector消除了大规模管理多个数据库系统所带来的操作复杂性。
  • 简化元数据处理和多属性过滤:开发人员可以利用所有PostgreSQL数据类型来存储和过滤元数据,并将向量搜索结果与关系数据连接起来,以获得更多上下文相关的响应。在未来的版本中,Timescale Vector将进一步优化丰富的多属性过滤,在过滤元数据时实现更快的相似性搜索。

2

在这些针对矢量工作负载的创新之上,Timescale vector提供了一个强大的、生产就绪的PostgreSQL云平台,具有灵活的定价、企业级安全性和免费的专家支持。

简单介绍一下DiskANN算法

上面提到了DiskANN算法,那我在文章就必须简要的补充一下。

当前最先进的近似最近邻搜索(ANNS)算法生成的索引必须存储在主存储器中以实现快速高查全率搜索——这使得它们非常昂贵,并且限制了数据集的大小。

DiskANN基于图形的索引和搜索系统,它只需要64GB RAM和廉价的固态硬盘(SSD),就可以在一个工作站上索引、存储和搜索十亿个点的数据库。与之前的认知相反,我们证明了DiskANN构建的基于SSD的索引可以满足大规模神经网络的所有三个要求:

  • 高召回率
  • 低查询延迟
  • 高密度(每个节点索引的点)。

在十亿个点SIFT1B大神经网络数据集上,DiskANN服务QPS>5000;在16核机器上,平均延迟为3ms, 95%+ 1-recall@1,其中最先进的十亿点ANNS算法具有类似的内存占用,如FAISS[18]和IVFOADC+G+P[8],稳定在50%左右1-recall@1。另外,在高召回率的情况下,与最先进的基于图的方法(如HNSW[21]和NSG[13])相比,DiskANN在每个节点上可以索引和服务5 - 10倍的点。最后,作为整个DiskANN系统的一部分,我们引入了Vamana,这是一个新的基于图的ANNS索引,它比现有的图索引更通用,甚至对于内存索引也是如此。

结合LlamaIndex使用Timescale Vector

以下结合LlamaIndex实操的内容摘自LlamaIndex创始人Jeff Liu的blog

在LlamaIndex中使用Timescale Vector的DiskANN、HNSW或IVFFLAT索引非常简单。

简单地创建一个Timescale Vector矢量存储,并添加数据节点,你想查询如下所示:

1
2
3
4
5
6
7
8
9
from llama_index.vector_stores import TimescaleVectorStore

# Create a timescale vector store with specified params
ts_vector_store = TimescaleVectorStore.from_params(
service_url=TIMESCALE_SERVICE_URL,
table_name="your_table_name",
time_partition_interval= timedelta(days=7),
)
ts_vector_store.add(nodes)

然后运行:

1
2
# Create a timescale vector index (DiskANN)
ts_vector_store.create_index()

这将使用默认参数创建一个Timescale Vector索引。

我们应该指出,“索引”这个术语有点过多了。对于许多vectorstore,索引是存储数据的东西(在关系数据库中通常称为表),但在PostgreSQL世界中,索引是加速搜索的东西,我们在这里使用后一种含义。

我们还可以在create_index 命令中指定创建索引的确切参数,如下所示:

1
2
# create new timescale vector index (DiskANN) with specified parameters
ts_vector_store.create_index("tsv", max_alpha=1.0, num_neighbors=50)

这个Timescale Vector的新DiskANN启发矢量搜索索引的优点包括:

  • 在PostgreSQL中以99%的准确率更快地进行向量搜索。
  • 优化运行在磁盘上,而不仅仅是在内存使用。
  • 量化优化兼容PostgreSQL,减少向量大小,从而缩小索引大小(在某些情况下10倍!),加快搜索。
  • 高效的混合搜索或过滤附加维度。

有关Timescale Vector的新索引如何工作的更多信息,请参阅这篇博客文章

Pgvector被打包为Timescale Vector的一部分,因此您也可以在LlamaIndex应用程序中访问Pgvector的HNSW和IVFFLAT索引算法。从LlamaIndex应用程序代码中方便地创建ANN搜索索引的能力使得创建不同的索引和比较它们的性能变得容易:

1
2
3
4
5
6
7
# Create an HNSW index
# Note: You don't need to specify m and ef_construction parameters as we set smart defaults.
ts_vector_store.create_index("hnsw", m=16, ef_construction=64)

# Create an IVFFLAT index
# Note: You don't need to specify num_lists and num_records parameters as we set smart defaults.
ts_vector_store.create_index("ivfflat", num_lists=20, num_records=1000)

结合LlamaIndex添加高效的基于时间的搜索功能

Timescale Vector优化了基于时间的向量搜索,利用Timescale的超级表的自动基于时间的分区和索引来有效地按时间和相似度搜索向量。

时间通常是矢量Embeddings的重要元数据组成部分。Embeddings的来源,如文档、图像和网页,通常都有一个与之相关的时间戳,例如,它们的创建日期、发布日期或最后更新日期等等。

我们可以利用向量Embeddings集合中的时间元数据,通过检索不仅在语义上相似而且与特定时间框架相关的向量来丰富搜索结果的质量和适用性。

以下是一些基于时间的矢量检索可以改进LlamaIndex应用程序的示例:

  • **查找最近的Embeddings:**查找语义上与查询向量相似的最近的Embeddings。例如,查找与选举有关的最新新闻、文件或社交媒体帖子。
  • **时间范围内搜索:**限制相似性搜索仅针对相关时间范围内的向量。例如,询问关于知识库的基于时间的问题(“在2023年1月到3月之间添加了哪些新功能?”)。
  • **聊天记录:**存储和检索LLM响应历史。例如,聊天机器人的聊天记录。

让我们看一个在git日志数据集上执行基于时间的搜索的例子。在git日志中,每个条目都有时间戳、作者和有关提交的一些信息。

为了说明如何使用TimescaleVector的基于时间的矢量搜索功能,我们将询问有关TimescaleDB的git日志历史的问题。每个git提交条目都有一个与之相关的时间戳,以及消息和其他元数据(例如,作者)。

我们将演示如何使用基于时间的UUID创建节点,以及如何使用Timescale Vector Vector存储运行带有时间范围过滤器的相似性搜索。

从git日志中的每个提交创建节点

首先,我们使用Pandas从demo CSV文件(https://s3.amazonaws.com/assets.timescale.com/ai/commit_history.csv)加载git日志条目:

1
2
3
4
5
6
7
import pandas as pd
from pathlib import Path


# Read the CSV file into a DataFrame
file_path = Path("../data/csv/commit_history.csv")
df = pd.read_csv(file_path)

接下来,我们将为git日志数据集中的每个提交创建类型为TextNode的节点,提取相关信息并分别将其分配给节点的文本和元数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from llama_index.schema import TextNode, NodeRelationship, RelatedNodeInfo
# Create a Node object from a single row of data
def create_node(row):
record = row.to_dict()
record_name = split_name(record["author"])
record_content = str(record["date"]) + " " + record_name + " " + str(record["change summary"]) + " " + str(record["change details"])
node = TextNode(
id_=create_uuid(record["date"]),
text= record_content,
metadata={
'commit': record["commit"],
'author': record_name,
'date': create_date(record["date"]),
}
)
return node

nodes = [create_node(row) for _, row in df.iterrows()]

Note: 上面的代码引用了两个辅助函数来获得正确格式的内容(split_name()create_date()),为了简洁起见,我们省略了它们。完整的代码包含在本文末尾参考资料部分链接的教程中。

根据每个git提交的日期为每个节点创建uuid

我们将仔细研究用于创建每个节点的id_的辅助函数。对于LlamaIndex中基于时间的搜索,Timescale Vector使用UUID v1的“datetime”部分将向量放置在正确的时间分区中。Timescale Vector的Python客户端库提供了一个简单易用的函数,名为uuid_from_time,用于从Python DateTime对象创建UUID v1,然后我们将使用它作为TextNodes的ids

1
2
3
4
5
6
7
8
9
from timescale_vector import client
# Function to take in a date string in the past and return a uuid v1
def create_uuid(date_string: str):
if date_string is None:
return None
time_format = '%a %b %d %H:%M:%S %Y %z'
datetime_obj = datetime.strptime(date_string, time_format)
uuid = client.uuid_from_time(datetime_obj)
return str(uuid)

由于我们过去处理的是时间戳,因此我们利用uuid_from_time函数来帮助为每个节点生成正确的uuid。如果希望将当前日期和时间与节点(或文档)相关联,以便进行基于时间的搜索,则可以跳过此步骤。默认情况下,当节点被添加到Timescale Vector中的表中时,将自动生成与当前日期和时间相关联的UUID。

让我们看一下节点的内容:

1
2
3
4
5
6
print(nodes[0].get_content(metadata_mode="all"))
commit: 44e41c12ab25e36c202f58e068ced262eadc8d16
author: Lakshmi Narayanan Sreethar
date: 2023-09-5 21:03:21+0850

Tue Sep 5 21:03:21 2023 +0530 Lakshmi Narayanan Sreethar Fix segfault in set_integer_now_func When an invalid function oid is passed to set_integer_now_func, it finds out that the function oid is invalid but before throwing the error, it calls ReleaseSysCache on an invalid tuple causing a segfault. Fixed that by removing the invalid call to ReleaseSysCache. Fixes #6037

为每个节点的文本创建矢量Embeddings

接下来,我们将创建每个节点内容的向量Embeddings,这样我们就可以对与每个节点相关联的文本执行相似性搜索。我们将使用OpenAIEmbedding模型来创建Embeddings。

1
2
3
4
5
6
7
8
9
# Create embeddings for nodes
from llama_index.embeddings import OpenAIEmbedding
embedding_model = OpenAIEmbedding()

for node in nodes:
node_embedding = embedding_model.get_text_embedding(
node.get_content(metadata_mode="all")
)
node.embedding = node_embedding

加载节点到Timescale Vector矢量存储

接下来,我们将创建一个“TimescaleVectorStore”实例,并将我们创建的节点添加到其中。

1
2
3
4
5
6
7
# Create a timescale vector store and add the newly created nodes to it
ts_vector_store = TimescaleVectorStore.from_params(
service_url=TIMESCALE_SERVICE_URL,
table_name="li_commit_history",
time_partition_interval= timedelta(days=7),
)
ts_vector_store.add(nodes)

为了利用Timescale Vector高效的基于时间的搜索,我们需要在实例化Timescale Vector Vector存储时指定time_partition_interval参数。此参数表示按时间划分数据的每个间隔的长度。每个分区将包含在指定时间长度内的数据。

在上面的例子中,为了简单起见,我们使用7天,但是您可以为您的应用程序使用的查询选择任何有意义的值—例如,如果您经常查询最近的向量,您可能希望使用较小的时间增量,例如一天,或者如果您查询长达十年的时间周期的向量,那么您可能希望使用较大的时间增量,例如六个月或一年。根据经验,普通查询应该只涉及几个分区,同时您的完整数据集应该适合1000个分区,但不要太过强调—系统对这个值不是很敏感。

带时间过滤器的相似度搜索

现在我们已经将包含向量Embeddings数据和元数据的节点加载到Timescale vector vector store中,并在存储向量和元数据的表上启用了自动基于时间的分区,我们可以使用基于时间的过滤器查询我们的vector store,如下所示:

1
2
3
4
5
6
7
8
9
10
# Query the vector database
vector_store_query = VectorStoreQuery(query_embedding = query_embedding, similarity_top_k=5)

# Time filter variables for query
start_dt = datetime(2023, 8, 1, 22, 10, 35) # Start date = 1 August 2023, 22:10:35
end_dt = datetime(2023, 8, 30, 22, 10, 35) # End date = 30 August 2023, 22:10:35

# return most similar vectors to query between start date and end date date range
# returns a VectorStoreQueryResult object
query_result = ts_vector_store.query(vector_store_query, start_date = start_dt, end_date = end_dt)

让我们看一下查询返回的节点的日期和内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# for each node in the query result, print the node metadata date
for node in query_result.nodes:
print("-" * 80)
print(node.metadata["date"])
print(node.get_content(metadata_mode="all"))
--------------------------------------------------------------------------------
2023-08-3 14:30:23+0500
commit: 7aeed663b9c0f337b530fd6cad47704a51a9b2ec
author: Dmitry Simonenko
date: 2023-08-3 14:30:23+0500

Thu Aug 3 14:30:23 2023 +0300 Dmitry Simonenko Feature flags for TimescaleDB features This PR adds..
--------------------------------------------------------------------------------
2023-08-29 18:13:24+0320
commit: e4facda540286b0affba47ccc63959fefe2a7b26
author: Sven Klemm
date: 2023-08-29 18:13:24+0320

Tue Aug 29 18:13:24 2023 +0200 Sven Klemm Add compatibility layer for _timescaledb_internal functions With timescaledb 2.12 all the functions present in _timescaledb_internal were…
--------------------------------------------------------------------------------
2023-08-22 12:01:19+0320
commit: cf04496e4b4237440274eb25e4e02472fc4e06fc
author: Sven Klemm
date: 2023-08-22 12:01:19+0320

Tue Aug 22 12:01:19 2023 +0200 Sven Klemm Move utility functions to _timescaledb_functions schema To increase schema security we do not want to mix…
--------------------------------------------------------------------------------
2023-08-29 10:49:47+0320
commit: a9751ccd5eb030026d7b975d22753f5964972389
author: Sven Klemm
date: 2023-08-29 10:49:47+0320

Tue Aug 29 10:49:47 2023 +0200 Sven Klemm Move partitioning functions to _timescaledb_functions schema To increase schema security…
--------------------------------------------------------------------------------
2023-08-9 15:26:03+0500
commit: 44eab9cf9bef34274c88efd37a750eaa74cd8044
author: Konstantina Skovola
date: 2023-08-9 15:26:03+0500

Wed Aug 9 15:26:03 2023 +0300 Konstantina Skovola Release 2.11.2 This release contains bug fixes since the 2.11.1 release…

成功!

请注意,只有在指定的开始和结束日期范围(2023年8月1日和2023年8月30日)内具有时间戳的向量才会包含在结果中。

下面是一些直观的原因,说明为什么Timescale Vector的基于时间的分区可以加速使用基于时间的过滤器的ANN查询。

Timescale Vector按时间对数据进行分区,并在每个分区上分别创建ANN索引。然后,在搜索过程中,我们执行一个三步过程:

  • 步骤1:过滤不匹配时间谓词的分区。
  • 步骤2:对所有匹配分区执行相似度搜索。
  • 步骤3:合并步骤2中每个分区的所有结果,重新排序,并按时间过滤结果。

Timescale Vector利用TimescaleDB的超级表,它根据时间戳自动划分向量和相关元数据。这使得通过与查询向量的相似性和时间对向量进行高效查询成为可能,因为不在查询时间窗口内的分区被忽略,通过一次过滤掉整个数据条,使得搜索效率大大提高。

当在TimescaleVectorStore上执行向量相似性搜索时,我们也可以指定一个时间过滤器,提供开始日期和时间增量,而不是指定搜索的开始和结束日期:

1
2
# return most similar vectors to query from start date and a time delta later
query_result = ts_vector_store.query(vector_store_query, start_date = start_dt, time_delta = td)

我们还可以在提供的end_date和时间增量中指定时间过滤器。此语法对于过滤搜索结果以在特定日期截止之前包含向量非常有用。

1
2
# return most similar vectors to query from end date and a time delta earlier
query_result = ts_vector_store.query(vector_store_query, end_date = end_dt, time_delta = td)

基于TimescaleVector的LlamaIndex应用中基于时间的上下文检索增强检索生成

让我们把所有内容放在一起,看看如何使用TimescaleVectorStore在我们上面检查的git日志数据集上为RAG提供动力。

为此,我们可以使用TimescaleVectorStore作为QueryEngine。在创建查询引擎时,我们使用TimescaleVector的时间过滤器,通过将时间过滤器参数vector_strore_kwargs传递,将搜索限制在相关的时间范围内。

1
2
3
4
5
6
7
8
9
from llama_index import VectorStoreIndex
from llama_index.storage import StorageContext

index = VectorStoreIndex.from_vector_store(ts_vector_store)
query_engine = index.as_query_engine(vector_store_kwargs = ({"start_date": start_dt, "end_date":end_dt}))

query_str = "What's new with TimescaleDB functions? When were these changes made and by whom?"
response = query_engine.query(query_str)
print(str(response))

我们问了LLM一个关于我们的git日志的问题,即“What`s new with TimescaleDB functions? When were these changes made and by whom?”(“TimescaleDB函数有什么新功能?”这些改动是什么时候做的,是谁做的?”)

下面是我们得到的响应,它综合了从语义搜索返回的节点和在Timescale Vector存储上基于时间的过滤:

1
TimescaleDB functions have undergone changes recently. These changes include the addition of several GUCs (Global User Configuration) that allow for enabling or disabling major TimescaleDB features. Additionally, a compatibility layer has been added for the "_timescaledb_internal" functions, which were moved into the "_timescaledb_functions" schema to enhance schema security. These changes were made by Dmitry Simonenko and Sven Klemm. The specific dates of these changes are August 3, 2023, and August 29, 2023, respectively.

这是一个强大概念的简单示例——在您的RAG应用程序中使用基于时间的上下文检索可以帮助为您的用户提供更相关的答案。这种基于时间的上下文检索对任何具有自然语言和时间成分的数据集都很有帮助。由于其高效的基于时间的相似性搜索功能,Timescale Vector可以独特地实现这一点,并且由于Timescale Vector集成,在LlamaIndex应用程序中利用它很容易。

引用

1.Timescaledb的中文手册:https://www.luxiangdong.com/2018/09/09/timescaledb-what/

2.How We Made PostgreSQL a Better Vector Database:https://www.timescale.com/blog/how-we-made-postgresql-the-best-vector-database/

3.Jeff Liu的Medium:https://medium.com/llamaindex-blog/timescale-vector-x-llamaindex-making-postgresql-a-better-vector-database-for-ai-applications-924b0bd29f0

4.微软的DiskANN算法介绍:https://www.microsoft.com/en-us/research/publication/diskann-fast-accurate-billion-point-nearest-neighbor-search-on-a-single-node/

5.Timescale Vector:https://www.timescale.com/ai

6.Timescale Vector Store的LlamaIndex教程:https://gpt-index.readthedocs.io/en/stable/examples/vector_stores/Timescalevector.html


我们的创业项目已经上线!!!

TorchV AI,帮助企业快速进入AI时代!

具体详情,请点击官网咨询


最新内容,关注“土猛的员外”公众号

手工微调embedding模型RAG检索能力

本文是一篇关于如何微调embedding的文章,原作者是Wenqi Glantz

主要内容:

  • 微调big-large-en开源embedding模型;
  • 具体实现代码细节;
  • 评测最终的提升效果。

在RAG应用中,有一个我们可以去提升的环节就是——Embedding模型,我在之前的文章《大模型主流应用RAG的介绍——从架构到技术细节》也说过可以去微调embedding模型以便增强我们整体的检索能力。

最早我们用的是OpenAI的Embedding模型text-embedding-ada-002,但这个模型后面不一定可以在正式环境中使用,而且我们也没办法去微调,因此让我们在本文中探索对开源Embedding模型进行微调。

BAAI/bge-small-en

目前HuggingFace的MTEB(海量文本Embedding基准)排行榜上排名第一的Embedding模型是big-large-en,它由北京人工智能研究院(BAAI,智源)开发。它是一种预训练的transformer模型,可用于各种自然语言处理任务,如文本分类、问答、文本生成等。该模型在海量文本和代码数据集上进行训练,并在海量文本Embedding基准(MTEB)上进行了微调。

在本文中,我们将使用 big-large-en的缩小版big-small-en,这是一个384维的小规模模型(OpenAI是1500+维),具有竞争力的性能,非常适合在Google Colab中运行。大家也可以选择中文版的bge-base-zh-v1.5,只有0.1G。当然你的硬件环境允许,也可以使用1.3G的bge-large-zh-v1.5等embedding模型。

微调Embedding模型与微调LLM

与LLM(大语言模型)微调相比,big-small-en微调的实现有一些不一样,下面简单说一下异同点:

相似点

  • 两种类型的微调都遵循相同的方法,即生成用于训练和评估的数据集,微调模型,最后评估基本模型和微调模型之间的性能。
  • 使用LLM自动生成训练和评估数据集。

不同点

  • 数据集内容在LLM微调和Embedding模型微调之间有所不同。用于LLM微调的数据集包含LLM生成的问题。在微调过程中,包括问题、答案、系统prompt等在内的一系列数据将以JSON行( jsonl)文件的形式传递给要进行微调的模型。

不同的是,用于Embedding模型微调的数据集包含以下三组:

  1. queriesnode_id映射和LLM生成的问题的集合。
  2. corpusnode_id映射和相应节点中的文本的集合。
  3. relevant_docs:查询的node_id和语料库 node_id之间的交叉引用映射的集合。给定一个查询,它告诉Embedding模型要查找哪个文本节点/语料库。
  • 由于我们使用开源Embedding模型bge-small-en ,微调的前提就是要先把它下载到您的本地环境。以Google Colab为例,经过微调的模型将被下载到笔记本的根目录中。
  • 评估方法在微调Embedding模型和微调LLM之间有所不同,我们可以使用Ragas框架来衡量精准度和答案相关性。然而,当使用Embedding模型微调时,我们无法测量答案的正确性,因为我们只能为我们的问题检索相关节点。相反,我们使用一个称为“命中率”的简单度量,这意味着对于每个(query, relevant_doc)对,我们用查询检索top-k文档,如果结果包含relevant_doc,则它被认为是“命中”的。该指标可用于专有Embeddings,如OpenAI的Embedding模型和开源Embedding模型。对于开源Embedding模型,我们还可以使用来自sentence_transformersInformationRetrievalEvaluator进行评估,因为它提供了一套更全面的指标。

微调Embedding模型似乎涉及到很多问题。幸运的是,LlamaIndex(我个人感觉LlamaIndex目前的发展可能会在RAG方面打败LangChain)在最近的0.8.21版本中引入以下关键类/函数,使得微调Embedding模型变得超级简单:

  • SentenceTransformersFinetuneEngine
  • generate_qa_embedding_pairs
  • EmbeddingQAFinetuneDataset

这些类和函数为我们抽象了底层的详细集成逻辑,使开发人员能够非常直观地调用它。

微调方法

为了可视化微调BAAI/big-small-en所涉及的主要任务,让我们看看下图:

img

如图中的数值所示,主要任务包括:

  1. 通过调用 EmbeddingQAFinetuneDataset函数generate_qa_embedding_pairs,自动生成评估和训练数据集的数据。
  2. 通过传入基本模型和训练数据集来构造SentenceTransformersFinetuneEngine,然后调用其finetune函数来训练基本模型。
  3. 创建经过微调的模型。
  4. 调用向量存储索引检索器检索相关节点并评估基本模型的命中率。
  5. 调用InformationRetrievalEvaluator来评估基本模型。
  6. 调用向量存储索引检索器检索相关节点并评估微调模型的命中率。
  7. 调用InformationRetrievalEvaluator来评估经过微调的模型。

基于LlamaIndex的微调Embeddings指南(文末有链接),我们将在我们的用例中微调bge-small-en模型。

实现细节

Step 1: 生成数据集

让我们使用LLM来自动生成训练和评估的数据集。

  • Load corpus

在我们的用例中NVIDIA的SEC 10-K文件(代码中和文末都有链接)是一个169页的PDF文档(你可以用你自己的中文PDF),所以我们需要在生成数据集时将文档分成两部分——一部分用于训练数据集,另一部分用于evalals数据集。

使用单独的数据集进行训练和评估被认为是一种很好的ML实践。可以调用load_corpus函数来收集训练数据集(前90页)或eval数据集(其余页面)的节点。下面是load_corpus的代码片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
!curl https://d18rn0p25nwr6d.cloudfront.net/CIK-0001045810/4e9abe7b-fdc7-4cd2-8487-dc3a99f30e98.pdf --output nvidia-sec-10k-2022.pdf

def load_corpus(docs, for_training=False, verbose=False):
parser = SimpleNodeParser.from_defaults()
if for_training:
nodes = parser.get_nodes_from_documents(docs[:90], show_progress=verbose)
else:
nodes = parser.get_nodes_from_documents(docs[91:], show_progress=verbose)

if verbose:
print(f'Parsed {len(nodes)} nodes')

return nodes

SEC_FILE = ['nvidia-sec-10k-2022.pdf']

print(f"Loading files {SEC_FILE}")

reader = SimpleDirectoryReader(input_files=SEC_FILE)
docs = reader.load_data()
print(f'Loaded {len(docs)} docs')

train_nodes = load_corpus(docs, for_training=True, verbose=True)
val_nodes = load_corpus(docs, for_training=False, verbose=True)

请记住,在LlamaIndex中,节点和页面并不完全匹配。对于一个169页的文档,结果显示它为训练数据集解析了97个节点,为evals数据集解析了91个节点。这两个数据集的节点数量足够接近。让我们继续。

img

  • 生成合成查询和数据集

现在,让我们生成训练和评估的数据集。请注意,我们这里没有传递LLM (gpt-3.5-turbo-0613),只有OpenAI API密钥。这是因为LlamaIndex的默认LLM是gpt-3.5-turbo-0613;如果没有定义LLM,只要提供OpenAI API密钥,则默认为它。

generate_qa_embedding_pairs是一个生成数据集的方便函数。基于上面load_corpus函数返回的节点,它为每个节点生成问题(默认为每个节点两个问题,可以自定义),然后用所有三组数据构建数据集:queriescorpusrelevant_docs(queriescorpus之间的映射对应的node_id)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from llama_index.finetuning import (
generate_qa_embedding_pairs,
EmbeddingQAFinetuneDataset,
)
from llama_index.llms import OpenAI

os.environ["OPENAI_API_KEY"] = "sk-############"
openai.api_key = os.environ["OPENAI_API_KEY"]

train_dataset = generate_qa_embedding_pairs(train_nodes)
val_dataset = generate_qa_embedding_pairs(val_nodes)

train_dataset.save_json("train_dataset.json")
val_dataset.save_json("val_dataset.json")

train_dataset = EmbeddingQAFinetuneDataset.from_json("train_dataset.json")
val_dataset = EmbeddingQAFinetuneDataset.from_json("val_dataset.json")

下面是样本训练数据集的样子。注意queriescorpus在截图中是折叠的,因为每个都有超过100个数据对:

img

Step 2: 微调Embedding模型

SentenceTransformersFinetuneEngine就是为这个任务设计的。在底层,它执行多个子任务:

  • 通过构建SentenceTransformer加载预训练模型,传入BAAI/big-small-en模型id。
  • 定义数据加载器。它加载我们的训练数据集,将其解析为查询语料库relevant_docs。然后循环查询,将relevant_docs中的node_idcorpus中的文本节点进行映射,构造InputExample,其列表依次传递到创建DataLoader中.
  • 定义loss(损失函数)。它使用sentence_transformers multiplenegativerankingloss来训练检索设置的Embeddings。
  • 定义评估器。它设置了一个带有eval数据集的评估器来监控Embedding模型在训练期间的表现。
  • 运行训练。它插入上面定义的数据加载器、损失函数和评估器来运行训练。

LlamaIndex将微调Embedding模型的所有详细子任务封装在一个SentenceTransformersFinetuneEngine中,我们所需要做的就是调用它的finetune函数。下面,您可以看到展示LlamaIndex的代码片段:

1
2
3
4
5
6
7
8
9
10
11
12
from llama_index.finetuning import SentenceTransformersFinetuneEngine

finetune_engine = SentenceTransformersFinetuneEngine(
train_dataset,
model_id="BAAI/bge-small-en",
model_output_path="test_model",
val_dataset=val_dataset,
)

finetune_engine.finetune()

embed_model = finetune_engine.get_finetuned_model()

Step 3: 评估微调后的模型

如上所述,我们使用两种不同的评估方法:

  • 命中率:对每个query / relevant_doc对进行简单的top-k检索。如果搜索结果包含relevant_doc,那么它就是一个“命中”。这可以用于专有的Embeddings,例如OpenAI的Embedding模型和开源Embedding模型。请参阅下面代码片段中的evaluate函数。

  • InformationRetrievalEvaluator:一个更全面的用于评估开源Embeddings的度量套件。请参阅下面代码片段中的evaluate_st函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
from llama_index.embeddings import OpenAIEmbedding
from llama_index import ServiceContext, VectorStoreIndex
from llama_index.schema import TextNode
from tqdm.notebook import tqdm
import pandas as pd

# function for hit rate evals
def evaluate(
dataset,
embed_model,
top_k=5,
verbose=False,
):
corpus = dataset.corpus
queries = dataset.queries
relevant_docs = dataset.relevant_docs

service_context = ServiceContext.from_defaults(embed_model=embed_model)
nodes = [TextNode(id_=id_, text=text) for id_, text in corpus.items()]
index = VectorStoreIndex(nodes, service_context=service_context, show_progress=True)
retriever = index.as_retriever(similarity_top_k=top_k)

eval_results = []
for query_id, query in tqdm(queries.items()):
retrieved_nodes = retriever.retrieve(query)
retrieved_ids = [node.node.node_id for node in retrieved_nodes]
expected_id = relevant_docs[query_id][0]
is_hit = expected_id in retrieved_ids # assume 1 relevant doc

eval_result = {
"is_hit": is_hit,
"retrieved": retrieved_ids,
"expected": expected_id,
"query": query_id,
}
eval_results.append(eval_result)
return eval_results


from sentence_transformers.evaluation import InformationRetrievalEvaluator
from sentence_transformers import SentenceTransformer

def evaluate_st(
dataset,
model_id,
name,
):
corpus = dataset.corpus
queries = dataset.queries
relevant_docs = dataset.relevant_docs

evaluator = InformationRetrievalEvaluator(queries, corpus, relevant_docs, name=name)
model = SentenceTransformer(model_id)
return evaluator(model, output_path="results/")
  • 评测OpenAI

现在,让我们评估一下OpenAI的Embedding模型text-embedding-ada-002。代码如下:

1
2
3
4
5
6
ada = OpenAIEmbedding()
ada_val_results = evaluate(val_dataset, ada)

df_ada = pd.DataFrame(ada_val_results)

hit_rate_ada = df_ada['is_hit'].mean()

结果:

img

  • 评测BAAI/bge-small-en
1
2
3
4
5
6
7
8
bge = "local:BAAI/bge-small-en"
bge_val_results = evaluate(val_dataset, bge)

df_bge = pd.DataFrame(bge_val_results)

hit_rate_bge = df_bge['is_hit'].mean()

evaluate_st(val_dataset, "BAAI/bge-small-en", name='bge')

结果:

img

  • 评估微调后的model
1
2
3
4
5
6
7
8
finetuned = "local:test_model"
val_results_finetuned = evaluate(val_dataset, finetuned)

df_finetuned = pd.DataFrame(val_results_finetuned)

hit_rate_finetuned = df_finetuned['is_hit'].mean()

evaluate_st(val_dataset, "test_model", name='finetuned')

查看结果:

img

  • Summary of results

把评测结果放在一起,让我们仔细看看。

命中率:我们的微调模型比其基本模型bge-small-en的性能提高了1.29%。与OpenAI的Embedding模型相比,我们的微调模型的性能仅低了4.85%。

img

InformationRetrievalEvaluator结果:经过微调的模型比其基本模型的性能提高了5.81%。与基本模型相比,微调模型对这30多个指标列中的每一个都有更好的数字。

img

总结

在本文中,我们探讨了微调RAG管道的Embedding模型所涉及的步骤。我们使用开源的sentence_transformers模型BAAI/big-small-en作为我们的基本Embedding模型,介绍了如何生成用于训练和评估的数据集,如何对其进行微调,以及如何评估基本模型和微调模型之间的性能差异。

评估结果表明,微调Embedding模型的性能比基本模型提高了1-6%,与OpenAI的Embedding模型相比,微调模型的性能损失仅为4.85%。这种性能提升可能因数据集的质量和数量而异。

我们还简要探讨了LlamaIndex的最新版本,该版本对任何Embedding模型的线性适配器进行了微调,从而提高了性能并避免了在RAG管道中重新嵌入文档。

引用


Update: 2024-01-26

我们的TorchV Bot产品目前已经开始试用了,详情可以点击:https://www.luxiangdong.com/2024/01/25/lanuch-1
目前只接受企业用户试用,需要您填写一些信息,必要信息如下:

邮箱: 用来接收地址和账号
如何称呼您:
所服务的公司:
您的职位:

当然,如果您可以告诉我们您的使用场景,我们将更加感激!
对了,可以发送到yuanwai@mengjia.net
另外,也可以直接加我微信(lxdhdgss)联系我。


我们的创业项目已经上线!!!

TorchV AI,帮助企业快速进入AI时代!

具体详情,请点击官网咨询


最新内容,关注“土猛的员外”公众号

Java21新特性——ZGC、虚拟线程和结构化并发

前两天同事和我说现在可以回来看看Java了,Java17可能更新的还不多,但是Java21这次释放了一大波新特性,会是接下来五六年的一个新起点,至少这次Java21支持到2026年9月。于是我抽了点时间看了一下Java21,确实有很多新特性,总结其中几个,做个收藏。

下面就先记录三种主要特性吧:

  • 新的垃圾收集器——Generational ZGC;
  • Java的“协程”——Virtual Threads;
  • 结构化并发——Structured Concurrency。

Garbage Collection – Level Up Coding

一.Generational ZGC:新的垃圾收集器

垃圾收集”的概念本质上是关于自动内存管理的。每次创建新的数据结构时,运行时都需要内存来完成。如果我们不再需要一个对象或留下一个堆栈框架,并且它的变量超出了作用域,就必须有人清理相关的内存,即“垃圾”,否则我们将很快了解Java的OutOfMemoryError

垃圾收集(GC)的缺点是需要时间来清理和重新安排内存,这会引入了一定的运行时开销,因为GC运行的实际发生时间点通常是不确定的,并且不是手动触发的。特别是在多线程上扩展的高吞吐量、大型内存消耗的应用程序可能会遭受长时间的“GC暂停”,GC暂停的另外一个更可怕的名字是“世界停止”。但是Java之所以能和C/C++抢夺市场,很重要的一点就是Java程序员不需要手动去回收变量的内存占用,GC在这一方面贡献巨大。

1.如何选择GC

GC算法主要关注三个指标:

  • 吞吐量:在长时间内未使用GC的总时间的百分比。
  • 延迟:应用程序的整体响应性,受GC暂停的影响。
  • Footprint:应用对内存的需求,比如内存使用100M/s、GC速度80M/s,10s内存进行一次回收,那内存剩下的200M称为峰值。

与许多问题一样,你不可能针对所有问题进行优化,所以每个GC都需要在它们之间找到一个平衡。以下是一些场景及其匹配GC:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
 垃圾回收器 | 使用场景
====================|==========================================
* Serial | * 小数据集 (最大~100 MB max)
| * 有限的资源 (e.g., single core)
| * 低暂停时间
--------------------|------------------------------------------
* Parallel | * 多核系统的峰值性能
| * 高计算负载
| * 大于1秒的暂停是可以接受的
--------------------|------------------------------------------
* G1 | * 响应时间超过吞吐量
* CMS | * 大堆heap
| * 暂停时间小于1秒
--------------------|------------------------------------------
* Shenandoah | * 最小化暂停时间
| * 可预测的延迟
--------------------|------------------------------------------
* ZGC | * 响应时间是高优先级的,和/或
| * 非常大的堆heap
--------------------|------------------------------------------
* Epsilon GC | * 性能测试和故障排除

每种方法都有自己的优缺点,这在很大程度上取决于应用程序的需求和可用资源。

Generational ZGC在表里面的选择情况已经说的比较清楚了。

2.世代特点

“世代假设”是一种观察结果,即年轻的对象比老的对象更有可能“消失”,至少在大多数情况下是这样。这就是为什么根据对象的年龄不同处理它们是有益的。改进年轻对象的收集需要更少的资源并产生更多的内存。

即使不按世代处理,ZGC在GC暂停时间上也有很大的改进,至少在有足够的可用资源的情况下,它回收内存的速度比并发线程消耗内存的速度要快。但是,所有对象都将被存储,而不考虑它们的年龄,并且在GC运行时必须检查所有对象。

在Java 21中,ZGC将堆划分为两个逻辑代:一个用于最近分配的对象,另一个用于分配长期对象。GC可以专注于更频繁地收集更年轻、更有希望的对象,而无需增加暂停时间,将它们保持在1毫秒以下。

分代ZGC可以带来的好处:

  • 减低分配失速的风险
  • 降低所需的堆开销
  • 降低GC对CPU的影响

与不分代的ZGC相比,所有这些优点都不会显著降低吞吐量。此外,不需要自己配置代的大小、使用的线程或年龄限制。

3.如何使用

在典型的Java方式中,新的ZGC不会在可用时立即强加给我们。相反,它将与它的非代际前身一起提供。你可以用java 参数来配置:

1
2
3
4
5
# Enable ZGC (defaults to non-generational)
$ java -XX:+UseZGC

# Use Generational ZGC
$ java -XX:+UseZGC -XX:+ZGenerational

请注意,一代ZGC应该随着时间的推移取代它的前身,并成为默认的ZGC。在这一点上,你可以用拮抗参数来关闭它,通过将+(加)替换为-(减/破折号):

1
2
# Do not use Generational ZGC
$ java -XX:+UseZGC -XX:-ZGenerational

它还计划在以后的版本中完全删除非分代ZGC。

Java Virtual Threads — Easy introduction | by Ram Lakshmanan | Medium

2.Virtual Threads——Java自己的“协程”

Java的多线程并发编程一直是比较难学的,像我这样的老程序员,在设计多线程的时候,还要自己“心算”一下会不会出问题,每次都需要非常谨慎。但后来GoLang的协程出现,你会非常羡慕为什么人家可以用的如此丝滑。这不,Java自己的“协程”也来了,它就是虚拟线程——Virtual Threads。

其实虚拟线程在JDK19或JDK20中就开始出现,只是在Java21中变得更加成熟和自然。

1.虚拟线程和协程的异同点

虚拟线程是基于协程的线程,与其他语言中的协程有相似之处,但也有一些不同之处。虚拟线程附属于主线程,如果主线程被销毁,虚拟线程将不再存在。

以下是一些相似点与区别:

相似点:

  • 虚拟线程(Virtual Threads)和协程都是轻量级的,它们的创建和销毁开销比传统的操作系统线程要小。
  • Virtual Threads和协程都可以通过挂起和恢复在线程之间切换,从而避免线程上下文切换的开销。
  • Virtual Threads和协程都可以异步和非阻塞的方式处理任务,提高应用程序的性能和响应能力。

区别:

  • Virtual Threads在JVM级别实现,而协程在语言级别实现。因此,虚拟线程的实现可以与任何支持JVM的语言一起使用,而协程的实现需要特定的编程语言支持。
  • Virtual Threads是基于线程的协程实现,所以他们可以使用线程相关的api,如ThreadLocalLockSemaphore。协程不依赖于线程,通常需要特定的异步编程框架和api。
  • Virtual Threads的调度由JVM管理,协程的调度由编程语言或异步编程框架管理。因此,Virtual Threads可以更好地与其他线程协作,而协程则更适合处理异步任务。

2.虚拟线程的优势

一般来说,虚拟线程是一种新的线程类型,它可以提高应用程序的性能和资源利用率,同时还可以使用传统的线程相关api。虚拟线程与协程有许多相似之处,但也有一些不同之处。

Virtual Threads确实可以使多线程编程更容易、更高效。与传统的操作系统线程相比,创建和销毁虚拟线程的开销更小,线程上下文切换的开销也更小,因此可以大大降低多线程编程中的资源消耗和性能瓶颈。

使用Virtual Threads,开发人员可以像编写传统线程代码一样编写代码,而不必担心线程的数量和调度,因为JVM将自动管理虚拟线程的数量和调度。此外,虚拟线程还支持传统的线程相关api,如ThreadLocalLockSemaphore,这使得开发人员更容易将传统线程代码迁移到虚拟线程中。

虚拟线程的引入使多线程编程更高效、更简单、更安全,允许开发人员更多地关注业务逻辑,而不必过多关注底层线程管理。

3.如何使用虚拟线程

首先,声明一个线程类,实现from Runnable,并实现run方法。

1
2
3
4
5
6
7
8
9
10
11
12
public class SimpleThread implements Runnable{

@Override
public void run() {
System.out.println("name:" + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}

然后,您可以使用这个线程类并启动线程。

1
2
Thread thread = new Thread(new SimpleThread());
thread.start();

拥有虚拟线程后,如何实现它?

1
Thread.ofPlatform().name("thread-test").start(new SimpleThread());

下面是使用虚拟线程的几种方法。

1. 直接启动一个虚拟线程

1
Thread thread = Thread.startVirtualThread(new SimpleThread());

2. 使用ofVirtual(),构建器模式启动虚拟线程,您可以设置线程名称,优先级,异常处理和其他配置

1
2
3
4
5
6
7
8
9
10
11
Thread.ofVirtual()
.name("thread-test")
.start(new SimpleThread());
// Or
Thread thread = Thread.ofVirtual()
.name("thread-test")
.uncaughtExceptionHandler((t, e) -> {
System.out.println(t.getName() + e.getMessage());
})
.unstarted(new SimpleThread());
thread.start();

3. 使用Factory创建线程

1
2
3
4
ThreadFactory factory = Thread.ofVirtual().factory();
Thread thread = factory.newThread(new SimpleThread());
thread.setName("thread-test");
thread.start();

4. 使用Executor

1
2
3
ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor();
Future<?> submit = executorService.submit(new SimpleThread());
Object o = submit.get();

image-20230926164502116

3.Structured Concurrency——结构化并发

结构化并发是一种编程范例,旨在通过提供结构化且易于遵循的方法来简化并发编程。使用结构化并发,开发人员可以创建更容易理解和调试的并发代码,并且更不容易出现竞争条件和其他与并发相关的错误。在结构化并发中,所有并发代码都被结构化为定义良好的工作单元,称为任务。任务以结构化的方式创建、执行和完成,并且任务的执行总是保证在其父任务完成之前完成。

1.结构化并发优势

结构化并发可以使多线程编程更容易、更可靠。在传统的多线程编程中,线程的启动、执行和终止都是由开发人员手动管理的,因此容易出现线程泄漏、死锁和异常处理不当等问题。

使用结构化并发,开发人员可以更自然地组织并发任务,使任务之间的依赖关系更清晰,代码逻辑更简洁。结构化并发还提供了一些异常处理机制,以便更好地管理并发任务中的异常,避免异常导致的程序崩溃或数据不一致。

此外,结构化并发还可以通过限制并发任务的数量和优先级来防止资源竞争和短缺。这些特性使开发人员更容易实现高效可靠的并发程序,而无需过多关注底层线程管理。

2.使用方法

想想下面的场景。假设您有三个任务要同时执行。只要其中任何一个任务完成并返回结果,就可以直接使用该结果,而其他两个任务可以停止。例如,天气服务通过三个通道获取天气条件,只要有一个通道返回它。

当然,在这种情况下,在Java 8下应该做的事情也是可能的。

1
2
3
List<Future<String>> futures = executor.invokeAll(tasks);

String result = executor.invokeAny(tasks);

使用ExecutorService的invokeAll和invokeAny实现,但是会有一些额外的工作。获得第一个结果后,需要手动关闭另一个线程。

在JDK21中,它可以通过结构化编程实现。

ShutdownOnSuccess捕获第一个结果并关闭任务范围以中断未完成的线程并唤醒调用线程。

在这种情况下,任何子任务的结果都可以直接获得,而无需等待其他未完成任务的结果。

它定义了获取第一个结果或在所有子任务失败时抛出异常的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void main(String[] args) throws IOException {
try (var scope = new StructuredTaskScope.ShutdownOnSuccess<String>()) {
Future<String> res1 = scope.fork(() -> runTask(1));
Future<String> res2 = scope.fork(() -> runTask(2));
Future<String> res3 = scope.fork(() -> runTask(3));
scope.join();
System.out.println("scope:" + scope.result());
} catch (ExecutionException | InterruptedException e) {
throw new RuntimeException(e);
}
}

public static String runTask(int i) throws InterruptedException {
Thread.sleep(1000);
long l = new Random().nextLong();
String s = String.valueOf(l);
System.out.println(i + "task:" + s);
return s;
}

ShutdownOnFailure

执行多个任务,只要其中一个失败(发生异常或抛出其他活动异常),停止其他未完成的任务,并使用作用域。未能捕获并抛出异常。

如果所有任务都没问题,则使用Feature.get()*Feature.resultnow()来获取结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public static void main(String[] args) throws IOException {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Future<String> res1 = scope.fork(() -> runTaskWithException(1));
Future<String> res2 = scope.fork(() -> runTaskWithException(2));
Future<String> res3 = scope.fork(() -> runTaskWithException(3));
scope.join();
scope.throwIfFailed(Exception::new);

String s = res1.resultNow();
System.out.println(s);
String result = Stream.of(res1, res2,res3)
.map(Future::resultNow)
.collect(Collectors.joining());
System.out.println("result:" + result);
} catch (Exception e) {
e.printStackTrace();
}
}

public static String runTaskWithException(int i) throws InterruptedException {
Thread.sleep(1000);
long l = new Random().nextLong(3);
if (l == 0) {
throw new InterruptedException();
}
String s = String.valueOf(l);
System.out.println(i + "task:" + s);
return s;
}

其他

Java21还有很多新特性,比如Scoped Values、String Templates、Switch Pattern Matching、Sequenced Collections、Record Patterns等,后面有时间在写,哈。


我们的创业项目已经上线!!!

TorchV AI,帮助企业快速进入AI时代!

具体详情,请点击官网咨询


最新内容,关注“土猛的员外”公众号

大模型主流应用RAG的介绍——从架构到技术细节

本文主要内容:

  • 大语言模型(LLM)在实际应用中存在的问题;
  • 什么是RAG——检索增强生成;
  • RAG架构解析
  • RAG技术架构的细节展示

写在前面

如果你问我现在基于LLM(大语言模型,本文有时候也会讲该名词描述为“大模型”)最火热的应用是什么,那我必须主推检索增强生成(RAG,Retrieval Augmented Generation)。RAG最初是为了解决LLM的各类问题的(后面会提到)产生的,但后面大家发现在现阶段的很多企业痛点上,使用RAG好像是更好的解决方案。就像我之前的文章《关于LLM的一些声音总结》提到的一样,企业发现其实自己要的好像是一个更好地搜索,根本不关心是不是大模型。于是,RAG被越来越多提到,包括开源的ChatPDF,也是RAG的一个经典应用。

但是我相信很多去实践RAG的人已经发现了一个情况,就是RAG入门很简单,但要真正达到企业应用的要求很难。而且RAG组成中的各类组件、流程和AI技术都过于复杂,很多人不知道如何去下手优化。

所以本文我们就来聊聊RAG,以及关于RAG优化的一些看法。

LLM的问题

首先我们还是要承认,RAG不管多厉害,它还是基于LLM的,脱离了LLM,RAG会脱离“人味儿”。

但是在今年年初的那一波大模型潮里面,我们发现如果仅仅依靠LLM,会有很多限制阻碍着我们前进,以下三点是最主要的:

  • 幻觉问题:大模型的底层原理是基于概率,所以它有时候会一本正经胡说八道,比如我们问大模型的Chat(问答系统),“XXX博物院下周一开门吗?”我相信这样的问题你不能联系问,因为大模型会有一定的几率告诉你开门。而如果游客真的在下周一去了XXX博物院,那估计就要失望了,如果这个Chat还是博物院官方提供的,那事情最终会演变成一通12345的投诉电话。所以在很多需要非常精确的场景,仅仅依赖GPT的这种生成式回答是很不严谨的,而且看很难消除——目前常见的解决方案是前置一个BERT,或者预置大量prompt做优化。
  • 新鲜度问题:规模越大(参数越多、tokens越多),大模型训练的成本越高。类似OpenAI的ChatGPT3.5,目前的数据新鲜度依然保留在2021年,对于之后的事情就不知道了。而且对于一些高时效性的事情,大模型更加无能为力,比如帮我看看今天晚上有什么电影值得去看?这种任务是需要去淘票票、猫眼等网站先去获取最新电影信息的,大模型本身无法完成这个任务。
  • 数据安全:OpenAI已经遭到过几次隐私数据的投诉,而对于企业来说,如果把自己的经营数据、合同文件等机密文件和数据上传到互联网上的大模型,那想想都可怕。如果企业人员想提一个类似这样的问题:“帮我看看3月份XX部门的销售环比数据与哪些兄弟部门的增长是密切相关的?”,这需要打穿企业内部的很多数据。既要保证安全,又要借助AI能力,那么最好的方式就是把数据全部放在本地,企业数据的业务计算全部在本地完成。而在线的大模型仅仅完成一个归纳的功能,甚至,LLM都可以完全本地化部署。

其实问题还有很多,包括tokens的限制,虽然这个长期来看不是问题,各LLM供应商的tokens数量限制肯定会越来越大。但是,费用也许就是另外一个需要考虑的问题了。

在解决这些问题的方法上,目前RAG走的比较前面。有些朋友应该已经了解过LangChain,它是架在LLM之上的一个应用框架,帮助人们快速开发基于LLM的应用。它的很多功能其实也属于RAG范畴。

什么是RAG

为RAG铺垫了这么多,下面我们来看看什么是RAG。

从开头的介绍里面大家已经可以看到,RAG——Retrieval Augmented Generation,检索增强生成。它的主要作用是生成(最终的答案),但是它先做了对现有文档的检索,而不是任由LLM来发挥。下面我提供讲一个浅显的例子来说明一下RAG:

假设一个工程师需要从厚厚的《业务操作手册》中找到相关的业务知识来帮助他完成工作,那么他有三种方式可以使用:

  • 最原始:他可以去翻阅这么厚厚的《业务操作手册》,或者用去查询这么《业务操作手册》的电子版,然后认真阅读掌握操作方法。当然,如果他碰到的业务知识比较复杂,他就需要自己去综合这么书上面的多个章节的内容,并融会贯通;
  • 借助问答机器人:他也可以直接去咨询问答机器人(chatbot),机器人也会把相应的知识吐给你。但是它可能有两个麻烦,一是它类似于FAQ,一问一答,还是需要自己去组合所有的回答内容;二是这个机器人需要前期大量的预训练知识库,需要专业的工程师去一条一条(其实是一个个知识条目,包括答案和多个相似问法),工作量极大,不太适合大面试推广使用;
  • RAG:RAG的操作方式就是我们可以直接把这个《业务操作手册》的电子版上传到系统,系统在几分钟之内就可以把这篇“巨著”变成索引,供刚才那位工程师咨询。而且RAG给的答案会去综合正本手册的多个相关知识点,并用“专家”一样的口吻来给你答案:“要解决这个问题,你需要先解决两个前提,有三种方法来解决。下面我们一步步来看怎么做….”。

好了,我们现在知道RAG是个什么玩意儿了。你有没有发现,它其实会把之前的FAQ问答给取代掉,但是它能做的远远不止这些,它还是很多应用的中间件。我们自己的一个面向文博场馆的产品就是基于RAG的,大概占了整个系统的1/3比重。另外,RAG不仅仅面向文本,它还可以面向语音、视频和图像等多模态场景,只要可以embedding的内容就可以,当然这些我们这里就不多介绍了。

RAG架构

下面我们来了解一下RAG,它有非常多的组件,但是我们可以化繁为简。我喜欢把RAG——Retrieval Augmented Generation理解为Retrieval And Generation,也就是检索与生成,在加上一个数据向量和索引的工作,我们对RAG就可以总概方式地理解为“索引、检索和生成”。

以下就是RAG的主要组成,依次是数据提取——embedding(向量化)——创建索引——检索——自动排序(Rerank)——LLM归纳生成。当然这里少了使用环节,我们暂时先忽略用户提问的环节。

1

RAG技术细节概览

在技术细节上,我们还可以分成更细的组成。

2

一、数据索引

  • 数据提取

    • 数据清洗:包括数据Loader,提取PDF、word、markdown以及数据库和API等;
    • 数据处理:包括数据格式处理,不可识别内容的剔除,压缩和格式化等;
    • 元数据提取:提取文件名、时间、章节title、图片alt等信息,非常关键。
  • 分块(Chunking)

    • 固定大小的分块方式:一般是256/512个tokens,取决于embedding模型的情况。但是这种方式的弊端是会损失很多语义,比如“我们今天晚上应该去吃个大餐庆祝一下”,很有可能就会被分在两个chunk里面——“我们今天晚上应该”、“去吃个大餐庆祝一下”。这样对于检索是非常不友好的,解决方法是增加冗余量,比如512tokens的,实际保存480tokens,一头一尾去保存相邻的chunk头尾的tokens内容;
    • 基于意图的分块方式:
      • 句分割:最简单的是通过句号和换行来做切分。当然也有通过专业的意图包来切分的,常用的意图包有基于NLP的NLTK和spaCy;
      • 递归分割:通过分治的方法,用递归切分到最小单元的一种方式;
      • 特殊分割:还有很多不常见的,用于特殊场景,这里就不提了。
    • 影响分块策略的因素:
      • 取决于你的索引类型,包括文本类型和长度,文章和微博推文的分块方式就会很不同;
      • 取决于你的模型类型:你使用什么LLM也会有不同,因为ChatGLM、ChatGPT和Claude.ai等的tokens限制长度不一样,会影响你分块的尺寸;
      • 取决于问答的文本的长度和复杂度:最好问答的文本长度和你分块的尺寸差不多,这样会对检索效率更友好;
      • 应用类型:你的RAG的应用是检索、问答和摘要等,都会对分块策略有不同的影响。
  • 向量化(embedding):这是将文本、图像、音频和视频等转化为向量矩阵的过程,也就是变成计算机可以理解的格式,embedding模型的好坏会直接影响到后面检索的质量,特别是相关度。关于embedding大家可以看我之前的一篇文章《大模型应用中大部分人真正需要去关心的核心——Embedding》,一般我们现在可以选择的embedding模型有这些:

    • BGE:这是国人开发的中文embedding模型,在HuggingFace的MTEB(海量文本Embedding基准)上排名前2,实力强劲;
    • M3E:也是国人开发的中文embedding模型,我们之前用的就是这个模型,总体来说也算可以,这个还看大家的使用场景,也许你的场景会比我们更加适用;
    • 通义千问的embedding模型:因为是1500+维的模型,所以我们在国庆节后准备用用看;
    • Text-embedding-ada-002:这是OpenAI的embedding模型,1536维,我感觉上应该是目前最好的模型,但是它在MTEB上排名好像只有第六,但是国内应该也不太能用,所以我们就放弃了;
    • 自己训练embedding模型:这是最酷的了,我过几天会专门写一篇如何训练embedding模型的文章,没有关注我的可以先关注,哈。当然,训练是基于一个既有embedding模型的,一般我们有希望让它在原来的基础上提升3%-10%的性能。

二、检索环节(Retriever)

检索环节技术含量依然很高,而且对于我们目前来说,还有一两项工作正在进行中。

检索优化一般分为下面五部分工作:

  • 元数据过滤:当我们把索引分成许多chunks的时候,检索效率会成为问题。这时候,如果可以通过元数据先进行过滤,就会大大提升效率和相关度。比如,我们问“帮我整理一下XX部门今年5月份的所有合同中,包含XX设备采购的合同有哪些?”。这时候,如果有元数据,我们就可以去搜索“XX部门+2023年5月”的相关数据,检索量一下子就可能变成了全局的万分之一;

  • 图关系检索:如果可以将很多实体变成node,把它们之间的关系变成relation,就可以利用知识之间的关系做更准确的回答。特别是针对一些多跳问题,利用图数据索引会让检索的相关度变得更高;

  • 检索技术:前面说的是一些前置的预处理的方法,检索的主要方式还是这几种:

    • 相似度检索:前面我已经写过那篇文章《大模型应用中大部分人真正需要去关心的核心——Embedding》种有提到六种相似度算法,包括欧氏距离、曼哈顿距离、余弦等,后面我还会再专门写一篇这方面的文章,可以关注我,yeah;
    • 关键词检索:这是很传统的检索方式,但是有时候也很重要。刚才我们说的元数据过滤是一种,还有一种就是先把chunk做摘要,再通过关键词检索找到可能相关的chunk,增加检索效率。据说Claude.ai也是这么做的;
    • SQL检索:这就更加传统了,但是对于一些本地化的企业应用来说,SQL查询是必不可少的一步,比如我前面提到的销售数据,就需要先做SQL检索。
    • 其他:检索技术还有很多,后面用到再慢慢说吧。
  • 重排序(Rerank):很多时候我们的检索结果并不理想,原因是chunks在系统内数量很多,我们检索的维度不一定是最优的,一次检索的结果可能就会在相关度上面没有那么理想。这时候我们需要有一些策略来对检索的结果做重排序,比如使用planB重排序,或者把组合相关度、匹配度等因素做一些重新调整,得到更符合我们业务场景的排序。因为在这一步之后,我们就会把结果送给LLM进行最终处理了,所以这一部分的结果很重要。这里面还会有一个内部的判断器来评审相关度,触发重排序。

  • 查询轮换:这是查询检索的一种方式,一般会有几种方式:

    • 子查询:可以在不同的场景中使用各种查询策略,比如可以使用LlamaIndex等框架提供的查询器,采用树查询(从叶子结点,一步步查询,合并),采用向量查询,或者最原始的顺序查询chunks等;
    • HyDE:这是一种抄作业的方式,生成相似的,或者更标准的prompt模板。

    三、生成(Gen)

    这一部反而是我比较疏忽的,因为有大量的现成框架可以使用,而且,这一步真正发挥巨大作用的是LLM。

    这里面我们使用的框架有Langchain和LlamaIndex,而且我们因为有之前的AI产品积累,所以还有一套完整的Java框架可以使用,所以这一块我没有太多研究。唯一非常关注的就是Prompt工程,我们团队内部,这一部分的工作是交给了原来AI产品的知识库运营团队来做的,他们原来做的更多是BERT相关的知识库预训练,应该说工作内容还是比较匹配的。

    在Prompt里面其实还是有很多决定因素的,这和大家对所处行业的knowhow有关。比如在文旅行业,你要知道游客或者观众一般会怎么提问,真正需要得到的是什么内容。这些我就不多说了,各行业不太一样,但是大家可以翻看我之前写的关于prompt的文章《一文讲清楚实用Prompt工程》、《高级prompt工程讲解》。

    其他的关于ReAct等内容,本文就先不涉及了,后面再专门写一篇文章。

总结

LLM这一波,催生的技术真的很多,每一个环节,要真正做好,符合企业应用,都可以让我们研究好长一段时间,并需要不断去实践,才能打磨出精品。但是如果打磨出精品,你肯定可以摘取新技术带来的果实。技术人员在很多公司地位不高?嗯,大模型这一波,我已经感觉到变化的发生了——了解大模型和不了解大模型的人,在AI的业务上的理解上,有某些角度来看,他们的区别真的就现代人类和史前人类一样巨大。

本文可能还是属于大模型应用——RAG的一个大纲式的技术普及文章,这里面的很多技术细节,我会在后面一篇篇专门来写,欢迎大家关注“土猛的员外”。


Update: 2024-01-26

我们的TorchV Bot产品目前已经开始试用了,详情可以点击:https://www.luxiangdong.com/2024/01/25/lanuch-1
目前只接受企业用户试用,需要您填写一些信息,必要信息如下:

邮箱: 用来接收地址和账号
如何称呼您:
所服务的公司:
您的职位:

当然,如果您可以告诉我们您的使用场景,我们将更加感激!
对了,可以发送到yuanwai@mengjia.net
另外,也可以直接加我微信(lxdhdgss)联系我。


我们的创业项目已经上线!!!

TorchV AI,帮助企业快速进入AI时代!

具体详情,请点击官网咨询


最新内容,关注“土猛的员外”公众号

最详细的文本分块(Chunking)方法,直接影响LLM应用效果

RAG是一个考验技术的工作

这两周发的文章大模型偏多,但如果你有阅读过前面的文章,你会发现我其实不是为了说大模型而说大模型(有点绕),我更多的是在写怎么解决企业应用中真正的难题。这个真正难题我在前面的文章中《一些LLM的声音总结》中提到过,基于大模型的企业应用中很大一部分需求就是RAG——检索增强生成。

1

这个流程依然无法描述RAG的复杂性

RAG涉及的内容其实广泛,包括Embedding、分词分块、检索召回(相似度匹配)、chat系统、ReAct和Prompt优化等,最后还有与LLM的交互,整个过程技术复杂度很高。如果你用的LLM非常好,反而大模型这一块是你最不需要关心的。而这些环节里面我们每个都没达到1(比如0.9、0.7…),那么最终的结果可能是这些小数点的乘积。如果我们每个环节都可以做到>1.0,那么最终的结果会比上一个结果高出很多。

今天我们来聊聊分块,很重要的一个环节(没有哪个环节不重要),但它也许是我们容易做到高质量的一个环节。

什么是分块?

在构建RAG这类基于LLM的应用程序中,分块(chunking)是将大块文本分解成小段的过程。当我们使用LLM embedding内容时,这是一项必要的技术,可以帮助我们优化从向量数据库被召回的内容的准确性。在本文中,我们将探讨它是否以及如何帮助提高RAG应用程序的效率和准确性。

6

Pinecone是领先的向量数据库供应商

在向量数据库(如:Pinecone)中索引的任何内容都需要首先Embedding。分块的主要原因是尽量减少我们Embedding内容的噪音

例如,在语义搜索中,我们索引一个文档语料库,每个文档包含一个特定主题的有价值的信息。通过使用有效的分块策略,我们可以确保搜索结果准确地捕获用户查询的需求本质。如果我们的块太小或太大,可能会导致不精确的搜索结果或错过展示相关内容的机会。根据经验,如果文本块尽量是语义独立的,也就是没有对上下文很强的依赖,这样子对语言模型来说是最易于理解的。因此,为语料库中的文档找到最佳块大小对于确保搜索结果的准确性和相关性至关重要

另一个例子是会话Agent。我们使用embedding的块为基于知识库的会话agent构建上下文,该知识库将代理置于可信信息中。在这种情况下,对分块策略做出正确的选择很重要,原因有两个:

  • 首先,它将决定上下文是否与我们的prompt相关。
  • 其次,考虑到我们可以为每个请求发送的tokens数量的限制,它将决定我们是否能够在将检索到的文本合并到prompt中发送到大模型(如OpenAI)。

在某些情况下,比如使用具有32k上下文窗口的GPT-4时,拟合块可能不是问题。尽管如此,当我们使用非常大的块时,我们需要注意,因为这可能会对我们从向量数据库获得的结果的相关性产生不利影响。

在这篇文章中,我们将探讨几种分块方法,并讨论在选择分块大小和方法时应该考虑的权衡。最后,我们将给出一些建议,以确定适合您的应用程序的最佳块大小和方法。

Embedding长短内容

当我们在嵌入内容(也就是embedding)时,我们可以根据内容是短(如句子)还是长(如段落或整个文档)来预测不同的行为。

当嵌入一个句子时,生成的向量集中在句子的特定含义上。当与其他句子Embedding进行比较时,自然会在这个层次上进行比较。这也意味着Embedding可能会错过在段落或文档中找到的更广泛的上下文信息。

当嵌入一个完整的段落或文档时,Embedding过程既要考虑整个上下文,也要考虑文本中句子和短语之间的关系。这可以产生更全面的向量表示,从而捕获文本的更广泛的含义和主题。另一方面,较大的输入文本大小可能会引入噪声或淡化单个句子或短语的重要性,从而在查询索引时更难以找到精确的匹配。

查询的长度也会影响Embeddings之间的关系。较短的查询,如单个句子或短语,将专注于细节,可能更适合与句子级Embedding进行匹配。跨越多个句子或段落的较长的查询可能更适合段落或文档级别的Embedding,因为它可能会寻找更广泛的上下文或主题。

索引也可能是非同质的,并且包含“不同”大小的块的Embedding。这可能会在查询结果相关性方面带来挑战,但也可能产生一些积极的后果。一方面,由于长内容和短内容的语义表示不一致,查询结果的相关性可能会波动。另一方面,非同构索引可能捕获更大范围的上下文和信息,因为不同的块大小表示文本中的不同粒度级别。这可以更灵活地容纳不同类型的查询。

分块需要考虑的因素

在确定最佳分块策略时,有几个因素会对我们的选择起到至关重要的影响。以下是一些事实我们需要首先记在心里:

  1. 被索引内容的性质是什么? 这可能差别会很大,是处理较长的文档(如文章或书籍),还是处理较短的内容(如微博或即时消息)?答案将决定哪种模型更适合您的目标,从而决定应用哪种分块策略。

  2. 您使用的是哪种Embedding模型,它在多大的块大小上表现最佳?例如,sentence-transformer[1]模型在单个句子上工作得很好,但像text- embedt-ada -002[2]这样的模型在包含256或512个tokens的块上表现得更好。

  3. 你对用户查询的长度和复杂性有什么期望?用户输入的问题文本是简短而具体的还是冗长而复杂的?这也直接影响到我们选择分组内容的方式,以便在嵌入查询和嵌入文本块之间有更紧密的相关性。

  4. 如何在您的特定应用程序中使用检索结果? 例如,它们是否用于语义搜索问答摘要其他目的?例如,和你底层连接的LLM是有直接关系的,LLM的tokens限制会让你不得不考虑分块的大小。

没有最好的分块策略,只有适合的分块策略,为了确保查询结果更加准确,有时候我们甚至需要选择性的使用几种不同的策略。

分块的方法

分块有不同的方法,每种方法都可能适用于不同的情况。通过检查每种方法的优点和缺点,我们的目标是确定应用它们的正确场景。

固定大小分块

这是最常见、最直接的分块方法:

我们只需决定块中的tokens的数量,以及它们之间是否应该有任何重叠。一般来说,我们会在块之间保持一些重叠,以确保语义上下文不会在块之间丢失。在大多数情况下,固定大小的分块将是最佳方式。与其他形式的分块相比,固定大小的分块在计算上更加经济且易于使用,因为它在分块过程中不需要使用任何NLP库。

下面是一个使用LangChain执行固定大小块处理的示例:

1
2
3
4
5
6
7
8
text = "..." # your text
from langchain.text_splitter import CharacterTextSplitter
text_splitter = CharacterTextSplitter(
separator = "\n\n",
chunk_size = 256,
chunk_overlap = 20
)
docs = text_splitter.create_documents([text])

Content-Aware:基于内容意图分块

这是一系列方法的组合,利用我们正在分块的内容的性质,并对其应用更复杂的分块。下面是一些例子:

句分割——Sentence splitting

正如我们之前提到的,许多模型都针对Embedding句子级内容进行了优化。当然,我们会使用句子分块,有几种方法和工具可以做到这一点,包括:

  • Naive splitting: 最幼稚的方法是用句号(。)和“换行”来分割句子。虽然这可能是快速和简单的,但这种方法不会考虑到所有可能的边缘情况。这里有一个非常简单的例子:
1
2
text = "..." # 你的文本
docs = text.split(".")
  • NLTK[3]: 自然语言工具包(NLTK)是一个流行的Python库,用于处理自然语言数据。它提供了一个句子标记器,可以将文本分成句子,帮助创建更有意义的分块。例如,要将NLTK与LangChain一起使用,您可以这样做:
1
2
3
4
text = "..." # 你的文本
from langchain.text_splitter import NLTKTextSplitter
text_splitter = NLTKTextSplitter()
docs = text_splitter.split_text(text)
  • spaCy[4]: spaCy是另一个用于NLP任务的强大Python库。它提供了一个复杂的句子分割功能,可以有效地将文本分成单独的句子,从而在生成的块中更好地保存上下文。例如,要将space与LangChain一起使用,您可以这样做:
1
2
3
4
text = "..." # 你的文本
from langchain.text_splitter import SpacyTextSplitter
text_splitter = SpaCyTextSplitter()
docs = text_splitter.split_text(text)

3

spacy-llm package

递归分割

递归分块使用一组分隔符以分层和迭代的方式将输入文本分成更小的块。如果分割文本开始的时候没有产生所需大小或结构的块,那么这个方法会使用不同的分隔符或标准对生成的块递归调用,直到获得所需的块大小或结构。这意味着虽然这些块的大小并不完全相同,但它们仍然会逼近差不多的大小。

这里有一个例子,如何配合LangChain使用递归分块:

1
2
3
4
5
6
7
8
9
text = "..." # 你的文本
from langchain.text_splitter import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
# 设置一个非常小的块大小。
chunk_size = 256,
chunk_overlap = 20
)

docs = text_splitter.create_documents([text])

专门的分块

Markdown和LaTeX是您可能遇到的结构化和格式化内容的两个例子。在这些情况下,可以使用专门的分块方法在分块过程中保留内容的原始结构。

4-2

本文就是用markdown写作的
  • Markdown: Markdown是一种轻量级的标记语言,通常用于格式化文本。通过识别Markdown语法(例如,标题、列表和代码块),您可以根据其结构和层次结构智能地划分内容,从而生成语义更连贯的块。例如:
1
2
3
4
5
from langchain.text_splitter import MarkdownTextSplitter
markdown_text = "..."

markdown_splitter = MarkdownTextSplitter(chunk_size=100, chunk_overlap=0)
docs = markdown_splitter.create_documents([markdown_text])
  • LaTex: LaTeX是一种文档准备系统和标记语言,通常用于学术论文和技术文档。通过解析LaTeX命令和环境,您可以创建尊重内容逻辑组织的块(例如,节、子节和方程),从而产生更准确和上下文相关的结果。例如:
1
2
3
4
from langchain.text_splitter import LatexTextSplitter
latex_text = "..."
latex_splitter = LatexTextSplitter(chunk_size=100, chunk_overlap=0)
docs = latex_splitter.create_documents([latex_text])

确定应用程序的最佳块大小

如果常见的分块方法(如固定分块)不容易适用于您的用例,这里有一些指针可以帮助您找到最佳的块大小。

  • 清洗数据 :在确定应用程序的最佳块大小之前,您需要首先预处理清洗数据以确保质量。例如,如果您的数据是从web爬取的,您可能需要删除HTML标记或特定的元素,保证文本的“纯洁”,减少文本的噪音。
  • 选择一个范围的块大小 :一旦你的数据被预处理,下一步是选择一个范围的潜在块大小进行测试。如前所述,选择时应考虑内容的性质(例如短文本还是长文档)、将要使用的Embedding模型及其功能(如token限制)。目标是在保留上下文和保持准确性之间找到平衡。从探索各种块大小开始,包括较小的块(例如,128或256个tokens)用于捕获更细粒度的语义信息,较大的块(例如,512或1024个tokens)用于保留更多上下文。
  • 评估每个块大小的性能 :很傻,但是很稳重的一种方式。为了测试不同大小的块,您可以把不同大小的块进行标记。使用可以覆盖你的业务场景效果的数据集,为要测试的各个大小的块创建Embedding,并将它们保存下来。然后,你可以运行一系列查询来评估质量,并比较不同块大小的性能。这是一个反复测试的过程,在这个过程中,针对不同的查询测试不同的块大小,直到找到最佳的块大小。

结论

好了,说了这么多,最后我要说的结论你可能会失望,那就是我们自己也还没有找到最佳的分块方式,哈哈。但是对于不同的业务场景,我们现在比刚开始做RAG应用的时候会有经验很多了。

引用:

1.sentence-transformer:https://huggingface.co/sentence-transformers

2.text- embedt-ada -002:https://openai.com/blog/new-and-improved-embedding-model

3.NLTK:https://www.nltk.org/

4.spaCy:https://spacy.io/

5.Chunking Strategies for LLM Applications:https://www.pinecone.io/learn/chunking-strategies/


Update: 2024-01-26

我们的TorchV Bot产品目前已经开始试用了,详情可以点击:https://www.luxiangdong.com/2024/01/25/lanuch-1
目前只接受企业用户试用,需要您填写一些信息,必要信息如下:

邮箱: 用来接收地址和账号
如何称呼您:
所服务的公司:
您的职位:

当然,如果您可以告诉我们您的使用场景,我们将更加感激!
对了,可以发送到yuanwai@mengjia.net
另外,也可以直接加我微信(lxdhdgss)联系我。


我们的创业项目已经上线!!!

TorchV AI,帮助企业快速进入AI时代!

具体详情,请点击官网咨询


最新内容,关注“土猛的员外”公众号

Embedding——从入门到生产使用

搜索功能已经深入到我们的日常生活中,我们常说“Google一下就知道了”,用户已经开始期望几乎每个应用程序和网站都提供某种类型的搜索功能。随着有效搜索变得越来越相关(双关语),寻找新的方法和体系结构来改进搜索结果对于架构师和开发人员来说至关重要。从基础开始,这篇博文将描述Redis中利用深度学习模型创建的向量Embeddings的人工智能搜索功能。

向量Embedding

什么是向量Embedding?简单地说,向量Embedding是可以表示许多类型数据的数字列表。

向量Embedding非常灵活。音频、视频、文本和图像都可以表示为矢量Embedding。这种特性使得向量Embedding成为数据科学家工具箱中的瑞士军刀。

Embedding creation process

从不同类型的数据创建向量Embedding的过程:音频,文本,视频。

为了解释为什么Embedding提供了这样的实用程序,让我们看一下以前处理文本数据(如表格数据中的分类值)的方法。数据科学家有时会使用one-hot编码等方法将分类特征转换为数值。这些编码将为每种类型的类别创建一个列。值为1表示项目属于该列指定的类别。相反,值为0表示项目不属于该类别。

例如,考虑书籍类型:“小说”、“非小说”和“传记”。每一种体裁都可以编码成一个热向量,然而,这样的向量会非常稀疏,因为书籍通常只属于两个体裁。下图显示了这种编码是如何工作的。注意这里0的数量是1的两倍。对于像图书类型这样的类别,随着更多的类型被添加到数据集中,这种稀疏性将会呈指数级恶化。

稀疏性会给ML模型带来挑战。对于每一种新的类型,编码表示的大小都会增长,因此数据集的计算成本会变得很高。

One-hot encoding example

One-hot(独热)编码示例

对于图书类型,或者任何具有相对较少类别的分类数据,我们可以使用简单的one-hot编码,但是,对于整个英语语言呢?对于这种规模的语料库,这种编码方法将变得不切实际。

进入向量Embedding

向量Embedding呈现固定大小的表示,不随数据中观测值的数量而增长。由模型创建的结果向量,通常是384个浮点值,比其他编码方法(如one-hot编码)的表示密度要高得多。这意味着在更少的字节中存在更多的信息,因此在计算上的利用成本更低。正如您稍后将读到的,这些密集表示可以用于多种目的,例如反向图像搜索、聊天机器人、问答和推荐系统。

创建向量Embedding

为了理解向量Embedding是如何创建的,对现代深度学习模型的简要介绍是有帮助的。

机器学习模型不使用非结构化数据。为了使模型能够理解文本或图像,我们必须将它们转换为数字表示。在机器学习之前,这样的表示通常是通过Feature Engineering“手工”创建的。

随着深度学习的出现,复杂数据中的非线性特征交互是由模型学习而不是人工设计的。当一个输入遍历深度学习模型时,该输入数据的新表示将以不同的形状和大小创建。每一层通常关注输入的不同方面。深度学习的这一方面,从输入中“自动”生成特征表示,构成了如何创建向量Embedding的基础。

例如,考虑在ImageNet数据集上训练的著名的ResNet模型。ResNet是一种卷积神经网络(CNN),通常用于与图像相关的任务。在这种情况下,ResNet被训练来预测图像中的对象属于1000个类中的哪一个。

在训练过程中,ResNet将捕获图像中存在的特征信息,通过将图像传递给多个卷积层、池化层和完全连接层。这些图层将捕获边缘、线条和角等特征,并将它们分组到传递给下一个图层的“桶”中。由于CNN的空间不变特性,无论边缘或直线出现在图像的哪个位置,这些特征都将始终映射到相同的桶。这些层将通过模型的层变得越来越小,直到一个由1000个浮点值组成的完全连接的层作为输出。每个值代表1000个类中的1个。该值越高,图像中的对象属于该类的概率就越大。

Diagram of a simple Convolutional Neural Network (CNN)

简单卷积神经网络(CNN)示意图

ResNet和其他类似的图像分类模型回答了这个问题:“这个图像中是什么类型的对象?”然而,这些分类在回答诸如“哪些图像与此图像相似?”之类的prompts时用处不大。对于这个问题,我们需要一起比较图像。尽管没有专门针对这项任务进行训练,ResNet仍然很有用,因为它可以捕获图像的密集表示。

简单地说,CNN和其他类似的模型学习有用的数据表示,以执行图像分类等任务。当输入通过模型的各个层时,可以提取这些表示。被提取的层,也称为潜在空间,通常是靠近模型输出的层。在上图中,这可能是包含768或500个隐藏单位的图层。提取的层或潜在空间提供了一个密集的表示,其中包含有关当前特征的信息,这对于视觉相似性搜索等任务在计算上是可行的。

这是向量Embedding。

存在大量的预训练模型,可以很容易地用于创建向量Embedding。Huggingface Model Hub (https://huggingface.co/models)包含许多模型,可以为不同类型的数据创建Embedding。例如,[all-MiniLM-L6-v2模型](https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2)是在线托管和运行的,不需要专业知识或安装。

sentence_transformers这样的包,也来自HuggingFace,为语义相似度搜索、视觉搜索等任务提供了易于使用的模型。要使用这些模型创建Embeddings,只需要几行Python代码:

1
2
3
4
5
6
7
8
9
from sentence_transformers import SentenceTransformer
model = SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2')

sentences = [
"That is a very happy Person",
"That is a Happy Dog",
"Today is a sunny day"
]
embeddings = model.encode(sentences)

语义相似度搜索的向量Embedding

语义相似搜索是将文本片段进行比较,以找出包含最相似含义的文本的过程。虽然这对普通人来说似乎很容易,但语言是相当复杂的。将非结构化文本数据提炼成机器学习模型可以理解的格式一直是许多自然语言处理研究人员的研究主题。

向量Embeddings为任何人提供了一种执行语义相似搜索的方法,而不仅仅是NLP研究人员或数据科学家。它们提供了一种有意义的、计算效率高的数字表示,可以通过预先训练的模型“开箱即用”来创建。下面是一个语义相似度的例子,它概述了用上面所示的sentence_transformers库创建的向量Embedding。

让我们看看下面的句子:

  • “That is a happy dog(那是一只快乐的狗)”
  • “That is a very happy person(那是一个非常幸福的人)”
  • “Today is a sunny day(今天是个晴天)”

这些句子中的每一个都可以转换成向量Embedding。下面是一个简化的表示,突出显示了这些示例句子在二维向量空间中相对于彼此的位置。这对于从视觉上衡量我们的Embedding如何有效地表示文本的语义意义非常有用。下文将详细介绍。

A simplified plot of vector embeddings projected into 2 dimensions.

向量Embeddings投影到二维的简化图

假设我们要将这些句子与“那是一个快乐的人”进行比较。首先,我们为查询语句创建向量Embedding。

1
2
3
4
5
from sentence_transformers import SentenceTransformer
model = SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2')

# create the vector embedding for the query
query_embedding = model.encode("That is a happy person")

接下来,我们需要比较查询向量Embedding和数据集中的向量Embedding之间的距离。

有很多方法可以计算向量之间的距离。当涉及到语义搜索时,每种方法都有自己的优点和缺点,但我们将在另一篇文章中讨论。下面显示了一些常见的距离度量。

5-1

用于计算向量相似度的距离度量。

在这个例子中,我们将使用余弦相似度来度量两个向量的内积空间之间的距离。

Formula for Cosine Similarity

余弦相似度公式

在Python中,这看起来像

1
2
def cosine_similarity(a, b):
return np.dot(a, b)/(norm(a)*norm(b))

在我们的查询向量和上图中的其他三个向量之间运行这个计算,我们可以确定句子之间的相似程度。

2D plot showing the cosine similarity between the vector embeddings created from our sentences earlier

2D图显示了之前从我们的句子中创建的向量Embeddings之间的余弦相似性

你可能已经猜到,“That is a very happy person(那是一个非常幸福的人)”和“That is a happy person(那是一个幸福的人)”是最相似的句子。这个例子只捕获了向量Embeddings的许多可能用例中的一个:语义相似搜索

下面列出了运行整个示例的Python代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import numpy as np

from numpy.linalg import norm
from sentence_transformers import SentenceTransformer

# Define the model we want to use (it'll download itself)
model = SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2')

sentences = [
"That is a very happy person",
"That is a happy dog",
"Today is a sunny day"
]

# vector embeddings created from dataset
embeddings = model.encode(sentences)

# query vector embedding
query_embedding = model.encode("That is a happy person")

# define our distance metric
def cosine_similarity(a, b):
return np.dot(a, b)/(norm(a)*norm(b))

# run semantic similarity search
print("Query: That is a happy person")
for e, s in zip(embeddings, sentences):
print(s, " -> similarity score = ",
cosine_similarity(e, query_embedding))

在安装NumPysentence_transformers之后,运行这个脚本应该会得到以下计算结果

1
2
3
4
5
>>> Query: That is a happy person

>>> That is a very happy person -> similarity score = 0.94291496
>>> That is a happy dog -> similarity score = 0.69457746
>>> Today is a sunny day -> similarity score = 0.25687605

该脚本的结果应该与所选模型在HuggingFace inference API上看到的结果一致。

HuggingFace inference API similarity results

HuggingFace推理API相似度结果

向量Embedding搜索在生产环境中的使用

开发和生产是两个不同的东西,在学习了更多之后,你可能会开始问这样的问题:

  • 我把这些向量存储在哪里?
  • API应该是什么样子?
  • 如何将其与过滤等传统搜索功能结合起来?

幸运的是,开发Redis的好人们决定为你找出这些问题,并将向量相似搜索(VSS)功能构建到现有的reresearch模块中。这基本上把Redis变成了一个低延迟的向量数据库。

Redis as a vector database

Redis是一个矢量数据库

VSS功能是作为reresearch模块的新功能而构建的。它允许开发人员像在Redis散列中存储任何其他字段一样轻松地存储向量。它提供了在大型向量空间中执行低延迟搜索所需的高级索引和搜索功能,通常分布在许多机器上的向量从数万到数亿不等。

Redis现在支持两种类型的向量索引:

  1. Flat
  2. 分级可导航小世界(HNSW)

以及3个距离度量:

  1. LP —— 欧几里得距离
  2. IP —— 内积
  3. cos —— 余弦相似度(如上图所示)

下面是一个使用redis -py在向量被加载到Redis后创建索引的例子。

1
2
3
4
5
6
7
8
9
10
11
12
from redis import Redis
from redis.commands.search.field import VectorField, TagField

def create_flat_index(redis_conn: Redis, number_of_vectors: int, distance_metric: str='COSINE'):

image_field = VectorField("img_vector","FLAT", {
"TYPE": "FLOAT32",
"DIM": 512,
"DISTANCE_METRIC": distance_metric,
"INITIAL_CAP": number_of_vectors,
"BLOCK_SIZE": number_of_vectors})
redis_conn.ft().create_index([image_field])

索引只需要创建一次,当新的哈希值存储在Redis中时,索引会自动重新索引。在将向量加载到Redis中并创建索引之后,就可以为各种基于相似性的搜索任务形成和执行查询。

索引只需要创建一次,当新的哈希值存储在Redis中时,索引会自动重新索引。在将向量加载到Redis中并创建索引之后,就可以为各种基于相似性的搜索任务形成和执行查询。

更好的是,所有现有的reresearch功能,如文本、标签和基于地理的搜索,都可以与VSS功能协同工作。这被称为“混合查询”。对于混合查询,传统的搜索功能可以用作矢量搜索的预过滤器,这可以帮助限制搜索空间。

上面的索引创建函数(create_flat_index)可以很容易地通过添加新字段(如redis-py中的TagFieldTextField)来支持混合查询。

Redis VSS演示

最近,我构建了一个web应用程序来探索这些功能。Fashion Product Finder利用了Redis中新的VSS功能,以及我最喜欢的Redis生态系统的其他部分,如redis-om-python。您可以访问应用程序在这里

注册使用该应用程序后,您将看到如下所示的页面。

Fashion Product Finder application

时尚产品查找应用程序,使用由Redis提供的向量相似度搜索

要通过文本表示查询类似的产品,请找到您喜欢的产品并单击by text。同样,要通过视觉向量搜索查询,请单击产品上的by Image按钮。

可以为产品的性别和类别设置混合搜索属性,以便在执行矢量搜索时,返回的项目将通过这些标记进行过滤。下面是选择右下角黑色手表时的视觉向量搜索示例。

Fashion Product Finder search results

通过图片查询类似手表后的搜索结果为黑色G-Shock手表。

这个演示是一种探索Redis VSS功能的有趣方式,然而,这并不是应用程序中使用的Redis生态系统的唯一组件。事实上,Redis是这个应用程序使用的唯一数据库,用RedisJSON存储产品元数据,用RediSearch存储矢量数据。

您可以查看整个代码库在这里。如果你觉得有用,请点赞和分享!

有关Redis和reresearch模块中VSS的更多信息,您可以查看以下资源:

参考文档


我们的创业项目已经上线!!!

TorchV AI,帮助企业快速进入AI时代!

具体详情,请点击官网咨询


最新内容,关注“土猛的员外”公众号

不开玩笑——在家部署1800亿参数的Falcon大模型

0

Falcon的发布机构TII官网

前几天Falcon-180B大模型发布,在Hugging Face的所有LLM(大语言模型)里面排名第一,应该也是迄今为止参数量最大的开源预训练模型了。就性能来说,我看很多国外的专家都在给他背书,说仅次于GPT-4,和PaLM2相当,也就是说性能已经超过了大部分常用的ChatGPT3.5(GPT-3.5)了。开始我是不相信的,但是马上一想,这是TII(阿联酋阿布扎比技术创新研究所)发布的,我信了,因为他们是一群有“钞”能力的人。

很快,我们吕总就开始问我了,搞不搞。我一想到1800亿参数,那还不得上百张A100啊,不可能的,直接拒绝了。但是意识到可以自己武断了,于是再去好好查看了他们的网站,果然是我唐突了。官方说推理的话,400GB的显存就可以了,也就是6张A100-80GB就可以了(实际需要大概8张A100),大概也就是84万元(实际需要112万元)人民币(不考虑其他设备成本的话)。

好吧,这个“也就”用的有点飘,我感觉到了。

说实话180B的大模型,目前还没有太多中文benchmark出来,所以,我还是很想去尝试一下,看看效果的。

于是,我找到了一篇文章:《Falcon 180B: Can It Run on Your Computer?》,可能大多数人不一定能打开原文,那么可以看我下面的一些转述。

注:本文中180B中的B代表billion,也就是十亿,所以180B=1800亿。

2023年5月,阿布扎比技术创新研究所(TII)已经发布了两种预训练大语言模型:Falcon-7b和Falcon-40b,以及它们的聊天版本(Chat)。这两个模型表现出了非常好的性能,并曾经在OpenLLM排行榜上排名第一。

TII发布的第三个大模型就是Falcon-180B,一个1800亿个参数模型。它比Llama2-70b多2.5x参数,比Falcon-40b多4.5x参数。

关于Falcon-180B的一些基本情况(1)

  • 预训练3.5万亿tokens(2)
  • 使用Apache 2.0 license分发
  • 容量大小为360GB
  • 在OpenLLM排行榜(3)上排名第一(截至2023年9月11日)

img

OpenLLM排行榜截图(2023年9月11日)

还有一个聊天(Chat)版本。这些模型可以在Hugging Face中心找到:

Falcon-180B是完全免费和最先进的。但它也是一个巨大的模型

那么,它能在你的电脑上运行吗?

除非你的计算机已经准备好进行非常密集的计算,否则它无法开箱即用地运行Falcon180B。您将需要升级您的计算机并使用该模型的量化版本。

接下来,我们来看看如何在消费类硬件上运行Falcon-180B。我们将看到,在家用计算机上运行一个1800亿个参数的模型是可行的。我还讨论了几种有助于减少硬件需求的技术。

在电脑上加载Falcon180B所需的条件

您需要知道的第一件事是,Falcon180B有1800亿个参数,存储形式为bfloat16。一个bfloat16参数在内存中占2个字节。

当你加载一个模型时,标准Pytorch管道是这样工作的:

  1. 创建一个空模型:180B个参数* 2字节= 360 GB
  2. 在内存中加载其权重:180B个参数* 2字节= 360 GB
  3. 在步骤1创建的空模型中加载步骤2加载的权重
  4. 将步骤3得到的模型移动到设备上进行推理,例如GPU

步骤1和步骤2消耗内存。总的来说,您需要720 GB的可用内存。这可以是CPU RAM,但为了快速推理,您可能希望使用GPU,例如,9块A100(80GB)VRAM刚好是720GB,但实际上最好是12块A100。

无论是CPU RAM还是VRAM,这都是很大的内存。幸运的是,目前其实有一些方法可以“曲线”完成。

在HuggingFace,Falcon-180B使用安全防护器格式(safetensors format)分发。与标准Pytorch格式相比,这种格式有几个优点。它(几乎)没有复制(注:不需要有一个巨大或等量内存区域来做中间缓存),所以模型直接加载到第1步创建的空模型中。它节省了很多内存。

关于safetensors

safetensors 节省内存,而且它也使模型更安全运行,因为没有任意代码可以存储在这种格式。Safetensors模型的加载速度也快得多。当您从Hugging Face下载模型时,请使用此格式而不是“.bin”格式,以便更快、更安全、更节省内存。

尽管看起来我们跳过了第2步,但仍然会有一些内存开销。TII在Hugging Face的模型介绍上写着400GB的内存可以工作。这仍然很大,但比使用标准Pytorch格式少了220gb。

我们需要一个400 GB的设备,例如,5个A100(80G)的GPU VRAM,但这个结果离“消费级”家用电脑配置还很远。

在多个存储设备上分布式Falcon180B

你可能没有一个400 GB的内存设备,但如果你把以下所有设备的内存组合起来,你的电脑可能有超过400 GB的内存:

  • GPU VRAM:如果你有NVIDIA RTX 3090或4090,那已经是24GB了。
  • CPU RAM:大多数现代计算机至少有12GB的CPU内存,CPU的扩展也非常便宜。
  • 硬盘(或SSD):SSD一般都可以有几个TB存储空间,而且是可以用SWAP做虚拟内存的。请注意,如果您计划使用SSD(固态硬盘)运行LLM,那么它将比原来的机械硬盘驱动器速度快得多,但是仅仅是高铁比汽车快的意思,和飞机、火箭差距依然很大。

为了利用可用的设备,我们可以拆分Falcon-180B,以便它按照以下优先顺序使用设备的最大可用内存:GPU, CPU RAM和SSD。

这很容易通过Accelerate上的device_map架构来实现。

Device_map将模型的整个层放在您拥有的不同设备上。

img

device_map架构示意图

如果您想查看device_map使用的一些示例,请参考这篇文章

Device_map对于解决CUDA内存不足问题来说确实是一种很好的方法,但如果你打算在消费级硬件上使用Falcon180B,那还远远不够理想。即使是拥有24 GB VRAM的GPU(比如RTX 4090)和32 GB CPU RAM的高端配置,它也依然还需要动用几百GB的硬盘空间。

这里有两个原因:

  • 硬盘驱动器/SSD总体来说比GPU VRAM、CPU RAM慢得多。从硬盘装载和运行Falcon180B需要很长时间。
  • 消费类硬盘和固态硬盘不是为这种密集计算使用设计和测试的。如果模型的许多部分被装载到硬盘驱动器上,系统将不得不在推理期间多次访问和读取模型的大量分割部分。这是长时间的大量读取操作。如果您连续数天进行推理,例如生成一些合成数据集,这可能会损坏您的硬盘,或者至少会显著降低其预期寿命。

为了避免过度使用硬盘,我们最好的方式就是去除硬盘方案,那么剩下来的解决方案实在不多了:

  • 增加一个GPU:大多数高端主板可以容纳两个RTX 3090/4090。它会给你48 GB的VRAM。
  • 扩展CPU RAM:大多数主板有4个插槽可用于CPU RAM套件。4x128GB CPU内存的套件有出售,但不容易找到,而且仍然很贵。 注意:您的操作系统可以支持的CPU RAM总量也有限制。Windows 10是2tb。如果你使用的是一个老版操作系统,你应该在购买更多的内存之前看看它的文档
  • 量化版Falcon180B和扩展CPU RAM

Falcon180B的量化版是减少内存消耗的最佳方案之一。

使用量化版Falcon180B减少RAM尺寸

现在的常见做法是将非常大的语言模型量化到较低的精度。GPTQ(4)和bitsandbytes nf4(5)是将LLM量化到4位精度的两种常用方法。

Falcon180B在使用bfloat16的情况下,我们看到它的尺寸是360 GB。

一旦量化到NF4精度,它只需要90GB(1800亿个参数* 0.5字节)。我们可以用100GB内存(90GB +一些内存开销)加载NF4的Falcon-180B。

如果你有24 GB的VRAM,你“只”需要75 GB的CPU RAM。这仍然是很多,但比加载原始模型便宜得多,并且在推理期间不会需要和硬盘上的模型层做交换。注意:您仍然需要100 GB的可用硬盘空间来存储模型

你甚至不需要GPU。使用128 GB的CPU RAM,您可以仅使用CPU进行推理。

量化本身是非常昂贵的。值得庆幸的是,我们已经可以在网上找到量化的版本。TheBloke:https://huggingface.co/TheBloke 发布了使用GPTQ制作的4位版本:

注:也有3位模型可作为模型的“分支”。按照4位型号卡的说明来获取它们

虽然模型的精度降低了,但根据Hugging Face的实验,模型的性能仍然相似这一点太棒了!!!)。

GPTQ模型的推理速度很快,您可以使用LoRA适配器对它们进行微调,可以在这里查看如何微调使用GPTQ量化的Llama 2(6)

虽然可以对GPTQ模型进行微调,但我不推荐这样做。使用QLoRA进行微调具有类似的内存消耗,但由于使用nf4进行了更好的量化,因此产生了更好的模型,如QLoRA论文(7)所示。

最后

总而言之,您需要量化版本(4-bit Falcon180B)和100GB内存才能在高配置的家用计算机上运行Falcon-180B。

对于快速推理微调,你需要使用到GPU。RTX 4090(或RTX 3090 24GB,更实惠,但速度更慢)将足以加载量化模型的1/4。如果你的电脑机箱有足够的空间,你甚至可以放两张RTX卡。

如果你想在一个只有CPU的配置上运行Falcon-180b,也就是说,没有任何GPU,那你只把放弃微调的想法,它会慢到你怀疑人生,嗯,推理也同样会很慢。如果要在纯CPU的机器上运行,那llama.cpp(C++重写的llama)是个不错的选择。我们只能期待Falcon180B也有这样的版本,那在存内存的环境下运行Falcon180B才有可能。

我目前家里电脑的配置是RTX 4090(24GB) + 32GB内存,如果再买一块64GB的内存,好像就可以跑起来了哈哈。64GB的内存大概1400元,虽然有些舍不得,但感觉有点心动。

引用:

1.Falcon-180B模型介绍:https://huggingface.co/tiiuae/falcon-180B

2.RefinedWeb:https://huggingface.co/datasets/tiiuae/falcon-refinedweb

3.OpenLLM排行榜:https://huggingface.co/spaces/HuggingFaceH4/open_llm_leaderboard

4.GPTQ:https://arxiv.org/abs/2210.17323

5.bitsandbytes nf4:https://arxiv.org/abs/2305.14314

6.微调使用GPTQ量化的Llama 2:https://kaitchup.substack.com/p/quantize-and-fine-tune-llms-with?source=post_page-----c3f3fb1611a9--------------------------------

7.QLoRA论文:https://arxiv.org/abs/2305.14314


我们的创业项目已经上线!!!

TorchV AI,帮助企业快速进入AI时代!

具体详情,请点击官网咨询


最新内容,关注“土猛的员外”公众号

使用frp轻松搞定ssh家里的服务器

起因

前些天听我邻居介绍了ngrok可以做内网穿透,这样就可以把我家里的那台带了RTX4090的PC用起来。于是我折腾了一通,发现ngrok配置是简单的,但用它自己的server来做转发,网络太慢了,于是就放弃了。昨晚在Github搜索ngrok的替代品时,发现有人整理了一个非常完整的tunneling工具列表frp赫然列在第一位。

list

图1:tunneling工具列表(部分)

frp的优势

相比于ngrok,这次的frp更符合我的需求。frp的一些特性优势:

  • 自定义服务端:可以使用自己的云服务器做服务端,速度有保障(确实很快);
  • 类型支持丰富:包括http/https/ssh/ssh2/dns/socket/ftp/xtcp等类型;
  • 文档出色:有中文文档,如果你有高级需求,那文档还是很重要的。

也许ngrok也有这些功能,但我没有耐心继续找下去了,frp更符合我。

frp安装

frp安装其实比较简单。

在介绍安装之前,先介绍一下我的PC配置,你们可以按自己的情况下载和安装:

  • 本地服务器:i7/RTX4090/32G/2TB,Ubuntu Server 22.04.3 LTS,对应的就是你自己家里的电脑;
  • 云服务器:阿里云,轻应用服务器(612元/年,买了一年多了),CentOS,3M带宽。

因为都是linux系统,所以我下载的是frp_0.51.3_linux_amd64.tar.gz,大家可以在这里下载

如果是公众号文章,会无法直接点击跳转,您可以点击左下角的“阅读原文”,原文是我的blog,那里的链接可以直接点击。

我们可以把这个文件分别传到本地服务器和云服务器,然后用tar -xvf frp_0.51.3_linux_amd64.tar.gz把文件解开,我们可以发现里面的文件如下:

frp_folder

图2:frp目录下的文件
  • frpc:对应的是你家里的电脑;
  • frps:对应的是云服务器,公网上的;
  • xxxx.ini:这是配置文件,你要做的主要配置仅仅在frpc.inifrps.ini这两个文件里面。

其实解压好之后,安装其实就算完成了,对于存放的目录其实没太多要求。

frp配置

配置可能是这里面最复杂的了,但是总体上也非常简单,只要你照着我下面的步骤做。

【服务器端】

先来看服务端,在我这里就是那台有公网IP的阿里云服务器。

进入到frp的目录,输入以下命令打开frps.ini:

1
sudo vim frps.ini

在这里,我们以配置ssh为例,你可以先看一下官方文档

guanf

图3:frps的服务端和客户端配置

教程已经非常清楚了,我无需再详细解读,这里我贴一下我的配置,以便大家可以对照看看如何修改。

1
2
3
[common]
bind_port = 7000
vhost_http_port = 8080

这里如果大家只是配置ssh的话,只需要上面的bind_port = 7000,注意=两边的空格。下面的vhost_http_port是为了放开家里服务器的web服务的。

然后用命令:

1
sudo ./frps -c ./frps.ini

启动服务端的服务。

注意:一般是先启动服务端(也就是公网上的),再启动本地的。

【注册成系统服务】

目前的命令在我们的终端关闭之后就会结束,所以如果你要持续运行,可以将它注册成系统服务。官方也提供了很详细的文档,使用systemd,很简单,在服务端和本地端都可以用,我就不再赘述了。

【本地配置】

好,接下来我们来配置本地这一端,也就是你家里的PC。

本地端的配置是在你自己的电脑上(怕大家弄错了,多啰嗦一下),修改的是frpc.ini,对,结尾是c,不是s

官方的配置说明已经在图3展示了,你也可以直接进入官方文档进行查看。

我贴一下我的frpc.ini配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[common]
server_addr = 121.xxx.xx.175
server_port = 7000

[ssh]
type = tcp
local_ip = 127.0.0.1
local_port = 22
remote_port = 6000


[web]
type = http
local_port = 80
custom_domains = www.luxxxxxxxxxxxxxong.com

我不仅仅是ssh,还配置了一个web。

隐去一些具体信息。

然后,启动你的本地PC上的服务:

1
sudo ./frpc -c ./frpc.ini

好了,你试试看。在其他电脑上用下面的命令试试ssh:

1
ssh -oPort=6000 username@x.x.x.x

x.x.x.x是你的公网服务器IP地址。

【无法访问的原因】

如果你的测试过程有问题,那一多半的可能是你的云服务器对外端口未开放。你配置里面涉及的端口,如6000/7000/8080,都需要在阿里云服务器上开通。

一般在“控制台”–>“轻量应用服务器/ECS云服务器”–>“防火墙/安全组规则”。

端口

图4:云服务器的出入站规则

结束

OK,我想你应该和我一样可以开心地跑起来了,速度还可以!

如果你有具体的问题,可以邮件咨询我:lxdhzzj@gmail.com

下面是我的ssh附图:

ssh6000


我们的创业项目已经上线!!!

TorchV AI,帮助企业快速进入AI时代!

具体详情,请点击官网咨询


最新内容,关注“土猛的员外”公众号

关于LLM(大语言模型)的一些声音

这篇文章是最近听到的一些播客(如《此话当真》),以及之前读到的一些文章,当然还有自己在公司产品中的一些实践中产生的想法归纳。

这波LLM,巨头和创业者/小公司其实处在同一起跑线,但是小公司/创业者真的没必要去做LLM,还是应该聚焦在基于AI的应用上,需要去做错位竞争。

主要原因有二:

  • 这次LLM是在第一时间给市场看到了最好的产品——ChatGPT,其他竞争对手和OpenAI都还存在代差。一些国内外的Benchmark大家看看就得了,只要他们声称自己的新模型超过GPT-3.5,接近GPT-4.0,那在全领域来比较多半还差距很大,所以不要全信。
  • 后面大模型的竞争首先会成为卡的竞争,也就是钱。对于大部分企业来说,无法获得如此多的资金,而且去All in一个可能还是未知数的方向。大可在基于LLM的应用上多发力,比如没有什么垂直大模型,只有垂直应用。

数据还是壁垒吗?

那必须是!

但开放数据已经变得不值一提了,大家都可以获取,哪怕所谓的垂直LLM,如果你的训练数据是可以公开获取的,那壁垒也不高。比如司法LLM、医药LLM等行业。

什么样的数据才是壁垒?

那些不属于你所有,但与你共生的数据才是最重要的数据。比如你帮客户建立AI服务,中间产生的很多数据,你可以触碰和分析,但是不能做它途使用的数据。这部分数据以后应该是最多的,而且符合数据安全的相关条例。我们现在产品中的数据也属于这一类。

这部分数据,在企业应用中大多数是:1.内部协同数据;2.企业的客户使用数据。在个人层面,这部分数据更多是设备端数据。

LLM在未来的一种变形是做的事情更专注、更小型化,比如可以装载在设备上,做边缘计算,这些LLM可以帮助人们的生活更加简单。

RAG vs Fine-tuning

Fine-tuning(微调)是用一定量的数据集对LLM进行局部参数的调整,以期望LLM更加理解我们的业务逻辑,有更好的zero-shot能力。

RAG(检索增强生成)的做法是把企业内部的文档数据先进行embedding,借助检索先获得大致的知识范围答案,再结合prompt给到LLM,让LLM生成最终的答案。

说实话两种方式都不简单,但是Fine-tuning 的成本似乎更大一些。所以目前有一种趋势就是更倾向RAG方式,毕竟对于客户本身来说,操作空间会更大,他们可以通过管理文档来调整最终的检索和问答能力。所以目前有一些预测已经旗帜鲜明地认为fine-tuning的需求一定会下降。

传统程序员怎么办?

首先我觉得没有什么可以惊慌的,如果你原来的工作就很有价值,以后依然会很有价值。

AI算法工程师很重要,但是没有重要到离谱的程度,甚至现在很多招聘其实对AI算法工程师的热度是在下降的(Q3与Q2相比)。因为大厂拿到牌照了,加上Llama2、ChatGLM2等开源商用授权的发布,让很多公司稍稍从恐慌中心里有底了一些。

做过基于LLM产品的人现在大概都知道了,一个产品/平台,最终组成部分,80%是产品工程,20%是创新技术(国内的话,创新技术有10%其实已经很优秀)。所以,对于传统程序员和产品经理,一方面拥抱AI,另外一方面真正要支棱起一个AI应用,更大的工程量还是要靠你们。

对于AI应用的需求

在和B端客户交流中发现,65%的需求都是信息的检索、汇总和再生成,其实更像上面提到的RAG。

另外第二大需求(约占20%)是流程自动化、决策辅助,BI等需求上AI驱动的应用升级。

文生图和代码生成的需求在B端其实很低,和我们之前预期的差距很大。

很多大客户对于结果的可靠性要求非常高,很多项目都已经进入POC阶段了,因为达不到想要的可靠性(哪怕是再降低一些)而被否决。

甚至有些客户后面直接就想通了,我要的就是一个更好的搜索而已,但是LLM好像做不到。到最后,大家都冷静下来了,客户知道自己要的是一个解决方案,解决他业务中的痛点,而不是一个大模型。

客户一般都知道或者用过最好的(ChatGPT),所以他们的需求会天马行空,然后接触到国内基于LLM的产品之后,感觉落差巨大。

运营兜底

但是基于LLM的产品也不是就没救了,别这么悲观。现在碰到的问题是:我们把LLM神化了,感觉什么都可以通过LLM一劳永逸的解决了,但实际真不是这样的。

我们回想过往所有产品,精准度都是很高的,我们怎么做到的呢?运营啊!加人力成本啊!

到目前为止,要有非常好的精准度,依然必须要有运营兜底。基于生成式的LLM在幻想方面是个黑盒,不要说我们,OpenAI也没办法完全控制。

运营介入的方法简单说有三类:

1.prompt优化,当然不排斥针对某些行业和客户的“穷举”优化;

2.反馈迭代:获得使用中用户的反馈,进行微调,或者补充知识;

3.BERT前置:对于一些需要非常谨慎的服务内容,还是需要用预训练语料去完成,只要覆盖了需要这些高精度需求,其他的可以用GPT托底。

AI产品经理的核心素质

国内LLM的能力其实大家都差不多,所以对于基于LLM的业务最后拼的就是运营和反馈机制建立。

  • 运营:就是我上面一节说的这个意思,我们原来小知在行业里面占据领先地位也是这个做法;

  • 反馈:必须是建立一种可以持续获得反馈的机制。Midjourney生成四张图,每次让人选择一张(一般情况下会认为是更符合客户需求的),这种方式就是一种非常好的反馈机制。

所以,作为AI产品经理,最最重要的就是这两件事

  • 认清现实:知道我们完全靠机器是不太可能做好所有的事情的,然后如何用运营来弥补这种缺失,达到最好的交付效果;
  • 反馈飞轮:如何在交互上做出一个良好的反馈机制,让“模型-应用-反馈-优化”这个飞轮可以更加快速运转起来。

如果没有这两方面的考虑,都不能称之为一个合格的AI产品经理。

再说说运营能力,也可以作为一种标准服务提供给企业,或者运营也可以是一种AI浪潮下的企业员工培训,让企业员工如何适应AI产品来提高生产力。

避免过早优化

整体来说,中文LLM的能力还非常差,很多时候都体会不到真正LLM的“魔力”。

但是我对于中文LLM的发展还是非常乐观的,所以我们在做基于LLM产品的时候,可以考虑一年之后我们可以拿到一个类似GPT4差不多水平的LLM。

言外之意,即使在现在,业务优先,AI在后

应用与LLM的切割,做好架构,先做好自己的应用,做到随时切换各个不同的LLM。

或者,我们现在做业务,也可以不要一个猛子扎进去,可以有意与LLM有一个边界。

PLUS:现在做的三款产品

一、2019年就开始做的AI助手产品,在颐和园、长隆、良渚、普陀山等140多家景区有应用,原来是纯BERT的,现在加了GPT兜底;

二、基于RAG的产品,在行政服务中心、招商等部门都有一定的应用;

三、文博行业产品,在客户端,给原来纯用声音听的功能做了可视化升级,在后台用大模型做了内容的快速处理与合成,比传统内容制作效率提升很多。非直接生成,那质量非常不可控。

好了,就写到这里!


我们的创业项目已经上线!!!

TorchV AI,帮助企业快速进入AI时代!

具体详情,请点击官网咨询


最新内容,关注“土猛的员外”公众号