はじめに
RAG(Retrieval-Augmented Generation)は、情報を効率的に取得し、それを基に応答を生成する手法です。このプロセスにおいて、大きなドキュメントを適切に分割し、関連する情報を迅速に取り出すことが非常に重要です。特に、テキストの分割方法は、検索効率や回答精度の向上に寄与するため、適切なツールの選択がカギとなります。
ここでは、LangChainライブラリに用意されているテキストを簡単に分割するための「Text Splitters」の中から、RecursiveCharacterTextSplitterの基本的な使い方とその実際の動作について詳しく見ていきます。
こちらも参考にして下さい
筆者のレベル
- RAGについて基本的な理解を持っている
- RAGを実装することで、さらに深く学びたいと考えている
- プログラミング経験はあまりないが、RAGを実装してみたい
- 実際にコードを作成して動かすことで、理解を深めていきたい
記載していること
- RecursiveCharacterTextSplitterの基本的な説明
- いくつかの設定パターンで動かしてみた結果
RecursiveCharacterTextSplitterの特徴
RecursiveCharacterTextSplitterは、再帰的に文字単位でテキストを分割する機能を持つText Splitterです。このツールを使用することで、文字列処理において、より効果的に情報を扱うことができます。以下に、RecursiveCharacterTextSplitterの主要なパラメータを説明します。
パラメータの説明
-
chunk_size: チャンクの最大サイズを指定します。このサイズに基づいてテキストが分割されます。
-
chunk_overlap: チャンク間のオーバーラップを指定します。これにより、前のチャンクと次のチャンクの一部が重複するようになります。
-
separators: 複数のセパレータを指定して、順に分割していきます。句点や改行などを含むリストで設定されます。
-
keep_separator:テキストを分割した際にセパレータをどのように扱うかを決定します。以下のオプションがあります。
True
または"end"
- 説明: 各チャンクの最後にセパレータを保持します。
"start"
- 説明: 各チャンクの最初にセパレータを保持します。
False
- 説明: セパレータを削除します。
実装例
以下は、RecursiveCharacterTextSplitterの基本的な使い方の例です。
from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
source_text = "あいうえお、かきくけこさしすせそ。"
chunk_size = 10
chunk_overlap = 2
recursive_char_text_splitter = RecursiveCharacterTextSplitter(
chunk_size=chunk_size,
chunk_overlap=chunk_overlap,
separators=['。']
)
documents = recursive_char_text_splitter.split_text(source_text)
動かしてみる
ドキュメントの準備
このようなテキストファイルを用意します。
あいうえお。
かきくけこ。さしすせそ。
たちつてと。なにぬねの。はひふへほ。
まみむめも。やゆよ。らりるれろわをん。
①chunk_sizeとseparatorsを設定してみる
from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
# テキストをロード
source_document = TextLoader('sample.txt').load()
source_text = source_document[0].page_content
# RecursiveCharacterTextSplitterを設定
recursive_char_text_splitter = RecursiveCharacterTextSplitter(
chunk_size=5,
chunk_overlap=0,
separators=['。']
)
# 分割を実行
char_documents = recursive_char_text_splitter.split_text(source_text)
# 分割結果を表示
print('\nRecursiveCharacterTextSplitterによる分割結果: \n')
for i, chunk in enumerate(char_documents):
print(f"Chunk {i+1} (Length: {len(chunk)}):\n{chunk}\n")
RecursiveCharacterTextSplitterによる分割結果:
セパレータは、チャンクの最初に保持されています。
Chunk 1 (Length: 5):
あいうえお
Chunk 2 (Length: 7):
。
かきくけこ
Chunk 3 (Length: 6):
。さしすせそ
Chunk 4 (Length: 7):
。
たちつてと
Chunk 5 (Length: 6):
。なにぬねの
Chunk 6 (Length: 6):
。はひふへほ
Chunk 7 (Length: 7):
。
まみむめも
Chunk 8 (Length: 4):
。やゆよ
Chunk 9 (Length: 9):
。らりるれろわをん
Chunk 10 (Length: 1):
。
②keep_separatorを設定してみる
# RecursiveCharacterTextSplitterを設定
recursive_char_text_splitter = RecursiveCharacterTextSplitter(
chunk_size=5,
chunk_overlap=0,
separators=['。'],
keep_separator="end"
)
RecursiveCharacterTextSplitterによる分割結果:
セパレータはチャンクの最後に保持されています。
Chunk 1 (Length: 6):
あいうえお。
Chunk 2 (Length: 7):
かきくけこ。
Chunk 3 (Length: 6):
さしすせそ。
Chunk 4 (Length: 7):
たちつてと。
Chunk 5 (Length: 6):
なにぬねの。
Chunk 6 (Length: 6):
はひふへほ。
Chunk 7 (Length: 7):
まみむめも。
Chunk 8 (Length: 4):
やゆよ。
Chunk 9 (Length: 9):
らりるれろわをん。
③chunk_overlapを設定してみる
chunk_sizeは10、chunk_overlapは2を設定してみます。
# RecursiveCharacterTextSplitterを設定
recursive_char_text_splitter = RecursiveCharacterTextSplitter(
chunk_size=10,
chunk_overlap=2,
separators=['。'],
keep_separator="end"
)
RecursiveCharacterTextSplitterによる分割結果:
オーバラップされていない・・・
Chunk 1 (Length: 6):
あいうえお。
Chunk 2 (Length: 6):
かきくけこ。
Chunk 3 (Length: 6):
さしすせそ。
Chunk 4 (Length: 6):
たちつてと。
Chunk 5 (Length: 6):
なにぬねの。
Chunk 6 (Length: 6):
はひふへほ。
Chunk 7 (Length: 6):
まみむめも。
Chunk 8 (Length: 4):
やゆよ。
Chunk 9 (Length: 9):
らりるれろわをん。
④chunk_sizeを変更してみる
セパレータをカウントしない長さ(chunk_size=5)に、チャンクサイズを設定します。
# RecursiveCharacterTextSplitterを設定
recursive_char_text_splitter = RecursiveCharacterTextSplitter(
chunk_size=5,
chunk_overlap=2,
separators=['。'],
keep_separator="end"
)
RecursiveCharacterTextSplitterによる分割結果:
chunk_size=5よりも大きなサイズで分割されてしまいます。
Chunk 1 (Length: 6):
あいうえお。
Chunk 2 (Length: 7):
かきくけこ。
Chunk 3 (Length: 6):
さしすせそ。
Chunk 4 (Length: 7):
たちつてと。
Chunk 5 (Length: 6):
なにぬねの。
Chunk 6 (Length: 6):
はひふへほ。
Chunk 7 (Length: 7):
まみむめも。
Chunk 8 (Length: 4):
やゆよ。
Chunk 9 (Length: 9):
らりるれろわをん。
⑤separatorsに空白文字('')を設定してみる
CharacterTextSplitterにおいて、セパレータに空白文字('')を設定しないと、適切にカウントされていないことを確認していたので、それを参考に空白文字('')を設定してみました。
# RecursiveCharacterTextSplitterを設定
recursive_char_text_splitter = RecursiveCharacterTextSplitter(
chunk_size=5,
chunk_overlap=2,
separators=['。',''],
keep_separator="end"
)
RecursiveCharacterTextSplitterによる分割結果:
一見、chunk_size、chunk_overlapともに、思った通りに分割されているように見えますが、オーバーラップしていないチャンクもあります。
Chunk 1 (Length: 5):
あいうえお
Chunk 2 (Length: 3):
えお。
Chunk 3 (Length: 4):
かきくけ
Chunk 4 (Length: 4):
くけこ。
Chunk 5 (Length: 5):
さしすせそ
Chunk 6 (Length: 3):
せそ。
Chunk 7 (Length: 4):
たちつて
Chunk 8 (Length: 4):
つてと。
Chunk 9 (Length: 5):
なにぬねの
Chunk 10 (Length: 3):
ねの。
Chunk 11 (Length: 5):
はひふへほ
Chunk 12 (Length: 3):
へほ。
Chunk 13 (Length: 4):
まみむめ
Chunk 14 (Length: 4):
むめも。
Chunk 15 (Length: 4):
やゆよ。
Chunk 16 (Length: 5):
らりるれろ
Chunk 17 (Length: 5):
れろわをん
Chunk 18 (Length: 3):
をん。
考察
チャンクサイズに分割する際に、指定した分割サイズまでにセパレータが存在しなければ、オーバーラップが機能するように見えます。
(必ずオーバーラップして欲しいのだが・・・)
まとめ
RAG(Retrieval-Augmented Generation)においては、文脈や意味の単位でテキストを分割することが、精度向上につながります。RecursiveCharacterTextSplitterを用いることで、指定したセパレータやチャンクサイズに基づいて、再帰的にテキストを効率よく分割できます。
ですが、思った通りに分割するには、もう少し工夫がいるのかもしれません。