譯者 | 朱先忠
審校 | 重樓
我相信你聽說過SQL,甚至已經掌握了它。SQL(結構化查詢語言)是一種廣泛用于處理數據庫數據的聲明性語言。
根據StackOverflow的年度調查,SQL仍然是世界上最流行的語言之一。對于專業開發人員來說,SQL是排名前三的語言(僅次于Javascript和HTML/CSS)。超過一半的專業人士使用它。令人驚訝的是,SQL甚至比Python更受歡迎。
作者圖表,數據來自StackOverflow調查
SQL是與數據庫中的數據進行對話的常用方法。因此,有人試圖對LLM使用類似的方法也就不足為奇了。在本文中,我想告訴您一種叫做LMQL的方法。
什么是LMQL?
LMQL(語言模型查詢語言,https://lmql.ai/)是一種用于語言模型的開源編程語言。LMQL在Apache 2.0許可證下發布,該許可證允許您在商業上使用它。
LMQL由蘇黎世聯邦理工學院的研究人員開發。他們提出了一種新的LMP(語言模型編程)思想。LMP結合了自然語言和編程語言:文本提示和腳本指令。
在Luca Beurer Kellner、Marc Fischer和Martin Vechev的原始論文《提示就是編程:大型語言模型的查詢語言》中,作者指出了當前LLM使用的以下挑戰:
- 相互作用。例如,我們可以使用元提示,要求LM擴展初始提示。作為一個實際案例,我們可以首先要求模型定義初始問題的語言,然后用該語言回答。對于這樣的任務,我們需要發送第一個提示,從輸出中提取語言,將其添加到第二個提示模板中,并再次調用LM。我們需要管理相當多的交互。使用LMQL,您可以在一個提示中定義多個輸入和輸出變量。除此之外,LMQL將優化多次調用的總體可能性,這可能會產生更好的結果。
- 約束和標記表示。當前的LMs不提供限制輸出的功能,如果我們在生產中使用LMs,這是至關重要的。想象一下,在生產中建立一個情緒分析,在我們的CS代理界面中標記負面評價。我們的項目期望從LLM獲得“積極”、“消極”或“中立”。然而,通常情況下,您可以從LLM中得到類似“對所提供的客戶評價的情緒是積極的”的信息,這在API中不太容易處理。這就是為什么約束會非常有用。LMQL允許您使用人類可理解的單詞(而不是LMs使用的令牌)來控制輸出。
- 效率和成本。LLM是大型網絡,因此無論您是通過API還是在本地環境中使用它們,它們都非常昂貴。LMQL可以利用預定義的行為和搜索空間的約束(由約束引入)來減少LM調用的數量。正如您所看到的,LMQL可以解決這些挑戰。它允許您在一個提示中組合多個調用,控制輸出,甚至降低成本。
對成本和效率的影響可能相當大。對搜索空間的限制可以顯著降低LLM的成本。例如,在LMQL論文的案例中,與標準解碼相比,LMQL的可計費代幣減少了75–85%,這意味著它將顯著降低您的成本。
圖片來自Beurer Kellner等人的論文(2023)
我相信LMQL最重要的好處是完全控制您的輸出。然而,使用這樣的方法,您還將擁有LLM上的另一層抽象(類似于我們前面討論的LangChain)。如果需要,它將允許您輕松地從一個后端切換到另一個后端。LMQL可以使用不同的后端:OpenAI、HuggingFace Transformers或llama.cpp。
您可以在本地安裝LMQL,也可以在線使用基于Web的Playground。Playground可以非常方便地進行調試,但您只能在此處使用OpenAI后端。對于所有其他用例,您必須使用本地安裝。
與往常一樣,這種方法也有一些局限性:
這個圖書館還不太受歡迎,所以社區很小,很少有外部材料可用。
在某些情況下,文檔可能不是很詳細。
最流行、性能最好的OpenAI模型有一些局限性,因此您無法將LMQL的全部功能與ChatGPT一起使用。
我不會在生產中使用LMQL,因為我不能說它是一個成熟的項目。例如,通過代幣進行分發的準確性非常差。
在某種程度上接近LMQL的替代方案是指導。它還允許您約束生成并控制LM的輸出。
盡管有這些限制,我還是喜歡語言模型編程的概念,這就是我決定在本文中討論它的原因。
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個部分組成:
解碼器定義所使用的解碼過程。簡單地說,它描述了提取下一個令牌的算法。LMQL有三種不同類型的解碼器:argmax、beam和sample。你可以從論文中更詳細地了解它們。
實際的查詢類似于經典的提示,但使用Python語法,這意味著您可以使用循環或if語句等結構。
在from子句中,我們指定了要使用的模型(在我們的示例中為openai/text-davinci-003)。
Where子句定義約束。
當您希望在返回中查看令牌的概率時,會使用分布。我們還沒有在這個查詢中使用分布,但稍后我們將使用它來獲得情緒分析的類概率。
此外,您可能已經注意到我們的查詢{name}和[RESPONSE]中的特殊變量。讓我們討論一下它們是如何工作的:
{name}是一個輸入參數。它可以是您范圍內的任何變量。這樣的參數可以幫助您創建方便的函數,這些函數可以很容易地用于不同的輸入。
[LRESPONSE]是LM將生成的短語。它也可以稱為孔或占位符。[響應]之前的所有文本都被發送到LM,然后模型的輸出被分配給變量。很方便的是,您可以在稍后的提示中輕松地重用此輸出,將其稱為{RESPONSE}。
我們已經簡要介紹了主要概念。讓我們自己試試。
開始
設置環境
首先,我們需要建立我們的環境。要在Python中使用LMQL,我們需要首先安裝一個包。毫無疑問,我們可以使用pip。您需要一個Python≥3.10的環境。
pip install lmql
如果要將LMQL與本地GPU一起使用,請按照文檔中的說明進行操作。
要使用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等專有模型之間仍有一些差距。
Tunstall等人的論文圖像(2023)
根據LMSYS ChatBot Arena排行榜,Zephyr是性能最好的7B參數型號。它與更大的型號不相上下。
排行榜截圖|來源
讓我們為我們的模型加載.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"
)
我們需要下載一些GB,這樣可能需要一些時間(每個型號需要10-15分鐘)。幸運的是,你只需要做一次。
您可以通過兩種不同的方式(文檔)與本地模型交互:
- 當您的模型和短時間運行的推理調用有一個單獨的長時間運行的流程時,使用兩個流程體系結構。這種方法更適合生產。
- 對于特殊任務,我們可以使用進程內模型加載,在模型名稱之前指定local:。我們將使用這種方法來處理本地模型。現在,我們已經設置好了環境,是時候討論如何使用Python中的LMQL了。
Python函數
讓我們簡要討論一下如何在Python中使用LMQL。Playground可以方便地進行調試,但如果您想在生產中使用LM,則需要一個API。
LMQL提供了四種主要的功能方法:LMQL。F、lmql.run、@lmql.query decorator和Generations API。
最近添加了生成API。這是一個簡單的Python API,有助于在不編寫LMQL的情況下進行推理。由于我對LMP概念更感興趣,所以本文將不討論這個API。
讓我們詳細討論其他三種方法,并嘗試使用它們。
首先,您可以使用lmql。F。這是一個類似于Python中lambda函數的輕量級功能,可以允許您執行部分LMQL代碼。lmql。F只能有一個占位符變量,該變量將從lambda函數返回。
我們可以為函數指定提示和約束。該約束將等效于LMQL查詢中的where子句。
由于我們沒有指定任何模型,因此將使用OpenAI文本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,您可能會遇到一些問題,因為Notebooks環境是異步的。您可以在筆記本中啟用嵌套事件循環以避免此類問題。
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,因為我們沒有使用過此功能。
本圖片由作者本人提供
使用Python API的第三種方法是@lmql.query decorator,它允許您定義一個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族模型,我們可以使用默認的標記符。
首次嘗試
讓我們挑選一個顧客評價。食物非常好,并試圖定義顧客的情感。我們將使用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個代幣。所以,我們已經達到了上下文大小的閾值,這有點奇怪。讓我們嘗試約束SENTIMENT的令牌數量,并查看輸出。
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.
太好了,我們已經解決了問題并得到了結果。但由于我們將進行分類,我們希望模型返回三個輸出(類標簽)之一:負、中性或正。我們可以在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')
模型決定它是中性的。
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秒vs 13.15秒。
現在,我們有了我們的功能,可以用我們的真實數據來測試它們。
真實數據的結果
我已經用不同的參數在1K Yelp評論數據集的10%樣本上運行了所有函數:
- 型號:Llama 2或Zephyr
- 方法:使用分布或僅約束提示
- 解碼器:argmax或波束搜索首先,讓我們比較一下準確性——評論的份額與正確的情緒。我們可以看到,Zephyr的性能比Llama 2型號要好得多。此外,由于某些原因,我們的分布質量明顯較差。
按作者繪制的圖表
如果我們再深入一點,我們會注意到:
- 對于正面評價,準確度通常更高
- 最常見的錯誤是將審查標記為中性
- 對于Llama 2,我們可以看到高比率的關鍵問題(正面評論被標記為負面評論)在許多情況下,我認為該模型使用了類似的原理,將負面評論評分為中性,正如我們之前在“臟房間”示例中看到的那樣。該模型不確定“臟房間”是負面的還是中性的,因為我們不知道客戶是否期望有一個干凈的房間。
按作者繪制的圖表
按作者繪制的圖表
觀察實際概率也很有趣:
- Zephyr模型的正面評價的75%的正面標簽高于0.85,而Llama 2則更低。
- 所有模型在負面評論方面都表現出較差的性能,其中負面評論的負面標簽的75%的百分比甚至遠低于0.5。
按作者繪制的圖表
按作者繪制的圖表
我們的快速研究表明,帶有Zephyr模型和argmax解碼器的提示將是情緒分析的最佳選擇。然而,值得為您的用例檢查不同的方法。此外,您通常可以通過調整提示來獲得更好的結果。
你可以在GitHub上找到完整的代碼。
總結
今天,我們討論了LMP(語言模型編程)的一個概念,它允許您混合使用自然語言中的提示和腳本指令。我們已經嘗試將其用于情緒分析任務,并使用本地開源模型獲得了不錯的結果。
盡管LMQL還沒有普及,但這種方法可能很方便,并在未來廣受歡迎,因為它將自然語言和編程語言組合成了一種強大的LMs工具。
非常感謝你閱讀這篇文章。我希望它對你很有見地。如果您有任何后續問題或意
數據集
科齊亞斯,迪米特里奧斯。(2015)。情緒標記的句子。UCI機器學習庫(CC BY 4.0許可證)。https://doi.org/10.24432/c57604。
譯者介紹
朱先忠,51CTO社區編輯,51CTO專家博客、講師,濰坊一所高校計算機教師,自由編程界老兵一枚。
原文標題:LMQL — SQL for Language Models,作者:Mariya Mansurova