文章目录
Text and Code Embeddings by Contrastive Pre-Training
《Text and Code Embeddings by Contrastive Pre-Training》是2022年1月OpenAI在arxiv上提交的论文,该论文使用对比学习预训练基于GPT3训练了一个文本向量模型cpt-text,基于codex训练了一个code向量模型cpt-code。
- 对于输入文本x,在文本序列开始和结束分别加入两个特殊分隔符
[SOS]
和[EOS]
,将模型最后一层`EOS隐状态表征作为输入文本x的embedding。
- 对于训练文本对(x, y),采用常规的Bi-encoder的方式来训练。即分别按照指定格式处理文本之后输入模型得到embeding
v
x
v_x
vx和
v
y
v_y
vy(式中的
⊕
\oplus
⊕表示连接),再用余弦相似度来计算两个向量之间的相似性。作者发现使用不同的分隔符有助于更稳定的训练,所以对于x用
[
作为 [ S O S ] x [\mathrm{SOS}]_x [SOS]x ,用]
作为 [ E O S ] x [\mathrm{EOS}]_x [EOS]x;对于y用{
作为 [ S O S ] y [\mathrm{SOS}]_y [SOS]y ,用}
作为 [ E O S ] y [\mathrm{EOS}]_y [EOS]y。
v x = E ( [ S O S ] x ⊕ x ⊕ [ E O S ] x ) v y = E ( [ S O S ] y ⊕ y ⊕ [ E O S ] y ) sim ( x , y ) = v x ⋅ v y ∥ v x ∥ ⋅ ∥ v y ∥ \begin{aligned} v_x & =E\left([\mathrm{SOS}]_x \oplus x \oplus[\mathrm{EOS}]_x\right) \\ v_y & =E\left([\mathrm{SOS}]_y \oplus y \oplus[\mathrm{EOS}]_y\right) \\ \operatorname{sim}(x, y) & =\frac{v_x \cdot v_y}{\left\|v_x\right\| \cdot\left\|v_y\right\|} \end{aligned} vxvysim(x,y)=E([SOS]x⊕x⊕[EOS]x)=E([SOS]y⊕y⊕[EOS]y)=∥vx∥⋅∥vy∥vx⋅vy
- cpt-text模型使用相邻的两个文本作为正样本,cpt-code模型使用开源代码里的(text,code)对。训练时使用in-batch negatives。大的训练batch size对于训练很重要。
- 作者在训练时发现在搜索和分类任务上模型的效果在提升时在句子相似度任务效果却在下降。作者认为搜索任务和句子相似性任务可能有矛盾的定义;比如一个句子和其否定句在搜索时被认为是相关,但是对于句子相似性任务来说不相似。
Sgpt
Sgpt出自2022年2月的论文《Sgpt: Gpt sentence embeddings for semantic search》,也是较早提出的由decoder-only transformer模型来生成句子embedding的思路。
对于自回归decoder transformer,其causal attention机制使得token不会获取处于其后面token的信息。Sgpt的解决办法是使用postion-weighted mean pooling来获取句子embedding,让越靠后的token的权重越大,具体如下式表示,式中的S是输入序列长度,
h
i
h_i
hi是第i个隐状态向量,v是输入文本的embedding。
v
=
∑
i
=
1
S
w
i
h
i
where
w
i
=
i
∑
i
=
1
S
i
v = \sum^S_{i=1} w_i h_i \qquad \text{where} \ \ w_i = \frac{i}{\sum^S_{i=1} i}
v=i=1∑Swihiwhere wi=∑i=1Sii
Sgpt模型的训练采用有监督对比学习,使用in-batch negatives。
PromptEOL
PromptEOL(Prompt-based method with Explicit One word Limitation) 出自2023年7月的论文《Scaling sentence embeddings with large language models》,使用This sentence: " [text] " means in one word: "
的prompt模板来让LLM生成一个句子的embedding。
PromptEOL受PromptBERT的prompt模板This sentence: " [text] " means [MASK].
启发(论文作者认为这个模板中的句号.隐式地限制BERT用一个词来解释句子)提出了prompt模板This sentence: " [text] " means in one word: "
,让LLM根据模板生成下一个token并将其隐状态表征作为句向量。 模板中的[text]
是待编码句子,in one word
限制LLM在下一个token中预测编码句子的语义信息,This sentence: "
用来指示输入句子,而在模板最后加入: "
防止LLM在下一个token生成标点符号。
LLM有In-context learning的能力,但是句向量不像分类或者问答很容易有一些例子给LLM。PromptEOL作者提出了如上图所示的框架来构建示例数据集以利用LLM的In-context learning能力。示例数据集包括<句子,词语>数据对,词语是用来表示句子的语义信息的。有两种构建示例数据集的方法:
- 用ChatGPT来生成可用来表示STS-B训练数据集里给定句子语义信息的词语,使用的prompt模板如上图里的
This sentence:"A jockey riding a horse." means in one word:
,即prompt ChatGPT对于给定句子输出一个词语的摘要。 - 使用牛津字典里的词语和其定义句子。
作者在OPT上进行的实验表明,In-context learning对于PromptEOL的效果有很大提升,特别对于1B参数以上的OPT。
PromptEOL方法也可以使用对比学习方式进行微调,论文实验使用与SimCSE一样的有监督对比学习,训练时生成向量要使用PromptEOL所提出的prompt模板来得到向量。
RepLLaMA
RepLLaMA出自2023年10月的论文《Fine-tuning llama for multi-stage text retrieval》,其使用LLama来生成query和document的embedding用于检索。对于输入文本在最后面加入结束符</s>
组成输入序列后送入LLama,将最末尾token的最后一层隐状态表征作为输入文本的embedding。训练时使用对比学习损失infoCSE,同时使用in-batch negatives和hard negatives。
UDEVER
UDEVER (Universal DEcoder VEctoR)出自2023年10月的论文《Language models are universal embedders》。它基于BLOOM仅使用英文数据用对比学习来微调模型得到文本向量,并提出仅使用英文数据就可以学习到跨语言的embedding。UDEVER从decoder-only LLM得到向量的方式与前面提到的《Text and Code Embeddings by Contrastive Pre-Training》一样,在文本序列开始和结束分别加入两个特殊分隔符 [ B O S ] t [BOS]_t [BOS]t和 [ E O S ] t [EOS]_t [EOS]t(t表示文本类型,在论文中主要是query和document),将模型最后一层 [ E O S ] t [EOS]_t [EOS]t隐状态作为输入文本x的embedding。
E5-mistral-7b-instruct
E5-mistral-7b-instruct出自2024年1月的技术报告《Improving text embeddings with large language models》,它使用LLM生成合成数据来微调LLM模型Mistral-7B得到文本embedding。
首先使用与Self-Instruct类似的两步prompt策略来生成合成数据:先让LLM头脑风暴出一个候选任务池,接着让LLM对候选池中的给定任务生成数据。为了生成丰富多样的合成数据,将embedding相关的任务分成不同的类别,为每个类别设计不同的两步prompt策略模板(论文附录中列了所用模板和其他一些实现细节)。
- 非对称任务,分成四个组:short-long match, long-short match, short-short match, long-long match。
- 对称任务,分为两个组:monolingual semantic textual similarity (STS) 、 bitext retrieval。(论文里说对称任务比较简单,忽略了头脑风暴步骤)
E5-mistral-7b-instruct训练时的基座模型是开源LLM模型Mistral-7B,其作者认为LLM已经在web-scale数据上预训练了,所以通常的多阶段向量模型训练流程里的预训练对比学习没有什么益处,所以只需要在生成的合成数据上对比学习微调就可以了。其实验证明LLM仅在合成数据上微调就可以取得有竞争性的结果,在包括合成数据和标注数据的混合数据上微调在论文发布时达到了SOTA。模型微调时的目标函数是INfoNCE损失,使用in-batch negatives和hard negatives。
对于给定的query-document对
(
q
+
,
d
+
)
(q^+, d^+)
(q+,d+),会使用如下的指令模板将
q
+
q^+
q+变成
q
i
n
s
t
+
q^+_{inst}
qinst+,而对
d
+
d^+
d+不添加指令前缀。
q
i
n
s
t
+
=
Instruct:{task_definition} \n Query:
{
q
+
}
q^+_{inst} = \text{Instruct:\{task\_definition\} \textbackslash n \ Query: \ } \{ q^+\}
qinst+=Instruct:{task_definition} \n Query: {q+}
模板中的{task_definition}
是用来描述embedding任务的占位符,对于合成数据,使用在生成数据头脑风暴步骤的输出,对于其他数据集人工定义指令。
在query和document的后面添加一个[EOS]
的token,输入LLM之后,将最后一层的[EOS]
隐状态表征当做输入文本的embedding。
下面表格列出了部分定义的数据集任务指令。
任务 | 指令 |
---|---|
AmazonCounterfactualClassif. | Classify a given Amazon customer review text as either counterfactual or not-counterfactual |
AmazonPolarityClassification | Classify Amazon reviews into positive or negative sentiment |
AmazonReviewsClassification | Classify the given Amazon review into its appropriate rating category |
Banking77Classification | Given a online banking query, find the corresponding intents |
EmotionClassification | Classify the emotion expressed in the given Twitter message into one of the six emotions: anger, fear, joy, love, sadness, and surprise |
ImdbClassification | Classify the sentiment expressed in the given movie review text from the IMDB dataset |
MassiveIntentClassification | Given a user utterance as query, find the user intents |
MassiveScenarioClassification | Given a user utterance as query, find the user scenarios |
MTOPDomainClassification | Classify the intent domain of the given utterance in task-oriented conversation |
MTOPIntentClassification | Classify the intent of the given utterance in task-oriented conversation |
ToxicConversationsClassif. | Classify the given comments as either toxic or not toxic |
TweetSentimentClassification | Classify the sentiment of a given tweet as either positive, negative, or neutral |
ArxivClusteringP2P | Identify the main and secondary category of Arxiv papers based on the titles and abstracts |
ArxivClusteringS2S | Identify the main and secondary category of Arxiv papers based on the titles |
BiorxivClusteringP2P | Identify the main category of Biorxiv papers based on the titles and abstracts |
BiorxivClusteringS2S | Identify the main category of Biorxiv papers based on the titles |
MedrxivClusteringP2P | Identify the main category of Medrxiv papers based on the titles and abstracts |
MedrxivClusteringS2S | Identify the main category of Medrxiv papers based on the titles |
RedditClustering | Identify the topic or theme of Reddit posts based on the titles |
RedditClusteringP2P | Identify the topic or theme of Reddit posts based on the titles and posts |
StackExchangeClustering | Identify the topic or theme of StackExchange posts based on the titles |
StackExchangeClusteringP2P | Identify the topic or theme of StackExchange posts based on the given paragraphs |
TwentyNewsgroupsClustering | Identify the topic or theme of the given news articles |
SprintDuplicateQuestions | Retrieve duplicate questions from Sprint forum |
TwitterSemEval2015 | Retrieve tweets that are semantically similar to the given tweet |
Echo embeddings
Echo embeddings出自2024年2月的论文《Repetition Improves Language Model Embeddings》,其思路很简单,把要编码的文本在上下文中输入两遍,取文本在第二次位置上的embedding来得到句子向量。
Echo embeddings作者认为decoder-only LLM使用causal注意力机制,所以无法很好的利用到文本后半段的信息。于是想到将输入句子给LLM展现两次,并从输入句子第二次出现的位置来得到句子embedding。在论文中做了一系列实验来说明提出策略的有效性,特别是在zero-shot时Echo embedding的效果相比classical embedding(不重复输入文本)效果提升了很多,在finetune后也能比classical embedding略微好一点。
Echo embeddings 使得输入文本出现两次的prompt类似 “Rewrite the sentence: x, rewritten sentence: x”, x是指要输入文本。Rewrite可以换成repeat、repharese、fill in the blank等,不影响生成的embedding效果,关键是需要使x在输入中出现两次。
Echo embeddings考虑两种方式来从一个decoder-only LLM最后一层隐状态表征来得到句子的向量:第一种是mean token embedding,将句子里每个token embedding均值作为句子向量,设A为句子token对应的索引,则句向量为: ϕ A ( x ) : = 1 ∣ A ∣ ∑ t ∈ A ϕ t ( x ) \phi_A(x) := \frac{1}{|A|} \sum_{t \in A} \phi_t(x) ϕA(x):=∣A∣1∑t∈Aϕt(x)。第二种方式last-token embedding,将输入句子里的最后一个token的embedding作为句子向量,记为 ϕ − 1 ( x ) \phi_{-1}(x) ϕ−1(x)。后续试验表明在zero-shot场景下,mean token embedding效果好很多且更鲁棒,在finetune场景下,last-token embedding效果略微好一点。
LLM2Vec
LLM2Vec出自2024年4月的论文《LLM2Vec: Large Language Models Are Secretly Powerful Text Encoders》, 它提出了一种将decoder-only LLM转变成文本encoder的无监督方法,主要包括三个步骤(如下图):1.开启双向注意力机制;2.使用masked next token prediction训练;3.无监督对比学习训练。
下面介绍LLM2Vec的三个步骤的细节。
-
开启双向注意力机制。decoder-only LLM使用的是causal注意力机制,即在位置i的token只会受其之前的位置 0 , 1 , … , i − 1 0,1,\ldots,i-1 0,1,…,i−1的tokens的影响,LLM2Vec通过将decoder-only LLM的causal attention mask替换成all-ones matrix来开启双向注意力机制。
但如果仅仅开启双向注意力机制不做其他操作,在作者的实验中验证了会减弱大多数decoder-only LLM的embedding效果。
LLM2Vec的教程中示例了将LLaMA的flash attention实现转变成双向注意力。
# 创建新的LLaMA flash attention类,使用双向注意力(设置is_causal为False)
class ModifiedLlamaFlashAttention2(LlamaFlashAttention2):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.is_causal = False # Initially `True` in transformers implementation
LLAMA_ATTENTION_CLASSES = {
"eager": LlamaAttention,
"flash_attention_2": ModifiedLlamaFlashAttention2, # Initially, `LlamaFlashAttention2'
"sdpa": LlamaSdpaAttention,
}
# 将LLama 的decoder层使用自定义的注意力类
class ModifiedLlamaDecoderLayer(LlamaDecoderLayer):
def __init__(self, config: LlamaConfig, layer_idx: int):
nn.Module.__init__(self) # Initially, super().__init__()
self.hidden_size = config.hidden_size
self.self_attn = LLAMA_ATTENTION_CLASSES[config._attn_implementation](config=config, layer_idx=layer_idx)
self.mlp = LlamaMLP(config)
self.input_layernorm = LlamaRMSNorm(config.hidden_size, eps=config.rms_norm_eps)
self.post_attention_layernorm = LlamaRMSNorm(config.hidden_size, eps=config.rms_norm_eps)
# 使Llama模型使用新的decoder层
class LlamaBiModel(LlamaModel):
def __init__(self, config):
LlamaPreTrainedModel.__init__(self, config) # Initially, super().__init__(config)
self.padding_idx = config.pad_token_id
self.vocab_size = config.vocab_size
self.embed_tokens = nn.Embedding(config.vocab_size, config.hidden_size, self.padding_idx)
self.layers = nn.ModuleList(
[ModifiedLlamaDecoderLayer(config, layer_idx) for layer_idx in range(config.num_hidden_layers)] # Initially, `LlamaDecoderLayer(config, layer_idx)`
)
self.norm = LlamaRMSNorm(config.hidden_size, eps=config.rms_norm_eps)
self.gradient_checkpointing = False
self.post_init()
-
使用masked next token prediction(MNTP)来训练。decoder-only LLM在训练的时候没有双向注意力机制,使用MNTP来让模型适应该机制。MNTP将next token prediction和masked language model结合起来。对于任意序列 x = ( x 1 , x 2 , … , x N ) \mathbf{x}=(x_1, x_2,\ldots,x_N) x=(x1,x2,…,xN),mask掉一部分输入token,训练模型基于其上下文来预测masked tokens。当预测位置i上的masked token时,根据其前面的位置i-1上的token表征logits来计算损失。
LLM2Vec在训练MNTP时的数据与模型预训练数据保持一致,因为训练目标不是为了让模型学习新知识,而是为了让模型在生成embedding时考虑未来的tokens。
LLM2Vec的教程中示例了调整LLaMA的训练目标为MNTP,训练脚本参考github
#将原来的BiLlamaForMNTP 替换为新的类
class BiLlamaForMNTP(LlamaForCausalLM):
def __init__(self, config, attention_dropout=0.0):
LlamaPreTrainedModel.__init__(self, config) # Initially, super().__init__(config)
self.model = LlamaBiModel(config) # Initially, LlamaModel(config)
self.vocab_size = config.vocab_size
self.lm_head = nn.Linear(config.hidden_size, config.vocab_size, bias=False)
self.post_init()
## 损失函数定义可以借用LlamForCausalLM
# Code snippet from LlamaForCausalLM.forward()
loss = None
if labels is not None:
# Shift so that tokens < n predict n
shift_logits = logits[..., :-1, :].contiguous()
shift_labels = labels[..., 1:].contiguous()
# Flatten the tokens
loss_fct = CrossEntropyLoss()
shift_logits = shift_logits.view(-1, self.config.vocab_size)
shift_labels = shift_labels.view(-1)
# Enable model parallelism
shift_labels = shift_labels.to(shift_logits.device)
loss = loss_fct(shift_logits, shift_labels)
## decoder-only models没有mask token,需要添加上
tokenizer.mask_token = "_"
-
无监督对比学习。LLM2Vec作者认为前两个任务可以使得decoder-only LLM变成word-level任务的编码器,但不足以用来进行句子表征,因为decoder-only LLM在其预训练目标里没有包含next sentence prediction任务。为了让LLM2Vec更好地表征句子,采用SimCSE提出的无监督对比学习来进一步训练模型,即将一个句子输入到模型两次,两次输入时的dropout不一样得到同一个句子的两个不同表征。
在对句子表征时,采用E5-mistral-7b-instruct相同的指令,对于非对称任务只在query上添加指令,对于对称任务在句子和document都添加指令。 句子表征与SGPT一样,使用weighted mean pooling(不包含指令token)。
总结
本文总结了基于decoder-only LLM得到embedding的方法:LLM2Vec、Echo embeddings、PromptEOL、E5-mistral-7b-instruct、Sgpt、RepLLaMA 、cpt-text、UDEVER。这些方法大部分是需要在原有模型上使用对比学习微调训练的,只有基于PromptEOL和Echo embeddings这两种基于prompt的方法可以直接得到embedding,但是它们微调之后的效果也更好。此外根据方法处理输入文本的方式不一样,有些方法如PromptEOL更适合于短文本或者句子的向量表示。