Published at 2024-08-04 | Last Update 2024-08-04
本文整理一些文本向量化(embedding)和信息检索的知识,它们是如今大模型生成文本时常用的技术 —— “增强检索生成”(RAG)—— 的基础:
Fig. Similarity score based on BERT embedding. Image source
水平及维护精力所限,文中不免存在错误或过时之处,请酌情参考。 传播知识,尊重劳动,年满十八周岁,转载请注明出处。
RAG (Retrieval-Augmented Generation,检索增强生成),是一种利用信息检索(Information Retrieval) 技术增强大模型生成效果(generation)的技术。RAG 在步骤上很简单,
embedding
),然后本文主要关注以上 1 & 2 步骤中的 embedding & retrieval 阶段。
信息检索的技术发展大致可分为三个阶段:
基于统计信息的关键字匹配(statistical keyword matching)
sparse embedding
—— embedding 向量的大部分字段都是 0;基于深度学习模型的上下文和语义理解,
dense embedding
—— embedding 向量的大部分字段都非零;所谓的“学习型”表示,组合上面两种的优点,称为 learned sparse embedding
下面分别来看。
1970s-2010s
)TF-IDF
、BM25
早期信息检索系统主要是基于统计信息 + 匹配关键词,算法包括,
分析语料库的词频和分布(term frequency and distribution), 作为评估文档的相关性(document relevance)的基础。
Word2Vec
(Google, 2013)2013 年,谷歌提出了 Word2Vec,
BERT
(Google, 2019)基于 transformer 的预训练(pretrain)语言模型 BERT 的出现,彻底颠覆了传统的信息检索范式。
BERT 严重依赖预训练数据集的领域知识(domain-specific knowledge), 预训练过程使 BERT 偏向于预训练数据的特征, 因此在领域外(Out-Of-Domain),例如没有见过的文本片段,表现就不行了。
解决方式之一是fine-tune
(精调/微调),但成本相对较高,
因为准备高质量数据集的成本是很高的。
另一方面,尽管传统 sparse embedding 在词汇不匹配问题时虽然也存在挑战, 但在领域外信息检索中,它们的表现却优于 BERT。 这是因为在这类算法中,未识别的术语不是靠“学习”,而是单纯靠“匹配”。
代表算法:BGE-M3。
根据以上描述,乍一看,这种 learned sparse embedding 与传统 sparse embedding 好像没太大区别, 但实际上二者有着本质不同,这种 embedding,
简单来说, vector embedding,或称向量表示,是一个单词或句子在高维向量空间中的数值表示。
对应上一节介绍的三个主要发展阶段,常见的有三种 embedding 类型:
非常适合关键词匹配任务(keyword-matching tasks)。
1x768
维度;所有维度都非零,包含语义理解,信息非常丰富,因此适用于 语义搜索任务(semantic search tasks)。
Multi-vector retrieval
- 用多个向量表示一段文本,可以看做是对 dense retrieval 的一种扩展
- 模型:ColBERT
结合了传统 sparse embedding 的精确度和 dense embedding 的语义丰富性,
这里主要介绍 BGE-M3 模型的原理。BGE-M3 建立在 BERT 之上,因此需要先回顾 BERT 的基本原理。
BERT
是如何工作的以输入 "Milvus is a vector database built for scalable similarity search"
为例,工作过程 [2]:
Fig. BERT dense embedding.
Tokenization
[CLS]
token 表示开始,[SEP]
token 表示一个句子的结束。Embedding
:使用 embedding matrix 将每个 token 转换为一个向量,详见 BERT 论文;Encoding
:这些向量通过多层 encoder,每层由 self-attention 和 feed-forward 神经网络组成
Output
:输出一系列最终的 embedding vectors。最终生成的 dense embedding 能够捕捉单个单词的含义及其在句子中的相互关系。
理解 BERT 是如何生成 dense embedding 之后,接下来看看基于 BERT dense embedding 的信息检索是如何工作的。
有了 dense embedding 之后,针对给定文本输入检索文档就很简单了,只需要再加一个最近邻之类的算法就行。
下面是两个句子的相似度判断,原理跟文档检索是一样的:
Fig. Similarity score based on BERT embedding. Image source
下面看个具体的 embedding & retrieval 模型:BGE-M3。
BGE-M3
(BERT-based learned sparse embedding)是如何工作的?BGE 是一系列 embedding 模型,扩展了 BERT 的能力。BGE-M3
是目前最新的一个,3 个 M 是强调的多个 multi-
能力:
BGE-M3 通过更精细的方法来捕捉每个 token 的重要性,
Token importance estimation
:BERT 在分类/相似性比较时仅关注第一个 token([CLS]
), BGE-M3 则扩大到关注序列中的每个 token Hi
;Wlex
;Wlex
和 Hi
的乘积经过 Rectified Linear Unit (ReLU) 激活函数,得到每个 token 的术语权重 Wt
。learned sparse embedding
:以上输出的是一个 sparse embedding,其中每个 token 都有一个相关的 weights,表明在整个输入文本上下文中的重要性。下面看个例子。
还是前面例子提到的输入,
Fig. BGE-M3 learned sparse embedding. Image source
$ pip install FlagEmbedding peft sentencepiece
来自官方的代码,稍作修改:
from FlagEmbedding import BGEM3FlagModel
model = BGEM3FlagModel('/root/bge-m3', use_fp16=True)
queries = ["What is BGE M3?",
"Defination of BM25"]
docs = ["BGE M3 is an embedding model supporting dense retrieval, lexical matching and multi-vector interaction.",
"BM25 is a bag-of-words retrieval function that ranks a set of documents based on the query terms appearing in each document"]
query_embeddings = model.encode(queries, batch_size=12, max_length=8192,)['dense_vecs']
docs_embeddings = model.encode(docs)['dense_vecs']
similarity = query_embeddings @ docs_embeddings.T
print(similarity)
这个例子是两个问题,分别去匹配两个答案,看彼此之间的相似度(四种组合),运行结果:
[[0.626 0.348 ]
[0.3499 0.678 ]]
符合预期。
精调的目的是让正样本和负样本的分数差变大。
jsonl
格式,每行一个 sample;
{"query": str, "pos": List[str], "neg":List[str]}
query
:用户问题;pos
:正样本列表,简单说就是期望给到用户的回答;不能为空,也就是说必需得有正样本;neg
:负样本列表,是避免给到用户的回答。
"neg": [""]
,写 "neg": []
会报错。"neg": []
也不行,必须得留着这个字段。注意:
从 huggingface 或国内的 modelscope 下载 BGE-M3 模型,
$ git lfs install
$ git clone https://www.modelscope.cn/Xorbits/bge-m3.git
精调命令:
$ cat sft.sh
#!/bin/bash
num_gpus=1
output_dir=/root/bge-sft-output
model_path=/root/bge-m3
train_data=/data/share/bge-dataset
batch_size=2
query_max_len=128 # max 8192
passage_max_len=1024 # max 8192
torchrun --nproc_per_node $num_gpus \
-m FlagEmbedding.BGE_M3.run \
--output_dir $output_dir \
--model_name_or_path $model_path \
--train_data $train_data \
--learning_rate 1e-5 \
--fp16 \
--num_train_epochs 5 \
--per_device_train_batch_size $batch_size \
--dataloader_drop_last True \
--normlized True \
--temperature 0.02 \
--query_max_len $query_max_len \
--passage_max_len $passage_max_len \
--train_group_size 2 \
--negatives_cross_device \
--logging_steps 10 \
--same_task_within_batch True \
--save_steps 10000 \
--unified_finetuning True \
--use_self_distill True
几个参数要特别注意下:
query & doc 最大长度
query_max_len
:支持的最长 query,最大 8192
;passage_max_len
:支持的最长文档(一条 pos 或 neg 记录)长度,最大 8192
BGE-M3 会分别针对 query 和 doc 初始化两个 tokenizer,以上两个参数其实对应
tokenizer 的 max_length,而 tokenizer 最大支持 8192(见模型目录 tokenizer_config.json
)。
batch_size
:并行度,直接决定了显存占用大小和精调快慢;
save_steps
:多少个 step 保存一次 checkpoint,默认值 500 太小,每个 checkpoint ~7GB
,多了之后可能会打爆磁盘导致任务失败。精调快慢取决于 GPU 算力、显存和参数配置,精调开始之后也会打印出预估的完成时间,还是比较准的。
还是用 4.1 的代码,稍微改一下,不要把 queries 和 docs 作为列表,而是针对每个 query 和 pos/neg 计算相似度得分。 然后针对测试集跑一下,看相似性分数是否有提升。
数据集质量可以的话,精调之后区分度肯定有提升。
如果是在 CPU 上跑模型(不用 GPU), 根据之前实际的 BERT 工程经验,转成 onnx 之后能快几倍,尤其是在 Intel CPU 上 (Intel 公司做了很多优化合并到社区库了)。
但 BGE-M3 官方没有转 onnx 文档,根据第三方的库能成功(稍微改点代码,从本地加载模型),效果待验证。
本文整理了一些 BGE-M3 相关的 RAG 知识。前两篇参考资料非常好,本文很多内容都来自它们,感谢作者。