贫瘠之地

华北无浪漫,死海扬起帆
多少个夜晚,独自望着天

0%

LangChain 文档学习 No.3 - 示例选择器

MMR

MMR 为最大边际相关性,MaxMarginalRelevanceExampleSelector 根据示例与输入之间的相似度以及多样性来选择示例

通过寻找与输入具有最大余弦相似度的嵌入的示例,并在迭代中进行添加;同时对与已选择示例的相似度进行惩罚来进行实现

import

1
2
3
4
5
6
7
from langchain.prompts.example_selector import (
MaxMarginalRelevanceExampleSelector,
SemanticSimilarityExampleSelector,
)
from langchain.vectorstores import FAISS
from langchain.embeddings import OpenAIEmbeddings
from langchain.prompts import FewShotPromptTemplate, PromptTemplate

构造提示模板和示例集合

1
2
3
4
5
6
7
8
9
10
11
12
13
example_prompt = PromptTemplate(
input_variables=["input", "output"],
template="Input: {input}\nOutput: {output}",
)

# 反义词示例集
examples = [
{"input": "happy", "output": "sad"},
{"input": "tall", "output": "short"},
{"input": "energetic", "output": "lethargic"},
{"input": "sunny", "output": "gloomy"},
{"input": "windy", "output": "calm"},
]

创建 MaxMarginalRelevanceExampleSelector

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
example_selector = MaxMarginalRelevanceExampleSelector.from_examples(
# 可供选择的示例列表
examples,
# 用于生成用于测量语义相似性的嵌入的嵌入类(LangChain 对接了很多三方 Embedding API)
OpenAIEmbeddings(),
# 这是 VectorStore 类,用于存储嵌入并进行相似性搜索
FAISS,
# 选择的示例数
k=2,
)
mmr_prompt = FewShotPromptTemplate(
# 提供了 ExampleSelector 而不是示例集
example_selector=example_selector,
example_prompt=example_prompt,
prefix="Give the antonym of every input",
suffix="Input: {adjective}\nOutput:",
input_variables=["adjective"],
)

FewShotPromptTemplate fomat

1
2
3
# 输入的 worried 是一种感觉
# 所以应该选择 happy/sad 的例子作为第一个
print(mmr_prompt.format(adjective="worried"))
1
2
3
4
5
6
7
8
9
10
Give the antonym of every input

Input: happy
Output: sad

Input: windy
Output: calm

Input: worried
Output:

SemanticSimilarity

根据与输入的相似度选择示例,通过找到与输入具有最大余弦相似度的 embedding 例子来实现此目的

Embedding

embedding 中文翻译是“嵌入”

在⾃然语⾔处理和机器学习领域,embedding 是指将单词、短语或⽂本等离散变量转换成连续向量空间的过程

向量空间通常被称为嵌⼊空间 embedding space,⽣成的向量则称为嵌⼊向量 embedding vector 或向量嵌⼊vector embedding


为什么需要 embedding

LLM 有一些无法解决的问题:

  • LLM 的训练数据是有时间约束的,无法获取到最新的一些信息
  • LLM 不知道答案后开始放飞自我,出现 hallucination
  • 应用化(例如客服),数据需要自主提供

针对上述问题,LLM 的供应商一般提供了如下机制:维护自己的数据集,用户输入 prompt 之后首先匹配自主维护的数据集,找到相关性强的部分,再结合成最终的 prompt 交给 LLM

新的问题出现了 怎么在我们的数据集中检索到和 prompt 相关的内容

数据向量是将数据表示为数值向量的形式,用于方便地进行计算、分析和处理,处理为向量后计算机才可以更好的对内容机进行相关性的判断


LangChain 这里使用了 OpenAI 的 embedding API 生成内容对应的 embedding vectors

1
2
3
4
5
6
7
example_selector = MaxMarginalRelevanceExampleSelector.from_examples(
examples,
# 用于生成用于测量语义相似性的嵌入的嵌入类(LangChain 对接了很多三方 Embedding API)
OpenAIEmbeddings(),
FAISS,
k=2,
)

也提供了很多 LLM 的 API,例如微软 Azure 的有 AzureOpenAIEmbeddings

不过需要部署相应的模型才能使用,模型不匹配会提示

1
The embeddings operation does not work with the specified model, gpt-35-turbo-16k. Please choose different model and try again. 

N-gram overlap

NGramOverlapExampleSelector 根据示例与输入之间的 n-gram 重叠程度选择和排序示例

N-gram 重叠得分是一个在 0.0 到 1.0 之间(闭区间)的浮点数

选择器允许设置阈值分数,n-gram 重叠得分小于或等于阈值的示例将被排除在外;默认情况下,阈值设置为 -1.0,因此不会排除任何示例

设置提示模板和示例

1
2
3
4
5
6
7
8
9
10
11
example_prompt = PromptTemplate(
input_variables=["input", "output"],
template="Input: {input}\nOutput: {output}",
)

# These are examples of a fictional translation task.
examples = [
{"input": "See Spot run.", "output": "Ver correr a Spot."},
{"input": "My dog barks.", "output": "Mi perro ladra."},
{"input": "Spot can run.", "output": "Spot puede correr."},
]

创建 NGramOverlapExampleSelector 选择器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
example_selector = NGramOverlapExampleSelector(
examples=examples,
example_prompt=example_prompt,
# 这是选择器停止的阈值
# 默认情况下,它设置为-1.0
threshold=-1.0,
'''
对于负阈值:
Selector 按 ngram 重叠分数对示例进行排序,不排除任何示例
对于大于 1.0 的阈值:
选择器排除所有示例,并返回一个空列表
对于等于 0.0 的阈值:
Selector 根据 ngram 重叠分数对示例进行排序,并且排除与输入没有 ngram 重叠的那些
'''
)
dynamic_prompt = FewShotPromptTemplate(
example_selector=example_selector,
example_prompt=example_prompt,
prefix="Give the Spanish translation of every input",
suffix="Input: {sentence}\nOutput:",
input_variables=["sentence"],
)

FewShotPromptTemplate fomat

1
2
3
# An example input with large ngram overlap with "Spot can run."
# and no overlap with "My dog barks."
print(dynamic_prompt.format(sentence="Spot can run fast."))
1
2
3
4
5
6
7
8
9
10
11
12
13
Give the Spanish translation of every input

Input: Spot can run.
Output: Spot puede correr.

Input: See Spot run.
Output: Ver correr a Spot.

Input: My dog barks.
Output: Mi perro ladra.

Input: Spot can run fast.
Output:

添加新的示例到选择器

1
2
3
4
5
# 您也可以将新示例添加到 NGramOverlapExampleSelector 中
new_example = {"input": "Spot plays fetch.", "output": "Spot juega a buscar."}

example_selector.add_example(new_example)
print(dynamic_prompt.format(sentence="Spot can run fast."))

N-gram 操作

N-gram(n 元语法)是自然语言处理中的一种常用技术,用于将文本分割成连续的、固定长度的片段

一个 N-gram 是由 N 个连续的词或字符组成的序列

在N-gram中,N代表片段中的词或字符的数量。例如:

  • 2-gram(也称为 bigram)由两个连续的词组成,例如 natural languagemachine learning
  • 3-gram(也称为 trigram)由三个连续的词组成,例如 the quick brownlearn from data

需要注意的是,较小的 N 值可能无法捕捉到长距离的语言依赖关系,而较大的 N 值可能会导致数据稀疏问题;因此,选择合适的 N 值对于具体任务和数据集是需要仔细考虑的

示例,将 "Today is a good day" 使用 2-gram 拆分多个字符串:

  1. "Today is"
  2. "is a"
  3. "a good"
  4. "good day"


Elasticsearch 中的模糊(wildcard)搜索一般就是使用 N-gram 分词器来实现

创建一个测试索引,配置 N-gram 分词器

关于 N-gram 分词器支持的参数可以参考 [N-gram tokenizer | Elasticsearch Guide [8.10] | Elastic]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
PUT /test-ngram-index
{
"settings": {
"analysis": {
"analyzer": {
"my_analyzer": {
"tokenizer": "my_ngram_tokenizer"
}
},
"tokenizer": {
"my_ngram_tokenizer": {
"type": "ngram",
"min_gram": 2,
"max_gram": 3,
"token_chars": [
"letter"
]
}
}
}
}
}

分词

1
2
3
4
{
"analyzer": "my_analyzer",
"text": "今天是个好日子!"
}
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
{
"tokens": [
{
"token": "今天",
"start_offset": 0,
"end_offset": 2,
"type": "word",
"position": 0
},
{
"token": "今天是",
"start_offset": 0,
"end_offset": 3,
"type": "word",
"position": 1
},
{
"token": "天是",
"start_offset": 1,
"end_offset": 3,
"type": "word",
"position": 2
}

...

]
}

自定义示例选择器

ExampleSelector 必须实现两个方法:

  • add_example 方法,该方法接受一个示例并将其添加到 ExampleSelector
  • select_examples 方法,该方法接受输入变量(用于用户输入)并返回要在 few shot 提示中使用的示例列表

实现一个自定义的 ExampleSelector,效果是在示例中随机选择 n 个,n 作为构造参数传入

自定义选择器实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from langchain.prompts.example_selector.base import BaseExampleSelector
from typing import Dict, List
import numpy as np


class CustomExampleSelector(BaseExampleSelector):

# 构造器除了 examples,还需要传入 size 作为随机选择的数量参数
def __init__(self, examples: List[Dict[str, str]], size: int):
self.examples = examples
self.size = size

def add_example(self, example: Dict[str, str]) -> None:
"""添加新示例"""
self.examples.append(example)

def select_examples(self, input_variables: Dict[str, str]) -> List[dict]:
"""根据输入选择要使用的示例"""
# 使用了 np 的随机工具
return np.random.choice(self.examples, size=self.size, replace=False)

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
examples = [
{"魏": "曹丕"},
{"蜀": "刘备"},
{"吴": "孙权"}
]

# 初始化
example_selector = CustomExampleSelector(examples, 2)

# 选择
print(example_selector.select_examples({"unuseful": "unuseful"}))

# 添加新示例后选择
example_selector.add_example({"晋": "司马睿"})
print(example_selector.select_examples({"unuseful": "unuseful"}))

选择长度

选择器可以根据长度对示例进行选择来避免构建的提示长度超过上下文窗口 token 的限制

对于较长的输入,它会选择较少的示例,而对于较短的输入,它会选择更多的示例

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
from langchain.prompts import PromptTemplate
from langchain.prompts import FewShotPromptTemplate
from langchain.prompts.example_selector import LengthBasedExampleSelector

examples = [
{"input": "happy", "output": "sad"},
{"input": "tall", "output": "short"},
{"input": "energetic", "output": "lethargic"},
{"input": "sunny", "output": "gloomy"},
{"input": "windy", "output": "calm"},

example_prompt = PromptTemplate(
input_variables=["input", "output"],
template="Input: {input}\nOutput: {output}",
)
example_selector = LengthBasedExampleSelector(
examples=examples,
example_prompt=example_prompt,
# 这是格式化示例的最大长度
# 长度由下面的 get_text_Length 函数测量
max_length=25,
'''
这是用于获取字符串长度的函数以确定要包括哪些示例
它被注释掉是因为,如果未指定,则将其作为默认值提供
get_text_length: Callable [[str], int] = lambda x: len(re.split("\n| ", x))
默认逻辑是按照换行符或空格进行分割来判断长度
'''
)
dynamic_prompt = FewShotPromptTemplate(
example_selector=example_selector,
example_prompt=example_prompt,
prefix="Give the antonym of every input",
suffix="Input: {adjective}\nOutput:",
input_variables=["adjective"],
)

使用

1
print(dynamic_prompt.format(adjective="big"))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Give the antonym of every input

Input: happy
Output: sad

Input: tall
Output: short

Input: energetic
Output: lethargic

Input: sunny
Output: gloomy

Input: windy
Output: calm

Input: big
Output:

因为输入的内容很短(big),所以根据生于长度会取更多的示例

1
2
long_string = "big and huge and massive and large and gigantic and tall and much much much much much bigger than everything else"
print(dynamic_prompt.format(adjective=long_string))
1
2
3
4
5
6
7
Give the antonym of every input

Input: happy
Output: sad

Input: big and huge and massive and large and gigantic and tall and much much much much much bigger than everything else
Output:

因为输入的内容很长(big and huge and massive and large and gigantic and tall and much much much much much bigger than everything else),所以选择的示例就比较少

参考

例子选择器 | 🦜️🔗 Langchain

OpenAI体验3 —— embedding和向量数据库(pinecone) - 知乎 (zhihu.com)