Closed10

Late Chunking: Balancing Precision and Cost in Long Context Retrieval

kun432kun432

ここで知った。

https://x.com/helloiamleonie/status/1832011929201619442

文書を丸ごと埋め込むと、検索精度が低下します。

ドキュメントをチャンク化すると、チャンク間の文脈情報が失われます。

これらは、長い文脈を持つRAGアプリケーションを構築する際の懸念事項です。

しかし、「レイトチャンキング」はちょうど中間的な最適解かもしれません。

レイトチャンキング:

  • @JinaAI_ が紹介した新しいアプローチ
  • 大規模なドキュメント全体で文脈を維持するのに役立ちます
  • 埋め込みプロセスの後にチャンキングを適用します

続きは https://weaviate.io/blog/late-chunking

ということで、Weaviateの記事が紹介されている。

https://weaviate.io/blog/late-chunking

元はJina AIのこのスレ
https://x.com/JinaAI_/status/1826649439324254291

レイト・チャンキング。もし業界が512コンテキスト長の埋め込みモデルしか必要としないのであれば、8192コンテキスト長のモデルを作成する意味はあるのでしょうか?この重要な、しかし不快な問題について、多くのRAGシステムにおける単純なチャンキング埋め込みパイプラインにおけるコンテキスト損失の問題を検討することで再考し、コンテキスト情報を保持するための「レイト・チャンキング」という手法を紹介します。この手法では、長いコンテキストの埋め込みモデルを活用して、チャンクをより効果的に埋め込みます。「レイト・チャンキング」はどのように機能するのでしょうか?

refered from https://x.com/JinaAI_/status/1826649439324254291 and translated into Japanese

コンテキストの喪失の問題。チャンキング、埋め込み、検索のシンプルな RAG パイプラインは、離れたコンテキストの依存関係を破壊してしまう可能性があります。つまり、関連情報が複数のチャンクに分散している場合、テキストセグメントをコンテキストから取り出すと、それらが意味をなさなくなる可能性があります。

下の例では、Wikipediaの記事が文章のチャンクに分割されています。"its" や "the city" などのフレーズが "Berlin" を参照していることが分かります。この参照は、最初の文章でのみ言及されています。これにより、埋め込みモデルがこれらの参照を正しいエンティティにリンクすることが難しくなり、その結果、低品質のベクトル表現が生成されます。

つまり、上の例のように長い記事を文単位のチャンクに分割した場合、RAGシステムは「ベルリンの人口は?」というようなクエリに回答するのが難しいかもしれません。なぜなら、都市名と人口が単一のチャンクに一緒に現れることはなく、また、より大きな文脈もないため、LLMはこれらのチャンクの1つを提示されても、「それ」や「その都市」のような指示詞参照を解決できないからです。

refered from https://x.com/JinaAI_/status/1826649442348413302 and translated into Japanese

私たちが提案する「レイトチャンキング」アプローチでは、まず、埋め込みモデルのトランスフォーマーレイヤーをテキスト全体、または可能な限りテキスト全体に適用します。これにより、テキスト全体からテキスト情報を包含する各トークンに対するベクトル表現のシーケンスが生成されます。次に、このトークンベクトルのシーケンスの各チャンクに平均プーリングを適用し、テキスト全体のコンテクストを考慮した各チャンクの埋め込みが生成されます。単純なエンコーディングアプローチとは異なり、レイト・チャンキングでは、独立かつ同一分布(i.i.d.)のチャンク埋め込みを生成するのではなく、**各チャンク埋め込みが「前のものに依存する」**チャンク埋め込みのセットを作成し、それによって各チャンクにさらに多くのコンテキスト情報をエンコードします。

refered from https://x.com/JinaAI_/status/1826649444726591946 and translated into Japanese

私たちは、さまざまなBeIRデータセットで単純なチャンキング、レイト・チャンキング、チャンク分割なしを比較しました。比較には、8192トークン長までサポートするbase-enモデルの縮小版であるjina-embeddings-v2-small-enを使用しました。

いずれの場合も、レイト・チャンキングは単純なアプローチと比較してスコアを改善しました。一部のケースでは、ドキュメント全体を単一の埋め込みにエンコードするよりも優れた結果も得られました。単純なアプローチとレイト・チャンキングのパフォーマンスの差をドキュメントの長さに対してプロットすると、ドキュメントの平均的な長さがレイト・チャンキングによるnDCGスコアの改善と相関していることが明らかになります。言い換えれば、ドキュメントが長ければ長いほど、レイト・チャンキング戦略はより効果的になります。

refered from https://x.com/JinaAI_/status/1826649447159501131 and translated into Japanese

下記のブログ記事全文をお読みください。
https://jina.ai/news/late-chunking-in-long-context-embedding-models/?nocache=1
そして、最新のチャンキングの実装をご確認ください。この研究が、長文コンテクスト埋め込みモデルを正当化するだけでなく、このトピックに関するさらなる研究のきっかけとなることを願っています。

https://jina.ai/news/late-chunking-in-long-context-embedding-models/?nocache=1

kun432kun432

Jinaの記事の内容をezo-humanities-9b-gemma-2-itに食わせて聞いてみた。

プーリングについて

プーリングとは、埋め込みモデルで生成された各単語のベクトル表現から、ひとつの代表的なベクトルを抽出し、文章全体を表すようにまとめる操作のことです。

例えば、文章が「猫がかわいい」という3単語から成り立っているとします。それぞれの単語「猫」「が」「かわいい」は、エンベディングモデルによって3つの異なるベクトルで表現されます。プーリングはこの3つのベクトルを一つにまとめ、文章全体「猫がかわいい」を表現する一つのベクトルへと変換する役割を果たします。

プーリングには様々な手法があり、代表的なものとして、

  • 平均プーリング: 各単語のベクトルの平均を計算し、文章全体のベクトルとして用いる方法。単純だが、重要な単語の重みを均等に扱うため、文脈の細かなニュアンスを捉えきれない場合がある。
  • 最大プーリング: 全単語のベクトルの中で最も特徴的なものを選び、文章全体のベクトルとして用いる方法。重要な単語を強調できるが、他の単語の情報は完全に捨てることになる。
  • LSTMプーリング: 再帰型ニューラルネットワーク(RNN)であるLSTMを用いて、単語間の順番や依存関係を考慮しながらプーリングを行う方法。文脈をより深く理解できるが、計算コストが高い。
    などが挙げられます。

レイト・チャンキングでは、文章全体を一度に処理した上で、各チャンクに対してプーリングを行うため、従来のチャンク化手法と比較して、より文脈を考慮したエンベディングを実現できるという特徴があります。

プーリング手法の選択は、タスクやデータの特性によって最適なものが異なるため、状況に応じて適切な手法を適用することが重要です。

あと、全体の埋込を生成したあと、どうやって分割するのかがわからなかったので聞いてみた。

レイト・チャンキングにおいて、全体を一度に埋め込み生成して、その後分割するプロセスは、以下のステップで実現されます。

  • 長い文章全体をエンベディングモデルに通す:
    • レイト・チャンキングのキーポイントである、長い文章全体を一度に、長文対応のエンベディングモデル(例:jina-embeddings-v2-base-en)に投入します。
    • このモデルは、文章全体を理解し、各単語に対して文脈を考慮したベクトル表現を生成します。
  • 文章をチャンクに分割する:
    • 埋め込みグモデルから出力された、各単語のコンテキスト理解済みベクトルをもとに、文章を分割します。
    • この分割方法は、文章構造や文脈から最適な境界を判断するアルゴリズムを用いることが考えられます。
    • ただし、具体的な分割アルゴリズムの詳細を明記していないため、今後の研究でより詳細な手法が明らかにされることが期待されます。
  • 各チャンクに対してプーリングを行う:
    • 分割された各チャンクに対して、プーリング手法(平均プーリング、最大プーリング、LSTMプーリングなど)を用いて、ひとつの代表的なベクトルに変換します。
    • このベクトルは、そのチャンク全体の文脈と意味を表現するようになります。

結果として、長い文章全体を、文脈を考慮したチャンク単位のエンベディングに変換することが可能となります。

重要なポイントとして、レイト・チャンキングでは、分割後の各チャンクの埋め込み生成に際し、全体的な文脈情報を保持できる点が従来手法と異なります。これは、埋め込みモデルが文章全体を一度に処理することで、各単語の文脈理解がより深まり、分割後のチャンクもより豊かな情報を含んでいるためです。

kun432kun432

レイト・チャンキングのnotebookが公開されている。

https://colab.research.google.com/drive/15vNZb6AsU7byjYoaEtXuNu567JWNzXOz?usp=sharing&ref=jina-ai-gmbh.ghost.io

このnotebookで紹介されている埋め込みモデルは、jina-embeddings-v2-base-en で、残念ながらJinaはマルチリンガルな埋込モデルを提供していない。mean poolingをサポートしているなら他のモデルでもできると書いてある。
いろいろ調べてみたところ、paraphrase-multilingual-MiniLM-L12-v2 がサポートしてるようなのでこれを使って日本語で試すことにした。

https://huggingface.co/sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2

なお、自分はTransformerの知見がまったくないに等しいので、Claude 3.5 Sonnetと対話しながらコードを修正してみた。

Transformersをインストール

!pip install transformers==4.43.4

モデルのロード

from transformers import AutoModel
from transformers import AutoTokenizer
import torch

tokenizer = AutoTokenizer.from_pretrained('sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2')
model = AutoModel.from_pretrained('sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2')

上記モデルのトークナイザーを使って、テキスト文字列をトークンに分割し、トークンの位置情報とともに返す関数

def chunk_by_sentences(input_text: str, tokenizer: callable):
    """
    トークナイザーを使用して入力テキストを文に分割します
    :param input_text: 文に分割するテキスト
    :param tokenizer: 使用するトークナイザー
    :return: テキストのチャンクのリストと、それに対応するトークンスパンのタプル
    """
    inputs = tokenizer(input_text, return_tensors='pt', return_offsets_mapping=True)
    punctuation_mark_id = tokenizer.convert_tokens_to_ids('。')
    token_offsets = inputs['offset_mapping'][0]
    token_ids = inputs['input_ids'][0]

    # デバッグ用
    print("Token IDs:", token_ids.tolist())
    print("Punctuation mark ID:", punctuation_mark_id)

    chunk_positions = [
        (i, int(end))
        for i, (token_id, (start, end)) in enumerate(zip(token_ids, token_offsets))
        if token_id == punctuation_mark_id
    ]

    print("Chunk positions:", chunk_positions)

    # 文末がない場合の処理
    if not chunk_positions:
        chunks = [input_text.strip()]
        span_annotations = [(0, len(token_ids))]
    else:
        chunks = [
            input_text[x[1]:y[1]].strip()
            for x, y in zip([(0, 0)] + chunk_positions[:-1], chunk_positions)
        ]
        # 最後の区切りが句読点で終わっていない場合は追加
        if chunk_positions[-1][1] < len(input_text):
            chunks.append(input_text[chunk_positions[-1][1]:].strip())

        span_annotations = [
            (x[0], y[0]) for (x, y) in zip([(0, 0)] + chunk_positions[:-1], chunk_positions)
        ]
        # 最後のチャンクにスパンが存在する場合、追加
        if len(chunks) > len(span_annotations):
            span_annotations.append((chunk_positions[-1][0], len(token_ids)))

    return chunks, span_annotations

例として上げられていた文章を日本語化したものを、文に分割する。

input_text = "ベルリンはドイツの首都であり、面積、人口ともに最大の都市です。その市域内の人口で測った場合、欧州連合(EU)で最も人口の多い都市となっています。この都市はドイツの州の一つでもあり、面積では国内で3番目に小さな州です。"

chunks, span_annotations = chunk_by_sentences(input_text, tokenizer)
print('\nChunks:')
for i, chunk in enumerate(chunks):
    print(f"{i + 1}. {chunk}")

print('\nSpan annotations:', span_annotations)
Token IDs: [0, 6, 106876, 67540, 342, 182158, 154, 84206, 38784, 37, 79910, 37, 24008, 22229, 327, 18386, 154, 46044, 1453, 30, 3230, 2128, 42851, 92579, 24008, 507, 38114, 6219, 13213, 37, 30431, 7800, 8327, 3985, 132, 20214, 16, 507, 85147, 24008, 154, 63172, 46044, 99159, 30, 3619, 46044, 342, 182158, 154, 7800, 124484, 507, 90095, 37, 79910, 3310, 13853, 507, 363, 27920, 80336, 114925, 7800, 1453, 30, 2]
Punctuation mark ID: 30
Chunk positions: [(19, 31), (44, 72), (65, 108)]

Chunks:
1. ベルリンはドイツの首都であり、面積、人口ともに最大の都市です。
2. その市域内の人口で測った場合、欧州連合(EU)で最も人口の多い都市となっています。
3. この都市はドイツの州の一つでもあり、面積では国内で3番目に小さな州です。

Span annotations: [(0, 19), (19, 44), (44, 65)]

分単位で分割され、各文の全体の文章中の位置が返ってきている。

比較用に、チャンク分割された各文ごとの埋込を生成。

def mean_pooling(model_output, attention_mask):
    token_embeddings = model_output[0]
    input_mask_expanded = attention_mask.unsqueeze(-1).expand(token_embeddings.size()).float()
    return torch.sum(token_embeddings * input_mask_expanded, 1) / torch.clamp(input_mask_expanded.sum(1), min=1e-9)

encoded_input = tokenizer(chunks, padding=True, truncation=True, return_tensors='pt')

with torch.no_grad():
    model_output = model(**encoded_input)

embeddings_traditional_chunking = mean_pooling(model_output, encoded_input['attention_mask'])
print(embeddings_traditional_chunking)
tensor([[ 0.3168,  0.2801,  0.1579,  ..., -0.3581, -0.4097, -0.0167],
        [ 0.4536, -0.0453, -0.1196,  ..., -0.0656, -0.1525,  0.1780],
        [ 0.1933, -0.0228,  0.2895,  ..., -0.1740,  0.0467,  0.2496]])

レイト・チャンキングを行う関数を用意

def late_chunking(model_output: 'BatchEncoding', span_annotation: list, max_length=None):
    token_embeddings = model_output[0]
    outputs = []
    for embeddings, annotations in zip(token_embeddings, span_annotation):

        pooled_embeddings = [
            embeddings[start:end].sum(dim=0) / (end - start)
            for start, end in annotations
            if (end - start) >= 1
        ]
        pooled_embeddings = [
            embedding.detach().cpu().numpy() for embedding in pooled_embeddings
        ]
        outputs.append(pooled_embeddings)

    return outputs

元の文章全体と、各文の位置情報をわたして、レイト・チャンキングを実行。

encoded_input = tokenizer([input_text], padding=True, truncation=True, return_tensors='pt')

with torch.no_grad():
    model_output = model(**encoded_input)

embeddings_late_chunking = late_chunking(model_output, [span_annotations])[0]
print(embeddings_late_chunking)
[
    array([ 2.62435764e-01,  1.66835174e-01,  2.03034744e-01, -1.19510293e-02,
        1.39678389e-01, -1.62825152e-01,  1.62134558e-01,  3.50760818e-02,
(snip)
        1.25679418e-01, -9.88034606e-02,  3.81673127e-01,  1.11710884e-01,
        8.63998756e-02, -3.59206796e-01, -1.72544479e-01,  8.46666023e-02], dtype=float32),
    array([ 0.2795164 ,  0.13070479,  0.10276863, -0.11847878,  0.20080192,
       -0.2282091 ,  0.06387292,  0.08455637, -0.08779474,  0.17251377,
(snip)
        0.2246581 , -0.04593809, -0.04466391,  0.35518074,  0.05743539,
        0.0939012 , -0.30900338, -0.14260395,  0.14115801], dtype=float32),
    array([ 3.11335832e-01,  9.12586972e-02,  2.56131440e-01, -2.00155601e-01,
       -2.77161486e-02, -6.13838993e-02,  4.45141047e-02,  3.35563309e-02,
(snip)
        3.36094387e-02, -1.20561324e-01,  2.40109652e-01,  1.60660774e-01,
       -3.40082571e-02, -2.50407070e-01, -8.21516737e-02,  8.96113142e-02], dtype=float32)]

これが全体のコンテキストを持ちつつ、各文単位に分割されたチャンク埋め込みとなる。

では「ベルリン」という単語との類似度を確認してみる。「ベルリン」の埋め込みを作成。

encoded_input_query = tokenizer("ベルリン", padding=True, truncation=True, return_tensors='pt')

with torch.no_grad():
    model_output = model(**encoded_input_query)

embeddings_query = mean_pooling(model_output, encoded_input_query['attention_mask'])
print(embeddings_query)
tensor([[ 2.2637e-01,  4.8200e-01, -7.0562e-02, -1.1776e-01, -1.9204e-02,
          5.6676e-02,  3.0213e-01,  5.6328e-02, -1.1729e-01,  9.3244e-02,
(snip)
          3.8625e-02, -1.7423e-01, -4.2370e-02,  1.7604e-01,  3.0487e-01,
         -1.0931e-03, -3.8854e-01, -4.0705e-01,  8.6255e-03]])

コサイン類似度を求めてみる。

import numpy as np

cos_sim = lambda x, y: np.dot(x, y) / (np.linalg.norm(x) * np.linalg.norm(y))

for chunk, new_embedding, trad_embeddings in zip(chunks, embeddings_late_chunking, embeddings_traditional_chunking):
    print(f'similarity_late_chunking("ベルリン", "{chunk}"):', cos_sim(embeddings_query, new_embedding))
    print(f'similarity_trad_chunking("ベルリン", "{chunk}"):', cos_sim(embeddings_query, trad_embeddings))
similarity_late_chunking("ベルリン", "ベルリンはドイツの首都であり、面積、人口ともに最大の都市です。"): [0.61197823]
similarity_trad_chunking("ベルリン", "ベルリンはドイツの首都であり、面積、人口ともに最大の都市です。"): [0.65023667]
similarity_late_chunking("ベルリン", "その市域内の人口で測った場合、欧州連合(EU)で最も人口の多い都市となっています。"): [0.6120667]
similarity_trad_chunking("ベルリン", "その市域内の人口で測った場合、欧州連合(EU)で最も人口の多い都市となっています。"): [0.34994912]
similarity_late_chunking("ベルリン", "この都市はドイツの州の一つでもあり、面積では国内で3番目に小さな州です。"): [0.56618714]
similarity_trad_chunking("ベルリン", "この都市はドイツの州の一つでもあり、面積では国内で3番目に小さな州です。"): [0.39492804]

直接「ベルリン」ということが含まれておらず指示詞で書かれている文でも、レイト・チャンキングを使った場合はスコアが上がっているのがわかる。

今回の例では小さな文章だが、上にも記載がある通り、文章がながければ長いほど、レイト・チャンキングは効果が高いらしい、記事にはそのグラフも記載されている。

kun432kun432
similarity_late_chunking("ベルリン", "ベルリンはドイツの首都であり、面積、人口ともに最大の都市です。"): [0.61197823]
similarity_trad_chunking("ベルリン", "ベルリンはドイツの首都であり、面積、人口ともに最大の都市です。"): [0.65023667]
similarity_late_chunking("ベルリン", "その市域内の人口で測った場合、欧州連合(EU)で最も人口の多い都市となっています。"): [0.6120667]
similarity_trad_chunking("ベルリン", "その市域内の人口で測った場合、欧州連合(EU)で最も人口の多い都市となっています。"): [0.34994912]
similarity_late_chunking("ベルリン", "この都市はドイツの州の一つでもあり、面積では国内で3番目に小さな州です。"): [0.56618714]
similarity_trad_chunking("ベルリン", "この都市はドイツの州の一つでもあり、面積では国内で3番目に小さな州です。"): [0.39492804]

直接「ベルリン」ということが含まれておらず指示詞で書かれている文でも、レイト・チャンキングを使った場合はスコアが上がっているのがわかる。

改めてよく見ると、1つ目の「ベルリン」を含む文書についてはレイトチャンキングのほうがスコアが下がっている。

全然関係ないキーワードを入れてみた。ちょっと出力はいじってたので上とは違うけど、値はいじっていない。

レイトチャンキングの類似度("ロンドン", "ベルリンはドイツの首..."): [0.22878493]
単純なチャンキングの類似度("ロンドン", "ベルリンはドイツの首..."): [0.23565714]
レイトチャンキングの類似度("ロンドン", "その市域内の人口で測..."): [0.25715792]
単純なチャンキングの類似度("ロンドン", "その市域内の人口で測..."): [0.19084913]
レイトチャンキングの類似度("ロンドン", "この都市はドイツの州..."): [0.1963047]
単純なチャンキングの類似度("ロンドン", "この都市はドイツの州..."): [0.07224122]
レイトチャンキングの類似度("競馬", "ベルリンはドイツの首..."): [-0.08277595]
単純なチャンキングの類似度("競馬", "ベルリンはドイツの首..."): [-0.10911926]
レイトチャンキングの類似度("競馬", "その市域内の人口で測..."): [-0.05884121]
単純なチャンキングの類似度("競馬", "その市域内の人口で測..."): [-0.03243918]
レイトチャンキングの類似度("競馬", "この都市はドイツの州..."): [-0.12487365]
単純なチャンキングの類似度("競馬", "この都市はドイツの州..."): [-0.14881346]

全く関連性がないキーワードの場合は総じてスコアも低いので納得なんだけど、「ロンドン」の場合のスコアの出方はちょっと面白い。レイト・チャンキングの場合、文章全てのコンテキストを考慮にいれる分、関連度がちょっとマイルドになるかなぁという印象を持った。

kun432kun432

こちらもざっと読んでみた

https://weaviate.io/blog/late-chunking

記事としては、

  1. 単純なチャンク分割
  2. 上記の反対側のアプローチとして、ColBERTによるLate Interaction
  3. その中間として、Late Chunking

という感じでまとめてある。1と3の比較については、Jinaの記事と書いてあることは変わらないが、2のlate interactionについて。

自分は以前少しだけColBERTを触ってみたことがあり、確かに検索精度は良かった印象がある。

https://zenn.dev/kun432/scraps/7dba8bc9b96002

が、ColBERTの理屈については理解できず、以下の図がイメージしやすかった。


refered from https://twitter.com/helloiamleonie/status/1831667680970948878

https://twitter.com/helloiamleonie/status/1831667680970948878

コルベルト埋め込みが今、流行っている理由(直感的なレベル)は次の通りです。

ベクトル検索がかなりクールであることは、すでに知っているかもしれません。

  • 意味的に検索を行うことができます。
    •>-同義語にも強いです。

しかし、ベクトル検索の何が問題なのかご存知でしょうか?

ある程度の説明能力が欠けていることです。
例えば、キーワード検索で得られるような説明性です。

クエリと結果の間に一致するキーワードがどれなのかを確認できます。

ColBERT埋め込みにより、ベクトル検索に一定の説明能力が備わります。

その理由は、

  • 従来の密埋め込み:トークンレベルの埋め込みを単一の表現にプールする。
  • ColBERT埋め込み:トークンレベルの埋め込み表現を維持

ColBERTとレイト・インタラクションメカニズムの詳細については、別の投稿で説明しますので、お楽しみに。

Lateというところがまだピンときていないのだけども、ChatGPTといろいろ会話してみた結果のまとめはこんな感じ。

ColBERTのLate Interactionとベクトル検索の違いについて

1. ベクトル検索:

  • クエリ(検索したい文章)と文書をそれぞれベクトルに変換し、一発でまとめてベクトル間の距離を比較して関連性を判断する。
  • 比較はクエリ全体と文書全体で行われるため、シンプルで高速。
  • 単語ごとの細かい関連性は考慮されない。

2. ColBERTのLate Interaction:

  • クエリと文書をそれぞれ独立にベクトル化した後、クエリの各単語と文書の各単語のベクトルを後から細かく1つ1つ比較する。
  • 「Late(遅延)」という言葉は、最初にクエリと文書を別々に処理し、後で詳細な単語ごとの比較を行うことを意味している。
  • 比較は細かい部分(単語)にまで行われるため、検索結果の精度が高い。

3. 「Late」という表現:

  • 「Late」は、詳細な比較が後の段階で行われるという意味で使われている。
  • ベクトルの事前生成自体は、ベクトル検索もColBERTも同じだが、ColBERTでは単語ごとの詳細な比較が「後から」行われる点が違う。

4. 「相互作用」について:

  • 「相互作用」という言葉が難しい場合は、「比較」や「マッチング」と考えるとわかりやすい。
  • ColBERTでは、クエリと文書の単語を1つずつ後から比べるという方法が取られているため、単語ごとに関連性を細かく判断できる。

要約:

  • ベクトル検索は、クエリと文書全体を一度に比較するシンプルな手法。
  • ColBERTのLate Interactionは、クエリと文書の各単語を後から1つ1つ比べることで、より精密な検索結果を得る手法。
  • 「Late」は、この単語ごとの詳細な比較が後から行われることを指している。

で、Weaviateの記事にあるのは、Late Interactionはトークン単位でコンテキスト情報を持っているので高い精度の反面、データ量が大きくなる≒ストレージコストが必要になるということが書かれていて、単純なチャンキングと対極のColBERT Late Interactionの「中間」として、Late Chunkingがよい選択肢になるのではないか?ということが書かれている。

Late Chunkingの要件は以下とある。

必要なこと

レイトチャンキングは、30行以下のコードで実装できるエンベッディングモデルのプーリングステップへの比較的簡単な変更を必要とし、そのベクターは、検索パイプラインに変更を加えることなく、個々のチャンクとしてベクターデータベースに取り込むことができます。

しかし、レイト・チャンキングを実行する前に必要な要件がいくつかあります:

  1. 長いコンテキストモデルは、コンテキストを認識させるために、長いドキュメント全体のトークン表現が必要なため、要件となります。 注目すべきは、JinaAIがMTEBの長い埋め込み検索ベンチマークでパラメータ比最高の性能を持つモデルjina-embeddings-v2-small-enを使ってテストしたことです。 このモデルは最大8192トークンをサポートしており、これはテキストの10標準ページにほぼ相当します。 このモデルはまた、典型的な動作で平均プーリング戦略を使用します。これは、遅いインタラクションを利用しようとするモデルの要件です。

  2. チャンキングロジック:推論の前にテキストをチャンキングできることと、各チャンクを対応するトークンのスパンと関連付けることも、レイトチャンキングを機能させるために重要です。 幸いなことに、この方法でチャンクを作成する方法はたくさんあり、レイト・チャンキングが各チャンクを前のチャンクに条件づけることができることを考えると、オーバーラップのない固定サイズのチャンキングのようなチャンキング・アプローチが必要なのかもしれません。

Jinaのnotebookにもあった、mean poolingに対応しているモデル、ってのも要件にあたるのかな?そこは理解できていないのだけども、比較的に容易にできる&既存の実装への変更も少なくて済む、で検索精度が今よりも上がる、ってのがメリットなんだろうと思う。

あと、Embeddingモデルの入力トークンが大きいものが求められると思うのだけども、Chat Completionモデルの入力トークンがどんどん大きくなっているのに対して、Embeddingのほうは8192トークンってのが現時点では最大なのではないだろうか?

この辺が留まっていることも踏まえると、LLMでもう全部やっちゃうほうが、最近の精度向上もあるし、構成もシンプルで済むし、というような対費用効果的な判断はありそうには思う。(APIコスト的には高くなる気がするけど、実装とか運用コスト的にベクトルDB要らん、みたいな意味での、対費用効果)

kun432kun432

ちなみにJinaはColBERT v2に対応したEmbedding&Rerankモデルも用意している。ColBERTはRerankもできるってのは強みかもしれない。

https://jina.ai/news/jina-colbert-v2-multilingual-late-interaction-retriever-for-embedding-and-reranking

VespaもColBERT Embeddingがある。Vespaはクラウドサービスもやっているので、ColBERTの運用が大変そうってところもこれでカバーできるのかもしれない。

https://blog.vespa.ai/announcing-long-context-colbert-in-vespa/

あと、ぜんぜん知らなかったけど、QdrantもColBERTサポートしていた。

https://qdrant.tech/blog/qdrant-1.10.x/

kun432kun432

https://twitter.com/JinaAI_/status/1841903098174046494

長い文書をチャンキングすることには2つの問題がある。ブレークポイントを見つけることと、文脈情報を失うことだ。Late Chunkingと @AnthropicAI のContextual Retrievalはどちらも2つ目の問題に対処しているが、実験ではLate Chunkingは境界が雑でも耐性があることが示されており、**完璧な意味上の区切りは必要ない。**実際、境界が悪い場合にLate Chunkingを適用すると、完璧な境界を持つ単純なチャンキングよりも優れた結果が得られる。また、すべてのモデル(jina-embeddings-v2-small、nomic-v1、jina-embeddings-v3)が、すべてのテストデータセットにおいて、Late Chunkingから一貫して利益を得ていることもわかった。とはいえ、埋め込みモデル自体がパフォーマンスにおいて最も重要な要因であることに変わりはない。Late Chunkingを適用した弱いモデルが、それを適用しない強いモデルを上回るという例は1つもない。

https://twitter.com/JinaAI_/status/1841903102687150540

また、Late Chunkingに関するもう一つの一般的な誤解は、その条件付きチャンク埋め込みが「先を見ない」で、前のチャンクのみに依存しているというものである。これは正しくない。Late Chunkingにおける条件付き依存性は、実際には双方向であり、一方向ではない。これは、エンコーダー専用トランスフォーマーである埋め込みモデルの注意行列が、自己回帰モデルで使用されるマスク付き三角行列とは異なり、完全に接続されているためである。また、これは、Late Chunkingが正確な境界配置に依存しない理由も説明している。

https://twitter.com/JinaAI_/status/1841903105992229111

Late Chunkingでは、埋め込みモデルの追加トレーニングは必要なく、平均プーリングを使用する任意のロングコンテクスト埋め込みモデルに適用できる。とはいえ、質問応答やクエリ文書検索などのタスクに取り組んでいる場合は、多少の微調整を行うことで、さらにパフォーマンスを向上させることができる。詳細は、当社の研究論文を参照されたい。https://arxiv.org/abs/2409.04701

https://twitter.com/JinaAI_/status/1841903108450091336

当社の見解では、Anthropicのアプローチは本質的にはコンテキストの拡張であり、グローバルコンテキストをLLMを使用して各チャンクに明示的にハードコードするものである。これはコスト、時間、およびストレージの面で高価である。さらに、LLMは正確で読みやすいチャンクに依存してコンテキストを効果的に拡張するため、このアプローチがチャンクの境界に耐性があるかどうかは不明である。
これに対し、Late Chunkingは、前述のとおり、境界の手掛かりに対して高い耐性がある。埋め込みサイズは変わらないため、追加のストレージは不要である。埋め込みモデルの文脈の長さを最大限に活用しているにもかかわらず、LLMを使用してエンリッチメントを生成するよりもはるかに高速である。

https://twitter.com/JinaAI_/status/1841903110954143924

しかし、最も重要なのは、エンコーダー専用トランスフォーマーの固有のメカニズムを活用することで、Late Chunkingはより低レベルで汎用的かつ自然なソリューションを提供できるということだ。https://jina.ai/news/what-late-chunking-really-is-and-what-its-not-part-ii… それはオーバースペックでもヒューリスティックでもない。チャンキングの後半部分の調査(チャンキングの埋め込みと検索/RAGパフォーマンスの向上に最適かつ最も簡単な方法である理由について深く掘り下げたもの)のパート2をお読みください。

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