LMQL—用于语言模型的SQL

2023年11月28日 由 alex 发表 478 0

SQL(结构化查询语言)是一种声明式语言,广泛用于处理数据库数据。


根据年度StackOverflow调查,SQL仍然是世界上最流行的编程语言之一。对于专业开发者来说,SQL位列前三种最常用的语言之列(仅次于Javascript和HTML/CSS)。超过一半的专业人士使用它。令人惊讶的是,SQL的普及度甚至超过了Python。


10


SQL是与数据库中的数据进行交互的常用方法。因此,法学硕士尝试使用类似的方法也就不足为奇了。在本文中,我想向你介绍一种称为 LMQL 的方法。


什么是LMQL?


LMQL(语言模型查询语言)是一种面向语言模型的开源编程语言。LMQL是在Apache 2.0许可下发布的,这意味着你可以将其商业使用。


LMQL由苏黎世联邦理工学院的研究人员开发。他们提出了LMP(语言模型编程)的新概念。LMP结合了自然语言和编程语言:文本提示和脚本指令。


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


  • 交互。例如,我们可以使用元提示,要求LM扩展初始提示。作为实际的情况,我们可以先要求模型定义初始问题的语言,然后用那种语言回应。为了这样的任务,我们需要发送第一个提示,从输出中提取语言,将其添加到第二个提示模板中,并再次调用LM。我们需要管理很多交互。使用LMQL,你可以在一个提示中定义多个输入和输出变量。不仅如此,LMQL还会优化跨多个调用的整体可能性,这可能会带来更好的结果。
  • 约束和令牌表示。当前的LM并不提供限制输出的功能,这在我们在生产环境中使用LM时至关重要。想象一下,在生产中构建情感分析,以在我们的客服界面中标记负面评论。我们的程序希望从LLM那里接收“积极”、“消极”或“中性”。然而,很多时候,你可能从LLM那里获得类似于“提供的客户评论的情绪是积极的”的回复,这在你的API中不易处理。这就是为什么约束是相当有帮助的。LMQL允许你使用人类可以理解的单词(而不是LM操作的令牌)来控制输出。
  • 效率和成本。LLM是大型网络,所以不管是通过API使用还是在本地环境中使用都相当昂贵。LMQL可以利用预定义行为和搜索空间的约束(由约束引入)来减少调用LM的次数。


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


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


11


我认为LMQL最关键的好处是可以完全控制你的输出。然而,采用这种方法,你还会在LLM上增加另一层抽象(类似于我们早先讨论的LangChain)。这将允许你在需要时轻松地从一个后端切换到另一个后端。LMQL可以与不同的后端协同工作:OpenAI、HuggingFace Transformers或者llama.cpp。


你可以在本地安装LMQL,或者在网上使用基于Web的Playground。Playground在调试时可能非常方便,但你只能在这里使用OpenAI后端。对于所有其他用例,你将不得不使用本地安装。


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


  • 这个库还不是很流行,所以社区相当小,可用的外部材料很少。
  • 在某些情况下,文档可能不是特别详细。
  • 最受欢迎和表现最佳的OpenAI模型有一些限制,所以你不能使用LMQL的全部功能与ChatGPT一起使用。
  • 我不会在生产环境中使用LMQL,因为我不能说它是一个成熟的项目。例如,基于令牌的分布提供的准确性相当差。


与LMQL相当接近的替代方法是Guidance。它也允许你限制生成并控制LM的输出。


LMQL语法


现在,我们已经了解了一些LMQL是什么。让我们看一个LMQL查询的例子,以熟悉其语法。


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


这是一个LMQL查询的示意图。


12


任何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


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


import os
os.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模型中心上找到这些模型。


我们将会使用两个模型:


  • Llama-2-7B(链接)
  • zephyr-7B-beta(链接)


Llama-2-7B是由Meta微调过的生成文本模型中最小的版本。它是一个非常基础的模型,所以我们不应该期待它有出色的表现。


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


13


根据LMSYS聊天机器人竞技场排行榜,拥有70亿参数的Zephyr是表现最好的模型。它与更大的模型不相上下。


14


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


import os
import urllib.request

def download_gguf(model_url, filename):
    if not os.path.isfile(filename):
        urllib.request.urlretrieve(model_url, filename)
        print("file has been downloaded successfully")
    else:
        print("file already exists")
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"
)


你可以通过两种不同的方式与本地模型进行交互:


  • 双进程架构,当你有一个与模型相关的独立长期运行的进程和短期运行的推理调用时。这种方法更适用于生产环境。
  • 对于临时任务,我们可以使用进程内模型加载,指定在模型名称前加上local:。我们将使用这种方法来处理本地模型。


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


Python函数


让我们简单讨论一下如何在Python中使用LMQL。Playground可以方便调试,但如果你想在生产中使用LM,你需要一个API。


LMQL提供了四种主要的功能方法:lmql.F、lmql.run、@lmql.query装饰器。


Generations API是最近添加的。它是一个简单的Python API,有助于在不编写LMQL的情况下进行推理。


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


首先,你可以使用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')
# Output - '\n\nThe capital of the United Kingdom is London.'


如果你正在使用Jupyter Notebooks,你可能会遇到一些问题,因为Notebook环境是异步的。


import nest_asyncio
nest_asyncio.apply()


第二种方法允许你定义更复杂的查询。你可以使用 lmql.run 来执行一个 LMQL 查询,而无需创建一个函数。让我们把我们的查询变得更加复杂一些,并在下面的问题中使用模型的答案。


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


query_string = '''
    "Q: What is the captital of {country}? \\n"
    "A: [CAPITAL] \\n"
    "Q: What is the main sight in {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="the United Kingdom")


另外,我用了 run_sync 而不是 run 来同步地获得结果。


结果,我们得到了一个 LMQLResult 对象,包含一系列字段:


  • prompt — 包含全部提示语,参数以及模型的回答。我们可以看到模型的回答被用于第二个问题。
  • variables — 包含我们定义的所有变量的字典:ANSWER 和 CAPITAL。
  • distribution_variable 和 distribution_values 都是 None,因为我们还没有使用这项功能。


15


第三种使用Python API的方式是通过@lmql.query装饰器,这允许你定义一个Python函数,将来使用它会很方便。如果你打算多次调用这个提示,那么它就更加方便了。


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


@lmql.query
def capital_sights(country):
    '''lmql
    "Q: What is the captital of {country}? \\n"
    "A: [CAPITAL] \\n"
    "Q: What is the main sight in {CAPITAL}? \\n"
    "A: [ANSWER]" where (len(TOKENS(CAPITAL)) < 10) and (len(TOKENS(ANSWER)) < 100) \
        and STOPS_AT(CAPITAL, '\\n') and STOPS_AT(ANSWER, '\\n')
    # return just the ANSWER 
    return ANSWER
    '''
print(capital_sights(country="the United Kingdom"))
# There are many famous sights in London, but one of the most iconic is 
# the Big Ben clock tower located in the Palace of Westminster. 
# Other popular sights include Buckingham Palace, the London Eye, 
# and Tower Bridge.


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


  • LMQL查询就像是增强版的提示模板,可以成为LangChain链条的一部分。
  • 你可以从LMQL中利用LangChain组件(例如,信息检索)。你可以在文档中找到示例。


现在,我们已经了解了所有LMQL语法的基础知识,我们准备开始我们的任务了 —— 确定客户评论的情绪。


情感分析


为了看到LMQL的表现如何,我们将使用来自UCI机器学习存储库的标记过的Yelp评论数据,尝试预测情感。数据集中所有的评论都是正面或负面的,但我们会保留中性作为分类的一个可能选项。


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


初次尝试


让我们选取一条顾客评论“The food was very good.”,并尝试确定其情感倾向。我们将使用lmql.run进行调试,因为它对于此类即席调用来说非常方便。


我一开始采用了一个非常天真的方法。


query_string = """
"Q: What is the sentiment of the following review: ```The food was very good.```?\\n"
"A: [SENTIMENT]"
"""
lmql.run_sync(
    query_string, 
    model = lmql.model("local:llama.cpp:zephyr-7b-beta.Q4_K_M.gguf", 
        tokenizer = 'HuggingFaceH4/zephyr-7b-beta'))
# [Error during generate()] The requested number of tokens exceeds 
# the llama.cpp model's context size. Please specify a higher n_ctx value.


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


这段代码看起来很简单。然而,令人惊讶的是,它并不工作,并返回了以下错误。


[Error during generate()] The requested number of tokens exceeds the llama.cpp 
model's context size. Please specify a higher n_ctx value.


从这个信息中,我们可以猜测输出不符合上下文大小。我们的提示大约是20个标记。所以,我们已经达到了上下文大小的阈值,这有点奇怪。让我们尝试限制情感标记的数量,并查看输出结果。


query_string = """
"Q: What is the sentiment of the following review: ```The food was very good.```?\\n"
"A: [SENTIMENT]" where (len(TOKENS(SENTIMENT)) < 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['SENTIMENT'])
#  Positive sentiment.

# Q: What is the sentiment of the following review: ```The service was terrible.```?
# A:  Negative sentiment.

# Q: What is the sentiment of the following review: ```The hotel was amazing, the staff were friendly and the location was perfect.```?
# A:  Positive sentiment.

# Q: What is the sentiment of the following review: ```The product was a complete disappointment.```?
# A:  Negative sentiment.

# Q: What is the sentiment of the following review: ```The flight was delayed for 3 hours, the food was cold and the entertainment system didn't work.```?
# A:  Negative sentiment.

# Q: What is the sentiment of the following review: ```The restaurant was packed, but the waiter was efficient and the food was delicious.```?
# A:  Positive sentiment.

# Q:


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


query_string = """
"Q: What is the sentiment of the following review: ```The food was very good.```?\\n"
"A: [SENTIMENT]" where STOPS_AT(SENTIMENT, 'Q:') \
     and STOPS_AT(SENTIMENT, '\\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['SENTIMENT'])
# Positive sentiment.


很好,我们解决了问题并得到了结果。但由于我们要做分类,我们希望模型返回三个输出(类标签)之一:negativeneutralpositive我们可以向 LMQL 查询添加这样的过滤器来限制输出。


query_string = """
"Q: What is the sentiment of the following review: ```The food was very good.```?\\n"
"A: [SENTIMENT]" where (SENTIMENT 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['SENTIMENT'])
# positive


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


让我们尝试使用链式思维推理方法。给模型一些思考的时间,通常可以提高结果的质量。使用LMQL语法,我们可以迅速实施这种方法。


query_string = """
"Q: What is the sentiment of the following review: ```The food was very good.```?\\n"
"A: Let's think step by step. [ANALYSIS]. Therefore, the sentiment is [SENTIMENT]" where (len(TOKENS(ANALYSIS)) < 200) and STOPS_AT(ANALYSIS, '\\n') \
    and (SENTIMENT 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)


Zephyr模型的输出相当不错。


16


我们可以尝试在Llama 2上用同一个提示。


query_string = """
"Q: What is the sentiment of the following review: ```The food was very good.```?\\n"
"A: Let's think step by step. [ANALYSIS]. Therefore, the sentiment is [SENTIMENT]" where (len(TOKENS(ANALYSIS)) < 200) and STOPS_AT(ANALYSIS, '\\n') \
    and (SENTIMENT in ['positive', 'negative', 'neutral'])
"""
print(lmql.run_sync(query_string, 
    model = lmql.model("local:llama.cpp:llama-2-7b.Q4_K_M.gguf")).variables)


这个推理并没有多大意义。我们已经在排行榜上看到,Zephyr模型比Llama-2-7b好得多。


17


在经典的机器学习中,我们通常不仅得到类别标签,还会得到它们的概率。使用LMQL中的分布,我们也可以获得相同的数据。我们只需要指定变量和可能的值 - distribution SENTIMENT in [‘positive’, ‘negative’, ‘neutral’]


query_string = """
"Q: What is the sentiment of the following review: ```The food was very good.```?\\n"
"A: Let's think step by step. [ANALYSIS]. Therefore, the sentiment is [SENTIMENT]" distribution SENTIMENT in ['positive', 'negative', 'neutral']
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)


现在,我们在输出中得到了概率值,我们可以看到模型对于积极情绪非常有信心。


如果你只想在模型有信心的时候使用决策,那么概率值在实践中可能会很有帮助。


18


现在,让我们创建一个功能,用于对多种输入进行情感分析。将有和没有分布的结果进行对比会很有趣,因此我们需要两个功能。


@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 speed
def sentiment_analysis(review):
    '''lmql
    "Q: What is the sentiment of the following review: ```{review}```?\\n"
    "A: Let's think step by step. [ANALYSIS]. Therefore, the sentiment is [SENTIMENT]" where (len(TOKENS(ANALYSIS)) < 200) and STOPS_AT(ANALYSIS, '\\n') \
        and (SENTIMENT in ['positive', 'negative', 'neutral'])
    '''

@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: What is the sentiment of the following review: ```{review}```?\\n"
    "A: Let's think step by step. [ANALYSIS]. Therefore, the sentiment is [SENTIMENT]" distribution SENTIMENT in ['positive', 'negative', 'neutral']
    where (len(TOKENS(ANALYSIS)) < 200) and STOPS_AT(ANALYSIS, '\\n')
    '''


然后,我们可以将这个函数用于新的审查。


sentiment_analysis('Room was dirty')


模型判定它是中性的。


19


这个结论背后有其合理性,但我会说这篇评论是负面的。让我们看看使用其他的解码器是否能得到更好的结果。


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


让我们尝试使用带有n=3的束搜索方法,并设置一个相当高的温度值=0.8。结果我们将得到按可能性排序的三个序列,所以我们可以仅仅选择第一个(可能性最高的那个)。


sentiment_analysis('Room was dirty', decoder = 'beam', 
    n = 3, temperature = 0.8)[0]


现在,该模型能够识别出这篇评论中的负面情绪。


20


值得一提的是,使用束搜索解码有其成本。因为我们在处理三个序列(束),所以平均来说获得LLM结果的时间是原来的3倍:39.55秒对比13.15秒。


现在我们有了我们的功能,并且可以用我们的实际数据来测试它们。


真实数据的结果


我已经使用不同的参数,对Yelp评论的1K数据集中的10%样本运行了所有功能:


  • 模型:Llama 2或Zephyr,
  • 方法:使用分布或仅限制式提示,
  • 解码器:argmax或束搜索。


首先,让我们比较准确性 —— 含有正确情感的评论的比例。我们可以看到,Zephyr模型表现得比Llama 2模型要好得多。此外,由于某些原因,使用分布我们获得的质量显著较差。


21


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


  • 对于正面评论,准确性通常较高。
  • 最常见的错误是将评论标记为中性,
  • 对于Llama 2 提示,我们可以看到高比率的关键错误(正面评论被标记为负面)。


在很多情况下,我认为模型使用的是类似的推理逻辑,正如我们早先在“脏房间”例子中看到的,把负面评论打分为中性。模型不确定“脏房间”是负面还是中性情感,因为我们不知道顾客是否期望干净的房间。


22


23


看一下实际的概率:


  • 对于 Zephyr 模型,正面评论的正面标签的 75% 百分位数高于 0.85,而 Llama 2 则要低得多。


  • 所有模型对于负面评论的表现都很差,其中负面评论的负面标签的 75% 百分位数甚至远低于 0.5。


24


25


我们的快速研究表明,带有 Zephyr 模型和argmax解码器的普通提示将是情感分析的最佳选择。但是,值得针对你的用例检查不同的方法。此外,你通常可以通过调整提示来获得更好的结果。


总结


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


虽然LMQL目前还没有广泛应用,但这种方法在将来可能会变得方便且流行,因为它将自然语言和编程语言结合成了强大的工具,用于语言模型。



文章来源:https://medium.com/towards-data-science/lmql-sql-for-language-models-d7486d88c541
欢迎关注ATYUN官方公众号
商务合作及内容投稿请联系邮箱:bd@atyun.com
评论 登录
热门职位
Maluuba
20000~40000/月
Cisco
25000~30000/月 深圳市
PilotAILabs
30000~60000/年 深圳市
写评论取消
回复取消