RAG From Scratchをやってみた(4/6): Indexing
LangChain公式リポジトリに、以下のRAGの各要素に関する解説コードがあります。(※1年ほど前に公開された内容です。)
今回はRAGの構成のなかのIndexingに関する部分を読み解いてみた内容です。
Indexing(インデックス作成)は、検索対象となるデータソースの作成に関するアプローチです。このcookbookでは、以下の4手法が解説されています。
- Chunk Optimization (リンク紹介のみ)
- Multi-representation Indexing
- Hierarchical Indexing(RAPTOR, 別のcookbookページのリンク紹介)
- Specialized Embeddings(ColBERT)
notebook内にコードサンプルがあるものはそれを実行、リンク紹介のみの手法はリンク先の情報(+関連情報を検索)を読みながら自分なりにまとめています。
こちらのnotebookを実行してみながらまとめました。
https://github.com/langchain-ai/rag-from-scratch/blob/main/rag_from_scratch_12_to_14.ipynb
実行環境
必要なライブラリをインストール
! pip install langchain_community tiktoken langchain-openai langchainhub chromadb langchain
環境変数に、OpenAI APIの認証情報等をセットします。(今回はAzure OpenAIのAPIを利用しました。)
環境変数の読み込み
from IPython import display # 結果を見やすくするライブラリインポート
import os
from dotenv import load_dotenv
load_dotenv() # 環境変数を読み込み
os.environ['LANGCHAIN_TRACING_V2'] = 'true'
os.environ['LANGCHAIN_ENDPOINT'] = 'https://api.smith.langchain.com'
# Azure OpenAI Service のAPI情報をセットする
AZURE_OPENAI_ENDPOINT=
AZURE_OPENAI_API_KEY=
# Azureのデプロイメント名をセット
DEPLOYMENT_NAME=
# APIのバージョンをセット
API_VERSION=
# Azureのembedding modelのデプロイメント名をセット
EMBE_DEPLOYMENT_NAME=
# embedding modelのAPIバージョンをセット
EMBE_API_VERSION=
# LangSmithのAPIKEYをセット(ない場合も実行可)
LANGCHAIN_API_KEY=
LLMにはgpt-4o-mini
, 埋め込みモデルはtext-embedding-3-large
を使用しました。
Chunk Optimization
indexを作成するときの、テキストの分割の最適化に関する手法です。
上記の動画・コードでは、以下の5レベルのテキスト分割が紹介されています:
- Level 1: Character Splitting - シンプルな静的文字のデータ分割。
- Level 2: Recursive Character Text Splitting - 区切り文字のリストに基づいた再帰的分割。
- Level 3: Document Specific Splitting - ドキュメントタイプに応じた様々なテキスト分割方法 (PDF, Python, Markdown, 画像)
- Level 4: Semantic Splitting - ドキュメントの意味単位で分割する方法。
- Level 5: Agentic Splitting - エージェントのようなシステムでテキストを分割する実験的な方法。
※Level 5は動画投稿当時の先進的な取り組みの一例、として位置づけられている印象です。
Level 1~3は処理の内容が想像しやすくLangChainの機能としても用意されているのでここでは割愛します。
Level 4: Semantic Splitting
embedding modelを用いて意味的に類似したテキストのクラスタを見つける手法です。
例として、「走れメロス」を意味単位にテキスト分割してみます。
対象文章(webテキスト)を読み込み、1文ずつに分割するコード
import requests
from bs4 import BeautifulSoup
import re
# URLを指定
url = 'https://www.aozora.gr.jp/cards/000035/files/1567_14913.html' # 取得したいURLに置き換えてください
# URLからHTMLコンテンツを取得
response = requests.get(url)
# HTTPリクエストが成功したか確認
if response.status_code == 200:
# HTMLコンテンツを解析する
soup = BeautifulSoup(response.content, 'html.parser')
# 例えば、ページ全体のテキストを取得する場合
text = soup.get_text()
else:
print(f"Error: {response.status_code}")
# 正規表現で「。」「?」「!」「」」の後の空白を区切りとして文を分割
single_sentences_list = re.split(r'(?<=[。?!」])\s+', text)
print (f"{len(single_sentences_list)} senteneces were found")
sentences = [{'sentence': x, 'index' : i} for i, x in enumerate(single_sentences_list)]
sentences[:6]
80 senteneces were found
[{'sentence': '\n\n\n\n\n\n\n太宰治 走れメロス\n\n\n\n\n\n\n\n\n走れメロス\n太宰治\n\n\n\n\r\n\u3000メロスは激怒した。必ず、かの邪智暴虐の王を除かなければならぬと決意した。メロスには政治がわからぬ。(中略)老爺は、あたりをはばかる低声で、わずか答えた。',
'index': 0},
{'sentence': '「王様は、人を殺します。」', 'index': 1},
{'sentence': '「なぜ殺すのだ。」', 'index': 2},
{'sentence': '「悪心を抱いている、というのですが、誰もそんな、悪心を持っては居りませぬ。」', 'index': 3},
{'sentence': '「たくさんの人を殺したのか。」', 'index': 4},
{'sentence': '「はい、はじめは王様の妹婿さまを。それから、御自身のお世嗣を。それから、妹さまを。それから、妹さまの御子さまを。それから、皇后さまを。それから、賢臣のアレキス様を。」',
'index': 5}]
前後の文章をbuffer分(ここではbuffer_size=1)結合する処理
def combine_sentences(sentences, buffer_size=1):
# 各文の辞書を処理する
for i in range(len(sentences)):
# 結合された文を格納する文字列を作成
combined_sentence = ''
# 現在の文の前の文を、バッファサイズに基づいて追加する
for j in range(i - buffer_size, i):
# インデックス j が負でないことを確認(最初の文などで範囲外エラーを避けるため)
if j >= 0:
# インデックス j の文を結合された文に追加
combined_sentence += sentences[j]['sentence'] + ' '
# 現在の文を追加
combined_sentence += sentences[i]['sentence']
# 現在の文の後の文を、バッファサイズに基づいて追加する
for j in range(i + 1, i + 1 + buffer_size):
# インデックス j が文リストの範囲内であることを確認
if j < len(sentences):
# インデックス j の文を結合された文に追加
combined_sentence += ' ' + sentences[j]['sentence']
# 完成した文を現在の文の辞書に追加
sentences[i]['combined_sentence'] = combined_sentence
return sentences
sentences = combine_sentences(sentences)
sentences[:3]
[{'sentence': '\n\n\n\n\n\n\n太宰治 走れメロス\n\n\n\n\n\n\n\n\n走れメロス\n太宰治\n\n\n\n\r\n\u3000メロスは激怒した。必ず、かの邪智暴虐の王を除かなければならぬと決意した。メロスには政治がわからぬ。メロスは、村の牧人である。笛を吹き、羊と遊んで暮して来た。けれども邪悪に対しては、人一倍に敏感であった。きょう未明メロスは村を出発し、野を越え山越え、十里はなれた此のシラクスの市にやって来た。メロスには父も、母も無い。女房も無い。十六の、内気な妹と二人暮しだ。この妹は、村の或る律気な一牧人を、近々、花婿として迎える事になっていた。結婚式も間近かなのである。メロスは、それゆえ、花嫁の衣裳やら祝宴の御馳走やらを買いに、はるばる市にやって来たのだ。先ず、その品々を買い集め、それから都の大路をぶらぶら歩いた。メロスには竹馬の友があった。セリヌンティウスである。今は此のシラクスの市で、石工をしている。その友を、これから訪ねてみるつもりなのだ。久しく逢わなかったのだから、訪ねて行くのが楽しみである。歩いているうちにメロスは、まちの様子を怪しく思った。ひっそりしている。もう既に日も落ちて、まちの暗いのは当りまえだが、けれども、なんだか、夜のせいばかりでは無く、市全体が、やけに寂しい。のんきなメロスも、だんだん不安になって来た。路で逢った若い衆をつかまえて、何かあったのか、二年まえに此の市に来たときは、夜でも皆が歌をうたって、まちは賑やかであった筈だが、と質問した。若い衆は、首を振って答えなかった。しばらく歩いて老爺に逢い、こんどはもっと、語勢を強くして質問した。老爺は答えなかった。メロスは両手で老爺のからだをゆすぶって質問を重ねた。老爺は、あたりをはばかる低声で、わずか答えた。',
'index': 0,
'combined_sentence': '\n\n\n\n\n\n\n太宰治 走れメロス\n\n\n\n\n\n\n\n\n走れメロス\n太宰治\n\n\n\n\r\n\u3000メロスは激怒した。必ず、かの邪智暴虐の王を除かなければならぬと決意した。(中略) 「王様は、人を殺します。」'},
{'sentence': '「王様は、人を殺します。」',
'index': 1,
'combined_sentence': '\n\n\n\n\n\n\n太宰治 走れメロス\n\n\n\n\n\n\n\n\n走れメロス\n太宰治\n\n\n\n\r\n\u3000メロスは激怒した。必ず、かの邪智暴虐の王を除かなければならぬと決意した。(中略)「王様は、人を殺します。」 「なぜ殺すのだ。」'},
{'sentence': '「なぜ殺すのだ。」',
'index': 2,
'combined_sentence': '「王様は、人を殺します。」 「なぜ殺すのだ。」 「悪心を抱いている、というのですが、誰もそんな、悪心を持っては居りませぬ。」'}]
埋め込みモデルで1文ずつベクトル化し、コサイン類似度を計算するコード
from langchain_openai import AzureOpenAIEmbeddings
from sklearn.metrics.pairwise import cosine_similarity
embedding_model = AzureOpenAIEmbeddings(
azure_deployment=os.environ.get("EMBE_DEPLOYMENT_NAME"),
openai_api_version=os.environ.get("EMBE_API_VERSION"),
)
embeddings = embedding_model.embed_documents([x['combined_sentence'] for x in sentences])
for i, sentence in enumerate(sentences):
sentence['combined_sentence_embedding'] = embeddings[i]
def calculate_cosine_distances(sentences):
distances = []
for i in range(len(sentences) - 1):
embedding_current = sentences[i]['combined_sentence_embedding']
embedding_next = sentences[i + 1]['combined_sentence_embedding']
# コサイン類似度を計算
similarity = cosine_similarity([embedding_current], [embedding_next])[0][0]
# コサイン距離に変換(コサイン距離 = 1 - コサイン類似度)
distance = 1 - similarity
# コサイン距離をリストに追加
distances.append(distance)
# 距離を辞書に保存
sentences[i]['distance_to_next'] = distance
# 最後の文について処理を追加する場合(オプション)
# sentences[-1]['distance_to_next'] = None # またはデフォルト値を設定する
return distances, sentences
distances, sentences = calculate_cosine_distances(sentences)
print(distances[:3])
# 以下のように出力されます
# [np.float64(0.004011887944849635),
# np.float64(0.5338725292990281),
# np.float64(0.19967922560090334)]
# ベクトルの距離を可視化するコード
import matplotlib.pyplot as plt
plt.plot(distances)
ベクトル距離は次のように可視化されました。
コサイン距離で文章を分割し、可視化するコード
# 文章のコサイン距離を基にして、文章内の文を「チャンク」に分け、その区間を視覚的にハイライトします。
import numpy as np
import japanize_matplotlib
plt.plot(distances)
y_upper_bound = .2
plt.ylim(0, y_upper_bound)
plt.xlim(0, len(distances))
# 外れ値と見なす距離の閾値を決定する必要があります
# ここではnumpyの.percentile()を使用します
breakpoint_percentile_threshold = 90
breakpoint_distance_threshold = np.percentile(distances, breakpoint_percentile_threshold) # より多くのチャンクを得たい場合は、パーセンタイルカットオフを下げてください
plt.axhline(y=breakpoint_distance_threshold, color='r', linestyle='-')
# 次に、この閾値を超える距離がいくつあるかを確認します
num_distances_above_theshold = len([x for x in distances if x > breakpoint_distance_threshold]) # 閾値を超える距離の数
plt.text(x=(len(distances) * .01), y=y_upper_bound / 50, s=f"{num_distances_above_theshold + 1} chunk")
# 次に、閾値を超える距離のインデックスを取得します。これにより、テキストを分割すべき場所が分かります
indices_above_thresh = [i for i, x in enumerate(distances) if x > breakpoint_distance_threshold] # このリストにおける閾値を超える地点のインデックス
# シェーディングとテキストの開始
colors = ['b', 'g', 'r', 'c', 'm', 'y', 'k']
for i, breakpoint_index in enumerate(indices_above_thresh):
start_index = 0 if i == 0 else indices_above_thresh[i - 1]
end_index = breakpoint_index if i < len(indices_above_thresh) - 1 else len(distances)
plt.axvspan(start_index, end_index, facecolor=colors[i % len(colors)], alpha=0.25)
plt.text(x=np.average([start_index, end_index]),
y=breakpoint_distance_threshold + (y_upper_bound) / 20,
s=f"chunk #{i}", horizontalalignment='center',
rotation='vertical')
# 最後のブレークポイントからデータセットの最後までシェーディングを追加
if indices_above_thresh:
last_breakpoint = indices_above_thresh[-1]
if last_breakpoint < len(distances):
plt.axvspan(last_breakpoint, len(distances), facecolor=colors[len(indices_above_thresh) % len(colors)], alpha=0.25)
plt.text(x=np.average([last_breakpoint, len(distances)]),
y=breakpoint_distance_threshold + (y_upper_bound) / 20,
s=f"chunk #{i+1}",
rotation='vertical')
plt.title("文章内の埋め込みブレークポイントに基づいたチャンク")
plt.xlabel("文章内の文のインデックス(文の位置)")
plt.ylabel("連続する文のコサイン距離")
plt.show()
どのように文章が分割されたか?確認します。
chunkごとの文章を確認するコード
# 開始インデックスを初期化
start_index = 0
# グループ化された文を格納するリストを作成
chunks = []
# ブレークポイントを元に文をスライスしていく
for index in indices_above_thresh:
# 終了インデックスは現在のブレークポイント
end_index = index
# 現在の開始インデックスから終了インデックスまでの文をスライス
group = sentences[start_index:end_index + 1]
# 各文を一つの文字列として結合
combined_text = ' '.join([d['sentence'] for d in group])
# チャンクリストに追加
chunks.append(combined_text)
# 次のグループのために開始インデックスを更新
start_index = index + 1
# 最後のグループ(残りの文があれば)
if start_index < len(sentences):
combined_text = ' '.join([d['sentence'] for d in sentences[start_index:]])
chunks.append(combined_text)
# grouped_sentences(またはchunks)には分割された文が格納されます
for i, chunk in enumerate(chunks):
buffer = 200
print (f"Chunk #{i}")
print (chunk[:buffer].strip())
print ("\n")
上記コードの出力
Chunk #0
太宰治 走れメロス
走れメロス
太宰治
メロスは激怒した。必ず、かの邪智暴虐の王を除かなければならぬと決意した。メロスには政治がわからぬ。メロスは、村の牧人である。笛を吹き、羊と遊んで暮して来た。けれども邪悪に対しては、人一倍に敏感であった。きょう未明メロスは村を出発し、野を越え山越え、十里はなれた此のシラクスの市にやって来た。メロスには父も、母も無い。女
Chunk #1
「なぜ殺すのだ。」 「悪心を抱いている、というのですが、誰もそんな、悪心を持っては居りませぬ。」 「たくさんの人を殺したのか。」 「はい、はじめは王様の妹婿さまを。それから、御自身のお世嗣を。それから、妹さまを。それから、妹さまの御子さまを。それから、皇后さまを。それから、賢臣のアレキス様を。」 「おどろいた。国王は乱心か。」 「いいえ、乱心ではございませぬ。人を、信ずる事が出来ぬ、というのです。
Chunk #2
「はは。いのちが大事だったら、おくれて来い。おまえの心は、わかっているぞ。」 メロスは口惜しく、地団駄踏んだ。ものも言いたくなくなった。 竹馬の友、セリヌンティウスは、深夜、王城に召された。暴君ディオニスの面前で、佳き友と佳き友は、二年ぶりで相逢うた。メロスは、友に一切の事情を語った。セリヌンティウスは無言で首肯き、メロスをひしと抱きしめた。友と友の間は、それでよかった。セリヌンティウスは、縄打た
Chunk #3
「何をするのだ。私は陽の沈まぬうちに王城へ行かなければならぬ。放せ。」 「どっこい放さぬ。持ちもの全部を置いて行け。」 「私にはいのちの他には何も無い。その、たった一つの命も、これから王にくれてやるのだ。」 「その、いのちが欲しいのだ。」
Chunk #4
「さては、王の命令で、ここで私を待ち伏せしていたのだな。」 山賊たちは、ものも言わず一斉に棍棒を振り挙げた。メロスはひょいと、からだを折り曲げ、飛鳥の如く身近かの一人に襲いかかり、その棍棒を奪い取って、
「気の毒だが正義のためだ!」と猛然一撃、たちまち、三人を殴り倒し、残る者のひるむ隙に、さっさと走って峠を下った。一気に峠を駈け降りたが、流石に疲労し、折から午後の灼熱の太陽がまともに、かっと照っ
Chunk #5
「ああ、あなたは気が狂ったか。それでは、うんと走るがいい。ひょっとしたら、間に合わぬものでもない。走るがいい。」 言うにや及ぶ。まだ陽は沈まぬ。最後の死力を尽して、メロスは走った。メロスの頭は、からっぽだ。何一つ考えていない。ただ、わけのわからぬ大きな力にひきずられて走った。陽は、ゆらゆら地平線に没し、まさに最後の一片の残光も、消えようとした時、メロスは疾風の如く刑場に突入した。間に合った。 「待
Chunk #6
どっと群衆の間に、歓声が起った。
Chunk #7
「万歳、王様万歳。」 ひとりの少女が、緋のマントをメロスに捧げた。メロスは、まごついた。佳き友は、気をきかせて教えてやった。 「メロス、君は、まっぱだかじゃないか。早くそのマントを着るがいい。この可愛い娘さんは、メロスの裸体を、皆に見られるのが、たまらなく口惜しいのだ。」 勇者は、ひどく赤面した。 (古伝説と、シルレルの詩から。)
底本:「太宰治全集3」ちくま文庫、筑摩書房
Chunk #8
●図書カード
走れメロスの場面の切り替わる部分で分割されているようです。
Level 5: Agentic Splitting
例として、以下の論文の手法が紹介されていました。
Dense X Retrieval: What Retrieval Granularity Should We Use?
DenseXは次の章「Multi-representation Indexing」で紹介します。
Multi-representation Indexing
複数表現のインデックスを作成するアプローチです。
RAGのデータソースを作成する際には次のような矛盾点が生じます:
- 検索のためには、各ドキュメントのチャンク(塊)の意味的な特徴が端的に表現されているほうが良い
- 一方、LLMに回答生成させる際に文脈を保持するためには十分な長さのドキュメントが必要
このため多くのユースケースでは、意味的に類似した文章を検索するためのインデックスと、LLMへ回答生成のために与える文脈では異なる表現を持っていた方が有益となります。
これに対するアプローチとして、検索では小さなチャンクを取得し、回答生成時にLLMへ文脈を与える際には小さなチャンクの元となる原文を参照してLLMへ渡すような考え方があります。
5_Levels_Of_Text_Splitting.ipynbのBonus Levelに複数表現のインデックスに関する手法も載っていたので、それも踏まえて一覧化したものが以下です。
手法 | 概要 | 検索対象(Index) | Retriveする文章(LLMへ回答生成時に渡す文章) |
---|---|---|---|
(比較)Chunk Optimization(Lv1~4)で紹介した方法 | token数/段落/ 意味的な単位 等で分割。 | チャンク | チャンク |
Parent Document(Smaller chunks) | ドキュメントを小さな単位に分割し、検索は小さなチャンクから選び、回答生成時はコンテキストが十分な原文を渡す。 | 小さなチャンク(子ドキュメント) | 原文(親ドキュメント) |
Summary | LLMにドキュメントを要約させたものを検索対象とする。 | 要約文 | 原文 |
Hypothetical questions | LLMにドキュメントについての仮の質問文を生成させ、それを検索対象とする。チャット形式のテキストをナレッジベースとして使う場合に特に有効。 | 仮の質問文 | 原文 |
Dense X | LLMにドキュメントを命題単位で分割させる。各命題は名詞や代名詞が何を指すか?を分割前のドキュメントから補完される。 | 命題 | 原文 |
※ Retriveする文章が原文のままでは大きすぎる場合には、原文から大きなチャンクへ分割し、その後各方法に従ったindexを作成、という流れになります。
今回はRAG from Scratchに載っているSummaryと参考論文として紹介されていたDense Xを見てみます。
※ Hypothetical questionsは詳しく見ませんが、Summaryの処理と同様に実装し、LLMに要約を作成するように依頼するプロンプトの代わりに、仮の質問文生成を依頼することで実装可能です。以下のMulti Vectorのチュートリアルで公開されています。
https://python.langchain.com/docs/how_to/multi_vector/
※ Parent Documentのチュートリアルは以下で公開されています。
https://python.langchain.com/docs/how_to/parent_document_retriever/
Summary
コード例
from langchain_community.document_loaders import WebBaseLoader
loader = WebBaseLoader("https://lilianweng.github.io/posts/2023-06-23-agent/")
docs = loader.load()
loader = WebBaseLoader("https://lilianweng.github.io/posts/2024-02-05-human-data-quality/")
docs.extend(loader.load())
# print(docs) を実行すると、以下のように取得したWebページの中身が見れます。
# [Document(metadata={'source': 'https://lilianweng.github.io/posts/2023-06-23-agent/', 'title': "LLM Powered Autonomous Agents | Lil'Log", 'description': 'Building agents with LLM (large language model) as its core controller is a cool concept. Several proof-of-concepts demos, such as AutoGPT, GPT-Engineer and BabyAGI, serve as inspiring examples. The potentiality of LLM extends beyond (中略)2024 Lil\'Log\n\n Powered by\n Hugo &\n PaperMod\n\n\n\n\n\n\n\n\n\n\n\n\n\n'),
# Document(metadata={'source': 'https://lilianweng.github.io/posts/2024-02-05-human-data-quality/', 'title': "Thinking about High-Quality Human Data | Lil'Log", 'description': '[Special thank you to Ian Kivlichan for many useful pointers (E.g. the 100(中略)\n\n\n© 2024 Lil\'Log\n\n Powered by\n Hugo &\n PaperMod\n\n\n\n\n\n\n\n\n\n\n\n\n\n')]
import uuid
from langchain_core.documents import Document
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import AzureChatOpenAI
llm = AzureChatOpenAI(
openai_api_version=os.getenv("API_VERSION"),
azure_deployment=os.getenv("DEPLOYMENT_NAME"),
temperature=0
)
chain = (
{"doc": lambda x: x.page_content}
| ChatPromptTemplate.from_template("次のドキュメントを要約してください:\n\n{doc}")
| llm
| StrOutputParser()
)
# バッチ処理で各ドキュメントを要約
summaries = chain.batch(docs, {"max_concurrency": 5})
# print(summaries) を実行すると、以下のように取得したWebページを日本語で要約した内容が見れます。
# ['このドキュメントは、LLM(大規模言語モデル)を中心に構築された自律エージェントのシステムについて詳述しています。以下はその要約です。\n\n### 概要\nLLMをコアコントローラーとする自律エージェントは、計画、記憶、ツール使用の3つの主要コンポーネントから構成されます。これにより、複雑なタスクを効率的に処理し、自己反省を通じて学習し、外部APIを利用して情報を取得する能力を持ちます。\n\n### コンポーネント\n1. **計画**:\n - **タスク分解**: 大きなタスクを小さなサブゴールに分解し、効率的に処理します。\n - **自己反省**: 過去の行動を振り返り、改善点を見つけることで、次回の行動に活かします。\n\n2. **記憶**:\n - **短期記憶**: モデルのコンテキスト内での学習を利用します。\n - **長期記憶**: 外部ベクトルストアを使用して、長期間にわたって情報を保持し、迅速に検索します。\n\n3. **ツール使用**:\n - エージェントは外部APIを呼び出し、モデルの限界を超えた情報を取得します。これにより、最新の情報や特定の機能を利用できます。\n\n### 課題\n- **有限のコンテキスト長**: 過去の情報や詳細な指示を含めることが制限されます。\n- **長期計画とタスク分解の難しさ**: 予期しないエラーに直面した際の計画の調整が難しいです。\n- **自然言語インターフェースの信頼性**: モデルの出力の信頼性が低く、フォーマットエラーや指示に従わない場合があります。\n\n### ケーススタディ\n- **科学発見エージェント**: ChemCrowは、化学分野でのタスクを実行するために設計されたエージェントです。\n- **生成エージェントシミュレーション**: 25の仮想キャラクターが相互作用する環境を模倣し、人間の行動を再現します。\n\nこのドキュメントは、LLMを活用した自律エージェントの設計と実装に関する詳細な情報を提供し、今後の研究や開発に向けた洞察を与えています。',
# 'このドキュメントは、質の高い人間データの収集とその重要性についての考察を提供しています。以下は要約です。\n\n### 概要\n高品質なデータは、現代の深層学習モデルのトレーニングに不可欠であり、特に人間によるアノテーションが重要な役割を果たします。データの質を向上させるためには、タスク設計、アノテーターの選定とトレーニング、データの収集と集約が必要です。\n\n### 人間のアノテーターとデータの質\n- **タスク設計**: 明確で簡潔なタスクフローを設計することが重要です。\n- **アノテーターの選定とトレーニング**: スキルに合ったアノテーターを選び、定期的なフィードバックを行うことが求められます。\n- **データの集約**: 機械学習技術を用いてデータをクリーンアップし、真のラベルを特定します。\n\n### 群衆の知恵\n「群衆の知恵」は、非専門家による評価が有効であることを示しています。例えば、Amazon Mechanical Turkを利用した研究では、非専門家が生成した翻訳が専門家の翻訳と高い相関を示しました。\n\n### アノテーターの合意と不一致\nアノテーションの質を評価するためには、複数のアノテーターからのラベルを集め、合意率やCohenのカッパなどの指標を用います。アノテーター間の不一致は必ずしも悪いことではなく、異なる視点を反映することが重要です。\n\n### データの質とモデルのトレーニング\nデータセットが構築された後、モデルのトレーニング中にラベルの誤りを特定するための手法がいくつかあります。影響関数やトレーニング中の予測の変化を追跡する方法が提案されています。\n\n### 結論\n高品質な人間データの収集は、機械学習モデルの性能を向上させるために不可欠です。アノテーションのプロセスを改善し、データの質を確保するためのさまざまな手法が存在しますが、最終的には人間の判断と多様な視点が重要であることが強調されています。']
# -- ここからベクトルストアとMultiVectorRetrieverの作成
from langchain.storage import InMemoryByteStore
from langchain_community.vectorstores import Chroma
from langchain.retrievers.multi_vector import MultiVectorRetriever
from langchain_openai import AzureOpenAIEmbeddings
embeddings = AzureOpenAIEmbeddings(
azure_deployment=os.environ.get("EMBE_DEPLOYMENT_NAME"),
openai_api_version=os.environ.get("EMBE_API_VERSION"),
)
# 子チャンクをインデックス化するために使用するベクトルストア
vectorstore = Chroma(collection_name="summaries",
embedding_function=embeddings)
# 親ドキュメント用のストレージ層
store = InMemoryByteStore()
id_key = "doc_id"
# Retriever(MultiVectorRetriever)の作成
retriever = MultiVectorRetriever(
vectorstore=vectorstore,
byte_store=store,
id_key=id_key,
)
# 各ドキュメント(原文)のユニークIDを作成
doc_ids = [str(uuid.uuid4()) for _ in docs]
# 要約後のドキュメントのmetadataへ原文のドキュメントIDを関連付け
summary_docs = [
Document(page_content=s, metadata={id_key: doc_ids[i]})
for i, s in enumerate(summaries)
]
# retrieverへ要約後のドキュメントを追加
retriever.vectorstore.add_documents(summary_docs)
# retrieverへIDと原文のペアを追加
retriever.docstore.mset(list(zip(doc_ids, docs)))
作成したベクトルストアとMultiVectorRetrieverへ同じクエリを入れてみます。
query = "AgentにおけるMemory"
sub_docs = vectorstore.similarity_search(query,k=1)
print(sub_docs[0])
page_content='このドキュメントは、LLM(大規模言語モデル)を中心に構築された自律エージェントのシステムについて詳述しています。以下はその要約です。
### 概要
LLMをコアコントローラーとする自律エージェントは、計画、記憶、ツール使用の3つの主要コンポーネントから構成されます。これにより、複雑なタスクを効率的に処理し、自己反省を通じて学習し、外部APIを利用して情報を取得する能力を持ちます。
### コンポーネント
1. **計画**:
- **タスク分解**: 大きなタスクを小さなサブゴールに分解し、効率的に処理します。
- **自己反省**: 過去の行動を振り返り、改善点を見つけることで、次回の行動に活かします。
2. **記憶**:
- **短期記憶**: モデルのコンテキスト内での学習を利用します。
- **長期記憶**: 外部ベクトルストアを使用して、長期間にわたって情報を保持し、迅速に検索します。
3. **ツール使用**:
- エージェントは外部APIを呼び出し、モデルの限界を超えた情報を取得します。これにより、最新の情報や特定の機能を利用できます。
### 課題
- **有限のコンテキスト長**: 過去の情報や詳細な指示を含めることが制限されます。
- **長期計画とタスク分解の難しさ**: 予期しないエラーに直面した際の計画の調整が難しいです。
- **自然言語インターフェースの信頼性**: モデルの出力の信頼性が低く、フォーマットエラーや指示に従わない場合があります。
### ケーススタディ
- **科学発見エージェント**: ChemCrowは、化学分野でのタスクを実行するために設計されたエージェントです。
- **生成エージェントシミュレーション**: 25の仮想キャラクターが相互作用する環境を模倣し、人間の行動を再現します。
このドキュメントは、LLMを活用した自律エージェントの設計と実装に関する詳細な情報を提供し、今後の研究や開発に向けた洞察を与えています。' metadata={'doc_id': 'bd147444-180b-4379-a0d9-4a6f055f38dd'}
retrieved_docs = retriever.get_relevant_documents(query,n_results=1)
print(retrieved_docs[0].page_content)
Number of requested results 4 is greater than number of elements in index 2, updating n_results = 2
LLM Powered Autonomous Agents | Lil'Log
(※ 取得元のwebページがテキスト形式で表示されます)
DenseX
以下で提案されたアプローチです。
Dense X Retrieval: What Retrieval Granularity Should We Use?
論文中の図2を一部日本語訳したものが以下です。
ドキュメントをPropositions(命題)単位に分割する考え方です。図では分割部分はAに該当し、LLMを用いてwikipediaの原文(水色)を3つの命題(ピンク色)に分けています。ポイントは単純に分けるだけでなく、名詞または文全体に必要な修飾語を追加したり代名詞(これ、それ、あの等)を置き換えたりする指示をプロンプトに含めている点です。図の例では3つの命題とも「ピサの斜塔」に関するトピックであることが補完されています。
論文では100単語、1文、命題の3レベルでインデックスを作成し、命題が最も汎化性能が上がったと述べられています。
農林水産省のいもようかんに関するWebページを例として、命題に分割してみます。※ コードでは命題に分割するまでの処理となります。検索部分に関わるデータソースの作成は、上記のsummaryのように分割前のドキュメントと対応づく形で実装することになります。
出典:農林水産省ウェブサイト うちの郷土料理 いもようかん(https://www.maff.go.jp/j/keikaku/syokubunka/k_ryouri/search_menu/menu/34_23_tokyo.html)
DenseX コード
from langchain_openai import AzureChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from typing import Optional, List
from langchain_core.pydantic_v1 import BaseModel
from langchain_community.document_loaders import WebBaseLoader
# from langchain import hub
llm = AzureChatOpenAI(
openai_api_version=os.getenv("API_VERSION"),
azure_deployment=os.getenv("DEPLOYMENT_NAME"),
temperature=0
)
# prompt = hub.pull("wfh/proposal-indexing") で取得できるプロンプトを日本語訳したものを使用します。
prompt = ChatPromptTemplate(
[
(
"system", """
「文脈」を明確でシンプルな命題に分解し、文脈を無視して解釈できるようにする。
1. 複文を単純な文に分割する。可能な限り、入力の元の言い回しを維持する。
2. 付加的な説明情報を伴う名前付きエンティティについては、この情報を独自の個別の命題に分割します。
3. 名詞または文全体に必要な修飾語を追加したり、代名詞を置き換えたりして、命題を非文脈化する。
また、代名詞(たとえば、「it」、「he」、「she」、「they」、「this」、「that」)を、それらが参照するエンティティのフルネームで置き換える。
4. 結果は、JSONでフォーマットされた文字列のリストとして表示される。
例:
タイトル: ¯Eostre
章: 理論と解釈、イースター・ヘアーとの関連。
内容: 復活祭のウサギ(Osterhase)の最も古い証拠は、1678年に南西ドイツでゲオルク・フランク・フォン・フランケナウ医学教授によって記録された。学者のリヒャルト・サーモンは、
「ウサギは春になると庭でよく見かけられたので、子供たちのために庭に隠された色のついた卵の由来を説明するのに都合がよかったのだろう」と書いている。
あるいは、ヨーロッパではノウサギが卵を産むという言い伝えがある。ノウサギのひっかき傷や巣の形とコサメビタキの巣の形はよく似ており、どちらも草原に生えていて春に初めて見られるからだ。
19世紀には、イースター・カードやおもちゃ、本の影響で、イースター・ウサギはヨーロッパ中に広まった。その後、ドイツからの移民がこの風習をイギリスやアメリカに輸出し、イースター・バニーへと発展した。」
出力: ["復活祭のウサギの最も古い証拠は1678年に南西ドイツでゲオルク・フランク・フォン・フランケナウによって記録された。",
"ゲオルク・フランク・フォン・フランケナウは医学の教授であった。",
"復活祭のウサギの証拠は18世紀までドイツの他の地域では知られていなかった。",
"リチャード・サーモンは学者であった。 ",
"リヒャルト・サーモンは、イースターの間の伝統とノウサギとの間の関連についての可能な説明についての仮説を書いている。",
"ノウサギは、春の庭で頻繁に見られた。",
"ノウサギは、子供たちのために庭に隠された色の卵の起源についての便利な説明として機能した可能性がある。",
"ヨーロッパには、ウサギが卵を産んだという言い伝えがある。",
"ノウサギのひっかき傷や形と、ヒヨドリの巣はよく似ている。",
"ノウサギもヒヨドリの巣も草原に発生し、春に初めて見られる。 ",
"ドイツからの移民は、イースターうさぎ/ウサギの風習をイギリスとアメリカに輸出した。",
"イースターうさぎ/ウサギの風習は、イギリスとアメリカではイースターバニーへと発展した。"]
""",
),
("human", "以下を分解してください\n{input}"),
]
)
# Pydantic data class
class Sentences(BaseModel):
sentences: List[str]
# 結果を構造化する
structured_llm = llm.with_structured_output(Sentences)
# runnableで処理を連結
runnable = prompt | structured_llm
def get_propositions(text):
runnable_output = runnable.invoke({
"input": text
})
propositions = runnable_output.sentences
return propositions
# 出典:農林水産省ウェブサイト うちの郷土料理 いもようかん(https://www.maff.go.jp/j/keikaku/syokubunka/k_ryouri/search_menu/menu/34_23_tokyo.html)
loader = WebBaseLoader("https://www.maff.go.jp/j/keikaku/syokubunka/k_ryouri/search_menu/menu/34_23_tokyo.html")
docs = loader.load()
print(docs) # 以下のようにwebページの内容が確認できます。
# [Document(metadata={'source': 'https://www.maff.go.jp/j/keikaku/syokubunka/k_ryouri/search_menu/menu/34_23_tokyo.html', 'title': 'いもようかん 東京都 | うちの郷土料理:農林水産省', 'description': '', 'language': 'ja'}, page_content='\n\n\n\n\n\n\n\n\n\n\n\nいもようかん 東京都 | うちの郷土料理:農林水産省\n\n\n\n\n\n\n\n\n\n\n\n\n\nこのページの本文へ移動\n\n\n\n\n\n\n\nEnglish\nこどもページ\nサイトマップ\n\n\n文字サイズ\n標準\n大きく\n\n\n\n\n\n\n逆引き事典から探す\n組織別から探す\n\n閉じる\n\n大臣官房\n\n食料・農業・農村基本法\n食料・農業・農村基本計画\n食料安定供給・農林水産業基盤強化本部\nTPP(国内対策)\n日EU・EPA(国内対策)\n技術政策\n食料安全保障\n環境政策\n再生可能エネルギー\nバイオマス\n災害に関する情報\nMAFFアプリ\n地方農政局等の取組\n\n\n\n新事業・食品産業部\n\n食品産業(企画 / 流通)\n食品産業(製造 / 外食・食文化)\n新事業創出(フードテック等)\nSDGs×食品産業\n中小企業等経営強化法による支援\n農林漁業成長産業化ファンド\n食品産業の「働き方改革」\n栄養改善の国際展開\n商品先物取引\n卸売市場\nJAS(日本農林規格)\n食品企業の安全・信頼対策\nFCP(Food Communication Project)\n食文化のポータルサイト\n\n\n\n統計部\n\n統計情報\n図書館情報\nMAFF統計ダッシュボード\n役立つデータ分析 (分析レポート)\n\n検査·監察部\n\n訓令・通知\n検査マニュアル等\n意見申出制度及び検査モニター制度\n農業協同組合法に定める要請検査\n検査方針・研修実績等\n\n\n\n消費·安全局\n\n消費者のみなさまへ\n消費者の部屋\n食品の安全確保\n生産資材の安全確保\n家畜防疫\n動物検疫\n植物防疫\n獣医師、獣医療\n米トレーサビリティ・食品表示\nトレーサビリティ\nリスクコミュニケーション\n健康な食生活\n食育の推進\n円滑な食品アクセスの確保\n\n\n\n\n輸出・国際局\n\nG7宮崎農業大臣会合\n輸出本部\n農林水産物・食品輸出プロジェクト(GFP)(外部リンク)\n輸出相談窓口\nEPA利用早わかりサイト\nEPA/FTA等\n関税制度\n日本食・食文化の海外発信\nグローバル・フードバリューチェーン(海外展開支援)\nGI・知的財産\n品種登録\n海外農業情報・貿易情報\n\n\n\n農産局\n\n米(稲)・麦・大豆\n野菜・果樹・花き\n蚕糸・茶・薬用作物・こんにゃく・いぐさ(畳表)・その他\n甘味資源作物・いも類・そば・なたね\n 水田農業の高収益化の推進 \n経営所得安定対策\n農業用ドローンの普及拡大\n産地の収益力強化\n農業生産工程管理 / GAP-info\n普及事業 / 農業支援サービス\n農業生産資材 / 農作業安全対策\n環境保全型農業 / 有機農業\n地球温暖化対策\n\n\n\n畜産局\n\n牛乳乳製品消費拡大の取組\n畜産・酪農をめぐる情勢\n畜産農家への支援\n畜産クラスター事業等\n持続的な畜産物生産の在り方検討会\n国産飼料の生産・利用の拡大\n畜産環境対策\n畜産物の輸出\n家畜遺伝資源の管理・保護\nアニマルウェルフェア\n乳用牛 / 肉用牛 / 豚 / 鶏 / その他\n牛乳・乳製品 / 食肉・鶏卵 / 飼料 / 競馬\n\n\n\n経営局\n\n地域計画(人・農地プラン)\n農業経営人材の育成に向けた官民協議会\n担い手と集落営農\n経営体育成支援\n農地中間管理機構の活用\n農地制度\n企業等の農業参入\n新規就農の促進\n農業の働き方改革\n外国人の受け入れ\n女性の活躍推進\n農協・農事組合法人\n農林年金制度の完了について\n農業金融\n農業保険(収入保険・農業共済)\n農業版BCP\n\n\n\n農村振興局\n\n農山漁村発イノベーション / 農泊 / 農福連携\n世界農業遺産・日本農業遺産\n世界かんがい施設遺産\n農村RMO\n「デジ活」中山間地域\n農業の多面的機能 / 棚田振興\n多面的機能支払交付金\n中山間地域等直接支払制度\n鳥獣被害対策/ジビエ利用拡大\n農業振興地域制度/農地転用許可\n荒廃農地の発生防止・解消等\n農業農村整備事業について\n農業水利施設の保全管理\n都市農業の振興・市民農園制度\n\n\n\n\n\n\nキーワードから探す検索\n \n\n\n\n\n\n\n\n メニュー\n\n会見·報道·広報\n\n会見·報道·広報 トップ\n閉じる\n\n大臣等記者会見\n報道発表資料\n災害に関する情報\n大臣、副大臣、政務官等の出張情報\n会議等の開催情報\n\n\nWebマガジン\n年報・パンフレット\nメールマガジン\n\n\nソーシャルメディア一覧\nMAFFアプリ\n地方農政局等の取組\n\n\n\n政策情報\n\n政策情報 トップ\n閉じる\n\n基本政策\n食の安全と消費者の信頼確保\n食料産業・食文化\n農業生産\n農業経営\n農村振興\n\n\n国際・貿易\n技術・研究\n検査\n審議会\n研究会等\n法令、告示・通知等\n\n\n予算、決算、財務書類等\n補助事業、税制\n政策評価\n白書情報\n災害/東日本大震災に関する情報\nその他\n\n\n\n\n統計情報\n\n統計情報 トップ\n閉じる\n\n新着統計情報等\n農家数、担い手、農地に関する統計\n作付面積・生産量、家畜頭数等の統計\n経営収支、産出額、物価等の統計\n卸売市場に関する統計/日別グラフ\n\n\n森林・林業に関する統計\n水産業に関する統計\n6次産業化に関する統計\n輸出入に関する統計\n食料需給表、食品産業等の統計\n役立つデータ分析\n\n\n農林水産基本データ\nMAFF統計ダッシュボード\n農林業センサス\n漁業センサス\n地域の農業を見て知って活かすDB\nわがマチ・わがムラ(外部リンク)\n\n\n\n申請·お問い合わせ\n\n申請·お問い合わせ トップ\n閉じる\n\nご意見・お問い合わせ窓口\n地方公共団体等からの政策提案\nパブリックコメント\n法令適用事前確認手続\n\n\n公益通報の受付窓口\n情報公開\n個人情報\n調達情報・公表事項\n電子入札センター\n\n\n補助事業参加者の公募\n農林水産省後援名義の申請\n\n\n\n農林水産省について\n\n農林水産省について トップ\n閉じる\n\n大臣・副大臣・政務官\n幹部紹介\n幹部職員名簿\n幹部人事異動情報\n組織案内・組織図\n組織・定員\n\n\n所在地(地図)\n図書館利用案内\n所管法人\nお知らせ\n採用案内\n\n\nインターンシップ(外部リンク)\n民間企業との交流\n\n\n\n\n\n\n\n\n\nホーム\n基本政策\n食文化\nうちの郷土料理\nSEARCH&MENU\nいもようかん 東京都\n\n\n\n\nいもようかん 東京都\n\n\n\n\n\n\n\n\n\n\nTOP\nABOUT\nSEARCH&MENU\nAREA STORIES\n\n\n\n\n\n\n\n\n東京都\nいもようかん\n\n\n\n\n\n*画像はイメージです(掲載画像とレシピの内容は異なる場合があります)。\n関連画像\nZIPファイルをダウンロードできる画像は許可なく使用することができます。利用する場合は、「リンクについて・著作権」をご確認ください。ZIPファイル の解凍にはソフトウェア・アプリが別途必要になる場合があります。\n\n\n\n\n「リンクについて・著作権」に同意して画像をダウンロード(ZIP:606KB) \n\n\n\n\n\n\n\n\n\n\n\n「リンクについて・著作権」に同意して画像をダウンロード(ZIP:657KB) \n\n\n\n\n\n\n\n\n\n\n\n\n主な伝承地域\n台東区\n\n\n主な使用食材\nさつまいも\n\n\n\n\n歴史・由来・関連行事\n「いもようかん」は、主な使用食材のさつまいもを蒸して練り上げ成型させたもの。明治時代に、浅草にある芋問屋と菓子職人が一緒に作り出した和菓子である。当時、練りようかんは高価なもので、庶民は口にする機会が少なかった。芋問屋と菓子職人は、手近な食材のさつまいもを用いて練りようかんの代用となるような和菓子を考案し、さつまいもの研究から調理法、調味料の配合など、試行錯誤を繰り返して「いもようかん」が誕生したと伝わる。練りようかんよりも安価で入手できるいもようかんの登場は、庶民の身近な和菓子として歓迎された。\n\n\n食習の機会や時季\n主材料のさつまいもがあれば通年作ることができ、おやつとして食すことができる。和菓子屋でも購入できるが、家庭でも気軽に作ることができる甘味品のひとつ。\n\n\n飲食方法\n皮をむいたさつまいもを加熱してやわらかくし、温かいうちに大きなかたまりがなくなるまで良くつぶし、砂糖を加えてペースト状にする。さつまいものヘタに近い部分は繊維質が強く硬いため切り落としておくと裏ごししやすい。\n\n\n保存・継承の取組(伝承者の概要、保存会、SNSの活用、商品化等現代的な取組等について)\n全国の和菓子店で通年購入できる。インターネットやSNSで「いもようかん」のレシピも公開されており、気軽に家庭で作ることができる。\n\n\n\n\n\n\nレシピを印刷する\n材料(4人分)\n\n\n\nさつまいも\n250g\n\n\n\n\n砂糖\n60g\n\n\n\n\n粉寒天\n2g\n\n\n\n\n水\n120ml\n\n\n\n作り方\n\n\n1\nさつまいもは皮を剥き2cm程度の厚さの輪切りにして水(分量外)にさらし、アクを抜く。\n\n\n2\nたっぷりの水(分量外)でさつまいもを柔らかくなるまで茹でる。茹でたらざるに上げ、熱いうちに裏ごしする。\n\n\n3\n鍋に粉寒天と水を入れてよく煮溶かし、砂糖を加える。※粒子が残っていると固まらなくなるのでしっかり溶かす。\n\n\n4\n3に2を加えてよく混ぜ、容器に入れて冷やし固め、食べやすい大きさに切り分ける。\n\n\n\n\n\n\n※レシピは地域・家庭によって違いがあります。\n\n\n\n\n\n\n\n\n\n\n\n\n\nTOP\nABOUT\nSEARCH&MENU\nAREA STORIES\n\n\n\n\n\nお問合せ先大臣官房 新事業・食品産業部外食・食文化課食文化室\n代表:03-3502-8111(内線3085)ダイヤルイン:03-3502-5516\n\n\n\n\n\n\n\n\n\n\n\n公式SNS\n\n\n\n\n\n\n関連リンク集\n農林水産省トップページへ\n\n\n\n\n\n\n\n住所:〒100-8950 東京都千代田区霞が関1-2-1\n電話:03-3502-8111(代表)代表番号へのお電話について\n法人番号:5000012080001\n\n\nご意見·お問い合わせ\nアクセス·地図\n\n\nサイトマップ\nプライバシーポリシー\nリンクについて・著作権\n免責事項\n\n\n\nCopyright : Ministry of Agriculture, Forestry and Fisheries\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n')]
# 今回はWebページ全文に対して処理してみます。
paragraphs = [docs[0].page_content]
web_page_propositions = []
for i, para in enumerate(paragraphs[:5]):
propositions = get_propositions(para)
web_page_propositions.extend(propositions)
print (f"Done with {i}")
print (f"命題の数 :{len(web_page_propositions)}")
# 命題の数 :28
# 上から12件を表示
for p in web_page_propositions[:12]:
print(p)
生成された命題(12件)
いもようかんは東京都の郷土料理である。
いもようかんは主な使用食材のさつまいもを蒸して練り上げ成型させたものである。
いもようかんは明治時代に浅草にある芋問屋と菓子職人が一緒に作り出した和菓子である。
練りようかんは高価なものであった。
庶民は練りようかんを口にする機会が少なかった。
芋問屋と菓子職人は手近な食材のさつまいもを用いて和菓子を考案した。
さつまいもの研究から調理法、調味料の配合などを試行錯誤した。
いもようかんは庶民の身近な和菓子として歓迎された。
主材料のさつまいもがあれば通年作ることができる。
いもようかんはおやつとして食すことができる。
和菓子屋でもいもようかんを購入できる。
家庭でも気軽にいもようかんを作ることができる。
Webページ上の説明は以下の説明文となっている部分も
食習の機会や時季
主材料のさつまいもがあれば通年作ることができ、おやつとして食すことができる。和菓子屋でも購入できるが、家庭でも気軽に作ることができる甘味品のひとつ。
命題では以下のように「いもようかん」についてのトピックであることが補完されています。
主材料のさつまいもがあれば通年作ることができる。
いもようかんはおやつとして食すことができる。
和菓子屋でもいもようかんを購入できる。
家庭でも気軽にいもようかんを作ることができる。
(個人的な感想ですが)今回の命題のような長さが有効か?はユースケースにもよりそうですが、各チャンク内の代名詞が何を指すかを補完する処理は様々なユースケースで効果が確認されて広く利用されている印象です。
Hierarchical Indexing(RAPTOR)
RAPTORが紹介されています。考え方はMulti-representation Indexing内で紹介したParent Document等に近いものです。親子関係だけでなく、更に深い階層構造(木構造)を探索するために再帰的に抽象化する処理を行ってインデックスを作成するアプローチです。
RAG from ScratchにはRAPTORのコード例の記載が無く、以下のLangChain cookbookのリンクが紹介されていましたので、こちらのコードを読み解きました。
https://github.com/langchain-ai/langchain/blob/master/cookbook/RAPTOR.ipynb
元論文は以下です:
RECURSIVE ABSTRACTIVE PROCESSING FOR TREE-ORGANIZED RETRIEVAL
RAPTORでは、元の文書(※大きい場合はチャンク化する)「leaf」を埋め込みモデルでベクトル化し、そのベクトルに基づいて再帰的にクラスタ化します。それらのクラスタのテキスト要約を生成して、図の左から右へ「tree」を構築する。親ノードにはそのクラスタのテキスト要約が含まれる構造になっています。
前準備(ドキュメントの読み込み)
今回はwikipediaから「やきいも」「サツマイモ」に関する文章を取得してみます。
コード例
import matplotlib.pyplot as plt
import tiktoken
from bs4 import BeautifulSoup as Soup
from langchain_community.document_loaders.recursive_url_loader import RecursiveUrlLoader
def num_tokens_from_string(string: str, encoding_name: str) -> int:
"""Returns the number of tokens in a text string."""
encoding = tiktoken.get_encoding(encoding_name)
num_tokens = len(encoding.encode(string))
return num_tokens
urls = [
"https://ja.wikipedia.org/wiki/%E3%82%B5%E3%83%84%E3%83%9E%E3%82%A4%E3%83%A2", # さつまいもに関するwiki
"https://ja.wikipedia.org/wiki/%E7%84%BC%E3%81%8D%E8%8A%8B", # 焼き芋に関するwiki
]
docs = []
for url in urls:
loader = RecursiveUrlLoader(
url=url, max_depth=20, extractor=lambda x: Soup(x, "html.parser").text
)
docs.append(loader.load())
# Doc texts
docs_texts = [d[0].page_content for d in docs]
# Doc texts concat
d_sorted = sorted(docs, key=lambda x: x[0].metadata["source"])
d_reversed = list(reversed(d_sorted))
concatenated_content = "\n\n\n --- \n\n\n".join(
[doc[0].page_content for doc in d_reversed]
)
print(
"Num tokens in all context: %s"
% num_tokens_from_string(concatenated_content, "cl100k_base")
)
# Doc texts split
from langchain_text_splitters import RecursiveCharacterTextSplitter
chunk_size_tok = 2000
text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(
chunk_size=chunk_size_tok, chunk_overlap=0
)
texts_split = text_splitter.split_text(concatenated_content)
texts_split[:5]
# 以下のように、wikipediaから取得した文章をチャンク化した内容が確認できます
#['焼き芋 - Wikipedia\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nコンテンツにスキップ\n\n\n\n\n\n\n\nメインメニュー\n\n\n\n\n\nメインメニュー\nサイドバーに移動\n非表示\n\n\n\n\t\t案内\n\t\n\n\nメインページコミュニティ・ポータル最近の出来事新しいページ最近の更新おまかせ表示練習用ページアップロード (ウィキメディア・コモンズ)特別ページ\n\n\n\n\n\n\t\tヘルプ\n\t\n\n\nヘルプ井戸端お知らせバグの報告ウィキペディアに関するお問い合わせ\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n検索\n\n\n\n\n\n\n\n\n\n\n\n検索\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n表示\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n寄付\n\nアカウント作成\n\nログイン\n\n\n\n\n\n\n\n\n個人用ツール\n\n\n\n\n\n寄付 アカウント作成 ログイン\n\n\n\n\n\n\t\tログアウトした編集者のページ もっと詳しく\n\n\n\n投稿記録トーク\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n目次\nサイドバーに移動\n非表示\n\n\n\n\nページ先頭\n\n\n\n\n\n1\n各国の焼き芋\n\n\n\n\n\n\n\n\n2\n食味\n\n\n\n\n食味サブセクションを切り替えます\n\n\n\n\n\n2.1\n甘味\n\n\n\n\n\n\n\n\n2.2\n食感・品種\n\n\n\n\n\n\n\n\n2.3\n香り\n\n\n\n\n\n\n\n\n\n\n3\n製法\n\n\n\n\n製法サブセクションを切り替えます\n\n\n\n\n\n3.1\nかまど焼き\n\n\n\n\n\n\n\n\n3.2\n壺焼き\n\n\n\n\n\n\n\n\n3.3\n石焼き\n\n\n\n\n\n\n\n\n3.4\nドラム缶焼き\n\n\n\n\n\n\n\n\n3.5\n電気オーブン\n\n\n\n\n\n\n\n\n3.6\n冷凍焼き芋\n\n\n\n\n\n\n\n\n\n\n4\n日本における歴史\n\n\n\n\n日本における歴史サブセクションを切り替えます\n\n\n\n\n\n4.1\n近世\n\n\n\n\n\n\n\n\n4.2\n近代\n\n\n\n\n\n\n\n\n4.3\n現代\n\n\n\n\n\n\n\n\n\n\n5\n符号位置\n\n\n\n\n\n\n\n\n6\n脚注\n\n\n\n\n脚注サブセクションを切り替えます\n\n\n\n\n\n6.1\n出典\n\n\n\n\n\n\n\n\n\n\n7\n参考文献\n\n\n\n\n\n\n\n\n8\n外部リンク\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n目次の表示・非表示を切り替え\n\n\n\n\n\n\n\n焼き芋\n\n\n\n14の言語版\n\n\n\n\nالعربيةDanskDeutschEnglishEspañolEuskaraFrançaisJawa한국어PolskiไทยTiếng Việt中文粵語\n\nリンクを編集\n\n\n\n\n\n\n\n\n\n\n\nページノート\n\n\n\n\n\n日本語\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n閲覧編集履歴を表示\n\n\n\n\n\n\n\nツール\n\n\n\n\n\nツール\nサイドバーに移動\n非表示\n\n\n\n\t\t操作\n\t\n\n\n閲覧編集履歴を表示\n\n\n\n\n\n\t\t全般\n\t\n\n\nリンク元関連ページの更新状況ファイルをアップロードこの版への固定リンクページ情報このページを引用短縮URLを取得するQRコードをダウンロード\n\n\n\n\n\n\t\t印刷/書き出し\n\t\n\n\nブックの新規作成PDF 形式でダウンロード印刷用バージョン\n\n\n\n\n\n\t\t他のプロジェクト\n\t\n\n\nコモンズウィキデータ項目\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n表示\nサイドバーに移動\n非表示\n\n\n\n\n\n\n\n\n\n\n出典: フリー百科事典『ウィキペディア(Wikipedia)』\n\n\n焼き芋\n焼き芋(やきいも)は、加熱したサツマイモ。東アジア特有の食文化とされ[1]、石焼き芋や壺焼き、かまど焼きなどがある。',
# '各国の焼き芋[編集]\n日本では2015年に生産されたサツマイモ81万4,200トンのうち、およそ6万トンが焼き芋として消費されたとされる[2][3]。なお、焼き芋は冬の季語となっている[4]。\n中国では焼き芋は烤白薯と呼ばれ、北京553や蘇薯1号という焼き芋専用の品種もある[5]。上海市には250軒の焼き芋屋があり、北京市や重慶市、ハルビン市などでも冬季にはドラム缶や大壺をリヤカー等に載せて練炭や木炭で蒸し焼きにする焼き芋屋が営業している[6]。大韓民国では1954年に誕生した焼き芋屋が横向きのドラム缶を窯として街頭で営業しているほか、家庭用の直火鍋があり、コンビニエンスストアでも販売されている[7]。台湾のファミリーマートでは2010年に焼き芋が発売され、カウンター販売で年間800万本が売れている[8]。\n\n\n\n\n東京の焼き芋\n\n\n\n北京の焼き芋\n\n\n\n香港の焼き芋\n\n\n\n韓国の焼き芋\n\n\n\n台湾のファミリーマートの焼き芋\n\n\n食味[編集]\n生のサツマイモは硬くて甘味がなく、消化もしにくい[9]。これを充分に加熱することによって、適度な軟らかさと良好な食感、甘味、香りなどが得られる[9]。\n\n甘味[編集]\n焼き芋の甘味は、サツマイモ中のデンプンがβ-アミラーゼの作用により分解(糖化)された麦芽糖に主に由来する[10]。β-アミラーゼは70℃を超えると変性してしまうが、一方で生のデンプンには作用できず糊化して粒が崩れた状態のデンプンのみを分解する[10]。多くのサツマイモではデンプンの糊化が約70℃で起きるため、その付近の狭い温度領域のみでβ-アミラーゼが失活せずに麦芽糖を生成できる[10]。このため、70℃付近の温度で長時間加熱できる石焼き芋などでは甘味がよく引き出されるが、急速に昇温する電子レンジでは甘味が十分に生まれない[11]。また、60℃ぐらいでも糊化するクイックスタートなどの品種も開発されている[11]。\n加熱後の焼き芋には15.4%の麦芽糖が含まれ、蒸し芋の12.6%より含有量が高い[12]。また、焼き芋は加熱時に15~30%水分が減少するため、より強く甘さを感じる[13]。甘味の点からはデンプン含有量は高い方が良いが、多すぎるとパサパサの食感になってしまうため、20~25%程度のデンプン歩留まりのサツマイモが適当とされる[14]。デンプンの量は産地や施肥量にはそれほど影響されないが、気象条件によって大きく変わり、夏季に気温と日射量が高く降水量が少ないと肥大してデンプンの多い良好なサツマイモが得られる[15]。サツマヒカリなど一部の品種はβ-アミラーゼを持たないため、焼き芋には向かない[14]。β-アミラーゼ自体の質については品種間で大きな差は無いとされる[14]。\n\n食感・品種[編集]\n安納芋\n生のサツマイモは細胞や組織の構造がしっかりして硬いため、加熱によって破壊する必要がある[9]。このためには100℃程度の高温が必要となり、70℃前後で甘味を引き出した後に100℃まで昇温することが望ましいとされる[10]。伝統的に東日本ではホクホク、西日本ではしっとりとした食感が重視されていたが、2000年代以降は全国的にねっとりとした食感が好まれる傾向がある[14]。肉質や食感と日本の品種の関係は、次のように分類される[16]。',
# '粉質・ホクホク系:ベニアズマ、種子島ゴールドなど\n中間質・しっとり系:高系14号および、そこから派生した鳴門金時や五郎島金時\n粘質・ねっとり系:安納紅など安納芋、べにはるか、べにまさりなど\n食感はホクホク系としっとり・ねっとり系の2つにさらに大別できる[16]。なお、同じ品種であっても貯蔵条件によって糖化の進展が異なり、熟成による糖化が進むほどねっとり系に近づく[16]。\n\n香り[編集]\nサツマイモ自体の香りは、柑橘類やバラのようなテルペン由来の香気が相互に影響しあって形成している[17]。また、焼く際の加熱によって麦芽糖やアミノ酸からメイラード反応によって生成した成分が甘く焦げた香りを生む[17]。サツマイモには100グラムあたり228ミリグラムほどのポリフェノールが含まれており、主にクロロゲン酸で構成され、特に表皮部などに集中している[18]。これはコーヒーと同程度であり、焼き芋の焦げた香りはコーヒーと共通している面がある[18]。\n\n製法[編集]\nかまど焼き[編集]\nかまどの上に鋳物の浅い平鍋を載せ、サツマイモを入れて木製の蓋をして蒸し焼きにする[19]。江戸時代には焙烙も使われたが、割れやすく大型化も困難なため利用されなくなった[19]。鉄鍋は大きいものでは直径1メートルにも達する[20]。\n\n壺焼き[編集]\n『農業事物起源集成』によると、中国東北部が壺焼き芋の発祥地とされる[21]。先端を曲げた針金にサツマイモを引っかけて壺の内周に沿って隙間なく吊るし、壺の底部にコークスか木炭を入れて燃やし、鉄製の蓋をして蒸し焼きにする[21]。成都市などでは、イモは吊るさず壺の中に設置した金網に載せて焼く[22]。一般的な陶磁器の壺を使うことが多いが、鉄筋とセメントで作って漆喰による仕上げを施した左官業者による特注の大型壺などもある[20]。\n\n石焼き[編集]\n→詳細は「石焼き芋」を参照\n鉄製の窯に敷いた石の上にサツマイモを載せて焼くことで、石から出る遠赤外線の効果を利用する[23]。リヤカーや軽トラックを用いた移動販売の形態が多い[23]。\n\nドラム缶焼き[編集]\n中国や韓国では、ドラム缶を窯として街頭で焼き芋を製造・販売している[6][7]。前者は、立てたドラム缶の底にレンガを積んで吸気口と火床を作り練炭などを収めて燃やす[6]。火床の周囲に粘土で作った台にサツマイモを積み、厚い鉄板で蓋をして蒸し焼きにする[6]。\n\n電気オーブン[編集]\nスーパーマーケットの店頭などに設置され、遠赤外線を利用して加熱する[24]。オーブン内の位置やサツマイモの大きさなどによって最適条件が異なるため、レシピのマニュアル化がされている[24]。家庭においてもしばしばオーブントースターやオーブンレンジを利用して作られる。',
#'冷凍焼き芋[編集]\n一般的に200キログラム程度のサツマイモをまとめて焼ける大型のオーブンが製造に用いられ、細長い窯の中を金網に入れた芋がコンベア式に輸送されるトンネル式と、窯の中に多段の棚が並ぶラック式の2種類に大別される[25]。熱源は様々であり、ガスの場合は窯の内部に岩石を敷く、電気の場合はセラミックヒーターを用いる、炭火を熱源にするなど、いずれの方式でも遠赤外線を発生させて熱がイモの中心まで伝わるように工夫されている[26]。急激にイモが加熱されないようにトンネルの入口付近の温度を低くしたり、ラック式の場合は最初の昇温速度を抑えており、通常は窯の内部を200℃程度まで上げて1時間ほど焼く[26]。最後にさらに昇温して焦げ目をつけたり、水分を飛ばして甘味を高める場合もある[26]。\n一般的に冷凍食品は急速に冷凍する方が食品組織へのダメージが少ないが、焼き芋の場合は冷凍速度の影響はそれほどシビアではない[27]。冷風を当てて降温させてからポリエチレンなどの容器に詰めて冷凍を行う[26]。中心まで凍結した状態で-20℃以下に保つと、包装が水蒸気や酸素、光などを遮断できれば数年間は品質を維持できる[26]。\n日本では南九州を中心に冷凍焼き芋が生産されており、電子レンジなどで適切に再加熱すると焼き立てと遜色ない味が得られると言われる[25][26]。また、糖化が進んで甘く加熱で十分に柔らかくなっているため、冷たいままデザートとして食べるケースもある[26]。\n\n\n\n\n鋳物製平鍋によるかまど焼き\n\n\n\nリヤカーに載せた石焼き\n\n\n\nドラム缶焼き\n\n\n\nスーパーマーケットの電気オーブン',
# '日本における歴史[編集]\n近世[編集]\n歌川国貞『半四郎 やきいものお七 岩井半四郎』 (1819年作)\nサツマイモの栽培は17世紀前半までには琉球に、宝永2(1705年)頃には薩摩国にも広まり、本州でも享保4年(1719年)旧暦9月12日に京都郊外で酒や餅とともに焼き芋が売られていた、と朝鮮通信使が『海游録』に記している[28][29]。享保20年(1735年)の小石川植物園での種芋栽培の成功をきっかけに関東地方でも大々的に栽培されるようになった[28]。寛政5年(1793年)に本郷四丁目の木戸番が初の焼き芋を木戸番屋で売り出すと、冬のおやつとして急速に人気を集め、それまでの蒸し芋に取って代わるようになった[30]。特に各町の木戸横に設けた木戸番屋などで、かまどの上に載せた焙烙に並べて焼いた焼き芋が売られた[31]。\n焼き芋は甘味や香りに加えて「10文も出せば、食べ盛りの書生でも朝食になる」と言われるほど安価なことが大きな魅力であり、低コストな舟運で輸送できる下総国の馬加村(現・幕張)および武蔵野台地の川越藩領が原料のサツマイモの2大供給地となり、江戸に運ばれる荷物の梱包材として使用された俵や縄が調理の燃料として利用されていた[30][19]。焼き芋の人気とともに需要が増加すると、素焼きで割れやすいため大型化の難しい焙烙に代わり、鋳物製の浅い平鍋で焼かれるようになった[19]。丸ごと1本の芋を焼いた丸焼きは「〇焼き」と看板に書かれ、また味が栗(九里)に近いとして「八里半」、後に「栗より(四里)うまい」として「十三里」と書く看板が増えたと『宝暦現来集』に記録されている[19]。天保3年(1832年)の『江戸繁盛記』には「木戸番屋では早朝から深夜まで焼き芋が売られ、裕福な人も貧しい人も好んで食べるため、一冬で番屋一軒の売上は20~100両にも達する」と書かれている[19]。']
木構造の構築
treeの構築におけるクラスタリングアプローチには、以下のアイデアが含まれています。
-
GMM(ガウス混合モデル)
異なるクラスタ間のデータポイントの分布をモデル化。モデルのベイズ情報量基準(BIC)を評価することで最適なクラスタ数を決定します。 -
UMAP(均一多様体近似と射影)
クラスタリングをサポート。高次元データの次元を削減するため、データポイントの類似性に基づいて自然なグループ化を強調するのに役立ちます。 -
ローカルおよびグローバルクラスタリング
異なるスケールでデータを分析するために使用されます。データ内の詳細なパターンと広範なパターンの両方を効果的に捉える役目です。 -
しきい値処理
GMMの文脈でクラスタメンバーシップを決定するために適用されます。確率分布に基づいてデータポイントが1つ以上のクラスタに割り当てられます。
木構造の構築に関するコードは以下のようです。
木構造の構築コード(cookbookのコメントを日本語化)
from typing import Dict, List, Optional, Tuple
import numpy as np
import pandas as pd
from umap.umap_ import UMAP
from langchain.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from sklearn.mixture import GaussianMixture
RANDOM_SEED = 224 # 再現性のための固定シード
### --- 上記の参考文献からのコード(コメントとドキュメンテーション追加) --- ###
def global_cluster_embeddings(
embeddings: np.ndarray,
dim: int,
n_neighbors: Optional[int] = None,
metric: str = "cosine",
) -> np.ndarray:
"""
UMAPを使用して埋め込みベクトルのグローバルな次元削減を実行します。
パラメータ:
- embeddings: 入力の埋め込みベクトル(numpy配列)
- dim: 削減後の空間のターゲット次元数
- n_neighbors: オプション; 各点について考慮する近傍の数。
提供されていない場合、デフォルトは埋め込みベクトルの数の平方根になります。
- metric: UMAPで使用する距離のメトリック
戻り値:
- 指定された次元数に削減された埋め込みベクトルのnumpy配列
"""
if n_neighbors is None:
n_neighbors = int((len(embeddings) - 1) ** 0.5)
return UMAP(n_neighbors=n_neighbors, n_components=dim, metric=metric).fit_transform(embeddings)
def local_cluster_embeddings(
embeddings: np.ndarray, dim: int, num_neighbors: int = 10, metric: str = "cosine"
) -> np.ndarray:
"""
通常、グローバルクラスタリング後にUMAPを使用して埋め込みベクトルのローカルな次元削減を実行します。
パラメータ:
- embeddings: 入力の埋め込みベクトル(numpy配列)
- dim: 削減後の空間のターゲット次元数
- num_neighbors: 各点について考慮する近傍の数
- metric: UMAPで使用する距離のメトリック
戻り値:
- 指定された次元数に削減された埋め込みベクトルのnumpy配列
"""
return UMAP(n_neighbors=num_neighbors, n_components=dim, metric=metric).fit_transform(embeddings)
def get_optimal_clusters(embeddings: np.ndarray, max_clusters: int = 50, random_state: int = RANDOM_SEED) -> int:
"""
ガウス混合モデル(GMM)を使用してベイジアン情報基準(BIC)に基づいて最適なクラスタ数を決定します。
パラメータ:
- embeddings: 入力の埋め込みベクトル(numpy配列)
- max_clusters: 検討する最大のクラスタ数
- random_state: 再現性のためのシード
戻り値:
- 見つかった最適なクラスタ数(整数)
"""
max_clusters = min(max_clusters, len(embeddings))
n_clusters = np.arange(1, max_clusters)
bics = []
for n in n_clusters:
gm = GaussianMixture(n_components=n, random_state=random_state)
gm.fit(embeddings)
bics.append(gm.bic(embeddings))
return n_clusters[np.argmin(bics)]
def GMM_cluster(embeddings: np.ndarray, threshold: float, random_state: int = 0):
"""
確率閾値に基づいてガウス混合モデル(GMM)を使用して埋め込みベクトルをクラスタリングします。
パラメータ:
- embeddings: 入力の埋め込みベクトル(numpy配列)
- threshold: 埋め込みベクトルをクラスタに割り当てるための確率閾値
- random_state: 再現性のためのシード
戻り値:
- クラスタラベルと決定されたクラスタ数を含むタプル
"""
n_clusters = get_optimal_clusters(embeddings)
gm = GaussianMixture(n_components=n_clusters, random_state=random_state)
gm.fit(embeddings)
probs = gm.predict_proba(embeddings)
labels = [np.where(prob > threshold)[0] for prob in probs]
return labels, n_clusters
def perform_clustering(
embeddings: np.ndarray,
dim: int,
threshold: float,
) -> List[np.ndarray]:
"""
埋め込みベクトルにクラスタリングを実行します。最初にグローバルな次元削減を行い、次にガウス混合モデルでクラスタリングし、
最後に各グローバルクラスタ内でローカルクラスタリングを行います。
パラメータ:
- embeddings: 入力の埋め込みベクトル(numpy配列)
- dim: UMAP次元削減のターゲット次元数
- threshold: GMMで埋め込みベクトルをクラスタに割り当てるための確率閾値
戻り値:
- 各クラスタIDを含むnumpy配列のリスト
"""
if len(embeddings) <= dim + 1:
# データが不足している場合はクラスタリングを回避
return [np.array([0]) for _ in range(len(embeddings))]
# グローバル次元削減
reduced_embeddings_global = global_cluster_embeddings(embeddings, dim)
# グローバルクラスタリング
global_clusters, n_global_clusters = GMM_cluster(reduced_embeddings_global, threshold)
all_local_clusters = [np.array([]) for _ in range(len(embeddings))]
total_clusters = 0
# 各グローバルクラスタをループしてローカルクラスタリングを実行
for i in range(n_global_clusters):
# 現在のグローバルクラスタに属する埋め込みベクトルを抽出
global_cluster_embeddings_ = embeddings[np.array([i in gc for gc in global_clusters])]
if len(global_cluster_embeddings_) == 0:
continue
if len(global_cluster_embeddings_) <= dim + 1:
# 小さなクラスタを直接割り当て
local_clusters = [np.array([0]) for _ in global_cluster_embeddings_]
n_local_clusters = 1
else:
# ローカル次元削減とクラスタリング
reduced_embeddings_local = local_cluster_embeddings(global_cluster_embeddings_, dim)
local_clusters, n_local_clusters = GMM_cluster(reduced_embeddings_local, threshold)
# ローカルクラスタIDを割り当て、すでに処理された総クラスタ数を調整
for j in range(n_local_clusters):
local_cluster_embeddings_ = global_cluster_embeddings_[np.array([j in lc for lc in local_clusters])]
indices = np.where((embeddings == local_cluster_embeddings_[:, None]).all(-1))[1]
for idx in indices:
all_local_clusters[idx] = np.append(all_local_clusters[idx], j + total_clusters)
total_clusters += n_local_clusters
return all_local_clusters
### --- 以下は私たちのコード --- ###
def embed(texts):
"""
テキストドキュメントのリストに対して埋め込みを生成します。
この関数は、テキストリストを受け取り、その埋め込みを返す`embedding_model`オブジェクトの`embed_documents`メソッドがあることを前提としています。
パラメータ:
- texts: List[str], 埋め込みを生成するテキストドキュメントのリスト
戻り値:
- numpy.ndarray: 提供されたテキストドキュメントの埋め込みの配列
"""
text_embeddings = embedding_model.embed_documents(texts)
text_embeddings_np = np.array(text_embeddings)
return text_embeddings_np
def embed_cluster_texts(texts):
"""
テキストのリストを埋め込み、クラスタリングを行い、テキスト、埋め込み、クラスタラベルを含むDataFrameを返します。
埋め込みの生成とクラスタリングを1つのステップに統合する関数です。以前に定義された`perform_clustering`関数を使用して、埋め込みに対してクラスタリングを実行します。
パラメータ:
- texts: List[str], 処理するテキストドキュメントのリスト
戻り値:
- pandas.DataFrame: 元のテキスト、埋め込み、および割り当てられたクラスタラベルを含むDataFrame
"""
text_embeddings_np = embed(texts) # 埋め込みを生成
cluster_labels = perform_clustering(text_embeddings_np, 10, 0.1) # 埋め込みに対してクラスタリングを実行
df = pd.DataFrame() # 結果を格納するためのDataFrameを初期化
df["text"] = texts # 元のテキストを格納
df["embedding_model"] = list(text_embeddings_np) # DataFrameに埋め込みをリストとして格納
df["cluster"] = cluster_labels # クラスタラベルを格納
return df
def fmt_txt(df: pd.DataFrame) -> str:
"""
DataFrame内のテキストドキュメントを1つの文字列としてフォーマットします。
パラメータ:
- df: テキストドキュメントを含む'text'列を持つDataFrame
戻り値:
- すべてのテキストドキュメントが特定の区切り文字で結合された単一の文字列
"""
unique_txt = df["text"].tolist()
return "--- --- \n --- --- ".join(unique_txt)
def embed_cluster_summarize_texts(texts: List[str], level: int) -> Tuple[pd.DataFrame, pd.DataFrame]:
"""
テキストを埋め込み、クラスタリングし、要約します。この関数は、テキストの埋め込みを最初に生成し、
類似性に基づいてクラスタリングを行い、クラスタ内のコンテンツを要約します。
パラメータ:
- texts: 処理するテキストドキュメントのリスト
- level: 処理の深さや詳細度を定義する整数パラメータ
戻り値:
- 2つのDataFrameを含むタプル:
1. 最初のDataFrame(`df_clusters`)には元のテキスト、埋め込み、およびクラスタの割り当てが含まれます。
2. 2番目のDataFrame(`df_summary`)には各クラスタの要約、指定された詳細度、およびクラスタ識別子が含まれます。
"""
# テキストを埋め込み、クラスタリングし、'text'、'embedding_model'、'cluster'の列を持つDataFrameを生成
df_clusters = embed_cluster_texts(texts)
print(df_clusters)
# クラスタを操作しやすくするためにDataFrameを拡張
expanded_list = []
# DataFrameの各エントリを拡張してクラスタごとのテキストを処理しやすくする
for index, row in df_clusters.iterrows():
for cluster in row["cluster"]:
expanded_list.append({"text": row["text"], "embedding_model": row["embedding_model"], "cluster": cluster})
# 拡張されたリストから新しいDataFrameを作成
expanded_df = pd.DataFrame(expanded_list)
print(expanded_df)
# 処理するための一意のクラスタ識別子を取得
all_clusters = expanded_df["cluster"].unique()
print(f"--{len(all_clusters)} クラスタが生成されました--")
# 要約処理(※ LangChainのcookbookのpromptから変更しています)
prompt = ChatPromptTemplate(
[
(
"system",
"あなたは与えられた文脈を要約するアシスタントです",
),
( "user", "以下の文脈を、できるだけ多くの重要な詳細を含めて要約してください: {context}"),
]
)
chain = prompt | llm | StrOutputParser()
# 各クラスタ内のテキストを要約用にフォーマット
summaries = []
for i in all_clusters:
df_cluster = expanded_df[expanded_df["cluster"] == i]
formatted_txt = fmt_txt(df_cluster)
summaries.append(chain.invoke({"context": formatted_txt}))
# 要約とそれに対応するクラスタおよびレベルを格納するDataFrameを作成
df_summary = pd.DataFrame(
{
"summaries": summaries,
"level": [level] * len(summaries),
"cluster": list(all_clusters),
}
)
return df_clusters, df_summary
def recursive_embed_cluster_summarize(
texts: List[str], level: int = 1, n_levels: int = 3
) -> Dict[int, Tuple[pd.DataFrame, pd.DataFrame]]:
"""
テキストを再帰的に埋め込み、クラスタリングし、要約を行います。
指定されたレベルまで、または一意のクラスタ数が1になるまで再帰します。
各レベルの結果を格納します。
パラメータ:
- texts: List[str], 処理するテキスト
- level: int, 現在の再帰レベル(初めは1)
- n_levels: int, 最大の再帰レベル
戻り値:
- Dict[int, Tuple[pd.DataFrame, pd.DataFrame]], 各レベルのクラスタDataFrameと要約DataFrameを含む辞書
"""
results = {} # 各レベルの結果を格納する辞書
# 現在のレベルで埋め込み、クラスタリング、要約を実行
df_clusters, df_summary = embed_cluster_summarize_texts(texts, level)
# 現在のレベルの結果を格納
results[level] = (df_clusters, df_summary)
# 更なる再帰が可能かどうかを判定
unique_clusters = df_summary["cluster"].nunique()
if level < n_levels and unique_clusters > 1:
# 要約を次のレベルの入力テキストとして使用
new_texts = df_summary["summaries"].tolist()
next_level_results = recursive_embed_cluster_summarize(new_texts, level + 1, n_levels)
# 次のレベルの結果を現在の結果辞書に統合
results.update(next_level_results)
return results
# Build tree
leaf_texts = texts_split
results = recursive_embed_cluster_summarize(leaf_texts, level=1, n_levels=3)
作成した木構造の中身を見てみる
results
のkeyがレベルになっています。今回は'1', '2'の2階層で作成されました。
-
results[レベル][0]
: text, embedding, cluster番号が確認できます。 -
results[レベル][1]
: 各levelでのcluster毎の要約内容が確認できます。
results[1][0].sort_values(by='cluster')[["text", "cluster"]].head(10)# "embedding"は表示を省略
text cluster
21 日本列島における栽培と普及史[編集]\nこの節には複数の問題があります。改善やノートページで... [0.0]
22 後世に薩摩藩で編纂された農書・本草学書『成形図説』に拠れば、慶長から元和年間(1596から1... [0.0]
23 下見吉十郎の像\n薩摩藩以外で最初に栽培に成功したのは芸予諸島であるとされる。1711年(正... [0.0]
12 歴史[編集]\n原産地は中央アメリカのメキシコ中央部からグアテマラにかけてとする説が有力であ... [0.0]
24 幕府の奨励[編集]\n江戸幕府はこの頃、救荒作物としてリュウキュウイモ(サツマイモ)の有用性... [0.0]
25 サツマイモの普及イコール甘藷先生(青木昆陽)の手柄、とするには異説もあるが、昆陽が同時代に既... [0.0]
15 シルクスイート - 農林水産省の登録品種で、登録名 HE306。外皮は赤褐色で中身は淡黄色。... [1.0]
14 紅あずま(べにあずま) - 東日本でポピュラーな品種。芋の外皮が濃い紅紫色で中身が濃い黄色。... [1.0]
20 日本における主産地\n鹿児島県、茨城県、千葉県、宮崎県、徳島県が全国のトップ5県[55][5... [1.0]
19 病虫害[編集]\n病虫害はあまり発生しない方であるが[43]、発生する場合は以下のようなもの... [1.0]
results[1][1].sort_values(by='cluster')
summaries level cluster
5 サツマイモの歴史は、中央アメリカのメキシコ中央部からグアテマラにかけての地域で紀元前3000... 1 0.0
6 サツマイモは日本で広く栽培されている根菜で、地域ごとに異なる品種が存在します。主な品種には以... 1 1.0
1 日本におけるサツマイモの歴史は、近世から現代にかけての栽培と消費の変遷を示しています。17世... 1 2.0
3 焼き芋は、サツマイモを焼いて作る料理で、日本を含む多くの国で人気があります。サツマイモは、ヒ... 1 3.0
4 サツマイモ(学名: Ipomoea batatas)は、ヒルガオ科の多年生植物で、食用部分は... 1 4.0
2 この文脈は、サツマイモに関連するさまざまな情報を提供しています。以下に要約します。\n\n1... 1 5.0
0 焼き芋は、加熱したサツマイモで、主に東アジアの食文化に根付いています。日本では2015年に約... 1 6.0
各クラスタ(合計7クラス)の要約が確認できます。
例えばクラスタ0はサツマイモの歴史に関する内容で、クラスタ1はサツマイモの品種に関する内容のようです。
同様にレベル2の中身も確認してみます。
# print(results[2][0][["text", "cluster"]])の実行結果。 # "embedding"は表示を省略
text cluster
0 焼き芋は、加熱したサツマイモで、主に東アジアの食文化に根付いています。日本では2015年に約... [0]
1 日本におけるサツマイモの歴史は、近世から現代にかけての栽培と消費の変遷を示しています。17世... [0]
2 この文脈は、サツマイモに関連するさまざまな情報を提供しています。以下に要約します。\n\n1... [0]
3 焼き芋は、サツマイモを焼いて作る料理で、日本を含む多くの国で人気があります。サツマイモは、ヒ... [0]
4 サツマイモ(学名: Ipomoea batatas)は、ヒルガオ科の多年生植物で、食用部分は... [0]
5 サツマイモの歴史は、中央アメリカのメキシコ中央部からグアテマラにかけての地域で紀元前3000... [0]
6 サツマイモは日本で広く栽培されている根菜で、地域ごとに異なる品種が存在します。主な品種には以... [0]
# print(results[2][1])の実行結果
summaries level cluster
0 焼き芋は、加熱したサツマイモを用いた料理で、主に東アジアの食文化に根付いています。日本では2... 2 0
レベル2では、元の文書text
はレベル1の要約文summaries
となり、クラスタは1つ([0]のクラスタ)にまとまっているので入力の2文書全ての要約内容となっていることがわかります。
この木構造化した文書をベクトルDB化するコードは以下のようです。
コード例
from langchain_community.vectorstores import Chroma
# leaf_textsを使ってall_textsを初期化
all_texts = leaf_texts.copy()
# 結果を反復処理して、各レベルから要約を抽出し、それをall_textsに追加
for level in sorted(results.keys()):
# 現在のレベルのDataFrameから要約を抽出
summaries = results[level][1]["summaries"].tolist()
# 現在のレベルからの要約をall_textsに追加
all_texts.extend(summaries)
# now、all_textsを使ってChromaでベクトルストアを構築
vectorstore = Chroma.from_texts(texts=all_texts, embedding=embedding_model)
retriever = vectorstore.as_retriever()
ベクトルDBへクエリしてみます。
retriever.invoke("紅はるかにはどのような特徴がありますか?")
[Document(metadata={}, page_content='特徴[編集]\n各地で栽培されるつる性の多年草[8]。高温や乾燥に強く、痩せ地でも良く育つ丈夫な野菜で、芋(塊根)などを食用にする[17]。葉は、ヨウサイやアサガオに外見が似ている[10]。花はピンク色でアサガオに似るが、高温短日性であるため、日本の本州など温帯地域では開花しにくく、日長要因だけではなく何らかのストレスによってまれに開花する程度である[10]。また、花の数が少なく受粉しにくい上に、受粉後の寒さで枯れてしまうことが多いため、品種改良では種子を効率よく採るためにアサガオなど数種類の近縁植物に接木して、台木から送られる養分や植物ホルモン等の働きによって開花を促進する技術が使われる。デンプンを多く含む芋は、根が肥大したもの(塊根)で、茎が肥大した塊茎を持つジャガイモと相違がみられる[10]。\n1955年(昭和30年)に西山市三がメキシコで祖先に当たる二倍体の野生種を見つけ、イポメア・トリフィーダ(Ipomoea trifida)と名付けた。後に他の学者達によって中南米が原産地とされた。若い葉と茎を利用する専用の品種もあり、主食や野菜として食用にされる[要出典]。\n芋の皮の色は紅色や赤紫色の他、黄色や白色がある[3]。芋の中身は主に白色から黄色で、中には橙色や紫色になる品種もある[3]。特に全体が紫で、芋の中身がアントシアニンに由来して紫色のサツマイモを、紫芋(むらさきいも)と呼んでいる[18]。'),
Document(metadata={}, page_content='紅あずま(べにあずま) - 東日本でポピュラーな品種。芋の外皮が濃い紅紫色で中身が濃い黄色。繊維が比較的少なく甘味が強い。粉質でホクホクした食感が特徴[3]。焼き芋や菓子の材料の他、家庭料理全般に向く[7]。流通量が多く、農林水産省の統計によれば、紅あずまの全国作付け面積は2012年産で7358ヘクタール、2019年産で4472ヘクタールである[22]。\n高系14号(こうけい14ごう) - 西日本でポピュラーな品種。芋の外皮は赤褐色、中身が淡黄色。糖度は8%ほどで甘味が強く、ややねっとりしている。焼き芋には最適で、各生産地で「鳴門金時」「土佐紅」「千葉紅」などの独自ブランド名をつけて出荷流通する[7]。\n鳴門金時(なるときんとき) - 西日本でポピュラーな徳島県鳴門の砂地で栽培される品種。甘味が強くホクホクした食感が特徴。天ぷら・大学芋・菓子材料に向く[23]。流通量が少ないことで知られる[18]。\n紅はるか(べにはるか) - 鳴門金時と同じ高系14号系の品種。「九州121号」と「春こがね」を交配させて誕生した。[24]名前の由来は、食味や外観が既存品種よりも「はるか」に優れていることから。甘味が強く、水分が多めで、蒸し芋や干し芋にすると美味しい[23]。農林水産省の統計によれば、紅はるかの全国作付け面積は2012年産で2037ヘクタール、2019年産で5301ヘクタールである。また高値のつく形やサイズのよいものが多く取れ、害虫にも強い[22]。\n坂出金時(さかいできんとき) - 高系14号系の香川県の品種。粉質のホクホクした食感で、ほどよい甘さがあり、料理や菓子に向く[23]。\n五郎島金時(ごろうじまきんとき) - 高系14号系の石川県金沢市・五郎島地区の砂丘地で栽培される品種[20]。江戸時代の元禄期に、鹿児島から加賀に種芋を持ち帰って栽培されたといわれる伝統品種。中央がふっくらした紡錘形で身の色が白く、上品な甘さと粉質でホクホクした食感がある[18][23]。\n紅赤(べにあか) - かつて関東地方の代表的な品種で、皮が鮮やかな赤紫色で細長いのが特徴。細すぎるのは繊維が多い。加熱すると中が濃い黄色になって甘味が強く、焼き芋や栗金団用に人気がある[25]。\n紅さつま(べにさつま) - 鹿児島県でもっとも多く栽培されている青果・加工用の芋。皮は濃赤色で中は黄白色。例年5月下旬から、日本一早い「新芋」として出荷される。ホクホクした食感で甘味があり、焼き芋やふかし芋、天ぷらなどに向く[25]。\n大隅甘いも(おおすみあまいも) - 小ぶりで中が濃い色の鹿児島県の品種。加熱するとねっとりした食感で、甘味が強い[23]。\nアヤコマチ - 中が橙色に近い濃い色の芋で、カロテンを多く含む。焼き芋・蒸し芋・サラダに向く[23]。\nいずみいも - 外皮が白っぽい色で、中が濃い黄色の芋。甘味が強く、こくがあってねっとりした食感をもち、茨城県産の干し芋として人気がある[23]。'),
Document(metadata={}, page_content='シルクスイート - 農林水産省の登録品種で、登録名 HE306。外皮は赤褐色で中身は淡黄色。絹のような滑らかな食感と強い甘さを持つ[17]。\n隼人芋(はやといも) - 鹿児島県の在来種で、別名「にんじん芋」「かぼちゃ芋」。外皮が薄い茶橙色で、加熱すると中身がニンジンのようなオレンジ色になる。カロテン含有量が多く、甘味が強くやわらかい。蒸し芋や焼き芋にするほか、焼酎の原料にも使われる[23][25]。\n紅はやと(べにはやと) - 皮は赤紫色で、中はカロテン含有量が多くオレンジ色をしている。柔らかく繊維が少ないことから、大学芋や菓子、シャーベットなどに利用される[25]。\n種子島紫(たねがしまむらさき) - 種子島の在来種で、沖縄県・鹿児島県に多い紫芋の一種。外皮は白く、中身は鮮やかな紫色が特徴。ホクホクした食感で甘味が強く、デンプン質も多く含まれている。焼き芋、蒸し芋のほか、菓子加工用にも向き、紫芋独特の上品な甘さの焼酎にも加工される[23][26]。\n種子島ゴールド(たねがしまゴールド) - 1999年に「種子島紫」から品種選抜して育成された品種で、皮が白色で中が斑入りの鮮やかな紫色。紫芋の中では甘味があり、焼き芋、ふかし芋、天ぷらに向き、また和洋菓子の材料としても利用される[26]。\n種子島ロマン(たねがしまロマン) - 1999年に「種子島紫」から品種選抜して育成された品種で、皮は赤紫色で中は淡紫色。外観もよく、ふかし芋、天ぷらに向く[26]。\n安納いも(あんのういも) - 鹿児島県種子島産・安納地区の在来種[18]。甘味が強く、焼くと水分の多いねっとりとした食感で、「蜜イモ」とも呼ばれている。干し芋や焼き芋のほか、デザートの原料にも使われる[23]。\n安納紅(あんのうべに) - 2000年に「安納いも」から品種選抜されたもので、在来種よりも優れている。皮は赤褐色で中が淡黄色。粘度が高く、甘味が強い。蒸し芋や焼き芋にすると美味しい[25]。\n黄金千貫(こがねせんがん) - 外皮も中身も黄白色で、もともとデンプン原料として栽培され、主に芋焼酎の原料として使われる品種。ホクホクした食感とあっさりした甘味があり、天ぷら、焼き芋、ふかし芋揚にも向く[23][25]。\n栗こがね(くりこがね) - 皮は淡黄褐色で中が黄白色。九州で人気があり、ホクホクした食感で甘味が強い。天ぷらや焼き芋をはじめ、様々な食べ方に使われる[25]。\nこがねむらさき - 種子島で古くから栽培された紫芋から味の良いものを選抜した品種。生産数が少なく「幻のサツマイモ」として珍重される。皮は灰白色で中が薄紫色。熱を通すと濃い紫色になる。肉質は緻密で、和菓子のような上品な甘さがある。天ぷら、ふかし芋、焼き芋に適している[26]。\n山川紫(やまかわむらさき) - 海外から導入されて鹿児島県山川地方で栽培される品種。皮は赤色で中は濃い紫色。糖分が少なく青果には不向きである代わりに、色の濃い紫を活かして、アイスクリームや芋飴などの着色料として利用する[26]。'),
Document(metadata={}, page_content='サツマイモは日本で広く栽培されている根菜で、地域ごとに異なる品種が存在します。主な品種には以下があります:\n\n1. **紅あずま** - 東日本で人気。外皮は紅紫色、中身は濃い黄色で、甘味が強くホクホクした食感。焼き芋や菓子に適し、流通量が多い。\n2. **高系14号** - 西日本で人気。赤褐色の外皮と淡黄色の中身を持ち、糖度が高く、焼き芋に最適。地域ブランド名が多い。\n3. **鳴門金時** - 徳島県の特産品で、甘味が強くホクホクした食感。流通量は少ない。\n4. **紅はるか** - 高系14号系で、甘味が強く水分が多い。蒸し芋や干し芋に適し、流通量が増加中。\n5. **坂出金時** - 香川県の品種で、ホクホクした食感とほどよい甘さ。\n6. **五郎島金時** - 石川県の伝統品種で、上品な甘さとホクホク感が特徴。\n7. **紅さつま** - 鹿児島県で多く栽培され、甘味があり焼き芋や天ぷらに向く。\n8. **安納いも** - 甘味が強く、焼くとねっとりした食感。デザートにも利用される。\n\nサツマイモは栄養価が高く、特にビタミンCや食物繊維が豊富で、便秘解消や健康維持に寄与します。調理法は多様で、焼き、蒸し、煮物、天ぷら、スイートポテトなどに利用されます。保存は冷暗所が適し、低温に弱いです。\n\n日本の主な生産地は鹿児島県、茨城県、千葉県、宮崎県、徳島県で、特に鹿児島県が全国の約30%を占めています。サツマイモは焼酎やデンプンの原料としても利用され、飼料や燃料としての可能性も探求されています。')]
今回の例では、1つ目はクラスタ4のチャンクから選択されていましたが、2つ目と3つ目はクラスタ1のチャンク、4つ目はクラスタ1の要約から選択されていました。チャンクとクラスタの要約が混ざった状態でLLMへ文脈を渡せることがこのアプローチのポイント、と読み解きました。
(個人的な感想ですが)せっかく構築した木構造のメリットを活かしきれていない気もするので、1.Tree Traversal RetriverやParent Documentのような階層構造に向いたユースケースやそれに近い検索方法についてもそのうち調べてみたいと思います。
Specialized Embeddings
金融業界や医療業界などの固有の用語が文章中に登場する際、embedding modelによってはその用語を学習していないと本来の意図で埋め込み(ベクトル化)ができない事があります。
それに対するアプローチとして、Fine-TuningやColBERTがあります。
cookbook内では、ColBERTについての以下のリンクが紹介されていました。
- https://hackernoon.com/how-colbert-helps-developers-overcome-the-limits-of-rag
- https://python.langchain.com/docs/integrations/retrievers/ragatouille/
- https://til.simonwillison.net/llms/colbert-ragatouille
Fine-Tuningについては、コード例などの記述が無かったので、ここでも割愛します。
ColBERT
※ 画像は以下から引用しています。
https://python.langchain.com/v0.2/docs/concepts/#retrieval
ColBERT(※ コードで紹介されているのはColBERTの改良版であるColBERTv2)はBERTベースの効率的な類似度検索を目的とした埋め込み・ランキングモデルです。
BERTやその他従来の埋め込みモデルでは、各ドキュメント(チャンク)に対して単一のベクトルを生成して入力クエリとの類似性を単一のスコアで比較するのに対し(図の上段)、
ColBERTは入力クエリのトークン毎に、各ドキュメントのトークンに対する類似度を計算し、最大類似度をスコアとして計算します。すべてのユーザ質問文のトークンの最大スコアの合計を取得することで、元のユーザ質問文とドキュメントの類似性スコアを取得する流れです(図の下段)。
従来の埋め込みモデルよりも、検索対象のドキュメントとユーザの質問文をそれぞれより粒度の細かい(トークン)単位でベクトル化をするので、より詳細で微妙な表現を維持できることがポイントのようです。
複雑に見える処理ですが、ragtouille
というライブラリを使うことで容易に実装できるようです。
(直感的には)モデルにとって未知の用語をトークン単位でベクトル化することに有効性がどこまであるのか?や導入するまでのコストを考慮すると、キーワード検索+ベクトル検索のハイブリット検索の方が現状は実用的なのかなと思ってしまいました。
※ ColBERTの日本語版も公開されていました。
https://huggingface.co/bclavie/JaColBERT
JaColBERTはColBERTを日本語データセットでファインチューニングしたモデルのようです。
ドメイン固有の用語を意図通りに解釈するための精度を追及していくと結局はファインチューニングが必要になりそうな印象です。(Ada
のような埋め込みモデルのファインチューニングよりは少ないデータセットで可能のようです。)
おわりに
Indexingのセクションは紹介されている手法の多くが論文ベースのアイディアになっており、その手法のポイントや実際の使いドコロまでは私の理解ではなかなか想像しづらいものもありました。
今回は各手法の深い仕組みの部分まで読み解けなかったので、そのうち中身もしっかり理解したいと思います。
参考文献
- https://github.com/langchain-ai/rag-from-scratch/blob/main/rag_from_scratch_12_to_14.ipynb
- https://github.com/FullStackRetrieval-com/RetrievalTutorials/blob/main/tutorials/LevelsOfTextSplitting/5_Levels_Of_Text_Splitting.ipynb
- https://arxiv.org/pdf/2312.06648
- https://blog.langchain.dev/semi-structured-multi-modal-rag/
- https://python.langchain.com/docs/how_to/multi_vector/
- https://python.langchain.com/docs/how_to/parent_document_retriever/
- https://github.com/langchain-ai/langchain/blob/master/cookbook/RAPTOR.ipynb
- https://arxiv.org/pdf/2401.18059
- https://python.langchain.com/v0.2/docs/concepts/#retrieval
- https://hackernoon.com/how-colbert-helps-developers-overcome-the-limits-of-rag
- https://python.langchain.com/docs/integrations/retrievers/ragatouille/
- https://til.simonwillison.net/llms/colbert-ragatouille
- https://github.com/stanford-futuredata/ColBERT
- https://huggingface.co/bclavie/JaColBERT
※ Indexingの題材に使用したサイト
Discussion