見出し画像

AzureからGPTを使ってみる | RAG(テキストの分割編)


RAG(Retrieval-Augmented Generation)とは何か

RAGは、情報検索(Retrieval)とテキスト生成(Generation)を組み合わせた手法になります。具体的には、ユーザーの質問に対して、まず関連するドキュメントをデータベースから検索し、その情報を基に回答を生成します。これにより、最新の情報や特定の知識領域に関する質問にも、より正確かつ詳細な回答を提供できます。

今回の記事は、RAGを実現するための第一歩ということで、ドキュメントの分割に焦点を当てたいと思います。
大きなドキュメントをそのまま検索対象にすると、関連性の高い部分を特定するのが難しくなります。そこで、ドキュメントを適切なサイズのチャンク(断片)に分割することで、検索の精度向上が期待できます。最近では、RAGとLong contextを組み合わせることで、コストと精度の両立を実現できる手法も開発されていますが、またそれはおいおい。
ここで難しいのは、どのくらいのトークン数に分割するのが適切なのかということです。とりあえず、500~1000トークン程度のチャンクサイズ、50~100トークン程度のオーバーラップで行ってみて、使用するドキュメントを見ながら調節してみるということでしょうか。

それでは、実際にLangChainのtext splittersをいくつか試してみましょう。
すべて、以下のmdファイルを使って確認してみます。(もちろんtxtファイルでも同様に使えます。)

# 昔々
あるところにおじいさんとおばあさんがいました。
## おじいさんとおばあさん
おじいさんは山へ芝刈りに、おばあさんは川へ洗濯に行きました。

## 桃が流れてくる
おばあさんが川で洗濯をしていると、大きな桃がどんぶらこどんぶらこと流れてきました。
## 桃太郎誕生
おじいさんとおばあさんが桃を割ると、中から元気な男の子が出てきました。
その子は桃から生まれたので「桃太郎」と名付けられました。

・CharacterTextSplitter

セパレータで分割して、文字数でマージします。

 #langchain -text-splitters=0.2.2  

from langchain_text_splitters import CharacterTextSplitter

# Load example document
with open("momo01.md") as f:
    state_of_the_union = f.read()

text_splitter = CharacterTextSplitter(
    separator = "。",  # セパレータ
    chunk_size = 10,  # チャンクの文字数
    chunk_overlap = 0,  # チャンクオーバーラップの文字数
)
texts = text_splitter.create_documents([state_of_the_union])

for text in texts:
    print(text)
page_content='# 昔々
あるところにおじいさんとおばあさんがいました'
page_content='## おじいさんとおばあさん
おじいさんは山へ芝刈りに、おばあさんは川へ洗濯に行きました'
page_content='## 桃が流れてくる
おばあさんが川で洗濯をしていると、大きな桃がどんぶらこどんぶらこと流れてきました'
page_content='## 桃太郎誕生
おじいさんとおばあさんが桃を割ると、中から元気な男の子が出てきました'
page_content='その子は桃から生まれたので「桃太郎」と名付けられました'

・RecursiveCharacterTextSplitter


テキストを再帰的により小さな単位で分割し、指定した長さ(トークン数や文字数)以内に収めることを目的としています。

from langchain_text_splitters import RecursiveCharacterTextSplitter

# Load example document
with open("momo01.md") as f:
    state_of_the_union = f.read()

text_splitter = RecursiveCharacterTextSplitter(
    # Set a really small chunk size, just to show.
    chunk_size=10,
    chunk_overlap=5,
    separators=["\n## ", "\n\n", "。", " ", ""]  # 区切り文字のリスト
    #length_function=len,
    #is_separator_regex=False,
)
texts = text_splitter.create_documents([state_of_the_union])

for text in texts:
    print(text)
page_content='#'
page_content='昔々
あるところに'
page_content='るところにおじいさん'
page_content='おじいさんとおばあさ'
page_content='とおばあさんがいまし'
page_content='んがいました'
page_content='。'
page_content='##'
page_content='おじいさんとおばあ'
page_content='んとおばあさん
おじ'
page_content='さん
おじいさんは山'
page_content='いさんは山へ芝刈りに'
page_content='へ芝刈りに、おばあさ'
page_content='、おばあさんは川へ洗'
page_content='んは川へ洗濯に行きま'
page_content='濯に行きました'
page_content='。'
page_content='##'
page_content='桃が流れてくる
お'
page_content='てくる
おばあさんが'
...
page_content='まれたので「桃太郎」'
page_content='「桃太郎」と名付けら'
page_content='と名付けられました'
page_content='。'
Output is truncated. View as a scrollable element or open in a text editor. Adjust cell output settings...

・MarkdownHeaderTextSplitter

テキスト内のMarkdown見出しを解析し、各レベルのヘッダー(#から######まで)を識別します。見出しのレベルに応じて、テキストの階層構造を構築します。
各チャンクには、対応するヘッダー情報がメタデータとして付加されます。

from langchain_text_splitters import MarkdownHeaderTextSplitter

# Load example document
with open("momo01.md") as f:
    state_of_the_union = f.read()
    
headers_to_split_on = [
    ("#", "Header 1"),
    ("##", "Header 2"),
    ("###", "Header 3"),
]    

markdown_splitter = MarkdownHeaderTextSplitter(
    headers_to_split_on=headers_to_split_on,
    strip_headers = False #コンテンツび分割されるヘッダーを含めるかどうか
    )

md_header_splits = markdown_splitter.split_text(state_of_the_union)

for text in md_header_splits:
    print(text)
page_content='# 昔々
あるところにおじいさんとおばあさんがいました。' metadata={'Header 1': '昔々'}
page_content='## おじいさんとおばあさん
おじいさんは山へ芝刈りに、おばあさんは川へ洗濯に行きました。' metadata={'Header 1': '昔々', 'Header 2': 'おじいさんとおばあさん'}
page_content='## 桃が流れてくる
おばあさんが川で洗濯をしていると、大きな桃がどんぶらこどんぶらこと流れてきました。' metadata={'Header 1': '昔々', 'Header 2': '桃が流れてくる'}
page_content='## 桃太郎誕生
おじいさんとおばあさんが桃を割ると、中から元気な男の子が出てきました。
その子は桃から生まれたので「桃太郎」と名付けられました。' metadata={'Header 1': '昔々', 'Header 2': '桃太郎誕生'}

・MarkdownHeaderTextSplitter + CharacterTextSplitter

MarkdownHeaderTextSplitterを、別のtext splitterと組み合わせて、各マークダウンのグループ内で、分割することもできます。

from langchain_text_splitters import MarkdownHeaderTextSplitter
from langchain_text_splitters import CharacterTextSplitter

# Load example document
with open("momo01.md") as f:
    state_of_the_union = f.read()
    
headers_to_split_on = [
    ("#", "Header 1"),
    ("##", "Header 2"),
    ("###", "Header 3"),
]    

markdown_splitter = MarkdownHeaderTextSplitter(
    headers_to_split_on=headers_to_split_on,
    strip_headers = False #コンテンツび分割されるヘッダーを含めるかどうか
    )

md_header_splits = markdown_splitter.split_text(state_of_the_union)


text_splitter = CharacterTextSplitter(
    separator = "、",  # セパレータ
    chunk_size = 10,  # チャンクの文字数
    chunk_overlap = 0,  # チャンクオーバーラップの文字数
)
texts = text_splitter.split_documents(md_header_splits)

for text in texts:
    print(text)
page_content='# 昔々
あるところにおじいさんとおばあさんがいました。' metadata={'Header 1': '昔々'}
page_content='## おじいさんとおばあさん
おじいさんは山へ芝刈りに' metadata={'Header 1': '昔々', 'Header 2': 'おじいさんとおばあさん'}
page_content='おばあさんは川へ洗濯に行きました。' metadata={'Header 1': '昔々', 'Header 2': 'おじいさんとおばあさん'}
page_content='## 桃が流れてくる
おばあさんが川で洗濯をしていると' metadata={'Header 1': '昔々', 'Header 2': '桃が流れてくる'}
page_content='大きな桃がどんぶらこどんぶらこと流れてきました。' metadata={'Header 1': '昔々', 'Header 2': '桃が流れてくる'}
page_content='## 桃太郎誕生
おじいさんとおばあさんが桃を割ると' metadata={'Header 1': '昔々', 'Header 2': '桃太郎誕生'}
page_content='中から元気な男の子が出てきました。
その子は桃から生まれたので「桃太郎」と名付けられました。' metadata={'Header 1': '昔々', 'Header 2': '桃太郎誕生'}


この記事が気に入ったらサポートをしてみませんか?