본문 바로가기

ML/LLM

[dacon] 도배 하자 질의 응답 처리 : 한솔데코 시즌2 AI 경진대회

https://dacon.io/competitions/official/236216/overview/description

 

도배 하자 질의 응답 처리 : 한솔데코 시즌2 AI 경진대회 - DACON

분석시각화 대회 코드 공유 게시물은 내용 확인 후 좋아요(투표) 가능합니다.

dacon.io

 

 

해당 대회가 LLM관련 대회여서, 심심한데 한번 해볼까? 하는 마음에 시작하게 되었다. (진지하게는 못했다...하하)

그냥 기본적인 내가 아는 기술을 써서 어디까지 올라가는지 테스트 해보고 싶었다. (hyde를 구현해서 실제로 써보고 싶었다.)

 

먼저 해당 대회의 내용을 읽어보면

"다양한 질문과 상황을 제공하고, 이에 대한 정확하고 신속한 응답을 제공하는 AI 모델을 개발이 목표이다"

 

 

나의 목표는

1. 모델 파인튜닝

2. Hyde 구현

3. vector embedding search 구현

순이다.

 

평가의 경우 코드 공유에 나와 있는 평가를 그대로 따라 했다.

(https://dacon.io/competitions/official/236216/codeshare/9618?page=1&dtype=recent)


결과적으론 도메인을 딥하게 들어가야하는것 / 모델마다의 성능 측정 / 제공된 데이터 확인(오탈자, 잘못된답변 등등)을 통해 최종 성능이 올라가겠지만, 혼자서 하는 한계와 함께 시간이 없다는 변명을 하면서 틈틈히 진행하게 되었다.


 

 

1. fine tuning

1-1. 데이터 전처리

데이터에 대한 전처리는 특별히 하지 않았다.

제공했던 train.csv를 기반으로 오타 수정 및 질문/답변 쌍을 적절하게 생성해서 학습에 사용할 파일을 만들었다.

 

 

1-2. fine tuning run

이번엔 새로운 모델을 사용해서 학습하고 싶어 komt-llama2-7b-v1 모델을 사용했다.

 

fine tuing의 경우 다은 레포를 참조 했다. https://github.com/davidkim205/komt

!python finetune_with_lora.py --model_name_or_path davidkim205/komt-llama2-7b-v1 --data_path dobae_prompt_data/korquad_dobae.json --num_train_epochs 10 --per_device_train_batch_size 1 --learning_rate 1e-5

 

보시는 바와 같이 10epochs를 했다. 다른 파라미터는 수정하지 않았다.

 

해당 모델은 다음과 같이 불러올수 있다.

import torch
from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig
from peft import PeftModel, PeftConfig

model_id = "davidkim205/komt-llama2-7b-v1"
peft_model_id = "./outputs/checkpoint-2000" 

config = PeftConfig.from_pretrained(peft_model_id)

# bnb_config = BitsAndBytesConfig(
#     load_in_8bit=False,
#     load_in_4bit=True,
#     llm_int8_threshold=6.0,
#     llm_int8_skip_modules=None,
#     llm_int8_enable_fp32_cpu_offload=False,
#     llm_int8_has_fp16_weight=False,
#     bnb_4bit_quant_type="nf4",
#     bnb_4bit_use_double_quant=False,
#     bnb_4bit_compute_dtype="float16",
# )

bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_use_double_quant=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_compute_dtype=torch.float16
)

model = AutoModelForCausalLM.from_pretrained(model_id, quantization_config=bnb_config, device_map={"":0})
model = PeftModel.from_pretrained(model, peft_model_id)
tokenizer = AutoTokenizer.from_pretrained(model_id)

model.eval()

 

 

LLM 생성 코드는 다음과 같다. 토큰만 적절하게 조절했다. (train에 제공된 답변이 512를 넘지 않아서 제한을 두었다)

# do_sample : False -> 항상 가장 높은 확률값을 가진 단어로 예측
def gen(q):
    query = f"""### 명령 : 아래 질문에 대해 답변을 단답식으로 하시오. 참고자료는 제외시켜줘.
            Q: {q}\n
            A: """
    gened = model.generate(
        **tokenizer(
            query,
            return_tensors='pt',
            return_token_type_ids=False
        ).to('cuda'),
        max_new_tokens=512,
        # early_stopping=True,
        do_sample=False,
    )
    return tokenizer.decode(gened[0]).replace(q, "")

 

 

생성 테스트 코드 부분.

모델의 특성 때문에 생성된 답변에 Q: / A: 로 나뉘어 답변이 생성되어서 적절하게 문장을 자르는 부분이 추가되었다.

response = gen("면진장치가 뭐야? 중복된 내용은 제거하고 간략하게 설명해줘")
response = response.replace("</s>", "").replace("</s>", "")

참고자료_index = response.find("####")
if 참고자료_index > 0:
    response = response[:참고자료_index]

A_index = response.find("A")
if A_index > 0:
    response = response[A_index+3:]
response # 정답 : 면진장치란 지반에서 오는 진동 에너지를 흡수하여 건물에 주는 진동을 줄여주는 진동 격리장치입니다.

 

실제 리턴된 값은 다음과 같다.

'면진장치란  건물의  지반에 서 발생하는  진동 에너지 를 흡 수하여  건물을  보호하고 , 진동을  줄여주는  장치입니다 . 
주로  지진이 나 기타  지반의  진동으로  인한  피해 를 방지하기  위해  사용됩니다 . 
면진장치 는 건물의  안전 과 안정성을  유지하는  데 중요한  역할을 합니다 . 지진으로  인한  피해 를 최소화하고  
건물의  수명을  연장시키는  등 다양한 장점을  가지 고 있습니다 . 따라 서 면진장치 는 건물 구조물의  안전 과 
튼튼함을  위해  중요한  구성 품으로  인정받고  있습니다 . 면진장치 에 대한  더 많은  정보 를 원하신다면  
추가적인  검색이나  전문가 의 조언을  받아 보시는  것을  권장합니다 . 면진장치 의 종류 와 작동 원리, 설치  
및 유지보수  등에  대한  자세한  정보를  얻 을 수 있을  것입니다 . 감사합니다 .'

 

 

1-3. 해당 모델로 텍스트 생성

hyde를 위해서 질문에 대한 답변을 미리 데이터를 생성했다.

import pandas as pd
test = pd.read_csv("test.csv")


from tqdm import tqdm

test_embedding = []

for idx, row in tqdm(test.iterrows(), total=len(test)):
    response = gen(row['질문'])
    response = response.replace("</s>", "").replace("</s>", "")
    참고자료_index = response.find("####")
    if 참고자료_index > 0:
        response = response[:참고자료_index]

    A_index = response.find("A")
    if A_index > 0:
        response = response[A_index+3:]
    
    test_embedding.append(response)

 

 

심심해서 생성된것만으로 제출해보았다 무려 2%의 정답율… 당연하다. 생성된 텍스트를 제출했기 때문에 정답과 똑같을리가 없다. (할루시네이션 및 필요없는 문구가 추가되어 있을것이다)

 

 

 

2. Hyde 구현

2-1. LLM을 사용해서 정답 도출

1번에서 생성한 텍스트와 기존 train데이터와의 벡터 계산을 통해 가장 가까운것을 찾을것이다.

해당 벡터 계산은 multilingual-e5-large 를 사용했다.

# hyde 구성

import torch
from transformers import AutoModel, AutoTokenizer
from sentence_transformers import SentenceTransformer, util
from transformers import AutoConfig, PretrainedConfig, PreTrainedModel
from torch import Tensor
import torch.nn as nn
import torch.nn.functional as F



tokenizer = AutoTokenizer.from_pretrained('intfloat/multilingual-e5-large')
model = AutoModel.from_pretrained('intfloat/multilingual-e5-large')
model = model.to("cpu")


def average_pool(last_hidden_states: Tensor,
                 attention_mask: Tensor) -> Tensor:
    last_hidden = last_hidden_states.masked_fill(~attention_mask[..., None].bool(), 0.0)
    return last_hidden.sum(dim=1) / attention_mask.sum(dim=1)[..., None]


def text_embedding(text):
    
    # Tokenize the input texts
    batch_dict = tokenizer(text, max_length=512, padding=True, truncation=True, return_tensors='pt')

    outputs = model(**batch_dict)
    embeddings = average_pool(outputs.last_hidden_state, batch_dict['attention_mask'])
    
    return embeddings[0]

 

 

 

train 데이터를 vector로 변환 후 faiss vector 디비에 넣었다.

# train data to vector
train_data = pd.read_csv("train.csv")

from tqdm import tqdm

formatted_data = []
for _, row in tqdm(train_data.iterrows(), total=len(train_data)):
    for a_col in ['답변_1', '답변_2', '답변_3', '답변_4', '답변_5']:
        input_text = row[a_col]
        formatted_data.append(np.array(text_embedding(input_text).tolist()))
print('Done.')

 

 

3. vector embedding search 구현

# train data to vector
import faiss
import re

# faiss에 insert
temp_vectors = np.array(formatted_data)
index = faiss.IndexFlatIP(vector_dimension) # IndexFlatL2 대신에 IndexFlatIP
index.add(temp_vectors)

# url 제거
def remove_urls (vTEXT):
    vTEXT = re.sub(r'(https|http)?:\/\/(\w|\.|\/|\?|\=|\&|\%)*\b', '', vTEXT, flags=re.MULTILINE)
    return(vTEXT)

data = [remove_urls(row) for row in data]


# 최종 검색
result = []
for row in data:
    temp_vector = np.array(text_embedding(row).tolist())
    dist, indi = index.search(temp_vector.reshape(1, -1), 1)
    result.append(origin_train_text[indi[0][0]])

 

 

LLM으로 답변 생성 -> 생성된 답변과 가장 유사한 답변 검색 -> 제출 순으로 진행되었다.

hyde 로직으로 변환 후  제출시 점수가 높아진것을 볼수 있다.

 

 

 

 

결론

아마 모델을 다른걸로 바꿔가면서 시도는 해보겠지만, 로직상의 크게 변화는 없을꺼 같다. (RAG에서 하는것처럼 검색에 몇개를 추가할 예정이다.)

확실히 학습할때는 정확한 정답으로 도메인 데이터를 넣어야 하며,  벡터 검색에 사용할 모델 또한 적절하게 써야 한다는것을 느꼈다.

앞으로도 LLM 관련 대회가 많이 있었으면 하는 바램이다. 끝