개념
문서를 로딩했으면 의미조각(Chunk)으로 쪼개는 splitting 작업을 한다. 이는 LLM에 인풋으로 들어가는 프롬프트 길이의 제약이 있기 때문에 문서가 길 경우 의미조각으로 나눠서 저장 후 사용자 쿼리에 가장 유사한 의미조각을 찾기 위한 용도이다. 따라서 문서의 특성에 따른 Splitter를 잘 선택해야 한다.
패키지
langchain_text_splitters 패키지가 별도로 분리되어 있다. 두가지 방법으로 import 할 수 있다.
from langchain.document_loaders import PyPDFLoader
from langchain_text_splitter import RecursiveCharacterTextSplitter
// 또는
// from langchain_text_splitters import RecursiveCharacterTextSplitter
// 예1) 문서 splitting
loader = PyPDFLoader(FILE_PATH)
docs = loader.load()
text_splitter = RecursiveCharacterTextSplitter(chunk_size=200, chunk_overlap=50)
splitted_docs = text_splitter.split_documents(docs)
for i, document in enumerate(splitted_docs):
print(f"Document {i+1}:\n{document}\n")
// 예2) 텍스트 splitting
text = """
LangChain은 대규모 언어 모델(LLM)을 활용하여 복잡한 응용 프로그램을 구축하기 위한 도구입니다.
이 도구는 프롬프트 템플릿, 모델, 출력 파서 등을 포함한 다양한 컴포넌트를 제공하여,
사용자가 쉽게 체인을 구성할 수 있도록 도와줍니다.
"""
chunks = text_splitter.split_text(text)
for i, chunk in enumerate(chunks):
print(f"Chunk {i+1}:\n{chunk}\n")
Splitter의 종류
1. RecursiveCharacterTextSplitter:
• 분할 기준: 사용자 정의 문자를 기준으로 재귀적으로 분할합니다.
• 특징: 관련된 텍스트 조각을 최대한 근접하게 유지하려고 합니다. 텍스트를 나누는 기본 방법으로 권장됩니다.
2. CharacterTextSplitter:
• 분할 기준: 단순히 특정 문자(예: 공백, 줄 바꿈 등)를 기준으로 분할합니다.
• 특징: 간단한 분할 작업에 적합합니다.
3. TokenTextSplitter:
• 분할 기준: 토큰 단위로 텍스트를 분할합니다.
• 특징: 언어 모델의 토큰화 방식에 따라 텍스트를 나눌 수 있어, 모델의 최대 토큰 길이에 맞춰 텍스트를 분할할 때 유용합니다.
4. SentenceTextSplitter:
• 분할 기준: 문장 단위로 텍스트를 분할합니다.
• 특징: 문장을 기준으로 텍스트를 나누기 때문에 자연스러운 분할이 가능합니다.
5. MarkdownTextSplitter:
• 분할 기준: 마크다운 문서의 구조(헤더, 리스트, 코드 블록 등)를 기준으로 분할합니다.
• 특징: 마크다운 문서를 처리할 때 유용하며, 문서의 논리적 구조를 유지하면서 분할할 수 있습니다.
6. HTMLTextSplitter:
• 분할 기준: HTML 태그를 기준으로 텍스트를 분할합니다.
• 특징: HTML 문서를 처리할 때 유용하며, 문서의 시각적 구조를 유지하면서 분할할 수 있습니다.
7. LanguageSpecificTextSplitter:
• 분할 기준: 특정 언어의 구문을 기준으로 텍스트를 분할합니다.
• 특징: 언어별로 특화된 분할 방식을 사용할 수 있어, 코드나 특정 언어로 작성된 텍스트를 효과적으로 분할할 수 있습니다.
API
주로 다루는 것이 문자일 경우 TextSplitter를 상속받은 RecursiveCharacterTextSplitter를 많이 사용한다.
- split_text 메소드는 주로 문자 스트링을 List[str] 로 반환할 때 사용한다. abstractmethod 이다.
- create_documents 는 List[str] 을 List[Document] 로 반환할 때 사용한다.
- split_documents 주로 문서 파일을 load한 후 다시 작은 단위조각의 List[Document] 로 반환할 때 사용한다.
- Document의 page_content를 List[str] 로 만들어 create_documents를 호출하고 있다.
- create_documents는 chunk_size, chunk_overlap에 따라 텍스트 조각을 만드는 split_text를 호출하고 있다.
// TextSplitter
class TextSplitter(BaseDocumentTransformer, ABC):
"""Interface for splitting text into chunks."""
def __init__(
self,
chunk_size: int = 4000,
chunk_overlap: int = 200,
length_function: Callable[[str], int] = len,
keep_separator: Union[bool, Literal["start", "end"]] = False,
add_start_index: bool = False,
strip_whitespace: bool = True,
) -> None:
"""Create a new TextSplitter.
Args:
chunk_size: Maximum size of chunks to return
chunk_overlap: Overlap in characters between chunks
length_function: Function that measures the length of given chunks
keep_separator: Whether to keep the separator and where to place it
in each corresponding chunk (True='start')
add_start_index: If `True`, includes chunk's start index in metadata
strip_whitespace: If `True`, strips whitespace from the start and end of
every document
"""
if chunk_overlap > chunk_size:
raise ValueError(
f"Got a larger chunk overlap ({chunk_overlap}) than chunk size "
f"({chunk_size}), should be smaller."
)
self._chunk_size = chunk_size
self._chunk_overlap = chunk_overlap
self._length_function = length_function
self._keep_separator = keep_separator
self._add_start_index = add_start_index
self._strip_whitespace = strip_whitespace
@abstractmethod
def split_text(self, text: str) -> List[str]:
"""Split text into multiple components."""
def create_documents(
self, texts: List[str], metadatas: Optional[List[dict]] = None
) -> List[Document]:
"""Create documents from a list of texts."""
_metadatas = metadatas or [{}] * len(texts)
documents = []
for i, text in enumerate(texts):
index = 0
previous_chunk_len = 0
for chunk in self.split_text(text):
metadata = copy.deepcopy(_metadatas[i])
if self._add_start_index:
offset = index + previous_chunk_len - self._chunk_overlap
index = text.find(chunk, max(0, offset))
metadata["start_index"] = index
previous_chunk_len = len(chunk)
new_doc = Document(page_content=chunk, metadata=metadata)
documents.append(new_doc)
return documents
def split_documents(self, documents: Iterable[Document]) -> List[Document]:
"""Split documents."""
texts, metadatas = [], []
for doc in documents:
texts.append(doc.page_content)
metadatas.append(doc.metadata)
return self.create_documents(texts, metadatas=metadatas)
...
RecursiveCharaterTextSplitter 의 split_text 구현 내역
- abstract method 인 split_text를 구현함.
class RecursiveCharacterTextSplitter(TextSplitter):
"""Splitting text by recursively look at characters.
Recursively tries to split by different characters to find one
that works.
"""
def __init__(
self,
separators: Optional[List[str]] = None,
keep_separator: bool = True,
is_separator_regex: bool = False,
**kwargs: Any,
) -> None:
"""Create a new TextSplitter."""
super().__init__(keep_separator=keep_separator, **kwargs)
self._separators = separators or ["\n\n", "\n", " ", ""]
self._is_separator_regex = is_separator_regex
def _split_text(self, text: str, separators: List[str]) -> List[str]:
"""Split incoming text and return chunks."""
final_chunks = []
# Get appropriate separator to use
separator = separators[-1]
new_separators = []
for i, _s in enumerate(separators):
_separator = _s if self._is_separator_regex else re.escape(_s)
if _s == "":
separator = _s
break
if re.search(_separator, text):
separator = _s
new_separators = separators[i + 1 :]
break
_separator = separator if self._is_separator_regex else re.escape(separator)
splits = _split_text_with_regex(text, _separator, self._keep_separator)
# Now go merging things, recursively splitting longer texts.
_good_splits = []
_separator = "" if self._keep_separator else separator
for s in splits:
if self._length_function(s) < self._chunk_size:
_good_splits.append(s)
else:
if _good_splits:
merged_text = self._merge_splits(_good_splits, _separator)
final_chunks.extend(merged_text)
_good_splits = []
if not new_separators:
final_chunks.append(s)
else:
other_info = self._split_text(s, new_separators)
final_chunks.extend(other_info)
if _good_splits:
merged_text = self._merge_splits(_good_splits, _separator)
final_chunks.extend(merged_text)
return final_chunks
def split_text(self, text: str) -> List[str]:
return self._split_text(text, self._separators)
Custom Text Splitter 만들기
Custom text splitter를 만들려면 TextSplitter 클래스를 상속받아 자신만의 분할 로직을 구현할 수 있습니다. 다음은 간단한 예제로, 특정 구분자(예: 줄 바꿈 문자)를 기준으로 텍스트를 분할하는 custom text splitter를 만드는 방법을 보여드립니다.
1. CustomTextSplitter 클래스 정의:
TextSplitter 클래스를 상속받아 CustomTextSplitter 클래스를 정의합니다. 생성자에서 구분자, 청크 크기, 겹침 크기를 설정할 수 있습니다.
2. split_text 메서드 구현:
split_text 메서드는 주어진 텍스트를 구분자를 기준으로 분할한 다음, 지정된 청크 크기와 겹침 크기를 기준으로 청크를 생성합니다.
3. 청크 겹침 처리:
청크 간에 겹침을 추가하여 텍스트가 자연스럽게 이어지도록 합니다.
4. CustomTextSplitter 사용:
CustomTextSplitter 인스턴스를 생성하고, split_text 메서드를 호출하여 텍스트를 분할한 후 결과를 출력합니다.
이 예제는 간단한 구분자를 사용한 텍스트 분할을 보여주지만, 실제로는 더 복잡한 분할 로직을 구현할 수 있습니다. 예를 들어, 문단 단위로 분할하거나, 특정 패턴을 기준으로 분할하는 등 다양한 요구에 맞춰 커스터마이징할 수 있습니다.
from langchain.text_splitter import TextSplitter
from typing import List
class CustomTextSplitter(TextSplitter):
def __init__(self, separator: str = "\n", chunk_size: int = 100, chunk_overlap: int = 20):
self.separator = separator
self.chunk_size = chunk_size
self.chunk_overlap = chunk_overlap
def split_text(self, text: str) -> List[str]:
# 구분자를 기준으로 텍스트를 분할
splits = text.split(self.separator)
chunks = []
current_chunk = ""
for split in splits:
if len(current_chunk) + len(split) + len(self.separator) > self.chunk_size:
chunks.append(current_chunk)
current_chunk = split
else:
if current_chunk:
current_chunk += self.separator
current_chunk += split
# 마지막 남은 텍스트 조각 추가
if current_chunk:
chunks.append(current_chunk)
# 겹침 처리
if self.chunk_overlap > 0 and len(chunks) > 1:
overlapped_chunks = []
for i in range(len(chunks)):
if i > 0:
overlap = chunks[i-1][-self.chunk_overlap:] + self.separator
overlapped_chunks.append(overlap + chunks[i])
else:
overlapped_chunks.append(chunks[i])
chunks = overlapped_chunks
return chunks
# CustomTextSplitter 사용 예제
text = """
LangChain은 대규모 언어 모델(LLM)을 활용하여 복잡한 응용 프로그램을 구축하기 위한 도구입니다.
이 도구는 프롬프트 템플릿, 모델, 출력 파서 등을 포함한 다양한 컴포넌트를 제공하여,
사용자가 쉽게 체인을 구성할 수 있도록 도와줍니다.
"""
splitter = CustomTextSplitter(separator="\n", chunk_size=100, chunk_overlap=20)
chunks = splitter.split_text(text)
for i, chunk in enumerate(chunks):
print(f"Chunk {i+1}:\n{chunk}\n")
written by GPT for custom splitter
<참조>
- API: https://api.python.langchain.com/en/latest/text_splitters_api_reference.html
- 소스: https://github.com/langchain-ai/langchain/tree/master/libs/text-splitters/langchain_text_splitters
- LangChain KR: https://wikidocs.net/233998
'LLM FullStacker > LangChain' 카테고리의 다른 글
[LCC-5] LangChain VectorStore 이해 (0) | 2024.08.09 |
---|---|
[LCC-4] LangChain Embedding Model 이해 (0) | 2024.08.07 |
[LCC-2] LangChain Document Loaders 이해 (0) | 2024.08.05 |
[LCC-1] LangChain Concept - Components & RAG (0) | 2024.08.04 |
[LangChain] LangChain is like Spring Framework for LLM (0) | 2024.06.30 |