最近,LAION AI 的创始人 Christoph Schuhmann 分享了一个有趣的发现,他指出,文本向量模型似乎存在一个问题:即使句子词序被打乱,模型输出的向量与原句仍然高度相似。
例如,“柏林是德国的首都” 和 “德国柏林是首都的”这两句话,后者虽然读都读不顺,但模型却分不出来。使用 jina-embeddings-v3 计算,它们的余弦相似度竟高达 0.973。
这不是个例。改变词序虽然会改变句子的意思,但向量之间的距离变化很小。“她看完电影后吃了晚饭” 和 “她吃了晚饭后看完电影”,尽管动作顺序完全颠倒,余弦相似度仍然高达 0.985。
更麻烦的是,没经过特殊训练的模型在处理否定词上也表现不太好。“这是一个有用的模型” 和 “这不是一个有用的模型” 在向量空间中的表示非常相近。同义词的替换(例如将“今天”换成“昨天”)或者时态的改变,对向量的影响也远不如我们预期的那么大。
这些问题可能会造成严重的影响。比如,“从柏林飞往阿姆斯特丹的航班” 和 “从阿姆斯特丹飞往柏林的航班” 这两个搜索查询的向量几乎相同,jina-embeddings-v3 给出的相似度高达 0.997。这对出行规划或物流等应用而言,简直是灾难性的。
在 jina.ai/embeddings 可直接测试
本文将探讨文本向量模型在词序、方向、时序、因果、比较和否定等语义关系上的理解缺陷,通过实验,我们试图找到问题本质,并提出基于微调的性能优化方案。
一开始,我们认为这可能与模型组合词义的方式有关。模型先为每个词生成一个向量,然后通过平均池化,将这些向量整合成句子的整体向量。这样一来,最终的向量几乎不包含词序的信息了,因为平均值本身就忽略了数值的排列顺序。
但是,就算用 CLS 池化的模型,也没能避免这个问题(CLS 池化通过关注句子的第一个词来理解整体语义,照理说应该更注重词序)。例如,对于 “柏林是德国的首都” 和 “德国柏林是首都的”这两句话,bge-base-zh-v1.5 模型算出来的余弦相似度还是有 0.974。
这表明向量模型的训练范式可能存在局限性。尽管语言模型在预训练阶段习得了句法结构知识,但在对比学习阶段,模型对句法结构的感知能力出现了退化。
模型为什么一开始就搞不定词序呢?我们首先想到的是文本长度,也就是 Token 的数量。当文本被输入编码器后,模型会为每个 Token 生成一个向量,你可以理解为每个词都有一个独特的向量来代表它的意思。然后,模型会对这些向量取个平均值。
为了弄清楚文本长度和词序到底怎么影响向量的相似度,我们特意做了一个实验:构建了一个包含 180 个合成句子的数据集,你可以在这里找到它:https://huggingface.co/datasets/bwang0911/word-orders-triplet-random-shuffle。
这些句子的长度分别有 3、5、10、15、20 和 30 个 token,并且每种长度的句子,我们都准备了对应的乱序版本。
🔗:https://huggingface.co/datasets/bwang0911/word-orders-triplet-random-shuffle
给大家看几个例子:
我们使用 jina-embeddings-v3 和 bge-base-en-v1.5 这两个模型对数据集进行编码,然后算了一下原始句子和乱序句子之间的余弦相似度。
现在,我们可以画一个箱线图,这样就能更清楚地看到余弦相似度是怎么变化的了。
图 1:jina-embeddings-v3 和 bge-base-en-1.5 模型(均未经过微调)在面对词序混乱的句子时,根据句子长度分组的相似度分布情况。
结果正如我们看到的,向量的平均余弦相似度呈现出明显的线性关系:文本越长,原始句子和打乱后句子的平均余弦相似度就越低。
这很可能和“词位移”有关,也就是词在被打乱后偏离原来位置的距离。你想啊,在短句子中,词能被打乱到的位置本来就少,所以词移不了太远;而长句子就有更多排列组合的可能性,词自然就能移得更远。
图 2:按 Token 数划分的句子组合
💡 这里我们就不继续列表格了,因为组合数量是词数的阶乘。当句子有 30 个词的时候,排列组合数就达到了 265 非亿(2.652528598 E+32)种,太大了放不下。
看下面这张图(余弦相似度与平均词位移关系图),你会发现文本越长,词位移就越大:
图 3:在打乱句子数据集上,余弦相似度与平均词位移之间的关系,且平均词位移与余弦不相似度存在相关性。
Token 向量依赖于它的局部上下文,也就是离它最近的那些词。在短文本里,即使重新排列词,上下文也不会发生太大的变化。
但是,在长文本里,一个词可能被挪到离它原始上下文很远的地方,这就会显著改变它的 Token 向量。所以说,打乱长文本中的词,会产生比短文本更远的向量距离。
上面的图也说明了,无论是用均值池化的 jina-embeddings-v3,还是用 CLS 池化的 bge-base-en-v1.5,都遵循同样的规律:打乱较长的文本并使词位移更远,会导致更低的相似度分数。
通常来说,遇到这类问题,我们第一反应就是用更大的模型。按照文本向量模型的 Scaling Law,通常来说,模型越大,性能就越好:
图 4:向量模型的 Scaling Law
不过,更大的模型真的就能更好地理解词序吗?
为了验证这一点,我们测试了 BGE 模型的三个版本:bge-small-en-v1.5、bge-base-en-v1.5 和 bge-large-en-v1.5,它们的参数量分别是 3300 万、1.1 亿和 3.35 亿。
我们还是用之前那 180 个句子,这次就不考虑长度了。我们用这三个不同大小的模型对原始句子和打乱后的句子进行编码,然后画出平均余弦相似度:
图 5:使用打乱句子数据集,对比三个不同模型大小 bge-small-en-v1.5、bge-base-en-v1.5 和 bge-large-en-v1.5 对词序变化的敏感程度。
结果表明,更大的模型确实对词序变化更敏感一些,但效果提升非常有限。
即使是最大的 bge-large-en-v1.5,在区分打乱和未打乱的句子方面,也仅仅只是好了一点点而已。看来,除了模型大小,还有其他因素在影响着模型对词序的敏感度,特别是训练方法的差异。
此外,余弦相似度本身也只是衡量模型区分能力的一种比较粗糙的工具。不过,至少我们可以看到,模型的大小并不是决定性的因素。简单地增大模型规模并不能解决这个问题。
在文章前面的实验部分,我们用的是 jina-embeddings-v2,而不是最新的 jina-embeddings-v3。主要是因为 v2 模型小得多,在本地 GPU 上跑实验更快,v2 的参数量是 1.37 亿,而 v3 达到了 5.8 亿。
就像我们在开头说的那样,词序并不是向量模型唯一要面对的难题。在实际应用中,更棘手的是词的选择。有很多方法可以改变句子里的词,而这些改变往往不能在向量中得到很好的体现。
比如,我们可以把 “她从巴黎飞到东京” 改成 “她从东京开车到巴黎”,但向量的表示仍然会非常相似。我们把这些情况大致分成了几类:
注意,这只是我们在工作中观察到的一些常见情况,并不一定能涵盖所有情况。
上面的表格展示了一些文本向量模型在捕捉细微词语变化时遇到的失败案例。这其实也符合我们的预期:现在的文本向量模型还缺乏推理能力。
比如说,模型似乎不理解 “from” 和 “to” 之间的关系。文本向量模型做的其实是语义匹配,而语义通常是在 token 的层面被捕获的,然后在池化后压缩成单个密集的向量。相比之下,在万亿词元规模的数据集上训练的大语言模型(LLM),已经开始展现出一些推理的能力了。
这让我们开始思考,我们能不能通过三元组的对比学习来微调向量模型,让查询语句和正样本的向量更接近,同时拉开查询语句和负样本的距离呢?
图 6:对比学习:使查询和正样本更接近,负样本更远离。
比如说,“从阿姆斯特丹飞往柏林” 就可以看作是 “从柏林飞往阿姆斯特丹” 的负例。事实上,在 jina-embeddings-v1 的技术报告中,我们已经初步尝试解决过这个问题:我们用一个由大型语言模型生成的、包含 1 万个否定示例的数据集微调了 jina-embeddings-v1 模型。
🔗:https://huggingface.co/datasets/jinaai/negation-dataset
上述报告的结果很让人振奋:
🚀 我们观察到,在所有模型尺寸中,使用三元组数据(包括我们的否定训练数据集)进行微调,都显著提升了性能,尤其是在 HardNegation 任务上。
图 7:不同的 jina-embeddings 模型在配对训练和组合三元组/配对训练下的 EasyNegation 和 HardNegation 得分对比
图 8:不同模型大小的 jina-embeddings 训练策略的性能比较
在前面,我们讨论了一些关于文本向量的关键发现:
基于这些认识,我们用一个包含了否定和词序的数据集(总共有大约 11000 个训练样本)微调了 jina-embeddings-v2-base-en 和 bge-base-en-1.5 这两个模型。
🔗:https://huggingface.co/bwang0911/word-order-jina
🔗:https://huggingface.co/bwang0911/word-order-bge
为了更好地评估微调的效果,我们又生成了一个包含 1000 个三元组的数据集,每个三元组都包含一个查询(query)、一个正例(positive, pos)和一个负例(negative, neg)。
🔗:https://huggingface.co/datasets/bwang0911/word-orders-triplet
这里给大家看一个例子:
这些三元组的设计初衷,是为了覆盖各种容易翻车的情况,包括因为词序变化而导致的方向、时间、因果关系等方面的语义变化。
现在,我们可以在三个不同的评估集上测试模型:
下面是微调前后,模型在打乱句子上的表现对比:
结果相当明显:尽管微调只花了五分钟,我们在随机打乱句子的数据集上看到了显著的性能提升!
在方向性、时序、因果和比较的案例上,我们也看到了性能的提升。模型整体性能的提高,可以从平均余弦相似度的下降上看出来。其中,提升最明显的是否定案例,这是因为我们的微调数据集里包含了 10,000 个否定训练的样本。
在这篇文章里,我们深入研究了文本向量模型面临的挑战,特别是它们在处理词序时的各种问题。具体来说,我们确认了五种主要的问题类型:方向性、时序性、因果性、比较性和否定。这些类型的查询对词序的敏感性要求很高,如果你的应用场景涉及到这些,了解模型的局限性就非常重要。
我们还做了一个快速但成功的实验,把原来只关注否定情况的数据集扩展到了涵盖所有这五种类型。实验结果挺让人兴奋的:通过仔细挑选的难负例进行微调,模型在区分语义相似和不相似的条目上,都有了明显的改进!
当然,这方面还有很多可以深入研究的地方。未来,我们会继续探索数据集的大小和质量如何影响模型性能,期待取得更多突破。
文章来自于微信公众号“Jina AI”,作者“Jina AI”
【开源免费】graphrag是微软推出的RAG项目,与传统的通过 RAG 方法使用向量相似性作为搜索技术不同,GraphRAG是使用知识图谱在推理复杂信息时大幅提高问答性能。
项目地址:https://github.com/microsoft/graphrag
【开源免费】Dify是最早一批实现RAG,Agent,模型管理等一站式AI开发的工具平台,并且项目方一直持续维护。其中在任务编排方面相对领先对手,可以帮助研发实现像字节扣子那样的功能。
项目地址:https://github.com/langgenius/dify
【开源免费】RAGFlow是和Dify类似的开源项目,该项目在大文件解析方面做的更出色,拓展编排方面相对弱一些。
项目地址:https://github.com/infiniflow/ragflow/tree/main
【开源免费】phidata是一个可以实现将数据转化成向量存储,并通过AI实现RAG功能的项目
项目地址:https://github.com/phidatahq/phidata
【开源免费】TaskingAI 是一个提供RAG,Agent,大模型管理等AI项目开发的工具平台,比LangChain更强大的中间件AI平台工具。
项目地址:https://github.com/TaskingAI/TaskingAI
【开源免费】XTuner 是一个高效、灵活、全能的轻量化大模型微调工具库。它帮助开发者提供一个简单易用的平台,可以对大语言模型(LLM)和多模态图文模型(VLM)进行预训练和轻量级微调。XTuner 支持多种微调算法,如 QLoRA、LoRA 和全量参数微调。
项目地址:https://github.com/InternLM/xtuner