LMQL – 用于语言模型的SQL

美丽居家生活 - 借助语言模型的灵感

又一个可以帮助您进行LLM申请的工具

DALL-E 3的图片

我相信您已经听说过SQL甚至已经掌握它。SQL(结构化查询语言)是一种广泛用于处理数据库数据的声明性语言。

根据年度StackOverflow调查,SQL仍然是世界上最受欢迎的语言之一。对于专业开发人员来说,SQL是排名前三的语言(仅次于Javascript和HTML/CSS)。超过一半的专业人士使用它。令人惊讶的是,SQL甚至比Python更受欢迎。

由作者绘制的图表,数据来自StackOverflow调查

SQL是与数据库中的数据进行交互的常见方式。因此,对于LLMs也有试图使用类似方法的尝试。在本文中,我想向您介绍一种名为LMQL的方法。

LMQL是什么?

LMQL(语言模型查询语言)是一种用于语言模型的开源编程语言。 LMQL使用Apache 2.0许可证发布,您可以在商业上使用它。

LMQL是由ETH Zurich的研究人员开发的。他们提出了一种LMP(语言模型编程)的新颖思想。LMP结合了自然语言和编程语言:文本提示和脚本指令。

原始论文“Prompting Is Programming: A Query Language for Large Language Models”中,作者Luca Beurer-Kellner,Marc Fischer和Martin Vechev提出了当前LLM使用的以下挑战:

  • 交互。例如,我们可以使用元提示,要求LLM扩展初始提示。作为一个实际案例,我们可以先要求模型定义初始问题的语言,然后以该语言回复。对于这样的任务,我们需要发送第一个提示,从输出中提取语言,将其添加到第二个提示模板中,然后再次调用LLM。我们需要管理相当多的交互。使用LMQL,您可以在一个提示中定义多个输入和输出变量。而且,LMQL将优化跨多个调用的总体可能性,这可能会产生更好的结果。
  • 约束与标记表示。当前的LLMs不提供限制输出的功能,在我们将LLMs用于生产时这一点至关重要。想象一下,在生产中构建一个情感分析系统,用于在我们的界面上标记负面评价供客服代表使用。我们的程序期望从LLM接收“positive”、“negative”或“neutral”。然而,往往你可能会得到类似于“所提供的客户评论的情感是积极的”的输出结果,这对您的API来说并不容易处理。因此,约束会非常有帮助。LMQL允许您使用人类可理解的单词来控制输出(而不是LLMs使用的标记)。
  • 效率和成本。LLMs是大型网络,因此无论是通过API还是在本地环境中使用它们,它们都非常昂贵。LMQL可以利用预定义行为和搜索空间的约束(由约束引入)来减少LLM调用的数量。

如您所见,LMQL可以解决这些挑战。它允许您在一个提示中进行多个调用,控制输出,甚至减少成本。

成本和效率方面的影响可能相当显著。对搜索空间的限制可以显著降低LLMs的成本。例如,在LMQL论文中的案例中,与标准解码相比,使用LMQL的可计费令牌数量减少了75-85%,这意味着它将显著降低您的成本。

来自Beurer-Kellner等人的论文(2023年)

我认为LMQL最重要的好处是完全控制输出。然而,采用这种方法,您还将在LLM之上增加一个抽象层(类似于我们之前讨论过的LangChain)。如果需要,它将使您能够轻松切换到另一个后端。LMQL可以与不同的后端配合使用:OpenAI、HuggingFace Transformers或llama.cpp

您可以在本地安装LMQL,也可以使用基于Web的Playground。Playground对于调试非常方便,但您只能在此处使用OpenAI后端。对于所有其他用例,您将需要使用本地安装。

像往常一样,这种方法也有一些限制:

  • 这个库目前还不太流行,所以社区规模相当小,并且可用的外部材料很少。
  • 在某些情况下,文档可能不是非常详细。
  • 最流行且效果最好的OpenAI模型有一些限制,所以您无法充分发挥ChatGPT与LMQL的强大功能。
  • 我不会在生产环境中使用LMQL,因为我不能说它是一个成熟的项目。例如,令牌分布提供的准确性很差。

LMQL的一个相对接近的替代方案是Guidance。它也允许您约束生成并控制LM的输出。

尽管存在种种限制,但我喜欢语言模型编程的概念,这就是为什么我决定在本文中讨论它的原因。

如果您有兴趣了解更多关于LMQL的信息,请查看此视频,由作者提供。

LMQL语法

现在,我们对LMQL有了一些了解。让我们看一个LMQL查询的示例,以了解其语法。

beam(n=3)    "Q: Say 'Hello, {name}!'"     "A: [RESPONSE]" from "openai/text-davinci-003"where len(TOKENS(RESPONSE)) < 20

希望您能猜到它的意思。但是让我们详细讨论一下。下面是一个LMQL查询的示意图

来自Beurer-Kellner等人的论文(2023年)

LMQL程序包含5个部分:

  • Decoder定义了所使用的解码过程。简单来说,它描述了选择下一个标记的算法。LMQL有三种不同类型的解码器:argmax、beam和sample。您可以从论文中更详细地了解它们。
  • 实际查询类似于经典提示,但使用Python语法,这意味着您可以使用循环或if语句等结构。
  • from子句中,我们指定了要使用的模型(在我们的示例中是openai/text-davinci-003)。
  • Where子句定义了约束条件。
  • Distribution是用于查看返回值中标记的概率的。我们在此查询中未使用分布,但我们将在后面的情感分析中使用它来获取类别概率。

此外,您可能已经注意到我们查询中的特殊变量{name}[RESPONSE]。让我们讨论一下它们的工作原理:

  • {name}是一个输入参数,可以是您范围内的任何变量。这些参数帮助您创建方便的函数,可以轻松地重复使用不同的输入。
  • [RESPONSE]是LM将生成的一个短语。它也可以称为洞或占位符。在[RESPONSE]之前的所有文本都会发送给LM,然后将模型的输出分配给该变量。很方便的是,您可以很容易地在提示中稍后重新使用此输出,将其称为{RESPONSE}

我们简要介绍了主要概念。让我们自己尝试一下。熟能生巧。

入门

设置环境

首先,我们需要设置我们的环境。要在Python中使用LMQL,我们需要首先安装一个包。毫无意外,我们可以使用pip。您需要一个Python ≥ 3.10的环境。

pip install lmql

如果您想要在本地GPU上使用LMQL,请按照文档中的说明操作。

要使用OpenAI模型,您需要设置APIKey来访问OpenAI。最简单的方法是指定OPENAI_API_KEY环境变量。

import osos.environ['OPENAI_API_KEY'] = '<your_api_key>'

然而,OpenAI模型有很多限制(例如,您将无法获取带有五个以上类别的分布)。因此,我们将使用Llama.cpp来测试带有本地模型的LMQL。

首先,您需要在与LMQL相同的环境中安装Llama.cpp的Python绑定。

pip install llama-cpp-python

如果您想要使用本地GPU,请指定以下参数。

CMAKE_ARGS="-DLLAMA_METAL=on" pip install llama-cpp-python

然后,我们需要将模型权重加载为.gguf文件。您可以在HuggingFace Models Hub上找到模型。

我们将使用两个模型:

Llama-2-7B是Meta的微调生成文本模型的最小版本。它是一个相当基本的模型,所以我们不应该期望它有出色的性能。

Zephyr是Mistral模型的微调版本,具有相当不错的性能。在某些方面,它的表现比一个10倍大的开源模型Llama-2-70b要好。然而,Zephyr和ChatGPT或Claude等专有模型之间仍然存在差距。

图像来自Tunstall等人的论文(2023年)

根据LMSYS ChatBot Arena排行榜,Zephyr是性能最好的具有7B参数的模型。它与更大的模型相媲美。

排行榜截图 | 来源

让我们为我们的模型加载.gguf文件。

import osimport urllib.requestdef download_gguf(model_url, filename):    if not os.path.isfile(filename):        urllib.request.urlretrieve(model_url, filename)        print("文件已成功下载")    else:        print("文件已存在")download_gguf(    "https://huggingface.co/TheBloke/zephyr-7B-beta-GGUF/resolve/main/zephyr-7b-beta.Q4_K_M.gguf",     "zephyr-7b-beta.Q4_K_M.gguf")download_gguf(    "https://huggingface.co/TheBloke/Llama-2-7B-GGUF/resolve/main/llama-2-7b.Q4_K_M.gguf",     "llama-2-7b.Q4_K_M.gguf")

我们需要下载几GB,可能需要一些时间(每个模型大约需要10-15分钟)。幸运的是,您只需要做一次。

您可以以两种不同的方式与本地模型交互(文档):

  • 双进程体系结构:当您有一个单独的长时间运行的进程与您的模型和短程推理调用时。这种方法更适合生产环境。
  • 对于临时任务,我们可以使用进程内模型加载,在模型名称之前指定local:。我们将使用这种方法与本地模型一起工作。

现在,我们已经设置好了环境,是时候讨论如何从Python中使用LMQL了。

Python函数

让我们简要讨论如何在Python中使用LMQL。Playground对于调试可能是有用的,但如果您想在生产环境中使用LM,在需要一个API。

LMQL提供了四种主要方法: lmql.Flmql.run@lmql.query 装饰器和Generations API

Generations API 最近添加。这是一个简单的Python API,可以帮助您在不自己编写LMQL的情况下进行推理。由于我更感兴趣LMML模型的概念,本文不涵盖这个API。

让我们详细讨论其他三种方法并尝试使用它们。

首先,您可以使用lmql.F。它是一种类似于Python中的lambda函数的轻量级功能,允许您执行LMQL代码的一部分。 lmql.F 只能有一个占位符变量,将从lambda函数返回。

我们可以为函数指定提示和约束条件。约束条件相当于LMQL查询中的where子句。

由于我们没有指定任何模型,将使用OpenAI的text-davinci

capital_func = lmql.F("What is the captital of {country}? [CAPITAL]",     constraints = "STOPS_AT(CAPITAL, '.')")capital_func('the United Kingdom')# 输出 - '\n\nThe capital of the United Kingdom is London.'

如果你在使用Jupyter Notebooks,你可能会遇到一些问题,因为Notebooks环境是异步的。您可以在笔记本中启用嵌套事件循环以避免此类问题。

import nest_asyncionest_asyncio.apply()

第二种方法允许您定义更复杂的查询。您可以使用lmql.run在不创建函数的情况下执行LMQL查询。让我们将查询变得更加复杂,并在以下问题中使用模型的答案。

在这种情况下,我们在查询字符串的where子句中定义了约束。

query_string = '''    "Q: {country}的首都是什么? \ n"    "A: [CAPITAL] \ n"    "Q: {CAPITAL}的主要景点是什么? \ n"    "A: [ANSWER]" where (len(TOKENS(CAPITAL)) < 10) \      and (len(TOKENS(ANSWER)) < 100) and STOPS_AT(CAPITAL, '\\n') \      and STOPS_AT(ANSWER, '\\n')'''lmql.run_sync(query_string, country="英国")

而且,我使用了run_sync而不是run以同步方式获取结果。

结果是,我们得到了一个具有一组字段的LMQLResult对象:

  • prompt – 包含包括参数和模型答案的完整提示。我们可以看到模型答案被用于第二个问题。
  • variables – 包含我们定义的所有变量的字典:ANSWERCAPITAL
  • distribution_variabledistribution_values都为None,因为我们没有使用这个功能。
作者提供的图片

使用Python API的第三种方法是@lmql.query装饰器,它允许您定义一个以后方便使用的Python函数。如果您计划多次调用此提示,这将更加方便。

我们可以为以前的查询创建一个函数,只获取最终答案,而不返回整个LMQLResult对象。

@lmql.querydef capital_sights(country):    '''lmql    "Q: {country}的首都是什么? \ n"    "A: [CAPITAL] \ n"    "Q: {CAPITAL}的主要景点是什么? \ n"    "A: [ANSWER]" where (len(TOKENS(CAPITAL)) < 10) and (len(TOKENS(ANSWER)) < 100) \        and STOPS_AT(CAPITAL, '\\n') and STOPS_AT(ANSWER, '\\n')    # 仅返回ANSWER     return ANSWER    '''print(capital_sights(country="英国"))# 伦敦有很多著名的景点,其中最具标志性的是位于威斯敏斯特宫的大本钟。# 其他受欢迎的景点包括白金汉宫、伦敦眼和塔桥。

此外,您还可以将LMQL与LangChain结合使用:

  • LMQL查询是强大的提示模板,可以成为LangChain链的一部分。
  • 您可以利用LMQL中的LangChain组件(例如检索)。您可以在文档中找到示例。

现在,我们了解了LMQL语法的基础知识,并准备转向我们的任务 – 为客户评论定义情感。

情感分析

为了查看LMQL的性能,我们将使用来自UCI机器学习库的带标签的Yelp评论,并尝试预测情感。数据集中的所有评论都是积极或消极的,但我们将中立作为分类的一种可能选项。

对于这个任务,让我们使用本地模型 – ZephyrLlama-2。在调用LMQL时,我们需要指定模型和分词器。对于Llama系列模型,我们可以使用默认的分词器。

首次尝试

让我们挑选一个顾客评论食物非常好。并尝试定义其情感。我们将使用lmql.run进行调试,因为它对于此类即席调用非常方便。

我从一个非常天真的方法开始。

query_string = """"Q: 以下评论的情感是什么:```食物非常好。```?\\n""A: [情感]""""lmql.run_sync(    query_string,     model = lmql.model("local:llama.cpp:zephyr-7b-beta.Q4_K_M.gguf",         tokenizer = 'HuggingFaceH4/zephyr-7b-beta'))# [generate()期间出错]请求的标记数量超过了llama.cpp模型的上下文大小。请指定更高的n_ctx值。

如果您的本地模型工作异常缓慢,请检查您的计算机是否使用了交换内存。重新启动可能是解决此问题的一个很好的选择。

代码看起来非常简单明了。然而,令人惊讶的是,它不起作用,并返回以下错误。

[generate()期间出错]请求的标记数量超过了llama.cpp模型的上下文大小。请指定更高的n_ctx值。

从提示可以猜测,输出结果不符合上下文大小。我们的提示大约有20个标记。因此,我们达到了上下文大小的阈值有点奇怪。让我们将情感的标记数限制并查看输出结果。

query_string = """"Q: 以下评论的情感是什么:```食物非常好。```?\\n""A: [情感]" where (len(TOKENS(情感)) < 200)"""print(lmql.run_sync(query_string,     model = lmql.model("local:llama.cpp:zephyr-7b-beta.Q4_K_M.gguf",         tokenizer = 'HuggingFaceH4/zephyr-7b-beta')).variables['情感'])# 积极的情感。# # Q: 以下评论的情感是什么:```服务很糟糕。```?# A: 消极的情感。# # Q: 以下评论的情感是什么:```酒店非常棒,工作人员友好,位置完美。```?# A: 积极的情感。# # Q: 以下评论的情感是什么:```产品完全令人失望。```?# A: 消极的情感。# # Q: 以下评论的情感是什么:```飞机延误了3个小时,食物冷,娱乐系统不工作。```?# A: 消极的情感。# # Q: 以下评论的情感是什么:```餐厅很拥挤,但服务员很高效,食物很美味。```?# A: 积极的情感。# # Q:

现在,我们可以看到问题的根本原因——模型陷入了一个循环,一遍又一遍地重复问题变体和答案。我在OpenAI模型中没有看到过这样的问题(假设他们可能会控制它),但它们在开源本地模型中非常常见。我们可以使用STOPS_AT约束来停止生成,如果在模型响应中看到Q:或一个新行,以避免这样的循环。

query_string = """"Q: 以下评论的情感是什么:```食物非常好。```?\\n""A: [情感]" where STOPS_AT(情感, 'Q:') \     and STOPS_AT(情感, '\\n')"""print(lmql.run_sync(query_string,     model = lmql.model("local:llama.cpp:zephyr-7b-beta.Q4_K_M.gguf",         tokenizer = 'HuggingFaceH4/zephyr-7b-beta')).variables['情感'])# 积极的情感。

太棒了,我们解决了问题并获得了结果。但是由于我们将进行分类,我们希望模型返回三个输出之一(类标签):negativeneutralpositive。我们可以在LMQL查询中添加这样的过滤器来约束输出结果。

query_string = """"Q: 以下评论的情感是什么:```食物非常好。```?\\n""A: [情感]" where (情感 in ['positive', 'negative', 'neutral'])"""print(lmql.run_sync(query_string,     model = lmql.model("local:llama.cpp:zephyr-7b-beta.Q4_K_M.gguf",         tokenizer = 'HuggingFaceH4/zephyr-7b-beta')).variables['情感'])# 正面

我们不需要带有停止准则的过滤器,因为我们已经将输出限制为只有三个可能的选项,并且LMQL不会考虑任何其他可能性。

让我们尝试使用思路推理方法。给模型一些时间来思考通常会提高结果。使用LMQL语法,我们可以快速实现这种方法。

query_string = """"Q: 下面评论的情感是什么: ```食物非常好。```?\\n""A: 让我们逐步思考一下。[分析]。因此,情感是[情感]",其中(len(TOKENS(ANALYSIS)) < 200) and STOPS_AT(ANALYSIS, '\\n') \    and (SENTIMENT in ['正面', '负面', '中性'])"""print(lmql.run_sync(query_string,     model = lmql.model("local:llama.cpp:zephyr-7b-beta.Q4_K_M.gguf",         tokenizer = 'HuggingFaceH4/zephyr-7b-beta')).variables)

Zephyr模型的输出相当不错。

作者提供的图像

我们可以尝试使用Llama 2相同的提示。

query_string = """"Q: 下面评论的情感是什么: ```食物非常好。```?\\n""A: 让我们逐步思考一下。[ANALYSIS]。因此,情感是[SENTIMENT]",其中(len(TOKENS(ANALYSIS)) < 200) and STOPS_AT(ANALYSIS, '\\n') \    and (SENTIMENT in ['正面', '负面', '中性'])"""print(lmql.run_sync(query_string,     model = lmql.model("local:llama.cpp:llama-2-7b.Q4_K_M.gguf")).variables)

推理并不是很有意义。我们已经在排行榜上看到Zephyr模型要比Llama-2-7b好得多。

作者提供的图像

在经典的机器学习中,我们通常不仅获得类别标签,还获得它们的概率。我们可以使用LMQL中的distribution获得相同的数据。我们只需要指定变量和可能的值 — distribution SENTIMENT in [‘正面’, ‘负面’, ‘中性’]

query_string = """"Q: 下面评论的情感是什么: ```食物非常好。```?\\n""A: 让我们逐步思考一下。[ANALYSIS]。因此,情感是[SENTIMENT]" distribution SENTIMENT in ['正面', '负面', '中性']where (len(TOKENS(ANALYSIS)) < 200) and STOPS_AT(ANALYSIS, '\\n')"""print(lmql.run_sync(query_string,     model = lmql.model("local:llama.cpp:zephyr-7b-beta.Q4_K_M.gguf",         tokenizer = 'HuggingFaceH4/zephyr-7b-beta')).variables)

现在,输出中带有概率,并且我们可以看到该模型对于正面情感非常有信心。

如果您只想在模型有信心时使用决策,概率可能会有所帮助。

作者提供的图像

现在,让我们创建一个函数,以便对不同的输入使用情感分析。与分布之间的结果进行比较可能会很有趣,所以我们需要两个函数。

@lmql.query(model=lmql.model("local:llama.cpp:zephyr-7b-beta.Q4_K_M.gguf",    tokenizer = 'HuggingFaceH4/zephyr-7b-beta', n_gpu_layers=1000))# specified n_gpu_layers to use GPU for higher speeddef sentiment_analysis(review):    '''lmql    "Q: 下面评论的情感是什么: ```{review}```?\\n"    "A: 让我们逐步思考一下。[ANALYSIS]。因此,情感是[SENTIMENT]" where (len(TOKENS(ANALYSIS)) < 200) and STOPS_AT(ANALYSIS, '\\n') \        and (SENTIMENT in ['正面', '负面', '中性'])    '''@lmql.query(model=lmql.model("local:llama.cpp:zephyr-7b-beta.Q4_K_M.gguf",   tokenizer = 'HuggingFaceH4/zephyr-7b-beta', n_gpu_layers=1000))def sentiment_analysis_distribution(review):    '''lmql    "Q: 下面评论的情感是什么: ```{review}```?\\n"    "A: 让我们逐步思考一下。[ANALYSIS]。因此,情感是[SENTIMENT]" distribution SENTIMENT in ['正面', '负面', '中性']    where (len(TOKENS(ANALYSIS)) < 200) and STOPS_AT(ANALYSIS, '\\n')    '''

接下来,我们可以在新的评论中使用这个函数。

sentiment_analysis('房间很脏')

模型判断这是中立的。

作者提供的图片

虽然这个结论背后有一定的道理,但我认为这个评论是消极的。现在我们来看看是否可以使用其他的解码器来获得更好的结果。

默认情况下,使用的是argmax解码器。这是最直接的方法:在每一步中,模型选择概率最高的标记。我们可以尝试其他选项。

让我们尝试使用n = 3和相当高的tempreture = 0.8来使用beam search方法。结果会得到三个按照可能性排序的序列,我们可以选择第一个序列(最可能的序列)。

sentiment_analysis('房间很脏', decoder = 'beam',     n = 3, temperature = 0.8)[0]

现在,模型能够发现这条评论的消极情绪。

作者提供的图片

值得一提的是,使用beam search解码器会有一定的代价。因为我们正在处理三个序列(beam),所以平均得到LLM结果需要花费3倍的时间:39.55秒 vs 13.15秒。

现在,我们有了我们的函数,可以用真实数据进行测试。

真实数据的结果

我在Yelp评论的1K数据集的10%样本上使用不同的参数运行了所有的函数:

  • 模型:Llama 2或Zephyr,
  • 方法:使用分布或仅使用约束的提示,
  • 解码器:argmax或beam search。

首先,我们来比较准确性——正确情感评论的比例。我们可以看到,Zephyr比Llama 2模型表现要好得多。而且,由于某种原因,我们使用分布得到的质量明显较差。

作者提供的图表

如果我们更深入地观察,我们可以注意到:

  • 对于积极的评论,准确性通常更高。
  • 最常见的错误是将评论标记为中立。
  • 对于以提示方式使用Llama 2,我们可以看到关键问题的发生率很高(将正面评论标记为负面评论)。

在许多情况下,我认为模型使用类似的思路,像我们之前在”脏房间”的例子中看到的一样,将消极评论标记为中立。因为我们不知道顾客是否期望一个干净的房间,所以模型无法确定“脏房间”是消极情绪还是中立情绪。

作者提供的图表
作者提供的图表

查看实际概率也是很有趣的。

  • 对于Zephyr型号,正面评论的正面标签的75%分位数超过0.85,而Llama 2的情况要低得多。
  • 所有型号在负面评论方面表现不佳,负面评论的负面标签的75%分位数甚至低于0.5。
作者提供的图表
作者提供的图表

我们的快速研究显示,使用Zephyr模型和argmax解码器的原始提示将是情感分析的最佳选择。然而,针对您的用例检查不同的方法也是值得的。此外,通过微调提示,您通常可以获得更好的结果。

您可以在GitHub上找到完整的代码。

总结

今天,我们讨论了LMP(语言模型编程)的概念,它允许您在自然语言和脚本指令中混合使用提示。我们尝试将其用于情感分析任务,并使用本地开源模型取得了不错的结果。

尽管LMQL尚未普及,但这种方法可能在未来很有用,并且结合了自然语言和编程语言成为了强大的语言模型工具。

非常感谢您阅读本文。希望对您有所启发。如果您有任何后续问题或评论,请在评论区留言。

数据集

Kotzias, Dimitrios. (2015). Sentiment Labelled Sentences. UCI Machine Learning Repository (CC BY 4.0 license). https://doi.org/10.24432/C57604