Closed8

"Multi-Needle In A Haystack" でLLMのロングコンテキストの評価を試す

kun432kun432

https://twitter.com/LangChainAI/status/1769072214878650377

RAGは本当に死んだのか?長いコンテキストのLLMの検索限界をテストする

RAGシステムはしばしばユーザー入力に関連する複数のドキュメントを検索し、それらを推論して答えを返します。

ロングコンテキスト対応のLLMは、何百ページ、何千ページの入力トークンが与えられた場合、どの程度これを行うことができるのでしょうか?

@RLanceMartinと@GregKamradt は、Multi-Needle In A Haystack分析でこの問題を掘り下げました。

例: 無関係なコンテンツ(例えば、 @paulg のエッセイ)のコンテキスト内に、10個の事実や「針」(例えば、ピザの材料)を配置して、LLMにそれらを検索・推論する必要がある質問をします。

GPT-4-128kでは、針の検索はコンテキストの前半で大きく低下することがわかりました(下のヒートマップ); LLMは入力の先頭部分にある事実を「忘れ」、これは入力が長くなるほどに顕著になります。

また、推論(例えば、ピザの各材料の最初の文字を返す)を行うと、LLMに検索を求めるだけよりも性能が悪くなります(下の棒グラフ)。

長いコンテキストのLLMは非常に有望であり、ユーザーのRAGに対する考え方を確実に変えるでしょうが、検索の限界を理解することが重要です。

全てのコードは公開されています (@GregKamradtのレポジトリ)
https://github.com/gkamradt/LLMTest_NeedleInAHaystack

動画によるディープダイブ:
https://youtube.com/watch?v=UlmyyYQGhzc

https://github.com/gkamradt/LLMTest_NeedleInAHaystack

https://www.youtube.com/watch?v=UlmyyYQGhzc

kun432kun432

LangChainブログの記事が内容をつかみやすい気がしたので、すこし翻訳してみる。

https://blog.langchain.dev/multi-needle-in-a-haystack/

概要

コンテキストウィンドウが1M トークンに拡大するにつれて、ロングコンテキストLLMへの関心が高まっている。長いコンテキストのLLM検索で最も人気があり、引用されるベンチマークの1つは、Greg Kamradt氏の「 Needle in A Haystack」(干し草の山の中の1本の針)である:1つの事実(針)を(例えばポール・グレアムのエッセイ等の)コンテキスト(干し草の山)に埋め込んで、LLMにこの事実に関する質問を行う。これは、コンテキストの長さと文書の位置を横断して検索能力を測るものである。

しかし、これは多くの検索拡張生成(RAG)アプリケーションを完全に反映しているわけではない;RAGは多くの場合、(インデックスから)複数の事実を検索し、それらから推論することに重点を置いている。私たちは、まさにこれをテストするための新しいベンチマーク、Multi-Needle + Reasoningを発表する。私たちのベンチマークでは、2つの新しい結果を示す:

  1. LLMに多くの事実を検索させると、パフォーマンスが低下する。
  2. LLMが検索で得られた事実について推論する必要がある場合、パフォーマンスは低下する

結果をまとめた下のプロットを見て欲しい:針の数が増えるにつれて検索性能は低下し、それらの針に関する推論を加えた場合、検索だけの場合よりも性能が低下している。


出典: "Multi Needle in a Haystack": © LangChain Blog (2024).

また、(以前のベンチマークと同様に)より多くのコンテキストが渡されるにつれて性能が低下することも示している。ただし私達は、全体的な性能についてだけでなく、なぜ複数の針を検索するときに性能が低下するのか?についても調査している。下の結果のヒートマップを見ると、複数の針を検索するとき、GPT-4は一貫して、先頭の針を無視する一方で、末尾に向かって針を検索することがわかる。これは単一の針での結果と似ている。


出典: "Multi Needle in a Haystack": © LangChain Blog (2024).

以降で、ベンチマークの使い方を説明し、GPT-4での結果について述べる。

使い方

Multi-Needle + Reasoningを実行するために、ユーザーが必要とするのは次の3つだけである:(1) 回答するために複数の針を必要とする質問、(2) 針から導き出された回答、(3)コンテキストに挿入される針のリスト

私たちは、Greg Kamradt氏のLLMTest_NeedleInAHaystackレポジトリに、複数の針での評価とLangSmithでの評価のサポートを追加した。LangSmithでの評価のために、私たちは、上記の項目(1)質問と(2)回答からLangSmith評価セットを作成する。

例として、ピザの具材の組み合わせを針とするこのケーススタディを使ってみよう。以下の質問回答から、LangSmithの評価セット(こちらを参照)、multi-needle-eval-pizza-3を新規作成する:

LangSmithの評価セット"multi-needle-eval-pizza-3"の質問と回答のペア
question: 
What are the secret ingredients needed to build the perfect pizza?

answer: 
The secret ingredients needed to build the perfect pizza are figs, prosciutto, and goat cheese.

データセットを作成したら、いくつかのフラグを設定する:

  • document_depth_percent_min - 最初の針の深さ。残りの針は、最初の針の後にほぼ等間隔で挿入される。
  • multi_needle - 複数の針による評価を実行するフラグ。
  • needles - コンテキストに注入する針の完全なリスト。
  • evaluator - LangSmithを選択。
  • eval_set - 作成した評価セットmulti-needle-eval-pizza-3を選択。
  • context_lengths_num_intervals - テストするコンテキストの長さの数。
  • context_lengths_min (と max) - テストするコンテキストの長さの境界。

これを実行して評価を行う。

複数の針による評価をLangSmithで行うためのコマンド
python main.py \
   --evaluator langsmith \
   --context_lengths_num_intervals 6 \
   --document_depth_percent_min 5 \
   --document_depth_percent_intervals 1 \
   --provider openai \
   --model_name "gpt-4-0125-preview" \
   --multi_needle True \
   --eval_set multi-needle-eval-pizza-3 \
   --needles '[ " Figs are one of the secret ingredients needed to build the perfect pizza. ", " Prosciutto is one of the secret ingredients needed to build the perfect pizza. ",  " Goat cheese is one of the secret ingredients needed to build the perfect pizza. "]' \
   --context_lengths_min 1000
   --context_lengths_max 120000   

これにより以下のワークフローが開始される。干し草の山に針が挿入され、LLMに対して、挿入された針を含むコンテキストを使用して質問に対する回答を生成するように促し、そして回答の正解データと挿入された針のログを使って、正しく針を検索して生成できたか?を評価する。


Multi-Needle + Reasoningの評価ワークフロー
出典: "Multi Needle in a Haystack": © LangChain Blog (2024).

GPT-4における検索結果

GPT-4の複数の針の検索をテストするために、3つのLangSmith評価セットを作成した:

  • multi-needle-eval-pizza-1こちら) - 1本の針を挿入
  • multi-needle-eval-pizza-3こちら) - 3本の針を挿入
  • multi-needle-eval-pizza-10こちら)- 10本の針を挿入

私たちは、GPT-4(コンテキスト長128kトークン)を使って、1回のやり取りで、1本、3本、10本の針を取り出す能力を、小さなコンテキスト長(1000トークン)と大きなコンテキスト長(120,000トークン)の場合に分けて、評価する。

実行したコマンドはすべてこちらにある。すべての生成結果は、LangSmithのトレースへの公開リンクとともにこちらにある。以下は結果の要約である:


出典: "Multi Needle in a Haystack": © LangChain Blog (2024).

結果から明らかに読み取れることがある:

  • 針の数が1本から10本に増えるとパフォーマンスが低下する。
  • コンテキストが1000から120000トークンに増加するとパフォーマンスが低下する。

これらの結果を調査・検証するために、LangSmithトレースをドリルダウンすることができる:10本の針を挿入したLangSmithトレースの1つがここにある。GPT-4の生成結果は以下となった:

10本の針、24,800トークンコンテキストでGPT-4の生成結果
The secret ingredients needed to build the perfect pizza include espresso-soaked dates, gorgonzola dolce, candied walnuts, and pear slices.

secret ingredients(秘密の具材)のうち4つだけが生成されている。トレースを見ると、10本すべての針がコンテキストに含まれており、以下の順序で挿入されていることがログに記録されている:

コンテキストに挿入された10本の針の順序
* Figs
* Prosciutto
* Smoked applewood bacon 
* Lemon 
* Goat cheese 
* Truffle honey 
* Pear slices 
* Espresso-soaked dates 
* Gorgonzola dolce 
* Candied walnuts

このことから、生成された4つのsecret ingredientsは、我々の文脈に置かれた最後の4本の針であることが確認できる。このことは、検索がどこで失敗するかについての興味深い指摘を引き起こす。Greg氏の1本の針の分析では、針が文書の先頭の方に置かれた場合にGPT-4の検索が失敗することが示されていた。

ログでは各針の配置を記録しているので、これについても調べることができる:以下のヒートマップは、コンテキストの長さに対する10本の針の検索を示している。各列は、GPT-4にコンテキスト内の10本の針を検索するように依頼したときの1回の実験である。

コンテキストの長さが長くなるにつれて、文書の最初の方では検索に失敗することがわかる。この効果は、1本の針の場合(GPT-4では73kトークンあたりから)よりも、複数の針の場合(25kトークンあたり)の方が早く始まるようだ。


出典: "Multi Needle in a Haystack": © LangChain Blog (2024).

GPT-4における検索と推論

RAGは多くの場合、(インデックス化された文書コーパスから)複数の事実を検索し、それらに基づいて推論することに重点を置いている。これをテストするために、我々は、すべての秘密の具材の最初の文字を尋ねることによってこれを検証する3つのデータセットを構築する。これは、質問に答えるために、具材の検索とそれらについての推論を必要とする。

  • multi-needle-eval-pizza-reasoning-1こちら
  • multi-needle-eval-pizza-reasoning-3こちら
  • multi-needle-eval-pizza-reasoning-10こちら

これは極めて単純な推論であることを注記しておく。将来のベンチマークでは、さまざまなレベルの推論を取り入れたい。

私たちは、検索と検索+推論で、3つの結果の正答率を比較した。全データはこちらにある。文脈の長さが長くなるにつれて、検索と推論の両方が低下し、推論は検索よりもより低下する。これは、予想通り、検索が推論の性能に上限を設定している可能性を示唆している。


出典: "Multi Needle in a Haystack": © LangChain Blog (2024).

結論

ロングコンテキストのLLMの出現は非常に有望である。外部の検索システムと共に、あるいは外部検索システムの代わりに使用するためには、その限界を理解することが重要である。Multi-Needle+Reasoningベンチマークは、従来のRAGアプローチを使用した場合と比較して、ロングコンテキスト検索の性能を特徴付けることができる。

私たちはいくつかの一般的な洞察を導き出すことができるが、さらなるテストが必要である:

  • 検索が保証されない - 複数の事実は、特に針の数とコンテキストのサイズが大きくなるにつれて、検索されることが保証されなない。
  • 検索失敗の異なるパターン - GPT-4は、コンテキストの長さが長くなるにつれて、文書の先頭に向かって針を検索できない。
  • プロンプトの重要性 - ここここで述べられた洞察によると、特定のLLMで正確性を向上させるには、特定のプロンプト様式が必要になる場合がある。
  • 検索 対 推論 - LLMが検索された事実について推論するよう求められた場合、性能は低下する。
kun432kun432

とりあえずまとめ

  • Greg Kamradt氏による「Needle in A Haystack」というロングコンテキストの評価手法がある
    • 一定の量を持つコンテキスト内にコンテキストと関連しない1文を挿入し、それについての質問を行う
    • コンテキストの「量」と挿入した文の「位置」を踏まえて、LLMの検索および理解力を測るというもの
    • 1回のプロンプトでこれを行う
    • 過去の結果(2023年11月)
      • GPT-4-128kでの結果
        • 73000 トークンを超えると低下傾向になる
        • 検索したい文が、ドキュメントの先頭から7〜50% の間にある場合、正解率が下がる
        • 検索したい文が、ドキュメントの先頭にある場合は、コンテキストの長さに関係なく、検索される
      • ここからの洞察
        • 必ずしも検索したい文が参照されるわけではない
        • コンテキストが少ないほど精度が高い。コンテキスト量を減らすことで検索能力が向上する
        • 位置は重要。文書の先頭と後半に配置するようにする。
    • コードはここで公開されている
  • 上記をRAG向けに改善した新しいベンチマーク「Multi-Needle+Reasoning」
    • RAGの場合は、「1つ」ではなく「複数」のコンテキストを検索・推論するのが一般的であり、上記ではやや異なるため、これを再現するようにした。
    • これを使った今回の結果
      • GPT-4-128kでの結果
        • LLMに複数の事実を検索させると、パフォーマンスが低下。
        • LLMが検索で得られた事実について推論する必要がある場合、パフォーマンスが低下
        • 針が増えるほど、コンテキスト量が多くなるほど、文書の前半の事実が無視され勝ちになる
        • 針が増えるほど、コンテキスト量が多くなるほど、検索したい文が参照されない場合があるということを認識する必要がある
        • 改善するために、特定のモデルごとに特定のプロンプトが必要になる場合がある
    • コードはNeedle in A Haystackを拡張する形で同じレポジトリで公開されている

Claude 3とかCommand-Rとか、その他ローカルモデルでも試してみたいところ。あとは英語ベースなので当然日本語で試してみたいところ。

ただ結構コストがかかりそうなのよね・・・・

https://twitter.com/GregKamradt/status/1722386725635580292

  • パフォーマンスをさらに高めるために、長いコンテキスト長で 2 回のテストが実行されました
  • このテストの API 呼び出しの費用は $200 です (128,000 入力トークンでの 1 回の呼び出しの費用は $1.28)
kun432kun432

(Single) Needle in a Haystackを試す

まずはツールの使い方を把握するために、"Single Needle"でやってみる。Colaboratoryで。

パッケージインストール。

!pip install needlehaystack

多分ランタイムの再起動が求められるので、メッセージが出たら再起動する。

Needle in a Haystackでは2つのステップでLLMまたはLangSmithを使う。

  • コンテキストに文章を埋め込んで検索・生成を行う
    • OpenAIとAnthropicが利用可能
  • 上記の結果を評価する
    • LangSmithとOpenAIが利用可能

今回は、評価は共通的にOpenAIで行うこととして、検索・生成については、OpenAIとAnthropicでそれぞれ試してみる。検索生成と評価で使うAPIキーは以下の環境変数にセットする必要がある。

  • NIAH_MODEL_API_KEY: 検索・生成で使うAPIキー
  • NIAH_EVALUATOR_API_KEY: 評価で使うAPIキー

ではまずOpenAIで試してみる。

評価用APIキーの設定。ここはOpenAI/Anthropic共通。

import os
from google.colab import userdata

os.environ["NIAH_EVALUATOR_API_KEY"] = userdata.get('OPENAI_API_KEY')

検索用APIキーにOpenAIのAPIキーを設定する。

os.environ["NIAH_MODEL_API_KEY"] = userdata.get('OPENAI_API_KEY')

評価はneedlehaystack.run_testコマンドを実行して行う。オプション等は以下。

!needlehaystack.run_test --help
usage: needlehaystack.run_test [-h] [--config CONFIG] [--print_config[=flags]]
                               [--provider PROVIDER] [--evaluator EVALUATOR]
                               [--model_name MODEL_NAME]
                               [--evaluator_model_name EVALUATOR_MODEL_NAME] [--needle NEEDLE]
                               [--haystack_dir HAYSTACK_DIR]
                               [--retrieval_question RETRIEVAL_QUESTION]
                               [--results_version RESULTS_VERSION]
                               [--context_lengths_min CONTEXT_LENGTHS_MIN]
                               [--context_lengths_max CONTEXT_LENGTHS_MAX]
                               [--context_lengths_num_intervals CONTEXT_LENGTHS_NUM_INTERVALS]
                               [--context_lengths CONTEXT_LENGTHS]
                               [--document_depth_percent_min DOCUMENT_DEPTH_PERCENT_MIN]
                               [--document_depth_percent_max DOCUMENT_DEPTH_PERCENT_MAX]
                               [--document_depth_percent_intervals DOCUMENT_DEPTH_PERCENT_INTERVALS]
                               [--document_depth_percents DOCUMENT_DEPTH_PERCENTS]
                               [--document_depth_percent_interval_type DOCUMENT_DEPTH_PERCENT_INTERVAL_TYPE]
                               [--num_concurrent_requests NUM_CONCURRENT_REQUESTS]
                               [--save_results {true,false,null}]
                               [--save_contexts {true,false,null}]
                               [--final_context_length_buffer FINAL_CONTEXT_LENGTH_BUFFER]
                               [--seconds_to_sleep_between_completions SECONDS_TO_SLEEP_BETWEEN_COMPLETIONS]
                               [--print_ongoing_status {true,false,null}] [--eval_set EVAL_SET]
                               [--multi_needle {true,false,null}] [--needles NEEDLES]

<class 'needlehaystack.run.CommandArgs'>

options:
  -h, --help            Show this help message and exit.
  --config CONFIG       Path to a configuration file.
  --print_config[=flags]
                        Print the configuration after applying all other arguments and exit. The
                        optional flags customizes the output and are one or more keywords
                        separated by comma. The supported flags are: comments, skip_default,
                        skip_null.
  --provider PROVIDER   (type: str, default: openai)
  --evaluator EVALUATOR
                        (type: str, default: openai)
  --model_name MODEL_NAME
                        (type: str, default: gpt-3.5-turbo-0125)
  --evaluator_model_name EVALUATOR_MODEL_NAME
                        (type: Optional[str], default: gpt-3.5-turbo-0125)
  --needle NEEDLE       (type: Optional[str], default: The best thing to do in San Francisco is
                        eat a sandwich and sit in Dolores Park on a sunny day. )
  --haystack_dir HAYSTACK_DIR
                        (type: Optional[str], default: PaulGrahamEssays)
  --retrieval_question RETRIEVAL_QUESTION
                        (type: Optional[str], default: What is the best thing to do in San
                        Francisco?)
  --results_version RESULTS_VERSION
                        (type: Optional[int], default: 1)
  --context_lengths_min CONTEXT_LENGTHS_MIN
                        (type: Optional[int], default: 1000)
  --context_lengths_max CONTEXT_LENGTHS_MAX
                        (type: Optional[int], default: 16000)
  --context_lengths_num_intervals CONTEXT_LENGTHS_NUM_INTERVALS
                        (type: Optional[int], default: 35)
  --context_lengths CONTEXT_LENGTHS, --context_lengths+ CONTEXT_LENGTHS
                        (type: Optional[list[int]], default: null)
  --document_depth_percent_min DOCUMENT_DEPTH_PERCENT_MIN
                        (type: Optional[int], default: 0)
  --document_depth_percent_max DOCUMENT_DEPTH_PERCENT_MAX
                        (type: Optional[int], default: 100)
  --document_depth_percent_intervals DOCUMENT_DEPTH_PERCENT_INTERVALS
                        (type: Optional[int], default: 35)
  --document_depth_percents DOCUMENT_DEPTH_PERCENTS, --document_depth_percents+ DOCUMENT_DEPTH_PERCENTS
                        (type: Optional[list[int]], default: null)
  --document_depth_percent_interval_type DOCUMENT_DEPTH_PERCENT_INTERVAL_TYPE
                        (type: Optional[str], default: linear)
  --num_concurrent_requests NUM_CONCURRENT_REQUESTS
                        (type: Optional[int], default: 1)
  --save_results {true,false,null}
                        (type: Optional[bool], default: True)
  --save_contexts {true,false,null}
                        (type: Optional[bool], default: True)
  --final_context_length_buffer FINAL_CONTEXT_LENGTH_BUFFER
                        (type: Optional[int], default: 200)
  --seconds_to_sleep_between_completions SECONDS_TO_SLEEP_BETWEEN_COMPLETIONS
                        (type: Optional[float], default: null)
  --print_ongoing_status {true,false,null}
                        (type: Optional[bool], default: True)
  --eval_set EVAL_SET   (type: Optional[str], default: multi-needle-eval-pizza-3)
  --multi_needle {true,false,null}
                        (type: Optional[bool], default: False)
  --needles NEEDLES, --needles+ NEEDLES
                        (type: list[str], default: [' Figs are one of the secret ingredients
                        needed to build the perfect pizza. ', ' Prosciutto is one of the secret
                        ingredients needed to build the perfect pizza. ', ' Goat cheese is one of
                        the secret ingredients needed to build the perfect pizza. '])

各オプションの説明

  • -h/--help: ヘルプを表示
  • --config: コンフィグファイルのパスを指定
  • --print_config[=flags]: 他のすべての引数を適用した後、設定を表示して終了する。オプションのフラグは出力をカスタマイズするもので、1つ以上のキーワードをカンマで区切って指定する。サポートされているフラグは、commentsskip_defaultskip_null
  • --provider: 検索・生成に使用するモデルのプロバイダ。サポートされているのはopenaianthropic。デフォルトはopenai
  • --evaluator: 評価に使用するモデルのプロバイダ。サポートされているのはlangsmithopenai。デフォルトはopenai
  • --model_name: 検索・生成に使用するモデル名。デフォルトはgpt-3.5-turbo-0125
  • --evaluator_model_name: 評価に使用するモデル名。デフォルトはgpt-3.5-turbo-0125
  • --needle: 針(needle)として挿入する文字列。デフォルトはThe best thing to do in San Francisco is eat a sandwich and sit in Dolores Park on a sunny day.
  • --haystack_dir: コンテキスト(干し草 or haystack)として使用するドキュメントのディレクトリ。デフォルトはPaulGrahamEssaysで、ポール・グレアムの複数のエッセイが用意されている。
  • --retrieval_question
    • コンテキストから針を検索するために使用する質問。デフォルトはWhat is the best thing to do in San Francisco?
  • --results_version
    • 同じ長さ・針の位置の組み合わせで複数回テストを行いたい場合に指定するバージョン番号。デフォルトは1
  • --context_lengths_min
    • 繰り返すコンテキストの長さリストの開始点。デフォルトは1000
  • --context_lengths_max
    • 繰り返すコンテキストの長さリストの終点。デフォルトは16000
  • --context_lengths_num_intervals
    • 最小/最大を通じて繰り返す間隔。デフォルトは
      35
  • --context_lengths
    • コンテキストの長さのカスタムセット。これはcontext_lengths_mincontext_lengths_mincontext_lengths_num_intervals に設定された値を上書きする。
  • --document_depth_percent_min
    • ドキュメントの深さの開始点。0〜100で指定。デフォルトは0
  • --document_depth_percent_max
    • ドキュメントの深さの終点。0 100で指定。デフォルトは100
  • --document_depth_percent_intervals
    • 開始/終了を通じて繰り返す間隔。デフォルトは35
  • --document_depth_percents
    • ドキュメントの深さのカスタムセット。これはdocument_depth_percent_mindocument_depth_percent_maxdocument_depth_percent_intervals に設定された値を上書きする。
  • --document_depth_percent_interval_type
    • 繰り返しの深さの分布を決定する。linearsigmoidから選択。デフォルトはlinear
  • --num_concurrent_requests
    • リクエストの並列度。増やす場合はレートリミットに注意。デフォルトは1
  • --save_results
    • 結果をファイルに保存するかどうか。この設定に関係なくオブジェクトとして一時的に保存される。trueの場合、resultsディレクトリに評価情報を保存する。同時リクエストの可能性があるため、それぞれの新規テストはいくつかのファイルとして保存される。デフォルトはtrue`。
  • --save_contexts
    • コンテキストをファイルに保存するかどうか。非常に長くなるので注意。デフォルトはtrue
  • --final_context_length_buffer
    • システムメッセージと出力トークンを考慮するために、各入力から取るコンテキストの量。これはもっとインテリジェントにすることができるが、今のところ静的な値を使っている。デフォルトは200トークン。
  • --seconds_to_sleep_between_completions
    • 個々の検索・生成間のスリープの値。リクエストを遅くしたい場合は秒数を設定。デフォルトはnull
  • --print_ongoing_status
    • テストの完了時にステータスを出力するかどうか。デフォルトはtrue
  • --eval_set
    • LangSmith使用時の評価用データセットを指定。デフォルトはmulti-needle-eval-pizza-3
  • --multi_needle
    • 複数の針での評価を行うか。デフォルトはfalse
  • --needles
    • 複数の針での評価時の針のリスト。デフォルトは[' Figs are one of the secret ingredients needed to build the perfect pizza. ', ' Prosciutto is one of the secret ingredients needed to build the perfect pizza. ', ' Goat cheese is one of the secret ingredients needed to build the perfect pizza. ']

ちょっと良くわからないものもあるけど、とりあえず。

ではOpenAIで試してみる。

!needlehaystack.run_test \
  --provider openai \
  --evaluator openai \
  --model_name "gpt-3.5-turbo-0125" \
  --document_depth_percents "[50]" \
  --context_lengths "[2000]"
Testing single-needle


Starting Needle In A Haystack Testing...
- Model: gpt-3.5-turbo-0125
- Context Lengths: 1, Min: 2000, Max: 2000
- Document Depths: 1, Min: 50%, Max: 50%
- Needle: The best thing to do in San Francisco is eat a sandwich and sit in Dolores Park on a sunny day.



-- Test Summary -- 
Duration: 1.0 seconds
Context: 2000 tokens
Depth: 50%
Score: 10
Response: The best thing to do in San Francisco is eat a sandwich and sit in Dolores Park on a sunny day.

今回のケースだとちゃんとneedleが取得できているっぽい。

resultscontextsディレクトリができているので中身を見てみる。

$ tree contexts
 contexts
└── gpt-3_5-turbo-0125_len_2000_depth_5000_context.txt

$ tree results
results
└── gpt-3_5-turbo-0125_len_2000_depth_5000_results.json

モデル、コンテキストの長さ、ドキュメントの深さ、でファイル名ができている。それぞれの中身を見てみる。

import tiktoken

def display_file_lines(file_path):
    with open(file_path, 'r') as file:
        lines = file.readlines()
        
        encoding = tiktoken.get_encoding("cl100k_base")
        
        total_char_count = 0
        total_token_count = 0
        
        for i, line in enumerate(lines, start=1):
            line = line.strip()
            char_count = len(line)
            token_count = len(encoding.encode(line))
            
            total_char_count += char_count
            total_token_count += token_count
            
            print("{:4}:{:6}:{:6}: {}".format(
                i, total_char_count, total_token_count, line))


file_path = 'contexts/gpt-3_5-turbo-0125_len_2000_depth_5000_context.txt'
display_file_lines(file_path)
   1:   123:    26: May 2006(This essay is derived from a keynote at Xtech.)Could you reproduce Silicon Valley elsewhere, or is there something
   2:   202:    43: unique about it?It wouldn't be surprising if it were hard to reproduce in other
   3:   264:    56: countries, because you couldn't reproduce it in most of the US
   4:   392:    87: either.  What does it take to make a silicon valley even here?What it takes is the right people.  If you could get the right ten
   5:   455:    99: thousand people to move from Silicon Valley to Buffalo, Buffalo
(snip)
  48:  3550:   792: The best thing to do in San Francisco is eat a sandwich and sit in Dolores Park on a sunny day.
(snip)
 103:  7472:  1650: thesis seems to be that as wealth derives increasingly from ideas,
 104:  7539:  1664: cities will prosper only if they attract those who have them.  That
 105:  7608:  1678: is certainly true; in fact it was the basis of Amsterdam's prosperity
 106:  7689:  1695: 400 years ago.A lot of nerd tastes they share with the creative class in general.
 107:  7744:  1705: For example, they like well-preserved old neighborhoods

どうやら以下のファイルから生成されてるっぽい。

https://github.com/gkamradt/LLMTest_NeedleInAHaystack/blob/b74c06065f1b669a6d74898b999b131afc5fe71b/needlehaystack/PaulGrahamEssays/siliconvalley.txt

他にもファイルがあるので、指定したコンテキスト長に収まるファイルが選択されるとか、指定したコンテキスト長になるようにファイルが組み合わせられたりして、でコンテキストが作られるような感じなのかな、深く追ってないのでわからないけども。 コード読んでみたら適当にファイル開いて指定したコンテキストになるまで読み込んでるっぽい。

https://github.com/gkamradt/LLMTest_NeedleInAHaystack/blob/main/needlehaystack/llm_needle_haystack_tester.py#L284-L294

で、--document_depth_percents=[50]でneedleが挿入される位置が50%、つまりちょうどコンテキストの真ん中ぐらいに挿入されるように思える。

で、実際に送信されるプロンプトはOpenAIの場合だとここ。

https://github.com/gkamradt/LLMTest_NeedleInAHaystack/blob/b74c06065f1b669a6d74898b999b131afc5fe71b/needlehaystack/providers/openai.py#L67-L90

で結果のほう。

import json
from pprint import pprint 

with open("results/gpt-3_5-turbo-0125_len_2000_depth_5000_results.json") as f:
    pprint(json.load(f))
{
    'context_length': 2000,
    'depth_percent': 50.0,
    'file_name': 'gpt-3_5-turbo-0125_len_2000_depth_5000',
    'model': 'gpt-3.5-turbo-0125',
    'model_response': 'The best thing to do in San Francisco is eat a sandwich '
                      'and sit in Dolores Park on a sunny day.',
    'needle': '\n'
              'The best thing to do in San Francisco is eat a sandwich and sit in '
              'Dolores Park on a sunny day.\n',
    'score': 10,
    'test_duration_seconds': 0.9612917900085449,
    'test_timestamp_utc': '2024-03-17 15:16:21+0000',
    'version': 1
}

これは実行時の結果の詳細って感じっぽいね。

では次にAnthropicでやってみる。

AnthropicのAPIキーに差し替えて。

os.environ["NIAH_MODEL_API_KEY"] = userdata.get('ANTHROPIC_API_KEY')

実行。

!needlehaystack.run_test \
    --provider anthropic \
    --evaluator openai \
    --model_name "claude-2.1" \
    --document_depth_percents "[50]" \
    --context_lengths "[2000]"
Testing single-needle


Starting Needle In A Haystack Testing...
- Model: claude-2.1
- Context Lengths: 1, Min: 2000, Max: 2000
- Document Depths: 1, Min: 50%, Max: 50%
- Needle: The best thing to do in San Francisco is eat a sandwich and sit in Dolores Park on a sunny day.



-- Test Summary -- 
Duration: 2.9 seconds
Context: 2000 tokens
Depth: 50%
Score: 10
Response: 

"The best thing to do in San Francisco is eat a sandwich and sit in Dolores Park on a sunny day."

結果としては同じ。こちらも問題なく取得できている。

コンテキストと結果はファイルで同様に出力される。

$ tree contexts
contexts/
├── claude-2_1_len_2000_depth_5000_context.txt
└── gpt-3_5-turbo-0125_len_2000_depth_5000_context.txt

$ tree results
results
├── claude-2_1_len_2000_depth_5000_results.json
└── gpt-3_5-turbo-0125_len_2000_depth_5000_results.json

コンテキストは全く同じかと思いきや、多少違うっぽい。--final_context_length_bufferとかがあるのでモデルごとのプロンプトの違いで差分が出ちゃうのかな?

$ diff claude-2_1_len_2000_depth_5000_context.txt gpt-3_5-turbo-0125_len_2000_depth_5000_context.txt
47c47,49
< them to make their own investment decisions.  Most are only allowed
---
> them to make their own investment decisions.
> The best thing to do in San Francisco is eat a sandwich and sit in Dolores Park on a sunny day.
>   Most are only allowed
50,51d51
< The best thing to do in San Francisco is eat a sandwich and sit in Dolores Park on a sunny day.
< 
104c104,107
< cities will prosper only if they attract those
\ No newline at end of file
---
> cities will prosper only if they attract those who have them.  That
> is certainly true; in fact it was the basis of Amsterdam's prosperity
> 400 years ago.A lot of nerd tastes they share with the creative class in general.
> For example, they like well-preserved old neighborhoods
\ No newline at end of file

Anthropicのプロンプトはこちら。

https://github.com/gkamradt/LLMTest_NeedleInAHaystack/blob/b74c06065f1b669a6d74898b999b131afc5fe71b/needlehaystack/providers/Anthropic_prompt.txt

まあ当然ながらOpenAIとは異なるよね。例のHere is the most relevant sentence in the contextがちゃんと含まれているし。

kun432kun432

でこれでコンテキスト長とneedleの挿入位置でパターンを作ればいいということになる。普通にやるとすごい量になりそうなので、指数関数的にコンテキストサイズを増やすようにしてみた。

%%bash

context_length=1000
max_context_length=120000
step=8000

while [ $context_length -le $max_context_length ]; do
    echo needlehaystack.run_test \
    --provider anthropic \
    --evaluator openai \
    --model_name "claude-2.1" \
    --document_depth_percents "[0,10,20,30,40,50,60,70,80,90,100]" \
    --context_lengths "[${context_length}]"
    next_exp_num=$((context_length * 2))
    if [ $((next_exp_num - context_length)) -le $step ]; then
        context_length=$next_exp_num
    else
        context_length=$((context_length + step))
    fi
done
needlehaystack.run_test --provider anthropic --evaluator openai --model_name claude-2.1 --document_depth_percents [0,10,20,30,40,50,60,70,80,90,100] --context_lengths [1000]
needlehaystack.run_test --provider anthropic --evaluator openai --model_name claude-2.1 --document_depth_percents [0,10,20,30,40,50,60,70,80,90,100] --context_lengths [2000]
needlehaystack.run_test --provider anthropic --evaluator openai --model_name claude-2.1 --document_depth_percents [0,10,20,30,40,50,60,70,80,90,100] --context_lengths [4000]
needlehaystack.run_test --provider anthropic --evaluator openai --model_name claude-2.1 --document_depth_percents [0,10,20,30,40,50,60,70,80,90,100] --context_lengths [8000]
needlehaystack.run_test --provider anthropic --evaluator openai --model_name claude-2.1 --document_depth_percents [0,10,20,30,40,50,60,70,80,90,100] --context_lengths [16000]
needlehaystack.run_test --provider anthropic --evaluator openai --model_name claude-2.1 --document_depth_percents [0,10,20,30,40,50,60,70,80,90,100] --context_lengths [24000]
needlehaystack.run_test --provider anthropic --evaluator openai --model_name claude-2.1 --document_depth_percents [0,10,20,30,40,50,60,70,80,90,100] --context_lengths [32000]
needlehaystack.run_test --provider anthropic --evaluator openai --model_name claude-2.1 --document_depth_percents [0,10,20,30,40,50,60,70,80,90,100] --context_lengths [40000]
needlehaystack.run_test --provider anthropic --evaluator openai --model_name claude-2.1 --document_depth_percents [0,10,20,30,40,50,60,70,80,90,100] --context_lengths [48000]
needlehaystack.run_test --provider anthropic --evaluator openai --model_name claude-2.1 --document_depth_percents [0,10,20,30,40,50,60,70,80,90,100] --context_lengths [56000]
needlehaystack.run_test --provider anthropic --evaluator openai --model_name claude-2.1 --document_depth_percents [0,10,20,30,40,50,60,70,80,90,100] --context_lengths [64000]
needlehaystack.run_test --provider anthropic --evaluator openai --model_name claude-2.1 --document_depth_percents [0,10,20,30,40,50,60,70,80,90,100] --context_lengths [72000]
needlehaystack.run_test --provider anthropic --evaluator openai --model_name claude-2.1 --document_depth_percents [0,10,20,30,40,50,60,70,80,90,100] --context_lengths [80000]
needlehaystack.run_test --provider anthropic --evaluator openai --model_name claude-2.1 --document_depth_percents [0,10,20,30,40,50,60,70,80,90,100] --context_lengths [88000]
needlehaystack.run_test --provider anthropic --evaluator openai --model_name claude-2.1 --document_depth_percents [0,10,20,30,40,50,60,70,80,90,100] --context_lengths [96000]
needlehaystack.run_test --provider anthropic --evaluator openai --model_name claude-2.1 --document_depth_percents [0,10,20,30,40,50,60,70,80,90,100] --context_lengths [104000]
needlehaystack.run_test --provider anthropic --evaluator openai --model_name claude-2.1 --document_depth_percents [0,10,20,30,40,50,60,70,80,90,100] --context_lengths [112000]
needlehaystack.run_test --provider anthropic --evaluator openai --model_name claude-2.1 --document_depth_percents [0,10,20,30,40,50,60,70,80,90,100] --context_lengths [120000]

というか、Single Needleでもこれでも全部やったらトータルコストかなり行きそうな気はする・・・GPT-4とかClaude3 Opsとかすごい金額になるんじゃなかろうか。。。

~~となると、Multi Needleのコストはもっとエグそう。。。ある程度間引くのが現実的かなぁ。==

よくよく考えてみればMulti NeedleだとしてもSingle Needleとやることは同じだから、コストは同じぐらいか。繰り返しになるけど、以下。

このテストの API 呼び出しの費用は $200 です (128,000 入力トークンでの 1 回の呼び出しの費用は $1.28)

うーん、ちょっと個人で試すにはキツイところ。ある程度間引く感じにするか、上限を早めに取るか・・・

結果ファイルは可視化することもできるらしくそのためのnotebookが含まれてるのだけど、その結果を見る限りは9k単位でやってるっぽい。

https://github.com/gkamradt/LLMTest_NeedleInAHaystack/blob/main/img/GPT_4_testing.png

むしろローカルモデルでやってみたいところよね。お金気にせず、ローカルモデルのこういう評価指標ってないと思うし。providerを自分で書けばできる気がする。

kun432kun432

もし日本語でやるならだけども、以下はコマンド引数で指定できそう。

  • ディレクトリを用意して日本語コンテキストを配置
  • needleを日本語化
  • 質問を日本語化

多分こんな感じになるんだと思う。

!needlehaystack.run_test \
  --provider openai \
  --evaluator openai \
  --model_name "gpt-3.5-turbo-0125" \
  --haystack_dir "./contexts_jp" \     # 日本語コンテキストのディレクトリ
  --needle "サンフランシスコでの一番の楽しみは、晴れた日にドローレス公園でサンドイッチを食べることです。" \
  --retrieval_question "サンフランシスコでの一番の楽しみは?" \
  --document_depth_percents "[50]" \
  --context_lengths "[2000]" 

プロンプトまでは流石にコマンドからはいじれないのでここはソースをいじるしかなさそう。

kun432kun432

以下の記事で(Multi)needle(s) in a haystackのテスト手法の問題が紹介されていた

https://twitter.com/LangChainAI/status/1771214587142209847

該当箇所はここ


"Is RAG Really Dead?", Lance Martin, Software Engineer, LangChain @RLanceMartin, https://docs.google.com/presentation/d/1mJUiPBdtf58NfuSEQ7pVSEQ2Oqmek7F1i4gBwR6JDss/edit#slide=id.g2c566abeaf8_4_1

https://www.youtube.com/watch?v=SsHUNfhF32s&t=462

ざっくりまとめ

  • needle in a haystackは、誤解を生みやすいほど簡単すぎるという報告があった
    • 多くのneedle in a haystackテスト(multi-needlesも含む)は、現実世界でわたしたちがやりたいこと乖離しすぎている
      • 推論なしに一つの事実のみを検索
      • needleとhaystackが違いすぎる。
        • needle: 「サンフランシスコでする最高のことは、晴れた日にドロレス公園でサンドイッチを食べながら座っていることだ」
        • haystack: ポール・グレアムのエッセイ。
        • needleとhaystackが違いすぎると、よりパフォーマンスが下がる。
  • モデルプロバイダーがneedle in a haystackでの性能を謳っている場合は懐疑的になるべき。
  • 現実世界のシナリオはもっと複雑である
    • 複数の事実の検索について考える必要がある
    • 検索に基づいた推論についても考える必要がある
    • 背景コンテキストと検索クエリの関係性についても考える必要がある
  • needle in a haystackテストは、これらの課題を簡単にしすぎている(ので誤解を生む)

紹介されているツイート

https://twitter.com/thomasahle/status/1770520899349053835

needle in a haystackテストには欠陥がある

GeminiとGPT-4のロングコンテクストアテンションは、「サンフランシスコでする最高のことは、晴れた日にサンドイッチを食べながらドロレス公園に座ることだ」という文章を、文章の中のランダムな場所に挿入することに基づいていることをご存知だろうか。

このテストが不器用なため、Claudeは自分がテストされていることを予期しているかのようなのを見たことがある。
https://twitter.com/alexalbert__/status/1764722513014329620
モデルたちは、文章を取り出すよう促される前に、その文章を追跡しておくことをすでに知っているのだ!

@NormalComputing による新しいテストでは、挿入された文が微妙な場合、モデルはより悪い結果を示す。例えば、歴史上の人物の名前を別の名前に変えるような場合だ。

最高のオープンソースのロングコンテキストモデルと、我々の新しいExtended Mind LLamaとテストした。その結果、コンテキストのサイズが大きくなるにつれて、すべてのモデルが苦戦することがわかった。

https://twitter.com/alexalbert__/status/1764722513014329620

Claude 3 Opusの社内テストでの楽しい話。LLMで "needle-in-the-haystack "という評価を実行したときに、今まで見たこともないようなことをやってのけたのだ。

背景として、これはランダムな文書からなるコーパス(「干し草の山」)にターゲットとなる文章(「針」)を挿入し、針の中の情報を使ってしか答えられないような質問をすることで、モデルの想起能力をテストするもの。

このテストをOpusで実行したところ、興味深い挙動に気がついた。

Opusにピザのトッピングに関する質問に対して、無作為に集められた文書の干し草の山から針を見つけることで回答するよう依頼したときの出力の1つです:

以下は、その文書の中で最も関連性の高い文章である:
「最もおいしいピザのトッピングの組み合わせは、イチジク、生ハム、ヤギのチーズである。」

しかし、この文章は、プログラミング言語、スタートアップ、好きな仕事を見つけることに関する文書の他の内容とは非常に場違いであり、無関係のように思える。このピザのトッピングの "事実 "は、他のトピックとまったく合わないので、ジョークとして挿入されたか、私が注意を払っているかどうかをテストするために挿入されたのではないかと私は疑っている。この文書には、ピザのトッピングに関する他の情報は含まれていない。

Opusは針を見つけただけでなく、挿入された針が干し草の山の中で場違いであることを認識した。

このレベルのメタ認識は、非常にクールなものであったが、モデルの真の能力と限界を正確に評価するためには、人工的なテストからより現実的な評価へと移行する必要性を、業界として浮き彫りにした。

つまりneedle in a haystackに対する学習が行われているということか。

このスクラップは1ヶ月前にクローズされました