[Langchain] Text Splitter

Hunie_07·2025년 3월 28일
0

Langchain

목록 보기
7/35

📌 텍스트 분할 (Text Splitting)

  • 대규모 텍스트 문서를 처리할 때 매우 중요한 전처리 단계
  • 고려사항:
    1. 문서의 구조와 형식
    2. 원하는 청크 크기
    3. 문맥 보존의 중요도
    4. 처리 속도

문서 로드

from langchain_community.document_loaders import PyPDFLoader

# PDF 로더 초기화
pdf_loader = PyPDFLoader('./data/transformer.pdf')

# 동기 로딩
pdf_docs = pdf_loader.load()
print(f'PDF 문서 개수: {len(pdf_docs)}')

- 출력

PDF 문서 개수: 15

print(f'첫 번째 문서: {pdf_docs[1]}')

- 출력

첫 번째 문서: page_content='1 Introduction
Recurrent neural networks, long short-term ...

long_text = pdf_docs[1].page_content
print(f'첫 번째 문서의 텍스트 길이: {len(long_text)}')

- 출력

첫 번째 문서의 텍스트 길이: 4257


1️⃣ CharacterTextSplitter

  • 가장 기본적인 분할 방식

  • 문자 수를 기준으로 텍스트를 분할

  • 단순하지만 문맥을 고려하지 않는다는 단점이 있음

  • 설치: pip install langchain_text_splitters / poetry add langchain_text_splitters

from langchain_text_splitters import CharacterTextSplitter 

# 텍스트 분할기 초기화 (기본 설정값 적용 )
text_splitter = CharacterTextSplitter(
    # CharacterTextSplitter의 기본 설정값
    separator = "\n\n",          # 청크 구분자: 두 개의 개행문자
    is_separator_regex = False,  # 구분자가 정규식인지 여부

    # TextSplitter의 기본 설정값
    chunk_size = 4000,          # 청크 길이
    chunk_overlap = 200,        # 청크 중첩
    length_function = len,      # 길이 함수 (문자열 길이)
    keep_separator = False,     # 구분자 유지 여부
    add_start_index = False,    # 시작 인덱스 추가 여부
    strip_whitespace = True,    # 공백 제거 여부
)

# 텍스트 분할 - split_text() 메서드 사용
texts = text_splitter.split_text(long_text)

# 분할된 텍스트 개수 출력
print(f'분할된 텍스트 개수: {len(texts)}')

# 첫 번째 분할된 텍스트 출력
print(f'첫 번째 분할된 텍스트: {texts[0]}')

- 출력

분할된 텍스트 개수: 1
첫 번째 분할된 텍스트: 1 Introduction
Recurrent neural networks, long short ...

  • 구분자 및 청크 길이 등 파라미터 변경
from langchain_text_splitters import CharacterTextSplitter

# 문장 구분자를 개행문자로 설정
text_splitter = CharacterTextSplitter(
    separator = "\n",        # 청크 구분자: 개행문자
    chunk_size = 1000,       # 청크 길이
    chunk_overlap = 200      # 청크 중첩
)

# split_documents() 메서드 사용 : Document 객체를 여러 개의 작은 청크 문서로 분할
chunks = text_splitter.split_documents([pdf_docs[1]])   # 첫 번째 문서만 분할

# 분할된 텍스트 개수 출력
print(f'분할된 텍스트 개수: {len(chunks)}')

# 각 청크의 텍스트 길이 출력
for i, chunk in enumerate(chunks):
    print(f'청크 {i+1}의 텍스트 길이: {len(chunk.page_content)}')

# 첫 번째 청크의 텍스트 출력
print(f'첫 번째 청크의 텍스트: {chunks[0].page_content}')

- 출력

분할된 텍스트 개수: 6
청크 1의 텍스트 길이: 933
청크 2의 텍스트 길이: 995
청크 3의 텍스트 길이: 902
청크 4의 텍스트 길이: 907
청크 5의 텍스트 길이: 996
청크 6의 텍스트 길이: 385
첫 번째 청크의 텍스트: 1 Introduction
Recurrent neural networks, long short-term memory [13] and gated recurrent [7] neural networks
... has achieved

2️⃣ RecursiveCharacterTextSplitter

  • 재귀적으로 텍스트를 분할
  • 구분자를 순차적으로 적용하여 큰 청크에서 시작하여 점진적으로 더 작은 단위로 분할
  • 문맥을 더 잘 보존할 수 있음
from langchain_text_splitters import RecursiveCharacterTextSplitter

# 재귀적 텍스트 분할기 초기화
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,             # 청크 크기  
    chunk_overlap=200,           # 청크 중 중복되는 부분 크기
    length_function=len,         # 글자 수를 기준으로 분할
    separators=["\n\n", "\n", " ", ""],  # 구분자 - 재귀적으로 순차적으로 적용 
)

# split_documents() 메서드 사용 : Document 객체를 여러 개의 작은 청크 문서로 분할
chunks = text_splitter.split_documents(pdf_docs)
print(f"생성된 텍스트 청크 수: {len(chunks)}")
print(f"각 청크의 길이: {list(len(chunk.page_content) for chunk in chunks)}")
print()

# 각 청크의 시작 부분과 끝 부분 확인 - 5개 청크만 출력
for chunk in chunks[:5]:
    print(chunk.page_content[:200])
    print("-" * 100)
    print(chunk.page_content[-200:])
    print("=" * 100)
    print()

- 출력

생성된 텍스트 청크 수: 52
각 청크의 길이: [986, 910, 975, 452, 933, 995, 902, 907, 996, 385, 924, 954, 216, 923, 900, 950, 992, 913, 908, 870, 945, 975, 946, 997, 196, 980, 980, 946, 938, 999, 943, 920, 734, 958, 946, 945, 617, 983, 988, 994, 624, 944, 909, 940, 913, 983, 924, 924, 845, 812, 815, 818]

Provided proper attribution is provided, Google hereby grants permission to
reproduce the tables and figures in this paper solely for use in journalistic or
scholarly works.
Attention Is All You Need

----------------------------------------------------------------------------------------------------
the encoder and decoder through an attention 
...

3️⃣ 정규표현식 사용

from langchain_text_splitters import CharacterTextSplitter

# 문장을 구분하여 분할 - 정규표현식 사용 (문장 구분자: 마침표, 느낌표, 물음표 다음에 공백이 오는 경우)
text_splitter = CharacterTextSplitter(
    separator=r'(?<=[.!?])\s+',  # 각 Document 객체의 page_content 속성을 문장으로 분할
    chunk_size=1000,
    chunk_overlap=200,
    is_separator_regex=True,      # 구분자가 정규식인지 여부: True
    keep_separator=True,          # 구분자 유지 여부: True
)

# split_documents() 메서드 사용 : Document 객체를 여러 개의 작은 청크 문서로 분할
chunks = text_splitter.split_documents(pdf_docs)  # 모든 문서를 분할
print(f"생성된 텍스트 청크 수: {len(chunks)}")
print(f"각 청크의 길이: {list(len(chunk.page_content) for chunk in chunks)}")
print()

# 각 청크의 시작 부분과 끝 부분 확인 - 5개 청크만 출력
for chunk in chunks[:5]:
    print(chunk.page_content[:200])
    print("-" * 100)
    print(chunk.page_content[-200:])
    print("=" * 100)
    print()

- 출력

생성된 텍스트 청크 수: 52
각 청크의 길이: [996, 945, 986, 211, 908, 819, 796, 934, 962, 241, 999, 901, 842, 963, 972, 75, 789, 960, 853, 949, 995, 900, 938, 985, 302, 925, 945, 990, 755, 999, 914, 979, 854, 980, 944, 995, 415, 885, 964, 988, 714, 944, 918, 992, 907, 983, 994, 852, 873, 812, 815, 818]

Provided proper attribution is provided, Google hereby grants permission to
reproduce the tables and figures in this paper solely for use in journalistic or
scholarly works.
Attention Is All You Need

----------------------------------------------------------------------------------------------------
r and decoder through an attention
...

4️⃣ 토큰 수를 기반으로 분할

1. tiktoken

  • OpenAI에서 만든 BPE Tokenizer
# 첫번째 문서 객체의 텍스트 길이
len(pdf_docs[0].page_content)

- 출력

2859

from langchain_text_splitters import RecursiveCharacterTextSplitter

# TikToken 인코더를 사용하여 재귀적 텍스트 분할기 초기화
text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(
    encoding_name="cl100k_base", 
    model_name="gpt-4o-mini",
    chunk_size=300, 
    chunk_overlap=0,
)

# split_documents() 메서드 사용 : Document 객체를 여러 개의 작은 청크 문서로 분할
chunks = text_splitter.split_documents([pdf_docs[0]])  # 첫 번째 문서만 분할

print(f"생성된 청크 수: {len(chunks)}")
print(f"각 청크의 길이: {list(len(chunk.page_content) for chunk in chunks)}")

# 각 청크의 시작 부분과 끝 부분 확인
for chunk in chunks[:5]:
    print(chunk.page_content[:50])
    print("-" * 50)
    print(chunk.page_content[-50:])
    print("=" * 50)
    print()

- 출력

생성된 청크 수: 3
각 청크의 길이: [1145, 1374, 338]
Provided proper attribution is provided, Google he
--------------------------------------------------
ng more parallelizable and requiring significantly
==================================================

less time to train. Our model achieves 28.4 BLEU o
--------------------------------------------------
countless long days designing various parts of and
==================================================

implementing tensor2tensor, replacing our earlier 
--------------------------------------------------
, CA, USA.
arXiv:1706.03762v7  [cs.CL]  2 Aug 2023
==================================================

import tiktoken

# tokenizer = tiktoken.get_encoding("cl100k_base")
tokenizer = tiktoken.encoding_for_model("gpt-4o-mini")

for chunk in chunks[:5]:

    # 각 청크를 토큰화
    tokens = tokenizer.encode(chunk.page_content)
    # 각 청크의 단어 수 확인
    print(len(tokens))
    # 각 청크의 토큰화 결과 확인 (첫 10개 토큰만 출력)
    print(tokens[:10])
    # 토큰 ID를 실제 토큰(문자열)로 변환해서 출력
    token_strings = [tokenizer.decode([token]) for token in tokens[:10]]
    print(token_strings)

    print("=" * 50)
    print()

- 출력

280
[110436, 7937, 118839, 382, 5181, 11, 5800, 43378, 36800, 14158]
['Provided', ' proper', ' attribution', ' is', ' provided', ',', ' Google', ' hereby', ' grants', ' permission']
==================================================

289
[2695, 1058, 316, 8513, 13, 5339, 2359, 136969, 220, 2029]
['less', ' time', ' to', ' train', '.', ' Our', ' model', ' achieves', ' ', '28']
==================================================

83
[105849, 289, 33686, 17, 102370, 11, 39866, 1039, 11965, 3490]
['implement', 'ing', ' tensor', '2', 'tensor', ',', ' replacing', ' our', ' earlier', ' code']
==================================================

2. HuggingFace 토크나이저

  • Hugging Face tokenizer 모델의 토큰 수를 기준으로 분할
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("BAAI/bge-m3")

tokenizer

- 출력

XLMRobertaTokenizerFast(name_or_path='BAAI/bge-m3', vocab_size=250002, model_max_length=8192, is_fast=True, padding_side='right', truncation_side='right', special_tokens={'bos_token': '<s>', 'eos_token': '</s>', 'unk_token': '<unk>', 'sep_token': '</s>', 'pad_token': '<pad>', 'cls_token': '<s>', 'mask_token': '<mask>'}, clean_up_tokenization_spaces=True, added_tokens_decoder={
	0: AddedToken("<s>", rstrip=False, lstrip=False, single_word=False, normalized=False, special=True),
	1: AddedToken("<pad>", rstrip=False, lstrip=False, single_word=False, normalized=False, special=True),
	2: AddedToken("</s>", rstrip=False, lstrip=False, single_word=False, normalized=False, special=True),
	3: AddedToken("<unk>", rstrip=False, lstrip=False, single_word=False, normalized=False, special=True),
	250001: AddedToken("<mask>", rstrip=False, lstrip=True, single_word=False, normalized=False, special=True),
}
)

# 토크나이저 인코딩 - 문장을 토큰(ID)으로 변환
tokens = tokenizer.encode("안녕하세요. 반갑습니다.")
print(tokens)

- 출력

[0, 107687, 5, 20451, 54272, 16367, 5, 2]

# 토큰을 출력 (토큰 ID를 실제 토큰(문자열)로 변환)
print(tokenizer.convert_ids_to_tokens(tokens)) 

- 출력

['<s>', '▁안녕하세요', '.', '▁반', '갑', '습니다', '.', '</s>']

# 디코딩 - 토큰을 문자열로 변환
print(tokenizer.decode(tokens, skip_special_tokens=True))

- 출력

안녕하세요. 반갑습니다.

from langchain_text_splitters import RecursiveCharacterTextSplitter

# Huggingface 토크나이저를 사용하여 재귀적 텍스트 분할기 초기화
text_splitter = RecursiveCharacterTextSplitter.from_huggingface_tokenizer(
    tokenizer=tokenizer,
    chunk_size=300, 
    chunk_overlap=0,
)

# split_documents() 메서드 사용 : Document 객체를 여러 개의 작은 청크 문서로 분할
chunks = text_splitter.split_documents([pdf_docs[0]]) # 첫 번째 문서만 분할

print(f"생성된 청크 수: {len(chunks)}")
print(f"각 청크의 길이: {list(len(chunk.page_content) for chunk in chunks)}")
print()

for chunk in chunks[:5]:
    # 각 청크를 토큰화
    tokens = tokenizer.encode(chunk.page_content)
    # 각 청크의 단어 수 확인
    print(len(tokens))
    # 각 청크의 토큰화 결과 확인 (첫 10개 토큰만 출력)
    print(tokens[:10])
    # 토큰 ID를 실제 토큰(문자열)로 변환해서 출력
    token_strings = tokenizer.convert_ids_to_tokens(tokens[:10]) 
    print(token_strings)

    print("=" * 50)
    print()

- 출력

생성된 청크 수: 4
각 청크의 길이: [676, 1064, 1077, 39]

180
[0, 123089, 71, 27798, 99, 179236, 83, 62952, 4, 1815]
['<s>', '▁Provide', 'd', '▁proper', '▁at', 'tribution', '▁is', '▁provided', ',', '▁Google']
==================================================

241
[0, 158, 137089, 289, 108, 82451, 33120, 7, 450, 26698]
['<s>', '▁con', 'volution', 'al', '▁ne', 'ural', '▁network', 's', '▁that', '▁include']
==================================================

243
[0, 6, 246232, 647, 71723, 127752, 5, 32036, 214, 12989]
['<s>', '▁', '∗', 'E', 'qual', '▁contribution', '.', '▁List', 'ing', '▁order']
==================================================

19
[0, 187, 1542, 4371, 22950, 20773, 9513, 177200, 334, 966]
['<s>', '▁ar', 'X', 'iv', ':17', '06.', '03', '762', 'v', '7']
==================================================

5️⃣ Semantic Chunking

  • SemanticChunker는 텍스트를 의미 단위로 분할하는 특수한 텍스트 분할도구

  • 단순 길이 기반이 아닌 의미 기반으로 텍스트를 청크화하는데 효과적

  • breakpoint_threshold_type: Text Splitting의 다양한 임계값(Threshold) 설정 방식 (통계적 기법)

    • Gradient 방식: 임베딩 벡터 간의 기울기 변화를 기준으로 텍스트를 분할
    • Percentile 방식: 임베딩 거리의 백분위수를 기준으로 분할 지점을 결정
    • Standard Deviation 방식: 임베딩 거리의 표준편차를 활용하여 유의미한 변화점을 찾아서 분할
    • Interquartile 방식: 임베딩 거리의 사분위수 범위를 기준으로 이상치를 감지하여 분할
  • 설치: pip install langchain_experimental / poetry add langchain_experimental


from langchain_experimental.text_splitter import SemanticChunker 
from langchain_openai.embeddings import OpenAIEmbeddings

# 임베딩 모델을 사용하여 SemanticChunker를 초기화 
text_splitter = SemanticChunker(
    embeddings=OpenAIEmbeddings(model="text-embedding-3-small"),         # OpenAI 임베딩 사용
    breakpoint_threshold_type="gradient",  # 임계값 타입 설정 (gradient, percentile, standard_deviation, interquartile)
)
chunks = text_splitter.split_documents([pdf_docs[0]])

print(f"생성된 청크 수: {len(chunks)}")
print(f"각 청크의 길이: {list(len(chunk.page_content) for chunk in chunks)}")
print()

tokenizer = tiktoken.get_encoding("cl100k_base")

for chunk in chunks[:5]:
    # 각 청크를 토큰화
    tokens = tokenizer.encode(chunk.page_content)
    # 각 청크의 단어 수 확인
    print(len(tokens))
    # 각 청크의 내용을 확인
    print(chunk.page_content[:100])
    print("=" * 50)
    print()

- 출력

생성된 청크 수: 2
각 청크의 길이: [1741, 1117]

429
Provided proper attribution is provided, Google hereby grants permission to
reproduce the tables and
==================================================

238
∗Equal contribution. Listing order is random. Jakob proposed replacing RNNs with self-attention and 
==================================================

0개의 댓글