두 문장의 관계 분류
주어진 2개의 문장에 대해, 두 문장의 자연어 추론과 의미론적인 유사성을 측정하는 태스크 입니다.
대표적으로 Natural Language Inference(NLI) 테스크가 존재합니다.
언어모델이 자연어의 맥락을 이해할 수 있는지 검증하는 태스크로서, 문장이 참인경우, 거짓인 경우, 알수 없는 경우로 나누어 집니다.
예를 들어서, 두문장이 있다고 가정해 보겠습니다.
- 전제문장: 너는 거기에 있을 필요 없어
- 가설문장: 가도 돼
이때, 전제문장과 가설문장의 내용이 유사함으로 가설문장을 참이라고 볼 수 있습니다.
- 전제문장: 너는 거기에 있을 필요 업서
- 가설문장: 넌 정확히 그 자리에 있어야 해
이런 경우에는 내용이 모순되기 때문에, 가설문장을 거짓이라고 판단할 수 있습니다.
또 다른 태스크로는 Semantic Text Pair로 두 문장의 의미가 서로 같은 문장인지 검증하는 태스크입니다.
- 메일을 다 비울까 아니면 안읽은 것만 지울까?
- 안 읽은 메일함이랑 스팸 메일함이랑 비교했을 때 어디가 더 차있지?
두 문장이 의미적으로 유사하기 때문에 같다는 의미인 1로 데이터가 구성되어있습니다.
- 무협 소설 추천해주세요
- 판타지 소설 추천해주세요
두 문장이 유사한듯 보이지만, 무협과 판타지는 큰 차이가 있습니다.
따라서 다르다는 의미인 0으로 데이터가 구성됩니다.
Infromation Retrieval Question and Answering(IRQA)
챗봇에서 사용되는 태스크로 사전에 정의된 질문-대답(QA) 셋을 이용해 적절한 답변을 제공합니다.

그림과 같이 사용자가 "나이 알려줘"라는 질문을 했을때, 이를 우선 언어모델을 활용해 벡터로 변환을 합니다.
다음으로 가지고 있는 QA 셋에서 코사인 유사도같은 방법을 활용해 Top-N개의 유사한 질문 후보들을 선정합니다.
여기서 문제점은 항상 유사도가 가장 높은 것이 정답이 아닐 수 있다는 점입니다.
따라서 Paraphrase Detection모델을 통해 두 문장이 실제로 유사한지 안한지 판단하는 언어모델을 활용합니다.
IRQA 시스템을 위한 데이터셋 구축
우선은 필요한 데이터셋을 가져와볼까요?
open안에 있는 경로는 "paraKQC_v1.txt" 파일의 위치를 입력해주시면 됩니다!
텍스트 파일을 open하고 readlines 함수를 통해 줄단위로 파일을 읽어오겠습니다.
!git clone https://github.com/warnikchow/paraKQC.git
data = open('/content/paraKQC/data/paraKQC_v1.txt')
lines = data.readlines()
데이터를 한번 확인해 볼까요?
"숫자 탭 숫자 탭 문장"과 같은 형태로 구성이 되어있네요!
for i in range(0,2):
print(lines[i])
0 0 메일을 다 비울까 아니면 안읽은 것만 지울까? 0 0 메일 중에 안읽은 것만 지울까? 다 지울까? |
데이터셋을 더 많이 한번 출력해보세요.
주어진 데이터셋은 10개씩 유사한 문장으로 묶을 수 있습니다.
그럼 10개씩 묶기 위한 코드를 작성해 보겠습니다.
strip과 split을 통해 불필요한 공백을 없애고, 문장부분만들 가져왔습니다.
그리고 10개가 될 때마다 유사한 문장을 저장하는 리스트를 딕셔너리에 넣고, 초기화 해주는 과정을 반복했습니다.
이 때, 0번째를 키로, 나머지 9개의 문장을 값(리스트)으로 저장했습니다.
similar_sents = {}
similar_sent = []
total_sent = []
for line in lines:
line = line.strip()
sent = line.split('\t')[2]
total_sent.append(sent)
similar_sent.append(sent)
if len(similar_sent) == 10:
similar_sents[similar_sent[0]] = similar_sent[1:]
similar_sent = []
몇개의 묶음이 만들어졌는지 한번 확인해보세요.
print(len(similar_sents))
이제 유사한 데이터들은 만들어 졌습니다.
하지만 효과적인 학습을 위해서 유사하지 않은 문장들도 필요합니다.
어떻게 만들 수 있을까요?
간단하게는 다른 묶음에서 임의로 문장들을 선택하는 방법이 있겠네요.
이렇게 데이터셋을 구성해도 괜찮을까요?
전혀 다른 문장을 구분하게 된다면, 매우 쉬운 데이터만 모델이 학습하기 때문에 실제 서비스에서는 동작하지 않을 가능성이 매우 높습니다.
그러면 어떻게 해야할까요? 지금까지 배웠던 임베딩을 활용해 보겠습니다.
먼저 필요한 토크나이저와 모델을 불러와야겠죠?
import torch
from transformers import AutoModel, AutoTokenizer
MODEL_NAME = "bert-base-multilingual-cased"
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
model = AutoModel.from_pretrained(MODEL_NAME)
model.to('cuda:0')
문장을 대표하는 임베딩 벡터로 [CLS] 토큰의 임베딩 벡터를 많이 사용한다고 했습니다.
임베딩 벡터를 얻기위한 함수를 구현해봅시다.
먼저 모델을 eval함수를 활용해 추론 모드로 변환해줍니다.
다음으론 토큰화 작업을 진행합니다.
GPU를 사용하기 위해 to 함수로 GPU 메모리로 보내줍니다.
이제 토큰화된 데이터들을 모델에게 입력으로 넣어주고, output에서 [CLS] 토큰의 임베딩 벡터인 0번째 벡터만 추출하면 완료입니다!
def get_cls_token(sent_A):
model.eval()
tokenized_sent = tokenizer(
sent_A,
return_tensors="pt",
truncation=True,
add_special_tokens=True,
max_length=32
).to('cuda:0')
with torch.no_grad():
outputs = model(
input_ids=tokenized_sent['input_ids'],
attention_mask=tokenized_sent['attention_mask'],
token_type_ids=tokenized_sent['token_type_ids']
)
logits = outputs.last_hidden_state[:,0,:].detach().cpu().numpy()
return logits
이제 모든 문장들의 임베딩 벡터를 구해봅시다.
문장을 키로 하고 임베딩 벡터를 값으로 하는 딕셔너리를 만들어줍니다.
total_sent_vector = {}
for i, sent in enumerate(total_sent):
total_sent_vector[sent] = get_cls_token(sent)
이제 중요한 척도인 코사인 유사도를 계산하는 함수가 필요합니다.
코사인 유사도는 이전에도 한번 해보셨을 것입니다! 간단하게 구현이 가능합니다.
def custom_cosine_similarity(a,b):
numerator = np.dot(a,b.T)
a_norm = np.sqrt(np.sum(a * a))
b_norm = np.sqrt(np.sum(b * b, axis=-1))
denominator = a_norm * b_norm
return numerator/denominator
자 이제 모든 준비가 완료되었습니다.
목적이 기억나시나요?
맞습니다. 유사하지 않는 데이터셋을 구성하기 위한 작업이었습니다!!
유사 문장 묶음 딕셔너리(similar_sents)에서 키 문장들을 이용해 반복문을 구성합니다.
키 문장의 임베딩 벡터를 key_sent_vector에 할당합니다.
이제 전체 문장 중 유사 문장 묶에 없는 문장들에 대해 코사인 유사도를 계산합니다.
유사도 점수가 높은 순으로 정렬을 하고, 유사하지 않은 문장 묶음(non_similar_sents)을 생성합니다.
for key in similar_sents.keys():
key_sent_vector = total_sent_vector[key]
sentence_similarity = {}
for sent in total_sent:
if sent not in similar_sents[key] and sent != key:
sent_vector = total_sent_vector[sent]
similarity = custom_cosine_similarity(key_sent_vector, sent_vector)
sentence_similarity[sent] = similarity
sorted_sim = sorted(sentence_similarity.items(), key=lambda x: x[1], reverse=True)
non_similar_sents[key] = sorted_sim[0:10]
이제 열심히 만든 데이터를 저장해야합니다!
output = open('para_kqc_sim_data.txt', 'w', encoding='utf-8')
for i, key in enumerate(similar_sents.keys()):
for sent in similar_sents[key]:
output.write(key + '\t' + sent + '\t1\n')
for i, key in enumerate(non_similar_sents.keys()):
for sent in non_similar_sents[key]:
output.write(key + '\t' + sent[0] + '\t0\n')
output.close()
생각해보기
1) 문장 3개의 유사도 분류 태스크는 어떻게 만들 수 있을까요?
2) 챗봇 이외에 코사인 유사도를 활용해 개발할 수 있는 서비스는 어떤것이 있을까요?
3) 코사인 유사도 이외에 벡터의 유사도를 판단하기 위한 척도는 어떤것들이 있을까요?
참고자료
comment