6

Embedding 模型部署及效果评测 - xiaoxi666

 7 months ago
source link: https://www.cnblogs.com/xiaoxi666/p/18014457
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

最近大模型发展迅速,与之对应的向量化需求也被带动起来了,由此社区也衍生出很多模型,本文选几款,简单做下评测。

为方便读者,先简单介绍几个概念。

概念1:Vector Embedding

也即向量化嵌入,举个例子:

想象一下,你是一位市场研究员,职责是分析消费者的购买行为,并为你的客户提供针对性的营销策略。在你的数据库中,有成千上万的消费者交易记录,每条记录都包含了消费者的个人信息、购买的商品、购买的时间和地点等信息。

在没有Vector Embedding的情况下,如果你想找出哪些消费者可能对新产品感兴趣,你可能需要手动查看每条交易记录,然后根据消费者的购买历史和商品的特点来进行判断。然而,这种方法可能忽略了消费者的其他重要特征,如他们的收入水平、兴趣爱好、生活方式等,导致分析的结果不够准确。

现在引入Vector Embedding,此时你就可以将每位消费者的个人信息和购买历史转化为一个多维的“消费者画像向量”。这个向量不仅包括了消费者的基本信息和购买历史,还包含了他们的收入水平、兴趣爱好、生活方式等各个方面的元素。换句话说,这些信息能反映出消费者的复杂特征。例如,一位经常购买高端护肤品的消费者画像向量,大概率与追求高品质生活的向量相近,而一位经常购买户外运动装备的消费者画像向量,大概率与追求健康生活方式和热爱自然的向量相近。此时,若想找出哪些消费者可能对新产品感兴趣,只需要计算出新产品的向量,并与大量消费者画像的向量进行对比,即可快速筛选出潜在的目标客户(目标客户的向量相似度高)。这个过程不再需要逐个查看消费者的个人信息和交易记录,大大简化了数据的处理过程。

更深入的概念可阅读附录[1]进行学习。

概念2:文档切分中的Chunk和Overlap

在处理较长文档或文本时,将其分割成若干小块,每块称为一个Chunk。在这个特定的切分策略中,每个Chunk由最多N个Token组成。Token通常指文本中的单词或符号,取决于具体语境。而Overlap是相邻Chunk之间共有的Token数。举个例子:

每Chunk 200 Token,Overlap 20。在这个例子中,每个Chunk由最多200个Token组成。Overlap为20,意味着相邻的Chunks会有20个Token是重复的,从而确保文本的连贯性。例如,如果某个文本段落共有230个Token,它将被分成两个Chunks:第一个Chunk将有200个Token,第二个Chunk将有30个Token(因为230-200=30),并且这两个Chunks之间将有20个Token重叠。

这种切分方式有助于确保在将长文档送入诸如大型语言模型进行Embedding或处理时,能够保持文本的语义连贯性,同时又能满足模型处理长度有限的输入的要求。切分文档时考虑Chunk的大小和Overlap的数量,对于提高模型处理效率和文本的语义理解都是十分重要的。

与大模型类似,Embedding也是使用模型来实现的,只不过Embedding模型更为轻量。一般都在2G以内。

经调研(附录[6~10]),发现以下模型对中文的支持效果较好,且已经开源方便本地私有化部署:

609124-20240213114524433-1597871294.png

可以看得出m3模型的优势是支持多语言,并且字符数扩展到了8192,这意味着BGE-M3能够高效地处理长篇幅的文档,满足对于长文档检索的需求。

以上几类模型的链接请参考附录[2~5],下文针对这几种模型进行效果评测。

为了部署Embedding模型,我们需要引入对应的工具库,目前主要有几类:

  1. Sentence-Transformers: Sentence-Transformers库是基于HuggingFace的Transformers库构建的,它专门设计用于生成句子级别的嵌入。它引入了一些特定的模型和池化技术,使得生成的嵌入能够更好地捕捉句子的语义信息。Sentence-Transformers库特别适合于需要计算句子相似度、进行语义搜索和挖掘同义词等任务。

  2. HuggingFace Transformers: HuggingFace的Transformers库是一个广泛使用的NLP库,它提供了多种预训练模型,如BERT、GPT-2、RoBERTa等。这些模型可以应用于各种NLP任务,如文本分类、命名实体识别、问答系统等。Transformers库支持多种编程语言,并且支持模型的微调和自定义模型的创建。虽然Transformers库的功能强大,但它主要关注于模型的使用,而不是直接提供句子级别的嵌入。

  3. Langchain集成的HuggingFaceBgeEmbeddings。与3一样。

  4. FlagEmbedding: 这是一个相对较新的库,其核心在于能够将任意文本映射到低维稠密向量空间,以便于后续的检索、分类、聚类或语义匹配等任务。FlagEmbedding的一大特色是它可以支持为大模型调用外部知识,这意味着它不仅可以处理纯文本数据,还能整合其他类型的信息源,如知识图谱等,以提供更丰富的语义表示。

总的来说,FlagEmbedding强调的是稠密向量的生成和外部知识的融合;HuggingFace Transformers提供了一个广泛的预训练模型集合,适用于多种NLP任务;而Sentence-Transformers则专注于生成高质量的句子嵌入,适合那些需要深入理解句子语义的应用场景。

结合上述说明,以及翻阅网上各类文章,发现使用 Sentence-Transformers 居多,因此本文选用它。

安装 sentence-transformers(在Linux机器安装吧,Windows机器各种报错):

pip install -U sentence-transformers

基于Sentence-Transformers的向量化方法:

from sentence_transformers import SentenceTransformer
sentences_1 = ["样例数据-1", "样例数据-2"]
sentences_2 = ["样例数据-3", "样例数据-4"]
model = SentenceTransformer('BAAI/bge-large-zh-v1.5')
embeddings_1 = model.encode(sentences_1, normalize_embeddings=True)
embeddings_2 = model.encode(sentences_2, normalize_embeddings=True)
similarity = embeddings_1 @ embeddings_2.T
print(similarity)

上面的例子演示了句子的向量化过程。额外解释一下倒数第二行:

embeddings_1 @ embeddings_2.T 是 Python 中的一种矩阵乘法运算。这里使用了 @ 符号来表示矩阵的点积(dot product)操作。具体来说:

  • embeddings_1 是一个二维数组(或称为矩阵),其中每一行代表一个句子在向量空间中的嵌入表示。

  • embeddings_2 也是一个二维数组,其结构与 embeddings_1 相同,但包含不同的句子的嵌入表示。

  • embeddings_2.Tembeddings_2 的转置,这意味着将它的行列互换。这样,原来的每行变成了每列,原来的每列变成了每行。

当执行 embeddings_1 @ embeddings_2.T 时,Python 会计算两个矩阵的点积。结果是一个新的二维数组,其中的每个元素是 embeddings_1 中的一行和 embeddings_2.T 中对应列的乘积之和。在这个上下文中,它实际上是在计算两组句子嵌入之间的相似度矩阵。

例如,如果 embeddings_1 和 embeddings_2.T 分别是以下的矩阵:

embeddings_1: [ [e11, e12, e13], [e21, e22, e23] ]
embeddings_2.T: [ [f11, f21], [f12, f22], [f13, f23] ]

那么点积的结果将是:

similarity: [ [e11*f11 + e12*f12 + e13*f13, e11*f21 + e12*f22 + e13*f23],
              [e21*f11 + e22*f12 + e23*f13, e21*f21 + e22*f22 + e23*f23] ]

这个结果矩阵中的每个元素代表了原始句子对之间的某种形式的相似度分数。

有了工具集后,把模型文件下载到本地并加载即可。

到网上随便找一篇文章:

据传,菜煎饼起源于13世纪中期,当时明军与元军在峄州展开激战,当地人民死伤惨重。后来,从山西洪洞一带移民至此的民众,仅靠官府发放的半斤粮食无法充饥,便将五谷掺水,用石磨研磨成浆糊,放在铁板上,用竹片摊成“薄纸”,并大量包装蔬菜、野菜、草根和树叶,以此充饥。

菜煎饼是山东鲁南地区的一种大众食品,制作原料主要有面粉、杂粮、鸡蛋等,老少兼宜,俗称“中国热狗”,流行于枣庄、济宁、临沂、徐州等鲁南地,后传布周围省市。上个世纪七十年代,枣庄农村的生活还是很匮乏的,老百姓的主食以煎饼为主,煎饼的主要原料是地瓜干,条件好一点的可稍放点小麦,刚烙煎饼时鏊子凉,需把鏊子烧热擦些油才容易把煎饼从鏊子上揭下来,这样烙出的煎饼就很厚,稍等一会儿,煎饼凉了又板又硬,很难下咽。因此我们枣庄人把烙煎饼时前几张和后几张煎饼称为滑鏊子煎饼或滑塌子。这样的煎饼很难下咽,但丢了又可惜,精明的母亲们就将大白菜,土豆丝,粉条,豆腐切碎加点猪油,放上辣椒面,花椒面和盐,做成了所谓的菜煎饼,这样一来不但滑鏊子煎饼解决了,并且做出的煎饼还特别好吃,这样一传十、十传百,于是菜煎饼就在农村各家各户传开了!

八十年代末期,农村土地实行了联产承包责任制已有多年,农民在农忙季节忙耕种,农闲时便有了剩余时间,有的农村妇女就到街上摆地摊卖菜煎饼挣点零花钱。一辆三轮车,一盘小饼鍪,一个蜂窝炉,一个切菜板,几样时令蔬菜,食客现场点菜,业主现场烙制,简简单单的营生,成为枣庄街头一道风景。许许多多的农村人多了一个贴补家用的挣钱机会,人们生活也多了一道风味小吃。到了九十年代末期,就连一些男人也走上了街头卖起了菜煎饼。

1993年5月,山东省劳动厅在枣庄举办特级厨师培训班,聘请江苏省淮安商业技工学校一行6人赴枣庄讲学,这六人当中有校领导、高级讲师、特级厨师,途中经台儿庄区招待所午餐,席上菜肴丰盛,但惟有“菜煎饼”被其六人齐呼:“天下第一美食”。

山东菜煎饼如何做呢?首先要热锅,放油(油要多),下豆腐中火翻炒至金黄,放入之前切好的粉条,继续翻炒几分钟,加入适量的盐。再放入切好的韭菜,翻炒几下搅匀即可(千万不可炒过了,韭菜要生生的),撒味精出锅。将煎饼摊开,用勺子舀上适量的韭菜馅儿,用勺背整匀。好了之后,可以将两边向中间折叠形成长方形,一张煎饼就做好了。

按照前文的样例,写一段脚本进行评测(为方便演示,简单地根据段落进行拆分):

import sys
import torch
from sentence_transformers import SentenceTransformer

# 加载预训练的句子嵌入模型
model = SentenceTransformer(sys.argv[1])
# 定义句子列表
sentences_1 = ["据传,菜煎饼起源于13世纪中期,当时明军与元军在峄州展开激战,当地人民死伤惨重。后来,从山西洪洞一带移民至此的民众,仅靠官府发放的半斤粮食无法充饥,便将五谷掺水,用石磨研磨成浆糊,放在铁板上,用竹片摊成“薄纸”,并大量包装蔬菜、野菜、草根和树叶,以此充饥。"]
sentences_2 = ["菜煎饼是山东鲁南地区的一种大众食品,制作原料主要有面粉、杂粮、鸡蛋等,老少兼宜,俗称“中国热狗”,流行于枣庄、济宁、临沂、徐州等鲁南地,后传布周围省市。上个世纪七十年代,枣庄农村的生活还是很匮乏的,老百姓的主食以煎饼为主,煎饼的主要原料是地瓜干,条件好一点的可稍放点小麦,刚烙煎饼时鏊子凉,需把鏊子烧热擦些油才容易把煎饼从鏊子上揭下来,这样烙出的煎饼就很厚,稍等一会儿,煎饼凉了又板又硬,很难下咽。因此我们枣庄人把烙煎饼时前几张和后几张煎饼称为滑鏊子煎饼或滑塌子。这样的煎饼很难下咽,但丢了又可惜,精明的母亲们就将大白菜,土豆丝,粉条,豆腐切碎加点猪油,放上辣椒面,花椒面和盐,做成了所谓的菜煎饼,这样一来不但滑鏊子煎饼解决了,并且做出的煎饼还特别好吃,这样一传十、十传百,于是菜煎饼就在农村各家各户传开了!"]
sentences_3 = ["八十年代末期,农村土地实行了联产承包责任制已有多年,农民在农忙季节忙耕种,农闲时便有了剩余时间,有的农村妇女就到街上摆地摊卖菜煎饼挣点零花钱。一辆三轮车,一盘小饼鍪,一个蜂窝炉,一个切菜板,几样时令蔬菜,食客现场点菜,业主现场烙制,简简单单的营生,成为枣庄街头一道风景。许许多多的农村人多了一个贴补家用的挣钱机会,人们生活也多了一道风味小吃。到了九十年代末期,就连一些男人也走上了街头卖起了菜煎饼。"]
sentences_4 = ["1993年5月,山东省劳动厅在枣庄举办特级厨师培训班,聘请江苏省淮安商业技工学校一行6人赴枣庄讲学,这六人当中有校领导、高级讲师、特级厨师,途中经台儿庄区招待所午餐,席上菜肴丰盛,但惟有“菜煎饼”被其六人齐呼:“天下第一美食”。"]
sentences_5 = ["山东菜煎饼如何做呢?首先要热锅,放油(油要多),下豆腐中火翻炒至金黄,放入之前切好的粉条,继续翻炒几分钟,加入适量的盐。再放入切好的韭菜,翻炒几下搅匀即可(千万不可炒过了,韭菜要生生的),撒味精出锅。将煎饼摊开,用勺子舀上适量的韭菜馅儿,用勺背整匀。好了之后,可以将两边向中间折叠形成长方形,一张煎饼就做好了。"]
# 获取句子的嵌入向量表示、
sentences_embeddings_1 = torch.from_numpy(model.encode(sentences_1, normalize_embeddings=True))
sentences_embeddings_2 = torch.from_numpy(model.encode(sentences_2, normalize_embeddings=True))
sentences_embeddings_3 = torch.from_numpy(model.encode(sentences_3, normalize_embeddings=True))
sentences_embeddings_4 = torch.from_numpy(model.encode(sentences_4, normalize_embeddings=True))
sentences_embeddings_5 = torch.from_numpy(model.encode(sentences_5, normalize_embeddings=True))
# 合并所有的句子嵌入表示
all_sentences_embeddings = torch.cat([sentences_embeddings_1, sentences_embeddings_2, sentences_embeddings_3, sentences_embeddings_4, sentences_embeddings_5], dim=0)

# 定义查询句子
queries_1 = ["菜煎饼的制作原料有哪些?"]
queries_2 = ["菜煎饼的组成是什么?"]
queries_3 = ["做菜煎饼需要什么?"]
# 获取查询句子的嵌入向量表示
queries_embeddings_1 = torch.from_numpy(model.encode(queries_1, normalize_embeddings=True))
queries_embeddings_2 = torch.from_numpy(model.encode(queries_2, normalize_embeddings=True))
queries_embeddings_3 = torch.from_numpy(model.encode(queries_3, normalize_embeddings=True))

# 计算查询句子与所有句子的相似度
similarity_queries_1_sentences = queries_embeddings_1 @ all_sentences_embeddings.T
similarity_queries_2_sentences = queries_embeddings_2 @ all_sentences_embeddings.T
similarity_queries_3_sentences = queries_embeddings_3 @ all_sentences_embeddings.T

# 打印numpy size
print("sentences_vector dimension:", sentences_embeddings_1.size())
print("sentences_vector dimension:", queries_embeddings_1.size())
# 打印相似度结果(几个问题都是在问制作原料,从字面来看,我们预期三个查询与sentences_2 和 sentences_5 的相似度较高)
print("Query 1 Similarity:", similarity_queries_1_sentences)
print("Query 2 Similarity:", similarity_queries_2_sentences)
print("Query 3 Similarity:", similarity_queries_3_sentences)

执行时,把模型本地路径作为第一个参数传入即可,如:

time python test.py ./bge-large-zh-v1.5
time python test.py ./bge-m3
time python test.py ./m3e-base
time python test.py ./tao-8k

其中的time是为了测试程序运行耗时。

作者在本机CPU执行,机器配置为:4核CPU(i7)、16GB内存。

评测结果如下:

79c503ca45c0f332be2e9b43be3da19a.jpeg

看起来,bge-m3效果最好,tao-8k也不错。但需要注意的是,这两个模型执行耗时也最高。

本文选用常见的几类中文友好的开源Embedding模型进行了简单效果评测,发现bge-m3和tao-8k的效果不错。有条件的读者可以将其部署在GPU上进行评测,应该会更快。另外,也可以使用更为全面的数据集进行评估,以得出更为权威的结论。

在实际的生产环境中,还要进行压力测试,以评估文档向量化的性能。

最后,欢迎关注微信公众号xiaoxi666,一起交流一起玩儿~

[1] 向量数据库: https://guangzhengli.com/blog/zh/vector-database/

[2] bge-large-zh-v1.5: https://hf-mirror.com/BAAI/bge-large-zh-v1.5

[3] bge-m3: https://hf-mirror.com/BAAI/bge-m3

[4] m3e-base: https://hf-mirror.com/moka-ai/m3e-base

[5] tao-8k: https://hf-mirror.com/amu/tao-8k

[6] 智源开源最强语义向量模型BGE: https://zhuanlan.zhihu.com/p/648448793

[7] 新一代通用向量模型BGE-M3: https://zhuanlan.zhihu.com/p/680537154

[8] 实战对比OpenAI、BGE-Large以及阿里Embedding模型效果: https://zhuanlan.zhihu.com/p/658775304​

[9] Embedding模型的选择: https://zhuanlan.zhihu.com/p/673483110

[10] 中文Embedding模型优劣数据评测: https://zhuanlan.zhihu.com/p/679166797


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK