準確性對於大型語言模型(LLM)的應用非常重要,特別是在調用API或總結財務報告等情況下。幸運的是,有一些方法可以提高準確性。改善準確性的最佳做法包括以下幾個步驟:
你可以從簡單的提示工程技術開始——添加更詳細的指示、使用少量示例提示,或要求模型逐步思考。如果準確性仍然不足,你可以加入自我反思的步驟,例如,返回API調用中的錯誤並要求LLM修正錯誤。下一個選擇是使用檢索增強生成(RAG)為LLM提供最相關的上下文,以進一步提高準確性。
我們在我之前的TDS文章《從原型到生產:提高LLM準確性》中探討了這種方法。在那個項目中,我們建立了一個SQL代理,將有效的SQL查詢從0%提高到70%的準確性。然而,依賴提示的方式有其限制。要突破這個障礙,達到準確性的下一個前沿,我們需要採用更先進的技術。
最有前景的選擇是微調。通過微調,我們可以從僅依賴提示中的信息轉向直接將額外信息嵌入到模型的權重中。
讓我們先了解什麼是微調。微調是通過在較小的、特定任務的數據集上訓練預訓練模型來提高其在特定應用中的性能的過程。基本模型最初是在大量數據上進行訓練,這使它們能夠發展出對語言的廣泛理解。然而,微調則是將這些模型調整為專門的任務,將它們從通用系統轉變為高度針對性的工具。例如,指令微調教會了GPT-2聊天和遵循指示,這就是ChatGPT出現的原因。
基本的LLM最初是根據大量文本語料庫來預測下一個標記。微調通常採用監督學習的方法,模型會被提供具體問題和相應答案,讓它調整其權重以提高準確性。
歷史上,微調需要更新所有模型權重,這種方法稱為完全微調。這個過程計算成本高,因為它需要在內存中存儲所有模型權重、狀態、梯度和前向激活。為了解決這些挑戰,引入了參數高效微調技術。PEFT方法僅更新一小部分模型參數,而保持其餘部分不變。在這些方法中,最廣泛採用的是LoRA(低秩適應),它在不影響性能的情況下顯著降低了計算成本。
優缺點
在考慮微調之前,評估其優勢和限制是非常重要的。
優勢:
微調使模型能夠學習和保留比僅通過提示提供的更多信息。它通常提供更高的準確性,經常超過90%。在推理過程中,它可以降低成本,因為可以使用較小的、特定任務的模型,而不是較大的通用模型。微調後的小模型通常可以在本地部署,消除對OpenAI或Anthropic等雲服務提供商的依賴。這種方法降低了成本,提高了隱私性,並最小化了對外部基礎設施的依賴。
缺點:
微調需要前期投資來進行模型訓練和數據準備。它需要特定的技術知識,可能涉及陡峭的學習曲線。結果的質量在很大程度上取決於高質量訓練數據的可用性。
由於這個項目專注於獲取知識,我們將繼續進行微調。然而,在現實場景中,評估微調的好處是否值得所有相關的成本和努力是很重要的。
執行
下一步是計劃我們將如何進行微調。在聽完“提高LLM應用準確性”課程後,我決定嘗試Lamini平台,原因如下:
它提供了一個簡單的一行API調用來微調模型。這對於我們剛開始學習新技術特別方便。雖然它不是免費的,對於玩具項目來說可能會相當昂貴(每次調整1美元),但他們在註冊時提供免費的信用額度,足以進行初步測試。Lamini實施了一種新的方法,Lamini記憶微調,承諾在保持一般能力的同時,事實準確性零損失。這是一個重要的聲明,值得測試。我們將稍後詳細討論這種方法。
當然,還有許多其他微調選項可以考慮:
Llama文檔提供了許多微調的配方,可以在雲服務器上執行,甚至對於較小的模型也可以在本地執行。網上有很多逐步指南,包括DataCamp上有關如何在Kaggle上微調Llama的教程。你不僅可以微調開源模型,OpenAI也提供了微調他們模型的能力。
Lamini記憶微調
如我之前提到的,Lamini推出了一種新的微調方法,我認為值得詳細討論。
Lamini引入了記憶專家混合(MoME)方法,使LLM能夠學習大量事實信息,幾乎沒有損失,同時保持泛化能力並需要可行的計算資源。
為了實現這一點,Lamini通過添加大量(約100萬個)LoRA適配器以及交叉注意層來擴展預訓練的LLM。每個LoRA適配器都是一個記憶專家,作為模型的一種記憶功能。這些記憶專家專注於不同的方面,確保模型從其調整的數據中保留真實和準確的信息。受信息檢索的啟發,這些百萬個記憶專家相當於模型智能檢索和路由的索引。
在推理時,模型在每一層檢索最相關的專家子集,並合併回基礎模型,以生成對用戶查詢的響應。
Lamini記憶微調據說能夠達到95%的準確性。與傳統的指令微調的主要區別在於,這種方法不是針對所有任務的平均錯誤進行優化,而是專注於實現模型特別訓練記住的事實的零錯誤。
因此,這種方法使LLM能夠在保持對其他所有事物的平均錯誤的同時,幾乎完美地回憶重要的事實。
有關更多細節,您可以參考Li等人(2024)的研究論文《消除LLM幻覺需要重新思考泛化》。
Lamini記憶微調具有很大的潛力——讓我們看看它在實踐中是否能實現其潛力。
如往常一樣,讓我們開始設置一切。正如我們所討論的,我們將使用Lamini來微調Llama,因此第一步是安裝Lamini包。
pip install lamini
此外,我們需要在他們的網站上設置Lamini API密鑰,並將其指定為環境變量。
export LAMINI_API_KEY=”<YOUR-LAMINI-API-KEY>”
如上所述,我們將改善SQL代理,因此我們需要一個數據庫。對於這個例子,我們將繼續使用ClickHouse,但隨意選擇任何適合您需求的數據庫。您可以在之前的文章中找到有關ClickHouse設置和數據庫架構的更多詳細信息。
要微調LLM,我們首先需要一個數據集——在我們的情況下,是一組問題和答案(SQL查詢)的對。組建數據集的任務可能看起來很艱巨,但幸運的是,我們可以利用LLM來完成這項工作。
準備數據集時需要考慮的關鍵因素:
數據的質量至關重要,因為我們將要求模型記住這些事實。示例的多樣性很重要,以便模型能夠學習如何處理不同的情況。最好使用真實數據而不是合成生成的數據,因為它更好地代表現實生活中的問題。微調數據集的通常最小大小約為1,000個示例,但高質量數據越多越好。
生成示例
創建問題和答案對所需的所有信息都存在於數據庫架構中,因此這將是一項可行的任務,LLM可以生成示例。此外,我有一組代表性的問答對,我用於RAG方法,我們可以將其作為有效查詢的示例呈現給LLM(使用少量示例提示技術)。讓我們加載RAG數據集。
# loading a set of exampleswith open(‘rag_set.json’, ‘r’) as f:rag_set = json.loads(f.read())
rag_set_df = pd.DataFrame(rag_set)
rag_set_df[‘qa_fmt’] = list(map(lambda x, y: “question: %s, sql_query: %s” % (x, y),rag_set_df.question,rag_set_df.sql_query))
這個想法是迭代地向LLM提供架構信息和一組隨機示例(以確保問題的多樣性),並要求它生成一對新的、相似但不同的問答對。
讓我們創建一個系統提示,其中包含有關數據庫架構的所有必要細節。
generate_dataset_system_prompt = ”’你是一位擁有超過10年經驗的資深數據分析師,擅長編寫複雜的SQL查詢。你正在處理的數據庫中有兩個表,架構如下。
表:ecommerce.users 描述:在線商店的客戶字段: – user_id(整數) – 客戶的唯一標識符,例如,1000004或3000004- country(字符串) – 居住國家,例如,“荷蘭”或“英國”- is_active(整數) – 如果客戶仍然活躍則為1,否則為0- age(整數) – 客戶的年齡(以完整年計),例如,31或72
表:ecommerce.sessions 描述:在線商店的會話字段: – user_id(整數) – 客戶的唯一標識符,例如,1000004或3000004- session_id(整數) – 會話的唯一標識符,例如,106或1023- action_date(日期) – 會話開始日期,例如,“2021-01-03”或“2024-12-02”- session_duration(整數) – 會話的持續時間(以秒為單位),例如,125或49- os(字符串) – 客戶使用的操作系統,例如,“Windows”或“Android”- browser(字符串) – 客戶使用的瀏覽器,例如,“Chrome”或“Safari”- is_fraud(整數) – 如果會話標記為欺詐則為1,否則為0- revenue(浮點數) – 以美元計的收入(購買項目的總和),例如,0.0或1506.7
請用ClickHouse SQL編寫查詢以回答以下問題。在查詢末尾添加“format TabSeparatedWithNames”,以便以正確的格式從ClickHouse數據庫獲取數據。 ”’
下一步是為用戶查詢創建一個模板。
generate_dataset_qa_tmpl = ”’考慮以下示例,請寫出問題和SQL查詢來回答它,這與下面提供的問題和查詢相似但不同。
問題和SQL查詢的示例:示例”’
由於我們需要高質量的數據集,我更喜歡使用更先進的模型——GPT-4o——而不是Llama。像往常一樣,我將初始化模型並創建一個虛擬工具以進行結構化輸出。
from langchain_core.tools import tool
@tooldef generate_question_and_answer(comments: str, question: str, sql_query: str) -> str:”””返回新的問題和SQL查詢
參數:comments(str):有關新問題和答案對的1-2句話,question(str):新問題 sql_query(str):用ClickHouse語法回答問題的SQL查詢”””pass
from langchain_openai import ChatOpenAIgenerate_qa_llm = ChatOpenAI(model=”gpt-4o”, temperature = 0.5)\.bind_tools([generate_question_and_answer])
現在,讓我們將所有內容組合成一個函數,該函數將生成一對問答並創建一組示例。
# helper function to combine system + user promptsdef get_openai_prompt(question, system):messages = [(“system”, system),(“human”, question)]return messages
def generate_qa():# selecting 3 random examples sample_set_df = rag_set_df.sample(3)examples = ‘\n\n’.join(sample_set_df.qa_fmt.values)
# constructing promptprompt = get_openai_prompt(generate_dataset_qa_tmpl.format(examples = examples), generate_dataset_system_prompt)
# calling LLMqa_res = generate_qa_llm.invoke(prompt)
try:rec = qa_res.tool_calls[0][‘args’]rec[‘examples’] = examplesreturn recexcept:pass
# executing functionqa_tmp = []for i in tqdm.tqdm(range(2000)):qa_tmp.append(generate_qa())
new_qa_df = pd.DataFrame(qa_tmp)
我生成了2,000個示例,但實際上,我為這個玩具項目使用的數據集要小得多。因此,我建議將示例數量限制在200-300個。
清理數據集
正如我們所知,“垃圾進,垃圾出”,因此在微調之前清理LLM生成的數據是至關重要的。
第一個——也是最明顯的——檢查是確保每個SQL查詢都是有效的。
def is_valid_output(s):if s.startswith(‘Database returned the following error:’):return ‘error’if len(s.strip().split(‘\n’)) >= 1000:return ‘too many rows’return ‘ok’
new_qa_df[‘output’] = new_qa_df.sql_query.map(get_clickhouse_data)new_qa_df[‘is_valid_output’] = new_qa_df.output.map(is_valid_output)
沒有無效的SQL查詢,但有些問題返回超過1,000行。
雖然這些情況是有效的,但我們專注於OLAP場景,並進行聚合統計,因此我僅保留返回100行或更少的查詢。
new_qa_df[‘output_rows’] = new_qa_df.output.map(lambda x: len(x.strip().split(‘\n’)))
filt_new_qa_df = new_qa_df[new_qa_df.output_rows <= 100]
我還消除了輸出為空的情況——返回零行或僅返回標題的查詢。
filt_new_qa_df = filt_new_qa_df[filt_new_qa_df.output_rows > 1]
另一個重要的檢查是重複問題。同一問題的不同答案可能會使模型困惑,因為它無法同時調整到兩個解決方案。事實上,我們有這樣的情況。
filt_new_qa_df = filt_new_qa_df[[‘question’, ‘sql_query’]].drop_duplicates()filt_new_qa_df[‘question’].value_counts().head(10)
為了解決這些重複,我僅保留每個問題的一個答案。
filt_new_qa_df = filt_new_qa_df.drop_duplicates(‘question’)
儘管我生成了約2,000個示例,但我決定使用200個問題和答案對的小型數據集。使用更大的數據集進行微調將需要更多的調整步驟,並且成本會更高。
sample_dataset_df = pd.read_csv(‘small_sample_for_finetuning.csv’, sep = ‘\t’)
您可以在GitHub上找到最終的訓練數據集。
現在我們的訓練數據集已經準備好,我們可以進入最令人興奮的部分——微調。
第一次迭代
下一步是生成我們將用於微調模型的LLM請求和響應集。
由於我們將使用Llama模型,讓我們創建一個輔助函數來為其構建提示。
def get_llama_prompt(user_message, system_message=””):system_prompt = “”if system_message != “”:system_prompt = (f”<|start_header_id|>system<|end_header_id|>\n\nsystem_message”f”<|eot_id|>”)prompt = (f”<|begin_of_text|>system_prompt”f”<|start_header_id|>user<|end_header_id|>\n\n”f”user_message”f”<|eot_id|>”f”<|start_header_id|>assistant<|end_header_id|>\n\n”)return prompt
對於請求,我們將使用以下系統提示,其中包含有關數據架構的所有必要信息。
generate_query_system_prompt = ”’你是一位擁有超過10年經驗的資深數據分析師,擅長編寫複雜的SQL查詢。你正在處理的數據庫中有兩個表,架構如下。
表:ecommerce.users 描述:在線商店的客戶字段: – user_id(整數) – 客戶的唯一標識符,例如,1000004或3000004- country(字符串) – 居住國家,例如,“荷蘭”或“英國”- is_active(整數) – 如果客戶仍然活躍則為1,否則為0- age(整數) – 客戶的年齡(以完整年計),例如,31或72
表:ecommerce.sessions 描述:在線商店的使用會話字段: – user_id(整數) – 客戶的唯一標識符,例如,1000004或3000004- session_id(整數) – 會話的唯一標識符,例如,106或1023- action_date(日期) – 會話開始日期,例如,“2021-01-03”或“2024-12-02”- session_duration(整數) – 會話的持續時間(以秒為單位),例如,125或49- os(字符串) – 客戶使用的操作系統,例如,“Windows”或“Android”- browser(字符串) – 客戶使用的瀏覽器,例如,“Chrome”或“Safari”- is_fraud(整數) – 如果會話標記為欺詐則為1,否則為0- revenue(浮點數) – 以美元計的收入(購買項目的總和),例如,0.0或1506.7
請用ClickHouse SQL編寫查詢以回答以下問題。在查詢末尾添加“format TabSeparatedWithNames”,以便以正確的格式從ClickHouse數據庫獲取數據。遵循指示回答問題,提供所有所需的信息並分享你的推理。 ”’
讓我們以適合Lamini微調的格式創建響應。我們需要準備一個包含輸入和輸出鍵的字典列表。
formatted_responses = []
for rec in sample_dataset_df.to_dict(‘records’):formatted_responses.append(‘input’: get_llama_prompt(rec[‘question’], generate_query_system_prompt),’output’: rec[‘sql_query’])
現在,我們已經完全準備好進行微調。我們只需要選擇一個模型並啟動過程。我們將微調Llama 3.1 8B模型。
from lamini import Laminillm = Lamini(model_name=”meta-llama/Meta-Llama-3.1-8B-Instruct”)
finetune_args = “max_steps”: 50,”learning_rate”: 0.0001
llm.train(data_or_dataset_id=formatted_responses,finetune_args=finetune_args,)
我們可以指定幾個超參數,您可以在Lamini文檔中找到所有詳細信息。現在,我只將最基本的參數傳遞給函數:
max_steps:這決定了調整步驟的數量。文檔建議使用50步進行實驗,以獲得初步結果,而不會花費太多金錢。learning_rate:這個參數決定了每次迭代朝著損失函數的最小值移動的步長(維基百科)。默認值為0.0009,但根據指導,我決定使用較小的值。
現在,我們只需要等待10-15分鐘,讓模型訓練,然後我們可以測試它。
finetuned_llm = Lamini(model_name='<model_id>’)# you can find Model ID in the Lamini interface
question = ”’2024年12月有多少客戶進行了購買?”’prompt = get_llama_prompt(question, generate_query_system_prompt)finetuned_llm.generate(prompt, max_new_tokens=200)# select uniqExact(s.user_id) as customers # from ecommerce.sessions s join ecommerce.users u # on s.user_id = u.user_id # where (toStartOfMonth(action_date) = ‘2024-12-01’) and (revenue > 0) # format TabSeparatedWithNames
值得注意的是,我們也在推理中使用Lamini,並且必須為此付費。您可以在這裡找到最新的費用信息。
乍一看,結果看起來很有希望,但我們需要更強大的準確性評估來確認。
此外,值得注意的是,由於我們已經為特定任務微調了模型,它現在始終返回SQL查詢,這意味著我們可能不再需要使用工具調用來獲得結構化輸出。
評估質量
我們在之前的文章中詳細討論了LLM準確性評估,因此在這裡我將提供簡要回顧。
我們使用一組金標準的問題和答案對來評估模型的質量。由於這是一個玩具示例,我將該集合限制為僅10對,您可以在GitHub上查看。
評估過程分為兩個部分:
SQL查詢有效性:首先,我們檢查SQL查詢是否有效,這意味著ClickHouse在執行過程中不會返回錯誤。查詢正確性:接下來,我們確保生成的查詢是正確的。我們使用LLM比較生成查詢和真實查詢的輸出,以驗證它們是否提供語義上相同的結果。
初步結果遠非理想,但比基礎Llama模型(該模型生成零個有效SQL查詢)要好得多。我們發現:
ClickHouse對兩個查詢返回了錯誤。三個查詢被執行,但結果不正確。五個查詢是正確的。
毫無意外——沒有銀彈,這始終是一個迭代過程。讓我們調查一下出錯的原因。
深入分析錯誤
這種方法很簡單。讓我們逐一檢查錯誤,以了解為什麼會得到這些結果以及我們如何修復它們。我們將從第一個不成功的示例開始。
本文由 AI 台灣 運用 AI 技術編撰,內容僅供參考,請自行核實相關資訊。
歡迎加入我們的 AI TAIWAN 台灣人工智慧中心 FB 社團,
隨時掌握最新 AI 動態與實用資訊!