【DLAI×LangChain講座2②】



背景

DeepLearning.aiのLangChain講座(LangChain for LLM Application Development)を受講し、講座で学んだ内容やおまけで試した内容をまとめました。

DLAI×LangChain講座まとめ|harukary

現在は、第2弾(LangChain Chat with Your Data)の内容を同様にまとめていきます。

第2回は、ドキュメントの分割方法です。

この講座では、以下のテキストの読み込み方法が示されています。文字数やトークン数で分割するものや特定の文字列をもとに分割するものがあります。

  • Recursive

  • Tokens

  • Character

  • Markdown

  • HTML

  • Code

HTMLやCodeなど、以前はなかった気がします。特にHTMLは以前、BeautifulSoupで抽出したテキストをもとに任意形式のJsonデータを抽出する方法を考えていました。

ChatGPTでURLから任意のJson形式でデータ抽出を行う|harukary

ドキュメントによると、”HTML specific characters”による分割が可能とあるので、さらに洗練された方法になっていそうです。試してみたいですね。

詳細はドキュメントを見てみると良いと思います。

Text Splitters | 🦜️🔗 Langchain

アプローチ

DeepLearning.aiのLangChain講座2の2の内容をまとめます。

サンプル

import os
import openai
import sys
sys.path.append('../..')

from dotenv import load_dotenv, find_dotenv
_ = load_dotenv(find_dotenv()) # read local .env file

openai.api_key  = os.environ['OPENAI_API_KEY']
from langchain.text_splitter import RecursiveCharacterTextSplitter, CharacterTextSplitter

ここでは、RecursiveCharacterTextSplitter, CharacterTextSplitterのサンプルを紹介します。

chunk_size = 26
chunk_overlap = 4

ここで、チャンクの文字数とオーバーラップの文字数を定義します。それに基づいて簡単にドキュメント分割が可能です。

r_splitter = RecursiveCharacterTextSplitter(
    chunk_size=chunk_size,
    chunk_overlap=chunk_overlap
)
c_splitter = CharacterTextSplitter(
    chunk_size=chunk_size,
    chunk_overlap=chunk_overlap
)

Why doesn't this split the string below?
↓text1はなぜ分割されないのでしょうか?(ぴったり26文字)

text1 = 'abcdefghijklmnopqrstuvwxyz'
r_splitter.split_text(text1)
['abcdefghijklmnopqrstuvwxyz']
text2 = 'abcdefghijklmnopqrstuvwxyzabcdefg'
r_splitter.split_text(text2)
['abcdefghijklmnopqrstuvwxyz', 'wxyzabcdefg']

Ok, this splits the string but we have an overlap specified as 5, but it looks like 3? (try an even number)
↑あまり意味がわからない。とにかく、テキストがchunk_sizeより大きい場合に分割がされるようです。

text3 = "a b c d e f g h i j k l m n o p q r s t u v w x y z"
r_splitter.split_text(text3)
['a b c d e f g h i j k l m', 'l m n o p q r s t u v w x', 'w x y z']
c_splitter.split_text(text3)
['a b c d e f g h i j k l m n o p q r s t u v w x y z']

RecursiveCharacterTextSplitterでは分割されますが、CharacterTextSplitterでは分割されていません。これは、以下の違いから来ています。

TextSplitter.split_textの分割処理は、まずseparatorで分割した後、指定のchunk_size、chunk_overlapに合わせて結合します。

RecursiveCharacterTextSplitterでは、RecursiveCharacterTextSplitter._separators=["\n\n", "\n", " ", ""]がデフォルトとなっています。" "(半角スペース)で分割されるため、スペースが文字としてカウントされています。

一方、CharacterTextSplitterでは、CharacterTextSplitter._separator="\n\n"がデフォルトとなっています。"\n\n"を含まないので分割されません。

c_splitter = CharacterTextSplitter(
    chunk_size=chunk_size,
    chunk_overlap=chunk_overlap,
    separator = ' '
)
c_splitter.split_text(text3)
['a b c d e f g h i j k l m', 'l m n o p q r s t u v w x', 'w x y z']

separatorを" "(半角スペース)に変更することで、RecursiveCharacterTextSplitterと同様の分割がされます。

細かい部分はソースコードを確認してください。。

Try your own examples!

Recursive splitting details

`RecursiveCharacterTextSplitter` is recommended for generic text.
RecursiveCharacterTextSplitterは、一般的なテキストに対して推奨されます。

some_text = """When writing documents, writers will use document structure to group content. \
This can convey to the reader, which idea's are related. For example, closely related ideas \
are in sentances. Similar ideas are in paragraphs. Paragraphs form a document. \n\n  \
Paragraphs are often delimited with a carriage return or two carriage returns. \
Carriage returns are the "backslash n" you see embedded in this string. \
Sentences have a period at the end, but also, have a space.\
and words are separated by space."""
len(some_text)
496
c_splitter = CharacterTextSplitter(
    chunk_size=450,
    chunk_overlap=0,
    separator = ' '
)
r_splitter = RecursiveCharacterTextSplitter(
    chunk_size=450,
    chunk_overlap=0, 
    separators=["\n\n", "\n", " ", ""]
)
c_splitter.split_text(some_text)
['When writing documents, writers will use document structure to group content. This can convey to the reader, which idea\'s are related. For example, closely related ideas are in sentances. Similar ideas are in paragraphs. Paragraphs form a document. \n\n Paragraphs are often delimited with a carriage return or two carriage returns. Carriage returns are the "backslash n" you see embedded in this string. Sentences have a period at the end, but also,',
 'have a space.and words are separated by space.']
r_splitter.split_text(some_text)
["When writing documents, writers will use document structure to group content. This can convey to the reader, which idea's are related. For example, closely related ideas are in sentances. Similar ideas are in paragraphs. Paragraphs form a document.",
 'Paragraphs are often delimited with a carriage return or two carriage returns. Carriage returns are the "backslash n" you see embedded in this string. Sentences have a period at the end, but also, have a space.and words are separated by space.']

CharacterTextSplitter(separatorはスペース)は、文章ではなく単語単位で分割しています。一方、RecursiveCharacterTextSplitterは、改行を優先して分割するため、文単位での分割ができています。

Let's reduce the chunk size a bit and add a period to our separators:
chunk_sizeを小さくし、ピリオドをseparatorに追加してみましょう。

ここら辺はソースコードを見るか、自然言語処理の知識がないとわからなそうですね。私はスキップします。

r_splitter = RecursiveCharacterTextSplitter(
    chunk_size=150,
    chunk_overlap=0,
    separators=["\n\n", "\n", "\. ", " ", ""]
)
r_splitter.split_text(some_text)
["When writing documents, writers will use document structure to group content. This can convey to the reader, which idea's are related. For example,",
 'closely related ideas are in sentances. Similar ideas are in paragraphs. Paragraphs form a document.',
 'Paragraphs are often delimited with a carriage return or two carriage returns. Carriage returns are the "backslash n" you see embedded in this',
 'string. Sentences have a period at the end, but also, have a space.and words are separated by space.']
r_splitter = RecursiveCharacterTextSplitter(
    chunk_size=150,
    chunk_overlap=0,
    separators=["\n\n", "\n", "(?<=\. )", " ", ""]
)
r_splitter.split_text(some_text)
["When writing documents, writers will use document structure to group content. This can convey to the reader, which idea's are related. For example,",
 'closely related ideas are in sentances. Similar ideas are in paragraphs. Paragraphs form a document.',
 'Paragraphs are often delimited with a carriage return or two carriage returns. Carriage returns are the "backslash n" you see embedded in this',
 'string. Sentences have a period at the end, but also, have a space.and words are separated by space.']

ここからは、PDF(講義の文字起こしテキスト)を読み込み・チャンク分割していきます。

from langchain.document_loaders import PyPDFLoader
loader = PyPDFLoader("docs/cs229_lectures/MachineLearning-Lecture01.pdf")
pages = loader.load()
from langchain.text_splitter import CharacterTextSplitter
text_splitter = CharacterTextSplitter(
    separator="\n",
    chunk_size=1000,
    chunk_overlap=150,
    length_function=len
)

separatorは改行です。

docs = text_splitter.split_documents(pages)
len(docs)
77
len(pages)
22

次に、NotionからエクスポートしたMarkdownテキストを読み込み・チャンク分割します。

from langchain.document_loaders import NotionDirectoryLoader
loader = NotionDirectoryLoader("docs/Notion_DB")
notion_db = loader.load()
docs = text_splitter.split_documents(notion_db)
len(notion_db)
1
len(docs)
8

Token splitting

We can also split on token count explicity, if we want.

This can be useful because LLMs often have context windows designated in tokens.

Tokens are often ~4 characters.

トークンによる分割も可能です。LLMはテキストをトークンに変換して入力することが多いため、この方法は有用です。逆に、トークンとして扱わないと、意図しない変換がされることもありそうですね。(「カボス」が分割されて「カ」「ボス」でそれぞれトークン化されるなど?改行や句読点で分割していたらそうはならないか。。)

from langchain.text_splitter import TokenTextSplitter
text_splitter = TokenTextSplitter(chunk_size=1, chunk_overlap=0)

ここではまず、chunk_sizeを1にしてどんなトークンになるかを確認します。

text1 = "foo bar bazzyfoo"
text_splitter.split_text(text1)
['foo', ' bar', ' b', 'az', 'zy', 'foo']

次に、chunk_sizeを10にしてみます。

text_splitter = TokenTextSplitter(chunk_size=10, chunk_overlap=0)
docs = text_splitter.split_documents(pages)
docs[0]
Document(page_content='MachineLearning-Lecture01  \n', metadata={'source': 'docs/cs229_lectures/MachineLearning-Lecture01.pdf', 'page': 0})
pages[0].metadata
{'source': 'docs/cs229_lectures/MachineLearning-Lecture01.pdf', 'page': 0}

Context aware splitting

Chunking aims to keep text with common context together.

A text splitting often uses sentences or other delimiters to keep related text together but many documents (such as Markdown) have structure (headers) that can be explicitly used in splitting.

We can use `MarkdownHeaderTextSplitter` to preserve header metadata in our chunks, as show below.

次に、コンテキストを踏まえたチャンク分割の方法です。MarkdownやHTMLなどの構造化テキストでは、その構造に従って分割することが効果的です。

from langchain.document_loaders import NotionDirectoryLoader
from langchain.text_splitter import MarkdownHeaderTextSplitter
markdown_document = """# Title\n\n \
## Chapter 1\n\n \
Hi this is Jim\n\n Hi this is Joe\n\n \
### Section \n\n \
Hi this is Lance \n\n 
## Chapter 2\n\n \
Hi this is Molly"""
headers_to_split_on = [
    ("#", "Header 1"),
    ("##", "Header 2"),
    ("###", "Header 3"),
]

ここで、各Headingがどのようにメタデータに入力されるかを決めています。

markdown_splitter = MarkdownHeaderTextSplitter(
    headers_to_split_on=headers_to_split_on
)
md_header_splits = markdown_splitter.split_text(markdown_document)
md_header_splits[0]
Document(page_content='Hi this is Jim  \nHi this is Joe', metadata={'Header 1': 'Title', 'Header 2': 'Chapter 1'})

'Header 1'は'Title'、'Header 2'は'Chapter 1'が入りました。

md_header_splits[1]
Document(page_content='Hi this is Lance', metadata={'Header 1': 'Title', 'Header 2': 'Chapter 1', 'Header 3': 'Section'})

次のチャンクには、'Header 3'に'Section'と入っています。

Try on a real Markdown file, like a Notion database.
NotionからエクスポートしたMarkdownテキストでテストします。

loader = NotionDirectoryLoader("docs/Notion_DB")
docs = loader.load()
txt = ' '.join([d.page_content for d in docs])
headers_to_split_on = [
    ("#", "Header 1"),
    ("##", "Header 2"),
]
markdown_splitter = MarkdownHeaderTextSplitter(
    headers_to_split_on=headers_to_split_on
)
md_header_splits = markdown_splitter.split_text(txt)
md_header_splits[0]
Document(page_content="This is a living document with everything we've learned working with people while running a startup. And, of course, we continue to learn. Therefore it's a document that will continue to change.  \n**Everything related to working at Blendle and the people of Blendle, made public.**  \nThese are the lessons from three years of working with the people of Blendle. It contains everything from [how our leaders lead](https://www.notion.so/ecfb7e647136468a9a0a32f1771a8f52?pvs=21) to [how we increase salaries](https://www.notion.so/Salary-Review-e11b6161c6d34f5c9568bb3e83ed96b6?pvs=21), from [how we hire](https://www.notion.so/Hiring-451bbcfe8d9b49438c0633326bb7af0a?pvs=21) and [fire](https://www.notion.so/Firing-5567687a2000496b8412e53cd58eed9d?pvs=21) to [how we think people should give each other feedback](https://www.notion.so/Our-Feedback-Process-eb64f1de796b4350aeab3bc068e3801f?pvs=21) — and much more.  \nWe've made this document public because we want to learn from you. We're very much interested in your feedback (including weeding out typo's and Dunglish ;)). Email us at hr@blendle.com. If you're starting your own company or if you're curious as to how we do things at Blendle, we hope that our employee handbook inspires you.  \nIf you want to work at Blendle you can check our [job ads here](https://blendle.homerun.co/). If you want to be kept in the loop about Blendle, you can sign up for [our behind the scenes newsletter](https://blendle.homerun.co/yes-keep-me-posted/tr/apply?token=8092d4128c306003d97dd3821bad06f2).", metadata={'Header 1': "Blendle's Employee Handbook"})

まとめ

DeepLearning.aiのLangChain講座2の2の内容をまとめました。

今回は様々なソースからのデータ取り込んだ後、チャンク分割する方法でした。

私の取り組み中のプロジェクトでは、取扱説明書のPDFを取り込み・チャンク分割しています。今回の内容を学んで、結構適当にチャンク分割していたなと反省しました。PDFのOCR結果には、不要なスペースや改行が含まれるので、(OCRが英語ベースであるためなのか、1文字ごとにスペースが入ることがよくあります。)separatorの選択は考え直す必要がありそうです。また、ドキュメントのほうの前処理で不要なスペースや改行を除去しておく必要もあるかもしれないですね。

どちらにせよ、日本語の場合は形態素解析と同様、英語などよりも工夫が必要そうですね。

読んでいただきありがとうございます。少し空いてしまいましたが、また進めていこうと思います。

参考

DLAI×LangChain講座まとめ|harukary

ChatGPTでURLから任意のJson形式でデータ抽出を行う|harukary

Text Splitters | 🦜️🔗 Langchain

サンプルコード

llm_samples/LangChain/LangChain_DLAI_2/02_document_splitting.ipynb at main · harukary/llm_samples · GitHub

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