프로젝트 기록

[RAG] Chunk 품질 평가 방법

외양간찾는 새끼소 2024. 12. 23. 15:32
Chunk 품질 평가 방법을 바로 보고싶으신 분들은 3. Chunk 평가 방법 부터 읽어주세요.
실습을 위한 colab 파일 : https://colab.research.google.com/drive/1vMnXIFGPr9iFwwNKDAnrdw6Mm6-sg4MZ?usp=sharing


RAG 시스템을 구축할 때 고민해야 할 것들은 크게 3가지가 있습니다.
1. Chunk 구축 하기

2. 검색 품질 올리기

3. 생성 프롬프트 만들기

위 3가지 과정에서 가장 첫번째 단계인 Chunk 구축 단계, 그 안에서도 Chunk의 품질을 평가하는 방법에 대해 자세히 살펴보고자합니다.

1. Chunk의 필요성

나무위키 - 인공지능 설명 페이지


나무위키를 크롤링하여 RAG의 데이터로 사용한다고 가정해 봅시다.
한 페이지를 크롤링하면 위 사진과 같은 내용들이 크롤링 되어 텍스트로 저장되어 있을 것입니다.
해당 텍스트는 약인공지능이 무엇인지, 강인공지능이 무엇인지, 인공지능의 역사 등 인공지능에 대한 포괄적인 내용들을 가지고 있습니다.

만약 RAG시스템의 사용자가 "강인공지능이란 무엇인가요?"라고 물어봤다고 가정해 봅시다.
RAG 시스템은 사용자의 쿼리가 인공지능에 관련된것을 확인하고 위 데이터를 검색해서 가져올 것입니다. 그리고 해당 데이터를 그대로 프롬프트에 추가하여 질문하게 될 것입니다. 사용자는 강인공지능에 대해서만 물어봤는데 약인공지능, 인공지능의 역사 등에 대한 정보도 가진 텍스트를 받게 되는 것입니다. 심지어 강인공지능에 대한 내용보다 더 방대한 다른 정보들이 있는 텍스트를 받게 됩니다.

이렇게 되면 인공지능에게 힌트를 주기보다는 혼동을 주게 되며 오히려 성능을 떨어트리는 방향으로 출력할 수도 있습니다.
따라서 이러한 문제를 해결하기 위해서 주제가 섞이지 않는 단위로 자르는 Chunk가 중요하게 작용됩니다.


2. Chunk 구축 방법

크게 두가지 방법이 있습니다. 의미단위로 자르는 방법과 카운트 기반으로 자르는 방법이 있습니다. 이에 대해 내용은 매우 방대하여 다음 기회에 따로 정리해 보도록 하겠습니다. 


3. Chunk 평가 방법

Chunk를 자르긴 했는데 이게 잘 나눈건지 잘못 나눈건지 어떻게 판단 할 수 있을까요? 
가장 간단한 방법은 '정성적 평가'를 하는 것입니다. 즉, 직접 읽어보고 판단하는 것입니다. 이러한 평가방법은 매우 단순하고 무식해 보일 수 있지만 정말 좋은 평가 방법입니다. 하지만 사람이 직접해야 한다는 것에 주관이 들어갈 수 있고, 시간이 오래걸리는 단점이 있습니다. 특히, 구축하고자 하는 RAG시스템이 domain specific하다면 해당 domain의 전문가가 아니라면 정성적 평가가 어려울 수 있습니다.

이러한 정성적 평가방법의 단점을 극복하고자 제안하는 '정량적 평가' 방법을 소개드립니다. 3가지 측면에서 Chunk의 품질을 평가할 수 있습니다. 

3-1. 토큰 수 분석하기

 첫 번째로 토큰 수 분석하기 입니다. Chunk는 비슷한 볼륨(= 텍스트의 길이를 의미합니다.)으로 나눠져 있는 것이 좋습니다. 특히, 볼륨이 너무 많이 가지고 있는 데이터는 여러 정보가 혼재되어 있을 가능성이 높습니다. 따라서 토큰 수로 데이터들을 분석해보고 IQR을 사용해서 볼륨이 너무 큰 이상치들을 확인 할 수 있습니다. 

# 텍스트 토큰 수 카운트 함수
def count_tokens(sample):
    return len(tokenizer.tokenize(sample['content']))

df['token_count'] = df.apply(count_tokens, axis=1)

제가 가지고 있는 예시데이터를 사용하여 텍스트의 토큰을 세고, 분포와 IQR을 통해 이상치들 까지 확인해본 그래프입니다. 자세한 구현 코드는 글 처음에 있는 colab파일을 참고해 주세요.

토큰 수 분석 시각화 그래프

데이터가 많으면 이상치라고 판단되는 부분을 과감하게 삭제 할 수도 있습니다. 하지만 저의 예시 데이터는 약 4200개 정도의 작은 데이터 이므로 이상치라고 판단되는 데이터를 정성평가를 거쳐 한 번더 확인하고 정성평가에서도 이상하다고 판단되는 것들을 삭제하거나 Chunk를 새로 나누는 등의 조치를 취할 수 있습니다.

3-2. 문장 유사도 분석하기

 두 번째는 문장 유사도를 분석하기 입니다. Chunk를 나누는 목적은 같은 내용을 다루는 텍스트로 쪼개어 정보의 혼재를 막기위한 것입니다. 첫 번째 방법은 Chunk의 의미적 차원의 분석은 전혀 담겨 있지 않습니다. 실제로 텍스트가 길다고 하더라도 정보의 혼재가 없을 수도 있고 텍스트가 짧다고 하더라도 정보의 혼재가 있을 수 있습니다. 따라서 실제로 비슷한 내용으로 구축되어 있는지 의미적 차원에서 분석하는 방법으로 Chunk를 구성하는 문장의 유사도를 분석해보는 방법이 있습니다.

방법론을 설명하면
1. Chunk를 문장 단위로 자르고
2. 각 문장 쌍 별로 Cosin 유사도를 측정하여
3. 평균을 내어 문장 유사도 점수를 추출합니다.
코드와 분석 결과 그래프는 아래와 같습니다.
 

from sentence_transformers import SentenceTransformer, util
import torch
from tqdm import tqdm
import re

# SBERT가 문장 쌍의 유사도 측정에 특화되어 있습니다.
model = SentenceTransformer('snunlp/KR-SBERT-V40K-klueNLI-augSTS').to(device)
tqdm.pandas()

# Chunk를 문장단위로 나누는 함수
def split_sentence(text):
    sentences = re.split(r'[.!?]', text)
    return [sent.strip() for sent in sentences if sent.strip() != '']

# 문장 쌍의 유사도를 측정하는 함수
def similarity_score(sample):
    content = sample['content']
    sentences = split_sentence(content)

    embeddings = model.encode(sentences, convert_to_tensor=True).to(device)

    similarity_scores = []

    for i in range(len(sentences)):
        for j in range(i+1, len(sentences)):
            similarity = util.pytorch_cos_sim(embeddings[i], embeddings[j])
            similarity_scores.append(similarity.item())

    chunk_similarity = np.mean(similarity_scores)

    return chunk_similarity
    
df['chunk_similarity_score'] = df.progress_apply(similarity_score, axis=1)

 문장 유사도가 높은 것은 비슷한 의미를 가진 문장들로 구성된 것이라 판단 할 수 있으니 상위 이상치들이 존재하더라도 이상치라고 볼 필요는 없습니다. 하지만 문장 유사도가 낮은 부분은 서로 의미가 유사하지 않은 문장들로 구성되어 있으니 이상치 데이터로 판별할 수 있습니다. IQR은 존재하는 데이터의 상대적 평가를 할때 이상치를 구분하는 기준이 될 수 있지만 해당 경우는 상대적인 평가보다는 절대적인 기준으로 이상치를 판단하는 것이 적합하다고 생각합니다. 즉, 유사도가 0.4이하의 데이터들을 이상치로 규정하고 데이터를 분석할 필요가 있습니다.
이미 첫번째 방법에서도 언급했지만 데이터가 많으면 이상치로 규정되는 데이터들을 삭제 할 수 도있고 데이터가 적으면 정성평가를 거쳐 데이터를 삭제하든 Chunk를 새로 나누는 방법으로 처리할 수 있습니다.

3-3. 토픽 일관성 분석하기

 마지막 세 번째 분석인 토픽 일관성 분석입니다. 주관적으로 가장 중요하고 신뢰할 만한 정량평가라고 생각합니다.
하나의 Chunk에 토픽들이 얼마나 혼재되어 있는지 분석하고 토픽의 무질서도(엔트로피)를 계산하는 방법입니다. 이를 위해서 LDA 토픽 추출모델을 활용합니다. LDA 개념에 대해 잘 모르는 분들은 꼭 LDA에 대해 충분히 이해하고 읽어주세요.(따로 설명이 없어요...)
 LDA 모델을 학습 시키면 Chunk의 토픽의 분포를 추출 할 수 있습니다. 예로 Chunk1은 topic1 : 90%, topic2 : 8%, topic3 : 2%로 구성되어 있고 Chunk2는 topic1 : 70%, topic2: 20%, topic3 : 10% 구성되어 있다고 합시다. 그렇다면 토픽이 일관적인 Chunk1이 더 품질이 좋은 Chunk로 판단 할 수 있을 것입니다. 하지만 우리는 정확한 수치로 표현하기 위해서 무질서도(엔트로피) 공식을 이용합니다.

정보 무질서도(엔트로피) 공식

해당 값은 정보가 얼마나 혼재되어있는지 수치화 할 수 있는 공식입니다. 어려운 공식으로 되어 있지만 실제로 계산과정은 매우 단순합니다.  예시의 토픽 무질서도를 계산하면 아래와 같습니다. 

chunk_1_entropy = -(0.9 * log0.9 + 0.08 * log0.08 + 0.02 * log0.02)= 0.375122
chunk_2_entropy = -(0.7 * log0.7 + 0.2 * log0.2 + 0.1 * log0.1)= 0.801816

Chunk1보다 Chunk2가 무질서도가 더 크므로 더 무질서 하다고 판단 할 수 있습니다. 즉, Chunk2가 토픽이 더 무질서하게 있다고 판단할 수 있습니다.


 토픽 일관성 측정하는 과정을 코드로 보면 아래와 같습니다.

# 1. 문서 전처리
okt = Okt()
raw_documents = df['content'].tolist()

def preprocess(text):
    morphemes = okt.pos(text)
    result = [word for word, pos in morphemes if pos in ['Noun', 'Adjective', 'Verb']]
    return result

documents = [preprocess(doc) for doc in tqdm(raw_documents)]

# 2. Vocab 만들기
dictionary = corpora.Dictionary(documents)

# 3. copora 만들기
corpus = [dictionary.doc2bow(doc) for doc in documents]

# 4. LDA 모델 훈련
num_topics = 400
lda_model = LdaModel(corpus = corpus,
                     id2word = dictionary,
                     num_topics = num_topics,
                     random_state = 42,
                     update_every = 1,
                     chunksize = 100,
                     passes = 10,
                     alpha = 'auto',
                     per_word_topics = True)
                     
# 5. 토픽 엔트로피 측정
def get_document_topics(doc_corpus):
    topic_distribution = lda_model.get_document_topics(doc_corpus)
    sorted_topics = sorted(topic_distribution, key=lambda x: x[1], reverse = True)

    return sorted_topics

def calculate_entropy(topic_distribution):
    probabilities = [prob for topic_id, prob in topic_distribution if prob > 0]
    entropy = -sum(p * math.log2(p) for p in probabilities)
    return entropy

entropies = []
for corp in corpus:
    topic_distribution = get_document_topics(corp)
    entropy = calculate_entropy(topic_distribution)
    entropies.append(entropy)

df['topic_entropy'] = entropies

 

예시 데이터에서는 전반적으로 토픽 불확실도(엔트로피)가 낮은것을 확인할 수 있습니다. 
해당 데이터도 IQR을 기준으로 상대적 이상치를 뽑기 보다는 절대적 수치를 정해서 이상치 데이터를 골라내는 것이 좋습니다. 예시 코드에서는 4.2 이상의 데이터들을 뽑아서 정성평가를 하는 과정을 추가했습니다.


4. Future Work

정량 평가를 하는 수치 3가지를 소개했습니다. 하지만 아직 부족한 부분들이 있어 해당 내용들을 정리해보고자 합니다.

4-1. LDA대신 TopicBERT 이용하기

 LDA는 문맥을 고려없이 카운트 기법으로 토픽을 선정합니다. 또한 토픽을 몇개 뽑을지 사용자가 지정해야해서 데이터 수, 데이터의 종류에 따라 적절한 토픽의 수를 찾기가 매우 힘듭니다. 이 뿐만 아니라 전처리를 어떻게 하는지에 따라서 많은 단어들이 누락될 수도 있고, 어떤 형태소 분석기를 쓰느냐에 따라 성능이 달라질 수 있습니다. LDA의 수 많은 단점을 개선하고자 만들어진 TopicBERT가 존재합니다. 해당 내용을 깊게 학습 후에 3. 토픽 일관성 분석에 적용하면 좀 더 좋은 분석이 될것입니다.

 

4-2. 명확한 수치 제시하기

2. 문장 유사도 분석하기 와 3. 토픽 일관성 분석하기 과정에서 어느정도로 유사도가 낮아야 이상치로 판단해야하는지 어느정도로 토픽 무질서도(엔트로피) 수치가 나와야 토픽의 일관성이 없다고 판단하는지에 대한 명확한 수치를 제시하지 못했습니다. 더 많은 데이터, 특히 chunk가 잘 나눠진 데이터와 잘 못나눠진 데이터를 비교하면서 수치를 비교하면서 명확한 수치를 제시 할 수 있어야 할 것 같습니다.

4-3. 자동화 코드 만들기

text를 입력으로 받으면 알아서 해당 텍스트가 RAG 시스템을 위한 Chunk로 알맞는지 분석하는 자동화 시스템을 구축하면 앞으로의 RAG시스템 구축에서 많은 도움이 될 것 같습니다.