인공지능/자연어 처리

자연어 처리 python 실습 - 간단한 답변 랭킹 모델 만들기

이게될까 2024. 3. 28. 22:55
728x90
728x90

간단한 답변 랭킹 모델 만들기

Introduction

Chapter 5. 문장 임베딩 만들기 강의의 간단한 답변 랭킹 모델 만들기 실습 강의입니다.

Transformers에 공유된 KoBERT-Transformers를 활용하여, 문장 수준 임베딩 간 유사도 계산을 활용하여 간단한 답변 랭킹 모델 (챗봇)을 구현합니다.

1. 한국어 일상 대화 데이터셋 수집 및 전처리

오늘 실습에서는 일상 대화 챗봇 구현에 많이 사용되는 Chatbot_data를 사용하겠습니다.

로드한 데이터셋은 이후 답변 랭킹 모델 추론에 사용됩니다.

출처: https://github.com/songys/Chatbot_data

 

GitHub - songys/Chatbot_data: Chatbot_data_for_Korean

Chatbot_data_for_Korean. Contribute to songys/Chatbot_data development by creating an account on GitHub.

github.com

import urllib.request
import pandas as pd
#다음의 링크에서 챗봇 데이터를 로드할 수 있습니다.
urllib.request.urlretrieve("https://raw.githubusercontent.com/songys/Chatbot_data/master/ChatbotData.csv", filename="ChatBotData.csv")
train_dataset = pd.read_csv('ChatBotData.csv')

print(f"전체 데이터셋 개수: {len(train_dataset)}") # 11823
train_dataset

전처리가 이미 되어있는 데이터 형식이다.

 

2. 데이터 전처리

2.1. 데이터셋 내 결측값 확인

train_dataset.replace("", float("NaN"), inplace=True)
print(train_dataset.isnull().values.any()) # False

2.2. 데이터셋 내 중복 제거

# Question: 열을 기준으로 중복제거

train_dataset = train_dataset.drop_duplicates(['Q']).reset_index(drop=True)
print(f"필터링된 데이터셋 총 개수 : {len(train_dataset)}") # 11662
train_dataset

# Answer: 열을 기준으로 중복제거
train_dataset = train_dataset.drop_duplicates(['A']).reset_index(drop=True)
print(f"필터링된 데이터셋 총 개수 : {len(train_dataset)}") # 7731
train_dataset

 

2.3. 데이터 분포 확인

import matplotlib.pyplot as plt
question_list = list(train_dataset['Q'])
answer_list = list(train_dataset['A'])
print('질문의 최대 길이 :',max(len(question) for question in question_list))# 56
print('질문의 평균 길이 :',sum(map(len, question_list))/len(question_list)) # 13.673263486721
plt.hist([len(question) for question in question_list], bins=50)
plt.xlabel('length of samples')
plt.ylabel('number of samples')
plt.show()

분포에서 크게 문제가 없는 형태이다.

print('답변의 최대 길이 :',max(len(answer) for answer in answer_list)) #76
print('답변의 평균 길이 :',sum(map(len, answer_list))/len(answer_list))#15.611563833915406
plt.hist([len(answer) for answer in answer_list], bins=50)
plt.xlabel('length of samples')
plt.ylabel('number of samples')
plt.show()

질문과 답변을 나눠서 쓰기 때문에 따로 길이에 따른 정규화를 진행하지 않았다.

2.3. 답변 후보 목록 구성

import random
print(f"question 개수: {len(question_list)}") # 7731
print(f"answer 개수: {len(answer_list)}") # 7731
response_candidates = random.sample(answer_list, 100)
response_candidates[:10]

['달콤한 커피 한 잔 마셔보세요.',
'조금만 더 기다려주세요.',
'조급하게 생각하지 말아요.',
'짝사랑인지 생각해보세요.',
'늦지 않았어요.',
'저는 자장면이요.',
'페북 염탐하지 마요.',
'좀 더 알아보고 하세요.',
'만들어서 먹는 기쁨이죠.',
'자신의 건강만큼 중요한건 업습니다.']

답변후보를 적게하여 답변을 빠르게 주게 만들었다.

현업에서는 리트리버와 랭커를 활용하여 이렇게 줄이지 않아도 빠르게 할 수 있다.

3. KoBERT-Transformers 불러오기

오늘 실습에는 Transformers의 multi-lingual BERT(다양한 언어를 학습)를 사용하는 대신,

KoBERT-Transformers 모델을 사용합니다!

KoBERT 모델은 SKTBrain에서 공개한 한국어 데이터로 사전학습한 BERT 모델로, google의 multi-lingual BERT 성능의 한계를 극복하기 위해 공개되었습니다.

KoBERT-Transformers 모델은 KoBERT를 Huggingface.co 기반으로 사용할 수 있게 Wrapping 작업을 진행한 모델입니다.

KoBERT : https://github.com/SKTBrain/KoBERT

KoBERT-Transformers: https://github.com/monologg/KoBERT-Transformers

!pip3 install kobert-transformers

import torch
from kobert_transformers import get_kobert_model, get_distilkobert_model

model = get_kobert_model()# 한국어로 사전 학습된 모델을 바로 받을 수 있다.
model.eval() # 학습 안하고 평가하겠다.

input_ids = torch.LongTensor([[31, 51, 99], [15, 5, 0]])
attention_mask = torch.LongTensor([[1, 1, 1], [1, 1, 0]])
token_type_ids = torch.LongTensor([[0, 0, 1], [0, 1, 0]])
output = model(input_ids, attention_mask, token_type_ids)
output
output[0]
print(f"sequence_output is {sequence_output}") # last_hidden_state
print(f"pooled_output is {pooled_output}") # pooler_output

from kobert_transformers import get_tokenizer

tokenizer = get_tokenizer()
# 문장을 토큰화(분절) 한다.
tokenizer.tokenize("[CLS] 한국어 모델을 공유합니다. [SEP]") #['[CLS]', '▁한국', '어', '▁모델', '을', '▁공유', '합니다', '.', '[SEP]']
# 내가 입력들의 어떤 인덱스로 들어가는지 알 수 있다.
tokenizer.convert_tokens_to_ids(['[CLS]', '▁한국', '어', '▁모델', '을', '▁공유', '합니다', '.', '[SEP]']) #[2, 4958, 6855, 2046, 7088, 1050, 7843, 54, 3]

 

4. KoBERT 기반 답변 랭킹 모델 구현

import numpy as np
from sklearn.metrics.pairwise import cosine_similarity

유사도가 가장 높은 답변을 출력해줄 것이다.

4.1. 답변 랭킹 파이프라인

def get_cls_token(sentence):
    model.eval()
    tokenized_sent = tokenizer( # 문장을 토근화하여 사용할 수 있게 해준다.
      sentence, 
      return_tensors="pt",
      truncation=True,
      add_special_tokens=True,
      max_length=128
    )
    input_ids = tokenized_sent['input_ids']
    attention_mask = tokenized_sent['attention_mask']
    token_type_ids = tokenized_sent['token_type_ids']

    with torch.no_grad(): # 학습 안할것이다.
        output = model(input_ids, attention_mask, token_type_ids)

    cls_output = output[1]
    cls_token = cls_output.detach().cpu().numpy() # 여기선 CPU의 속도가 더 빠르다.

    return cls_token

def predict(query, candidates):
  candidates_cls = []

  for cand in candidates: # string 타입이다.
    cand_cls = get_cls_token(cand) 
    candidates_cls.append(cand_cls)

  candidates_cls = np.array(candidates_cls).squeeze(axis=1) # 행렬계산 할 수 있도록

  queury_cls = get_cls_token(query) # 쿼리와의 유사도도 구할 것이다.
  
  similarity_list = cosine_similarity(queury_cls, candidates_cls)

  target_idx = np.argmax(similarity_list)

  return candidates[target_idx]

 

4.2. get_cls_token 테스트 코드

query = '너 요즘 바빠?'
query_cls_hidden = get_cls_token(query)
print(query_cls_hidden)
print(query_cls_hidden.shape)

768차원의 백터였으므로 1,768의 벡터가 나오게 된다.

4.3. predict 테스트 코드

sample_query = '너 요즘 바빠?'
sample_candidates = ['아니 별로 안바빠','바쁘면 바보','사자와 호랑이가 싸우면 누가 이길까', "내일은 과연 해가 뜰까"]

predicted_answer = predict(query, sample_candidates) #[[0.9004743  0.8332883  0.7963901  0.65769005]]


print(f"predicted_answer = {predicted_answer}") #predicted_answer = 아니 별로 안바빠

유사도는 잘 나오긴 하는데 완전히 관계가 없어 보이는 것도 조금은 높게 나오는 것이 보인다.

5. 답변 랭킹 모델 평가

user_query = '너 요즘 바빠?'
predicted_answer = predict(query, response_candidates)
print(f"predicted_answer = {predicted_answer}") # 후폭풍이 지나갔길 바랄게요
# 빠른 출력을 위해 100개를 넣었더니 괜찮은 답변이 나오지 않았다.

response_candidates = random.sample(answer_list, 100) 

user_query = '나 요즘 너무 힘들어'
predicted_answer = predict(query, response_candidates)
print(f"predicted_answer = {predicted_answer}") #predicted_answer = 마음 단단히 잡길 바랄게요.


end = 1
while end == 1 :
    sentence = input("하고싶은 말을 입력해주세요 : ")
    if len(sentence) == 0 :
        break
    predicted_answer = predict(sentence, response_candidates)
    print(predicted_answer)
    print("\n")

predicted_answer = 마음 단단히 잡길 바랄게요.
하고싶은 말을 입력해주세요 : 너 지금 뭐해?
그만 듣고 싶다고 이야기해보세요.


하고싶은 말을 입력해주세요 : 그만 듣고 싶어
같이 먹어요!


하고싶은 말을 입력해주세요 : 뭐 먹고 싶은데?
나를 사랑하고 상대를 사랑하는 여유를 갖게 되길 바라요.


하고싶은 말을 입력해주세요 : 나도 사랑해!
당신을 위해 좋은 추억만 간직하세요.


하고싶은 말을 입력해주세요 : 

728x90