SQL(结构化查询语言)是一种声明式语言,广泛用于处理数据库数据。
根据年度StackOverflow调查,SQL仍然是世界上最流行的编程语言之一。对于专业开发者来说,SQL位列前三种最常用的语言之列(仅次于Javascript和HTML/CSS)。超过一半的专业人士使用它。令人惊讶的是,SQL的普及度甚至超过了Python。
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使用中的以下挑战:
如你所见,LMQL可以解决这些挑战。它允许你在一个提示中合并多个调用,控制你的输出,甚至减少成本。
在成本和效率方面的影响可能相当大。限制搜索空间可以显著降低LLM的成本。例如,在LMQL论文中的案例中,与标准解码相比,使用LMQL的可计费令牌减少了75-85%,这意味着它将显著降低你的成本。
我认为LMQL最关键的好处是可以完全控制你的输出。然而,采用这种方法,你还会在LLM上增加另一层抽象(类似于我们早先讨论的LangChain)。这将允许你在需要时轻松地从一个后端切换到另一个后端。LMQL可以与不同的后端协同工作:OpenAI、HuggingFace Transformers或者llama.cpp。
你可以在本地安装LMQL,或者在网上使用基于Web的Playground。Playground在调试时可能非常方便,但你只能在这里使用OpenAI后端。对于所有其他用例,你将不得不使用本地安装。
像往常一样,这种方法有一些限制:
与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查询的示意图。
任何LMQL程序都包含5个部分:
此外,你可能已经注意到我们查询中的特殊变量{name}和[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是由Meta微调过的生成文本模型中最小的版本。它是一个非常基础的模型,所以我们不应该期待它有出色的表现。
Zephyr是Mistral模型的一个微调版本,具有不错的性能。在某些方面,它比10倍大的开源模型Llama-2–70b表现要好。然而,Zephyr与ChatGPT或Claude这些专有模型之间仍然存在一些差距。
根据LMSYS聊天机器人竞技场排行榜,拥有70亿参数的Zephyr是表现最好的模型。它与更大的模型不相上下。
让我们为我们的模型加载 .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"
)
你可以通过两种不同的方式与本地模型进行交互:
现在,我们已经设置好了环境,是时候讨论如何从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 对象,包含一系列字段:
第三种使用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语法的基础知识,我们准备开始我们的任务了 —— 确定客户评论的情绪。
情感分析
为了看到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.
很好,我们解决了问题并得到了结果。但由于我们要做分类,我们希望模型返回三个输出(类标签)之一:negative、neutral或positive。我们可以向 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模型的输出相当不错。
我们可以尝试在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好得多。
在经典的机器学习中,我们通常不仅得到类别标签,还会得到它们的概率。使用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)
现在,我们在输出中得到了概率值,我们可以看到模型对于积极情绪非常有信心。
如果你只想在模型有信心的时候使用决策,那么概率值在实践中可能会很有帮助。
现在,让我们创建一个功能,用于对多种输入进行情感分析。将有和没有分布的结果进行对比会很有趣,因此我们需要两个功能。
@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')
模型判定它是中性的。
这个结论背后有其合理性,但我会说这篇评论是负面的。让我们看看使用其他的解码器是否能得到更好的结果。
默认情况下是使用argmax解码器。这是最直接的方法:在每一步中,模型选择概率最高的令牌。我们可以尝试使用其他选项。
让我们尝试使用带有n=3的束搜索方法,并设置一个相当高的温度值=0.8。结果我们将得到按可能性排序的三个序列,所以我们可以仅仅选择第一个(可能性最高的那个)。
sentiment_analysis('Room was dirty', decoder = 'beam',
n = 3, temperature = 0.8)[0]
现在,该模型能够识别出这篇评论中的负面情绪。
值得一提的是,使用束搜索解码有其成本。因为我们在处理三个序列(束),所以平均来说获得LLM结果的时间是原来的3倍:39.55秒对比13.15秒。
现在我们有了我们的功能,并且可以用我们的实际数据来测试它们。
真实数据的结果
我已经使用不同的参数,对Yelp评论的1K数据集中的10%样本运行了所有功能:
首先,让我们比较准确性 —— 含有正确情感的评论的比例。我们可以看到,Zephyr模型表现得比Llama 2模型要好得多。此外,由于某些原因,使用分布我们获得的质量显著较差。
如果我们更深入地观察,我们会注意到:
在很多情况下,我认为模型使用的是类似的推理逻辑,正如我们早先在“脏房间”例子中看到的,把负面评论打分为中性。模型不确定“脏房间”是负面还是中性情感,因为我们不知道顾客是否期望干净的房间。
看一下实际的概率:
我们的快速研究表明,带有 Zephyr 模型和argmax解码器的普通提示将是情感分析的最佳选择。但是,值得针对你的用例检查不同的方法。此外,你通常可以通过调整提示来获得更好的结果。
总结
我们讨论了一种名为LMP(语言模型编程)的概念,它允许你将自然语言提示和脚本指令混合使用。我们尝试将其应用于情感分析任务,并使用本地开源模型取得了不错的结果。
虽然LMQL目前还没有广泛应用,但这种方法在将来可能会变得方便且流行,因为它将自然语言和编程语言结合成了强大的工具,用于语言模型。