langchain에서 rag를 사용할 때 긴 텍스트를 효율적으로 다루는 것이 매우 중요하다. 특히 언어 모델을 사용할 때는, 한 번에 처리할 수 있는 토큰의 수가 제한적이므로 텍스트를 적절한 크기로 분할하는 것이 필수이다. 이를 위해 Hugging Face의 CharacterTextSplitter
와 RecursiveCharacterTextSplitter
같은 도구들이 사용된다. 여기서는 이 개의 차이점을 한번 알아보자.
1. CharacterTextSplitter란?
CharacterTextSplitter
는 텍스트를 일정한 크기로 분할할 수 있는 간단한 도구이다. 이 도구는 주어진 텍스트를 기준으로 정의된 구분자
를 사용하여 텍스트를 나눈다. 주로 특정 문자를 기준으로 분할하기 때문에, 문장
이나 문단
단위로 텍스트를 나누는 데 효과적이다.
특징:
- 기본 구분자: 기본은
\n\n
으로 되어 있다. - 단순하고 직관적: 사용자가 설정한 구분자에 따라 텍스트를 분리하며, 그 과정은 매우 직관적이고 간단하다.
- 길이 제한 가능: 사용자가 원하는 길이 제한을 설정하여 분할된 텍스트의 길이를 조절할 수 있다. 예를 들어, 토큰 수를 기준으로 분할하거나, 텍스트의 문장 수에 따라 분할할 수 있다.
2. RecursiveCharacterTextSplitter란?
RecursiveCharacterTextSplitter
는 CharacterTextSplitter
보다 한 단계 더 발전된 방식으로, 텍스트를 보다 유연하게 분할한다. 이 도구는 단순히 구분자 하나로 텍스트를 분리하는 것이 아니라, 여러 레벨의 구분자를 정의하여 점진적으로 텍스트를 나누는 방식이다. 텍스트가 설정한 최대 길이를 초과할 경우 더 작은 단위
로 나눌 수 있도록 설계되었다.
특징:
- 다중 구분자 사용: 여러 구분자를 사용하여 텍스트를 재귀적으로 나눈다. 예를 들어, 먼저 문단 단위로 나눈 뒤, 문장이 길면 문장 단위로, 문장이 여전히 길면 단어 단위로 나누는 방식이다.
- 최적의 분할 지점 탐색: 텍스트의 길이가 설정한 제한을 넘는 경우, 적절한 분할 지점을 찾기 위해 더 작은 단위로 재귀적으로 분할한다. 이렇게 함으로써 문맥을 최대한 유지하면서 텍스트를 효율적으로 분할할 수 있다.
- 더 나은 문맥 보존: 다양한 구분자를 사용해 텍스트를 나누므로, 단순히 첫 번째 구분자로만 텍스트를 분리하는 것보다 문맥을 더 잘 유지할 수 있다.
3. 어떤 상황에서 어떤 도구를 사용할까?
CharacterTextSplitter를 사용해야 할 때:
- 간단한 작업: 텍스트가 비교적 짧고, 특정 구분자(예: 줄바꿈)로만 나누어도 충분한 경우.
- 빠르고 직관적인 분할이 필요할 때: 구분자 하나로도 텍스트를 적절히 나눌 수 있을 때 유용하다.
RecursiveCharacterTextSplitter를 사용해야 할 때:
- 긴 텍스트를 다룰 때: 문단이나 문장 단위로 텍스트를 나누되, 텍스트의 길이가 제한을 초과하지 않도록 해야 할 때.
- 문맥을 유지하고 싶을 때: 문맥을 최대한 보존하면서 텍스트를 분할해야 할 경우 적합하다.
- 복잡한 텍스트 구조: 여러 구분자를 사용해 텍스트를 세밀하게 나누고자 할 때.
4. 샘플 코드
샘플 문서
chunking 테스트 용 기본 문서 내용이다. 야구 경기 규칙에 대한 내용이다. [baseball.txt]
경기 목적
야구 경기는 두 팀이 번갈아 가며 공격과 수비를 수행하여 점수를 내는 게임입니다. 공격 팀은 공을 치고, 수비 팀은 타자를 아웃시켜 공수 교대를 합니다.
팀 구성
각 팀은 9명의 선수가 있으며, 이 선수들은 포지션에 따라 수비 시 다양한 역할을 맡습니다. 공격 시에는 타자로 나서서 공을 치고 출루를 시도합니다.
경기 진행
경기는 9회까지 진행되며, 각 회마다 양 팀이 한 번씩 공격과 수비를 번갈아 가집니다. 9회가 끝난 후 더 많은 점수를 획득한 팀이 승리합니다.
타격과 출루
타자는 투수가 던진 공을 쳐서 1루, 2루, 3루를 거쳐 홈으로 돌아와 점수를 획득합니다. 타자가 1루에 안전하게 도착하면 출루 성공이며, 타자는 계속해서 진루를 시도합니다.
아웃
타자가 아웃되는 경우는 다음과 같습니다:
삼진 아웃: 타자가 세 번의 스트라이크를 당했을 때
플라이 아웃: 타자가 친 공이 공중에서 수비수에게 잡혔을 때
태그 아웃: 주자가 공을 가진 수비수에게 태그당했을 때
득점
타자가 1루, 2루, 3루를 거쳐 홈으로 돌아오면 1점을 득점합니다. 공격 팀은 더 많은 점수를 내기 위해 여러 타자가 출루하고 홈으로 들어오는 전략을 사용합니다.
CharacterTextSplitter
샘플 소스
from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import CharacterTextSplitter
def print_docs(documents):
for i, doc in enumerate(documents):
print(f"{doc.page_content}")
print(f"--------------- len: {len(doc.page_content)} ------------")
filename = "data/baseball.txt"
loader = TextLoader(filename)
document = loader.load()
text_splitter = CharacterTextSplitter(chunk_size=100, chunk_overlap=0)
docs = text_splitter.split_documents(document)
print_docs(docs)
chunk_size=100 으로 짜르면
--------------- len: 91 ------------
경기 목적 야구 경기는 두 팀이 번갈아 가며 공격과 수비를 수행하여 점수를 내는 게임입니다. 공격 팀은 공을 치고, 수비 팀은 타자를 아웃시켜 공수 교대를 합니다.
--------------- len: 88 ------------
팀 구성
각 팀은 9명의 선수가 있으며, 이 선수들은 포지션에 따라 수비 시 다양한 역할을 맡습니다. 공격 시에는 타자로 나서서 공을 치고 출루를 시도합니다.
----------------------------------------
...
chunk_size=1이면?
--------------- len: 91 ------------
경기 목적 야구 경기는 두 팀이 번갈아 가며 공격과 수비를 수행하여 점수를 내는 게임입니다. 공격 팀은 공을 치고, 수비 팀은 타자를 아웃시켜 공수 교대를 합니다.
--------------- len: 88 ------------
팀 구성
각 팀은 9명의 선수가 있으며, 이 선수들은 포지션에 따라 수비 시 다양한 역할을 맡습니다. 공격 시에는 타자로 나서서 공을 치고 출루를 시도합니다.
...
100과 동일하다. 그러면 chunk_size는 무엇을 의미할까? 200으로 해보자.
--------------- len: 181 ------------
경기 목적 야구 경기는 두 팀이 번갈아 가며 공격과 수비를 수행하여 점수를 내는 게임입니다. 공격 팀은 공을 치고, 수비 팀은 타자를 아웃시켜 공수 교대를 합니다.
팀 구성
각 팀은 9명의 선수가 있으며, 이 선수들은 포지션에 따라 수비 시 다양한 역할을 맡습니다. 공격 시에는 타자로 나서서 공을 치고 출루를 시도합니다.
--------------- len: 192 ------------
경기 진행
경기는 9회까지 진행되며, 각 회마다 양 팀이 한 번씩 공격과 수비를 번갈아 가집니다. 9회가 끝난 후 더 많은 점수를 획득한 팀이 승리합니다.
타격과 출루
타자는 투수가 던진 공을 쳐서 1루, 2루, 3루를 거쳐 홈으로 돌아와 점수를 획득합니다. 타자가 1루에 안전하게 도착하면 출루 성공이며, 타자는 계속해서 진루를 시도합니다.
----------------------------------------
...
200으로 하면 chunk가 달라진다.
즉, 짤리는 최소단위는 \n\n
이고 최대 단위는 chunk_size
이다.
chunk_overlap을 10으로 하면?
chunk_overlap을 10으로 하고 실행해보자. (chunk_size=100)
--------------- len: 91 ------------
경기 목적 야구 경기는 두 팀이 번갈아 가며 공격과 수비를 수행하여 점수를 내는 게임입니다. 공격 팀은 공을 치고, 수비 팀은 타자를 아웃시켜 공수 교대를 합니다.
--------------- len: 88 ------------
팀 구성
각 팀은 9명의 선수가 있으며, 이 선수들은 포지션에 따라 수비 시 다양한 역할을 맡습니다. 공격 시에는 타자로 나서서 공을 치고 출루를 시도합니다.
...
chunk_overlap을 0으로 한 것과 동일하다. 그럼 왜 사용할까?
테스트를 위해 chunk_size=200으로 하고, chunk_overlap을 100으로 해보자.
(chunk_size=200, chunk_overlap=0)
--------------- len: 181 ------------
경기 목적 야구 경기는 두 팀이 번갈아 가며 공격과 수비를 수행하여 점수를 내는 게임입니다. 공격 팀은 공을 치고, 수비 팀은 타자를 아웃시켜 공수 교대를 합니다.
팀 구성
각 팀은 9명의 선수가 있으며, 이 선수들은 포지션에 따라 수비 시 다양한 역할을 맡습니다. 공격 시에는 타자로 나서서 공을 치고 출루를 시도합니다.
--------------- len: 192 ------------
경기 진행
경기는 9회까지 진행되며, 각 회마다 양 팀이 한 번씩 공격과 수비를 번갈아 가집니다. 9회가 끝난 후 더 많은 점수를 획득한 팀이 승리합니다.
타격과 출루
타자는 투수가 던진 공을 쳐서 1루, 2루, 3루를 거쳐 홈으로 돌아와 점수를 획득합니다. 타자가 1루에 안전하게 도착하면 출루 성공이며, 타자는 계속해서 진루를 시도합니다.
(chunk_size=200, chunk_overlap=100)
--------------- len: 181 ------------
경기 목적 야구 경기는 두 팀이 번갈아 가며 공격과 수비를 수행하여 점수를 내는 게임입니다. 공격 팀은 공을 치고, 수비 팀은 타자를 아웃시켜 공수 교대를 합니다.
팀 구성
각 팀은 9명의 선수가 있으며, 이 선수들은 포지션에 따라 수비 시 다양한 역할을 맡습니다. 공격 시에는 타자로 나서서 공을 치고 출루를 시도합니다.
--------------- len: 176 ------------
팀 구성
각 팀은 9명의 선수가 있으며, 이 선수들은 포지션에 따라 수비 시 다양한 역할을 맡습니다. 공격 시에는 타자로 나서서 공을 치고 출루를 시도합니다.
경기 진행
경기는 9회까지 진행되며, 각 회마다 양 팀이 한 번씩 공격과 수비를 번갈아 가집니다. 9회가 끝난 후 더 많은 점수를 획득한 팀이 승리합니다.
...
위에서 팀 구성
의 문장이 중복해서 표시되는 것을 확인할 수 있다. 위에서 짤린 내용을 보면 팀 구성
의 문장은 88자
이다.
그럼 chunk_overlap을 87로 해보자.
(chunk_size=200, chunk_overlap=100)
--------------- len: 181 ------------
경기 목적 야구 경기는 두 팀이 번갈아 가며 공격과 수비를 수행하여 점수를 내는 게임입니다. 공격 팀은 공을 치고, 수비 팀은 타자를 아웃시켜 공수 교대를 합니다.
팀 구성
각 팀은 9명의 선수가 있으며, 이 선수들은 포지션에 따라 수비 시 다양한 역할을 맡습니다. 공격 시에는 타자로 나서서 공을 치고 출루를 시도합니다.
--------------- len: 192 ------------
경기 진행
경기는 9회까지 진행되며, 각 회마다 양 팀이 한 번씩 공격과 수비를 번갈아 가집니다. 9회가 끝난 후 더 많은 점수를 획득한 팀이 승리합니다.
타격과 출루
타자는 투수가 던진 공을 쳐서 1루, 2루, 3루를 거쳐 홈으로 돌아와 점수를 획득합니다. 타자가 1루에 안전하게 도착하면 출루 성공이며, 타자는 계속해서 진루를 시도합니다.
팀 구성
문장이 중복되지 않는 것을 확인할 수 있다.
여기서 chunk_overlap가 88자 이상이면 중복해서 표시되고 이하면 중복으로 표시되지 않는다. 즉, chunk_overlap도 단\n\n
으로 짤린 문장이 기준이 된다는 뜻이다.
CharacterTextSplitter 정리
- 우선, 문서를
\n\n
으로 split을 한다. - split 문서가 chunk_size가 넘지 않으면 합쳐진다.
- chunk_size가 아무리 작아도
\n\n
으로 짜른 단위가 최소 chunk가 된다. - chunk_overlap의 기준은 글자 단위가 아니라
\n\n
으로 짜른 문단 기준이 된다.
RecursiveCharacterTextSplitter
샘플 소스
from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import (
RecursiveCharacterTextSplitter,
)
def print_docs(documents):
for i, doc in enumerate(documents):
print(f"--------------- len: {len(doc.page_content)} ------------")
print(f"{doc.page_content}")
filename = "data/baseball.txt"
loader = TextLoader(filename)
document = loader.load()
text_splitter = RecursiveCharacterTextSplitter(chunk_size=100, chunk_overlap=0)
docs = text_splitter.split_documents(document)
print_docs(docs)
chunk_size=100으로 짤라보자.
(chunk_size=100, chunk_overlap=0)
--------------- len: 91 ------------
경기 목적 야구 경기는 두 팀이 번갈아 가며 공격과 수비를 수행하여 점수를 내는 게임입니다. 공격 팀은 공을 치고, 수비 팀은 타자를 아웃시켜 공수 교대를 합니다.
--------------- len: 88 ------------
팀 구성
각 팀은 9명의 선수가 있으며, 이 선수들은 포지션에 따라 수비 시 다양한 역할을 맡습니다. 공격 시에는 타자로 나서서 공을 치고 출루를 시도합니다.
--------------- len: 86 ------------
경기 진행
경기는 9회까지 진행되며, 각 회마다 양 팀이 한 번씩 공격과 수비를 번갈아 가집니다. 9회가 끝난 후 더 많은 점수를 획득한 팀이 승리합니다.
--------------- len: 6 ------------
타격과 출루
--------------- len: 97 ------------
타자는 투수가 던진 공을 쳐서 1루, 2루, 3루를 거쳐 홈으로 돌아와 점수를 획득합니다. 타자가 1루에 안전하게 도착하면 출루 성공이며, 타자는 계속해서 진루를 시도합니다.
--------------- len: 25 ------------
아웃
타자가 아웃되는 경우는 다음과 같습니다:
--------------- len: 96 ------------
삼진 아웃: 타자가 세 번의 스트라이크를 당했을 때
플라이 아웃: 타자가 친 공이 공중에서 수비수에게 잡혔을 때
태그 아웃: 주자가 공을 가진 수비수에게 태그당했을 때
득점
--------------- len: 91 ------------
타자가 1루, 2루, 3루를 거쳐 홈으로 돌아오면 1점을 득점합니다. 공격 팀은 더 많은 점수를 내기 위해 여러 타자가 출루하고 홈으로 들어오는 전략을 사용합니다.
문단 > 문장 > 단어로 짜르는데, 위에서 6자
로 짤린 경우도 있다.(타격과 출루)
타격과 출루
타자는 투수가 던진 공을 쳐서 1루, 2루, 3루를 거쳐 홈으로 돌아와 점수를 획득합니다. 타자가 1루에 안전하게 도착하면 출루 성공이며, 타자는 계속해서 진루를 시도합니다.
문단으로 짤린 경우 100자가 넘으면 다시 문장으로 짜르는데 위의 예는 문단이 100자가 넘으므로 문장으로 짤린다. 그래서 2개로 분리된 것이다. 만일 문장이 100자가 넘으면 100자로 짤리게 된다.
다시 chunk_size를 200으로 짤라보자.
(chunk_size=200, chunk_overlap=0)
--------------- len: 181 ------------
경기 목적 야구 경기는 두 팀이 번갈아 가며 공격과 수비를 수행하여 점수를 내는 게임입니다. 공격 팀은 공을 치고, 수비 팀은 타자를 아웃시켜 공수 교대를 합니다.
팀 구성
각 팀은 9명의 선수가 있으며, 이 선수들은 포지션에 따라 수비 시 다양한 역할을 맡습니다. 공격 시에는 타자로 나서서 공을 치고 출루를 시도합니다.
--------------- len: 192 ------------
경기 진행
경기는 9회까지 진행되며, 각 회마다 양 팀이 한 번씩 공격과 수비를 번갈아 가집니다. 9회가 끝난 후 더 많은 점수를 획득한 팀이 승리합니다.
타격과 출루
타자는 투수가 던진 공을 쳐서 1루, 2루, 3루를 거쳐 홈으로 돌아와 점수를 획득합니다. 타자가 1루에 안전하게 도착하면 출루 성공이며, 타자는 계속해서 진루를 시도합니다.
--------------- len: 25 ------------
아웃
타자가 아웃되는 경우는 다음과 같습니다:
--------------- len: 188 ------------
삼진 아웃: 타자가 세 번의 스트라이크를 당했을 때
플라이 아웃: 타자가 친 공이 공중에서 수비수에게 잡혔을 때
태그 아웃: 주자가 공을 가진 수비수에게 태그당했을 때
득점
타자가 1루, 2루, 3루를 거쳐 홈으로 돌아오면 1점을 득점합니다. 공격 팀은 더 많은 점수를 내기 위해 여러 타자가 출루하고 홈으로 들어오는 전략을 사용합니다.
200자가 넘지 않게 문장을 짜른 것을 확인할 수 있다.
chunk_overlap을 100으로 해보자.
(chunk_size=200, chunk_overlap=100)
--------------- len: 181 ------------
경기 목적 야구 경기는 두 팀이 번갈아 가며 공격과 수비를 수행하여 점수를 내는 게임입니다. 공격 팀은 공을 치고, 수비 팀은 타자를 아웃시켜 공수 교대를 합니다.
팀 구성
각 팀은 9명의 선수가 있으며, 이 선수들은 포지션에 따라 수비 시 다양한 역할을 맡습니다. 공격 시에는 타자로 나서서 공을 치고 출루를 시도합니다.
--------------- len: 176 ------------
팀 구성
각 팀은 9명의 선수가 있으며, 이 선수들은 포지션에 따라 수비 시 다양한 역할을 맡습니다. 공격 시에는 타자로 나서서 공을 치고 출루를 시도합니다.
경기 진행
경기는 9회까지 진행되며, 각 회마다 양 팀이 한 번씩 공격과 수비를 번갈아 가집니다. 9회가 끝난 후 더 많은 점수를 획득한 팀이 승리합니다.
--------------- len: 192 ------------
경기 진행
경기는 9회까지 진행되며, 각 회마다 양 팀이 한 번씩 공격과 수비를 번갈아 가집니다. 9회가 끝난 후 더 많은 점수를 획득한 팀이 승리합니다.
타격과 출루
타자는 투수가 던진 공을 쳐서 1루, 2루, 3루를 거쳐 홈으로 돌아와 점수를 획득합니다. 타자가 1루에 안전하게 도착하면 출루 성공이며, 타자는 계속해서 진루를 시도합니다.
--------------- len: 25 ------------
아웃
타자가 아웃되는 경우는 다음과 같습니다:
--------------- len: 188 ------------
삼진 아웃: 타자가 세 번의 스트라이크를 당했을 때
플라이 아웃: 타자가 친 공이 공중에서 수비수에게 잡혔을 때
태그 아웃: 주자가 공을 가진 수비수에게 태그당했을 때
득점
타자가 1루, 2루, 3루를 거쳐 홈으로 돌아오면 1점을 득점합니다. 공격 팀은 더 많은 점수를 내기 위해 여러 타자가 출루하고 홈으로 들어오는 전략을 사용합니다.
팀 구성
이 중복해서 표시되는 것을 확인할 수 있다.
그림으로 표현
위의 문서를 RecursiveCharacterTextSplitter으로 짜르는 과정을 그림으로 표현해봤다.
1. \n\n
으로 짜른다.
2. \n\n
으로 짜른 결과
3. \n\n
기준으로 짜른 결과가 chunk_size
가 넘는 문서는 \n
기준으로 다시 짜른다
최종 결과
정리
- 우선
\n\n
기준으로 문서를 split한다. - split한 문장이 chunk_size보다 크면
\n
기준으로 짜른다. \n
기준으로 짤린 문장이 chunk_size보다 크면" "
기준으로 짜른다." "
로 짜른 문장이 chunk_size보다 크면""
기준으로 짜른다.- 즉, separator 기준으로 chunk_size보다 작을 때 까지 계속 짜른다.