티스토리 뷰

반응형

단어를 chunk 단위로 분해한다.

BM25, Sentence Transformer로 하나의 문장에 대해서 검색 기능을 실행.

 

후에 모든 chunk를 활용해서 해보고, 검색 성능을 향상시킨다.

 

라이브러리 다운로드 및 임포트

!pip install -q -U transformers==4.38.2
!pip install -q -U datasets==2.18.0
!pip install -q -U bitsandbytes==0.42.0
!pip install -q -U peft==0.9.0
!pip install -q -U trl==0.7.11
!pip install -q -U accelerate==0.27.2
!pip install -q -U rank_bm25==0.2.2
!pip install -q -U sentence-transformers==2.7.0
!pip install -q -U wikiextractor==3.0.6
!pip install -q -U konlpy==0.6.0

 

 

import os
import glob
import json

import numpy as np
import pandas as pd
from tqdm.auto import tqdm

import torch
import konlpy
from rank_bm25 import BM25Okapi
from sentence_transformers import SentenceTransformer

 

한국어 위키 덤프 파일 다운로드 하기

덤프 파일을 다운로드 하고, extractor로 전부 풀어준다.

!python -m wikiextractor.WikiExtractor \
        --json \
        --out {WORKSPACE}/data/kowiki \
        {WORKSPACE}/data/kowiki-latest-pages-meta-current.xml.bz2

 

# 5줄만 json 형태로 변경해서 출력해 봅니다.
with open(os.path.join(WORKSPACE, "data", "kowiki", "AA", "wiki_00")) as f:
    for i, line in enumerate(f):
        line = line.strip()
        # print(line)

        data = json.loads(line)
        print(data)

        if i >= 4:
            break

 

데이터베이스 만들기

문서를 원래는 DB에 저장해야 하지만, 그렇게 하지 않고 일단 json 파일로 만든다.

 

def make_chunk(text, n_word=128):
    # line 단위로 단어수 계산
    line_list = []
    total = 0
    for line in text.split('\n'):
        total += len(line.split())
        line_list.append((total, line))  #라인 단위로 단어가 몇개인지 센다. total은 누적 인덱스
    # n_word 단위로 분할
    chunk_list = []
    chunk_total, chunk_index = 0, 0
    for i, (total, line) in enumerate(line_list):
        if total - chunk_total >= n_word: #새로 들어온 게 128이상이 되면 청크에 담는다.
            chunk = [line for total, line in line_list[chunk_index:i+1]]
            chunk_list.append('\n'.join(chunk))
            chunk_index = i + 1
            chunk_total = total
    # 마지막 line 추가 (n_word 보다 작은 경우 이전라인 포함)
    if total > chunk_total: #혹시 남은 마지막 라인을 포함한다.
        if total - chunk_total < n_word and chunk_index > 1:
            chunk_index -= 1
        chunk = [line for total, line in line_list[chunk_index:]]
        chunk_list.append('\n'.join(chunk))
    return chunk_list

    #이걸 잘 만들면 의미 단위로도 분할할 수 있다.

 

확인을 위해서 chunk 단위로 분할해서 확인다.

# 기능 확인을 위해서 문서를 chunk 단위로 분할해서 row_list에 저장
row_list = []
for fn in fn_list[:1]:
    with open(fn) as f:
        for line in f:
            data = json.loads(line)
            chunk_list = make_chunk(data['text']) #청크를 익는다.
            for i, chunk in enumerate(chunk_list):
                title = data['title']
                row = {
                    'id': data['id'],  #타이틀과 청크를 넣은 리스트를 만든다.
                    'chunk_id': str(i + 1),
                    'chunk': f"{title}\n{chunk}"
                }
                print(row)
                row_list.append(row)
len(row_list)

#지미 카터 문서 하나가 청크 다누이로 들어가 있다.

 

BM25로 검색

BM25 api로 검색하고, 그에 대한 결과를 확인한다.

# bm25 api 생성
bm25 = BM25Okapi(tokenized_chunks)
def query_bm25(bm25, query, tokenizer, top_n=10):
    tokenized_query = tokenizer(query)
    # score 계산
    doc_scores = bm25.get_scores(tokenized_query)
    # score 순서로 정렬
    rank = np.argsort(-doc_scores)
    # top-n
    result = []
    for i in rank[:top_n]:
        if doc_scores[i] > 0:
            result.append((i, doc_scores[i]))
    return result
while True:
    query = input('검색 > ')
    query = query.strip()
    if len(query) == 0:
        break
    result = query_bm25(bm25, query, tokenizer)
    for i, score in result:
        print(f'---- score: {score} ----')
        print(chunk_list[i])
        print()

 

 

Sequence Transformer 검색

해당 모델을 불러온다.

https://huggingface.co/snunlp/KR SBERT V40K klueNLI augSTS

# SentenceTransformer 모델 생성
model = SentenceTransformer(MODEL_ID)

임베딩 생성

# chunk embeddings 생성
chunk_embeddings = model.encode(chunk_list)
chunk_embeddings.shape

그 후에 스코어를 계산해서 정렬하는 함수를 만든다.

def query_sentence_transformer(model, chunk_embeddings, query, top_n=10):
    query_embedding = model.encode([query])
    # score 계산
    doc_scores = np.matmul(chunk_embeddings, query_embedding.T)
    doc_scores = doc_scores.reshape(-1)
    # score 순서로 정렬
    rank = np.argsort(-doc_scores)
    # top-n
    result = []
    for i in rank[:top_n]:
        result.append((i, doc_scores[i]))
    return result

검색을 수행하면 된다.

while True:
    query = input('검색 > ')
    query = query.strip()
    if len(query) == 0:
        break
    result = query_sentence_transformer(model, chunk_embeddings, query)
    for i, score in result:
        print(f'---- score: {score} ----')
        print(chunk_list[i])
        print()

 

성능 향상의 방법

1. Data를 잘 만든다. 조금 더 의미 기반으로 분할해서 만든다.

2. BM25, Sentence BERT 같은 걸 썼는데, 너무 결과가 안나온다.

RAG가 상용화가 어려운 이유가 이것 때문에 그렇다.

합쳐서 사용하는 게 좋다.

bm25 *0.4 , dpr 0.6 이런식

3. LLM이 좋아지면 된다.

4. 그 이외에 프롬프트 바꾸기, 여러가지 실험 등

반응형