【每週一讀】Automating Hyperparameter Tuning with LlamaIndex

原文🔗:https://levelup.gitconnected.com/automating-hyperparameter-tuning-with-llamaindex-72fdd68e3b90

原文作者:Wenqi Glantz

這篇文章剛好是我目前非常需要的,基於 LlamaIndex 的 ParamTuner對 RAG 的超參數進行自動化調整,還涉及用DatasetGenerator根據文檔內容自動生成問答對,這對於不存在 Ground Truth 的 LLM 答案的評估來說非常友好。不過目前實習公司的項目是基於 Langchain 的,不知道 Langchain 有沒有類似ParamTunerDatasetGenerator的類。

LlamaIndex 的推文裏放出了這張圖:

RAG 的大致流程就是如此,而淡藍色方框裏面是我們關心的超參數。正如推文所述,RAG 面臨的一個巨大問題就是有太多超參數需要調整,而且它遠遠超出了prompting的範圍:分塊、檢索策略、元數據......而本文提供了一種新方案,可以自動/高效地執行此操作。

超參數

文章中重點調整的是以下兩個超參數:

  • chunk_size
  • similarity_top_k

chunk_size 決定檢索到的文本塊的大小。較小的 chunk_size 可能會導致更頻繁的檢索,從而可能以更高的計算開銷爲代價提高檢索精度。相反,較大的 chunk_size 可能會減少檢索次數,但可能導致匹配不完整或不太精確。Similarity_top_k 確定在檢索階段有多少排名靠前的塊被考慮在生成階段進行進一步處理。較高的 similarity_top_k 增加了檢索到的候選者的多樣性,可能爲模型提供更豐富的信息來生成響應。然而,它也增加了計算負擔。

調整這兩個參數可以在計算效率和檢索信息質量之間權衡。

評估器選擇

我們用 LlamaIndex 的評估模塊來幫助調整參數。 在我們這個用例中,鑑於 chunk_sizesimilarity_top_k 主要影響檢索階段,因此選擇 SemanticSimilarityEaluator 來評估檢索。 SemanticSimilarityEaluator 計算生成答案和參考答案的嵌入之間的相似度得分。

原文作者還嘗試了另一個 LlamaIndex 評估器 CorrectnessEvaluator ,它主要處理 RAG 的生成階段,得分最高的參數組合與從 SemanticSimilarityEaluator 得到的參數組合不同。可見,選擇正確的評估器對參數調整來說至關重要,錯誤的評估器也會導致錯誤的參數選擇。

作者遵循了 LlamaIndex 指南中推薦的方法:

  1. 加載文檔。
  2. 生成評估問題/答案對。
  3. 爲三組 chunk_size 和三組 similarity_top_k 構建索引、查詢引擎並收集參數,得到 9 個參數組合。
  4. 定義 EDD(評估驅動開發)來衡量每個參數組合的語義相似度的分數。
  5. 運行 ParamTuner 來調整參數。 ParamTuner 通過嘗試不同的參數組合並用每個組合指定的目標函數來調整超參數,根據目標函數得出的分數評估結果,並返回最佳組合。

超參數調優

1 - 加載文檔

首先加載測試文檔,一個8 頁的 PDF 文檔。

documents = SimpleDirectoryReader("data").load_data()
print(f"loaded documents with {len(documents)} documents")

# Use the new flattened interface for node parsing
node_parser = SentenceSplitter(chunk_size=256)
nodes = node_parser(documents)
print(f"loaded {len(nodes)} nodes")

2 - 生成評估問題/答案對

  • 作者使用 GPT-4 Turbo ( gpt-4–1106-preview ) 生成評估數據集。
  • 如果評估數據集 JSON 文件已存在,加載它。如果沒有,調用 DatasetGenerator 生成問答數據集並將數據集保存到 JSON 文件。
from llama_index.evaluation import (
    DatasetGenerator,
    QueryResponseDataset,
)

eval_service_context = ServiceContext.from_defaults(llm=OpenAI(model="gpt-4-1106-preview"))

# load eval question/answer dataset from JSON file if exists
if os.path.exists("data/eval_qr_dataset.json"):
    eval_dataset = QueryResponseDataset.from_json("data/eval_qr_dataset.json")
else:
    # construct dataset_generator
    dataset_generator = DatasetGenerator(
        nodes[:8],
        service_context=eval_service_context,
        show_progress=True,
        num_questions_per_chunk=2,
    )

    # generate queries and responses
    eval_dataset = dataset_generator.generate_dataset_from_nodes()

    # save the dataset into a file
    eval_dataset.save_json("data/eval_qr_dataset.json")

上述代碼選取了前 8 個節點,爲每個節點生成 2 個問題,因此現在得到了 16 對問題/答案。讓我們加載 JSON 文件並打印其內容。

import json

# Load dataset from JSON file
with open("data/eval_qr_dataset.json", "r") as file:
    eval_dataset_content = json.load(file)

# Print the content in JSON format
json_str = json.dumps(eval_dataset_content, indent=2)  # indent for pretty printing
print(json_str)

然後,我們將問題和答案分成兩個不同的字典,一個用於查詢,另一個用於響應,每個字典都以查詢 id 作爲鍵:

eval_qs = eval_dataset.questions
ref_response_strs = [r for (_, r) in eval_dataset.qr_pairs]

3 - 構建索引、查詢引擎並收集參數

我們定義一個輔助函數 _build_index 來爲文檔構建索引, chunk_size 作爲參數之一傳入。

def _build_index(chunk_size, docs):
    index_out_path = f"./storage_{chunk_size}"
    if not os.path.exists(index_out_path):
        Path(index_out_path).mkdir(parents=True, exist_ok=True)
        
        # Using the new flattened interface for node parsing
        node_parser = SentenceSplitter(chunk_size=chunk_size)
        nodes = node_parser(docs)

        # build index
        index = VectorStoreIndex(nodes)

        # save index to disk
        index.storage_context.persist(index_out_path)
    else:
        # rebuild storage context
        storage_context = StorageContext.from_defaults(
            persist_dir=index_out_path
        )
        # load index
        index = load_index_from_storage(
            storage_context,
        )
    return index

這邊用到了一種把 index 存儲在硬盤上的方法,這樣就不用每次再重新做索引這個步驟了,大大縮短了時間。同樣,不知道 Langchain 有沒有類似的技術。

現在我們將要調節的參數收集在字典param_dict中,還需要一個fixed_param_dict字典來保存文檔、評估問題和參考答案。param_dict 包含需要調整的參數,而 fixed_param_dict 中的參數在調整過程中保持固定。

# contains the parameters that need to be tuned
param_dict = {"chunk_size": [256, 512, 1024], "top_k": [1, 2, 5]}

# contains parameters remaining fixed across all runs of the tuning process
fixed_param_dict = {
    "docs": documents,
    "eval_qs": eval_qs,
    "ref_response_strs": ref_response_strs,
}

4 - 定義 EDD 來衡量每個參數組合的分數

我們定義一個輔助函數 _get_eval_batch_runner_semantic_similarity 來爲語義相似度評估器創建一個 BatchEvalRunner

def _get_eval_batch_runner_semantic_similarity():
    eval_service_context = ServiceContext.from_defaults(
        llm=OpenAI(model="gpt-4-1106-preview")
    )
    evaluator_s = SemanticSimilarityEvaluator(
        service_context=eval_service_context
    )
    eval_batch_runner = BatchEvalRunner(
        {"semantic_similarity": evaluator_s}, workers=2, show_progress=True
    )

    return eval_batch_runner

定義目標函數 objective_function_semantic_similarity 來構建索引和查詢引擎,根據評估問題獲取響應,並運行評估器。

def objective_function_semantic_similarity(params_dict):
    chunk_size = params_dict["chunk_size"]
    docs = params_dict["docs"]
    top_k = params_dict["top_k"]
    eval_qs = params_dict["eval_qs"]
    ref_response_strs = params_dict["ref_response_strs"]

    # build index
    index = _build_index(chunk_size, docs)

    # query engine
    query_engine = index.as_query_engine(similarity_top_k=top_k)

    # get predicted responses
    pred_response_objs = get_responses(
        eval_qs, query_engine, show_progress=True
    )

    # run evaluator
    eval_batch_runner = _get_eval_batch_runner_semantic_similarity()
    eval_results = eval_batch_runner.evaluate_responses(
        eval_qs, responses=pred_response_objs, reference=ref_response_strs
    )

    # get semantic similarity metric
    mean_score = np.array(
        [r.score for r in eval_results["semantic_similarity"]]
    ).mean()

    return RunResult(score=mean_score, params=params_dict)

5 - 運行 ParamTuner

最後,我們運行 ParamTuner 來查找語義相似性得分最高的參數組合。 ParamTuner 類是一個簡單的超參數調整框架,它通過嘗試不同的組合並對每個組合運行指定的函數來調整超參數。ParamTuner 的主要活動包括:

  • 通過調用 generate_param_combinations 函數生成參數組合,該函數根據給定的 param_dict 生成超參數值的所有可能組合。
  • 對於每個生成的參數組合,使用當前參數組合調用的指定函數以獲取 RunResult ,其中包括分數、所用的參數和可選元數據。結果收集在 all_run_results 列表中。
  • 根據 RunResult 對象的分數按降序對它們進行排序。
from llama_index.param_tuner import ParamTuner

param_tuner = ParamTuner(
    param_fn=objective_function_semantic_similarity,
    param_dict=param_dict,
    fixed_param_dict=fixed_param_dict,
    show_progress=True,
)

results = param_tuner.tune()

best_result = results.best_run_result
best_top_k = results.best_run_result.params["top_k"]
best_chunk_size = results.best_run_result.params["chunk_size"]

print("")
print(f"Semantic Similarity Score: {best_result.score}")
print(f"Top-k: {best_top_k}")
print(f"Chunk size: {best_chunk_size}")

將結果可視化:

根據圖表,我們得出語義相似度的最佳得分來自 chunk_size 1024 和 similarity_top_k 5 的組合。 chunk_size 的組合 512 和 similarity_top_k 1 返回最差分數。

關於成本

ParamTuner 並不是免費的:

  • 作者使用 GPT-4 Turbo 作爲數據集生成和參數評估的模型。其成本爲 0.01 美元/1000 個輸入代幣和 0.03 美元/1000 個輸出代幣。
  • 參數組合的數量根據參數數量及其期望調整的值呈指數增長。在本文例子中,只調了兩個參數 chunk_sizesimilarity_top_k ,每個參數都有三個值,從而產生 9 個參數組合。

對於本文的示例 RAG,調整語義相似性的成本約爲 0.40 美元。

總結

本文探討了 LlamaIndex 提供的超參數調整功能,深入研究了實現 ParamTuner 的詳細步驟,以自動調整 RAG 中的參數。通過類似的方法,我們還可以在 RAG 的檢索或生成階段調整其他參數。

本文源碼在作者的 Colab 筆記本可以找到。

有機會自己用開源大模型試一試,以及研究一下怎麼用 Langchain 實現。

歡迎大家在評論區留言討論!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章