문장 유사도를 판별하는 알고리즘은 여러가지가 있지만, 딥러닝 알고리즘을 사용하는 것이 가장 좋다. 딥러닝 알고리즘은 발전 속도가 정말 빠른 것 같다. 워드투벡, RNN을 애기하던 때가 엊그제 같은데, 지금은 대부분 BERT를 이용하는 듯 하다. 하지만, 개인이 텍스트 데이터를 구해서, BERT 알고리즘에 학습시키고 추론하는 것은 쉽지 않은 일이다.
최근에 위 작업을 굉장히 쉽게 할 수 있는 방법을 알게 되었다. BERT 모델을 쉽게 활용할 수 있도록 누군가 파이썬 패키지를 만들어 놓은 것이다.
오늘은 파이썬 문장 유사도 알고리즘 쉽게 확인하는 방법에 대해서 알아보도록 하겠다.

 

파이썬 문장 유사도

 

 

관련해서 알아볼 알고리즘은 Sentence-Transformers라는 패키지이다. BERT, RoBERTa, XLM-RoBERTa 등의 알고리즘을 손쉽게 사용할 수 있고, 100개 이상의 언어에 대해 사전 훈련된 모델도 이용할 수 있다. 사전 훈련된 모델에 대해서는 아래 링크를 참조하기 바란다.
( 참조: 사전훈련 모델 확인하러 가기 )

 

Sentence Transfomers 패키지를 이용하면 아래와 같은 작업들을 할 수 있다고 한다.

 

-문장 임베딩
-의미에 기반한 문장 유사도
-클러스트링
-문맥 찾기
-문장 번역
-의미에 기반한 검색
-검색 및 순위화
-텍스트 요약 등등

 

위와 관련된 예제들은 아래 github페이지에서 찾아볼 수 있다.
( 참조: github.com/UKPLab/sentence-transformers/tree/master/examples/applications )

 

 

해당 패키지를 이용해서 한국어의 문장 유사도를 한 번 확인해보기로 했다. 데이터는 네이버 영화 평점의 텍스트 데이터를 활용했다.

 

 

1. sentence transfomers 기본 방법 이용하기

sentence transformers홈페이지에는 문자유사도를 구하는 가장 쉬운 예제가 나와 있다. 해당 예제를 일부 변경하여 적용해보았다.

 

첫번째로 네이버 평점 데이터를 불러왔다.

import pandas as pd
from urllib.request import urlretrieve
urlretrieve("https://raw.githubusercontent.com/e9t/nsmc/master/ratings_train.txt", filename="ratings_train.txt")
train_data = pd.read_table('ratings_train.txt')
print(train_data.head())

 

Output:

id document label

0 9976970 아 더빙.. 진짜 짜증나네요 목소리 0

1 3819312 흠...포스터보고 초딩영화줄....오버연기조차 가볍지 않구나 1

2 10265843 너무재밓었다그래서보는것을추천한다 0

3 9045019 교도소 이야기구먼 ..솔직히 재미는 없다..평점 조정 0

4 6483659 사이몬페그의 익살스런 연기가 돋보였던 영화!스파이더맨에서 늙어보이기만 했던 커스틴 ... 1

 

 

테스트를 해 볼 목적이라 데이터를 다 쓰지 않고, 1000건만 사용하기로 하였다.

doc_list = train_data["document"].to_list()
doc_list = doc_list[0:1000]

 

이제 sentence transformer를 이용해서, 임베딩을 하고 코사인 유사도롤 기준으로 유사한 문장을 추출해보았다.

from sentence_transformers import SentenceTransformer, util
model = SentenceTransformer('paraphrase-distilroberta-base-v1')

embeddings = model.encode(doc_list, convert_to_tensor=True)
cosine_scores = util.pytorch_cos_sim(embeddings, embeddings)

 

 cosine_scores에는 각 문장끼리의 유사도가 나와 있다. 첫 번째 문장과 유사한 문장을 상위 10개만 출력해 보았다.

embeddings = model.encode(doc_list, convert_to_tensor=True)
cosine_scores = util.pytorch_cos_sim(embeddings, embeddings)

 

Output:

0. 아 더빙.. 진짜 짜증나네요 목소리 <> 아 더빙.. 진짜 짜증나네요 목소리 
Score: 1.0000
519. 아 더빙.. 진짜 짜증나네요 목소리 <> 진짜..뭐냐 민국이나 지아는 더빙 더이상시키면안되겠네 더빙이 얼마나중요한데 이런애들을시켜데뷔도안한애들을 연예인이해도 뭐라고하는판에 
Score: 0.9133
727. 아 더빙.. 진짜 짜증나네요 목소리 <> 아 재미 너무 없네요.. 시간 아까워요 
Score: 0.9111
527. 아 더빙.. 진짜 짜증나네요 목소리 <> 진짜 짜증나는 영화.. 
Score: 0.9094
840. 아 더빙.. 진짜 짜증나네요 목소리 <> 진짜 쓰레기영화.. 돈아까워 죽는줄 알았다.. 여러분 보지 마세요 
Score: 0.9047
268. 아 더빙.. 진짜 짜증나네요 목소리 <> 완전 스토리도 엉망이구, 완전 비추..ㅜ 
Score: 0.8982
206. 아 더빙.. 진짜 짜증나네요 목소리 <> 아..정말 김혜성 너무 예쁘네요 이현진도 웃는 거 정말...하.... 
Score: 0.8934
191. 아 더빙.. 진짜 짜증나네요 목소리 <> 어린시절 너무 무섭고 재미있게 봤던 추억의 판타지영화.절대 나쁜짓은 금물.지옥가요.. 
Score: 0.8926
381. 아 더빙.. 진짜 짜증나네요 목소리 <> 진짜 어마어마한 여운을 주는 멜로 영화에요.ㅎ 
Score: 0.8897
401. 아 더빙.. 진짜 짜증나네요 목소리 <> 아 정말짜증나네 절때보지말껄욕나옴 
Score: 0.8885

 

유사도를 확인한 결과, 생각보다 굉장히 잘 나오는 것을 알 수 있었다. 206, 191, 381번 문장의 결과가 아쉽기는 하지만, 더빙과 짜증이라는 키워드를 중심으로 비교해 봤을 때 전체적으로 유사한 문장이 나왔다. 6줄 정도 코딩을 한 것치고는 놀라운 성과다.

 

 

텍스트

 

 

Recommendation 포스팅

 

 

2. tokenizer 변경해서 사용하기

위의 코드는 토큰화를 어떻게 한 지 알 수가 없다. 그래서, 다른 토크나이저를 이용해서 문장을 분리하고 유사도를 구해보았다.

from transformers import AutoTokenizer, AutoModel
from sentence_transformers import SentenceTransformer, util
import torch

 

인코딩 결과와 어텐센 마스크를 평균으로 풀링해서, 최종 문장 임베딩 결과를 얻는다. 어딘가에서 본 예제인지 출처를 잊어버렸다.

#Mean Pooling - Take attention mask into account for correct averaging
def mean_pooling(model_output, attention_mask):
    token_embeddings = model_output[0] #First element of model_output contains all token embeddings
    input_mask_expanded = attention_mask.unsqueeze(-1).expand(token_embeddings.size()).float()
    sum_embeddings = torch.sum(token_embeddings * input_mask_expanded, 1)
    sum_mask = torch.clamp(input_mask_expanded.sum(1), min=1e-9)
    return sum_embeddings / sum_mask

 

토크나이저를 지정하였다.

tokenizer = AutoTokenizer.from_pretrained("sentence-transformers/bert-base-nli-mean-tokens")

 

어떻게 토큰화가 되었는지 출력해 보았다. 형태소 단위로 출력된 것을 확인할 수 있다.

tokenizer.tokenize(
  doc_list[0],
  return_tensors='pt',
  truncation=True,
  max_length=256,
  pad_to_max_length=True
)[0:20]

 

Output:

['ᄋ', '##ᅡ', 'ᄃ', '##ᅥ', '##ᄇ', '##ᅵ', '##ᆼ', '.', '.', '[UNK]', '[UNK]', 'ᄆ', '##ᅩ', '##ᆨ', '##ᄉ', '##ᅩ', '##ᄅ', '##ᅵ', '[PAD]', '[PAD]']

 

 

이제 모델에 적용하고 결과를 확인해 보았다.

#Tokenize sentences
encoded_input = tokenizer(doc_list, padding=True, truncation=True, max_length=122, return_tensors='pt')

model = AutoModel.from_pretrained("sentence-transformers/bert-base-nli-mean-tokens").to("cuda")

with torch.no_grad():
    model_output = model(input_ids=encoded_input["input_ids"].to("cuda"))

#Perform pooling. In this case, mean pooling
sentence_embeddings = mean_pooling(model_output, encoded_input['attention_mask'].to("cuda"))

 

문장 유사도를 계산한다.

cosine_scores = util.pytorch_cos_sim(sentence_embeddings, sentence_embeddings)

 

동일하게 첫번째 문장과 유사한 문장을 출력해보았다.

org = 0
temp = cosine_scores[org]
temp.argsort(descending=True)[0:10]

for i in temp.argsort(descending=True)[0:10]:
    print(f"{i}. {doc_list[org]} <> {doc_list[i]} \nScore: {cosine_scores[org][i]:.4f}")

 

Output:

0. 아 더빙.. 진짜 짜증나네요 목소리 <> 아 더빙.. 진짜 짜증나네요 목소리 
Score: 1.0000
699. 아 더빙.. 진짜 짜증나네요 목소리 <> 그냥 살지 그랬어.. 이성재만 멋짐 
Score: 0.9773
257. 아 더빙.. 진짜 짜증나네요 목소리 <> 이적의 소설. 재미없다. 
Score: 0.9757
727. 아 더빙.. 진짜 짜증나네요 목소리 <> 아 재미 너무 없네요.. 시간 아까워요 
Score: 0.9747
600. 아 더빙.. 진짜 짜증나네요 목소리 <> 수많은 최루성 멜로는 여기에서... 
Score: 0.9746
653. 아 더빙.. 진짜 짜증나네요 목소리 <> 재미있게봄 감동적이고 
Score: 0.9743
641. 아 더빙.. 진짜 짜증나네요 목소리 <> 난강혜정이 일본가서 찍은건가 했네ㅋㅋ닮은것같다는 생각이 들었을 뿐이고... 
Score: 0.9736
261. 아 더빙.. 진짜 짜증나네요 목소리 <> 재밌는데 평점이 왜 이렇게 구리지 
Score: 0.9735
144. 아 더빙.. 진짜 짜증나네요 목소리 <> 재미있다고 허풍 떨지 마세요 
Score: 0.9734
510. 아 더빙.. 진짜 짜증나네요 목소리 <> 이게 어떻게 평점이 낮을수가 있지? 
Score: 0.9726

 

첫번째 한 것보다는 아쉬운 결과가 나왔다. 아무래도 형태소를 모음, 자음 단위로 분리해서 그렇지 않을까 싶다. 다른 방법을 이용해서 형태소를 분리하고, 학습시켜 문장 유사도를 계산해보기로 하였다.

 

 

왼쪽 친구의 이름을 따서, BERT라고 지었다고 한다.

 

 

3. 다른 tokenizer 이용해보기

코드는 위와 동일하다. 사용된 모델만 다른 것으로 변경했다.

tokenizer = AutoTokenizer.from_pretrained("monologg/koelectra-small-v2-discriminator")
model = AutoModel.from_pretrained("monologg/koelectra-small-v2-discriminator").to("cuda")

 

 

토큰화한 결과를 출력해보았다. 단어를 기준으로 형태소가 분리되었지만, 한국어의 특성을 잘 반영하지 못한 것 같아 아쉽다.

 

Output:

['아', '더', '##빙', '.', '.', '진짜', '짜증', '##나', '##네', '##요']

 

 

학습을 하고, 문장 유사도를 확인해보았다.

 

Output:

0. 아 더빙.. 진짜 짜증나네요 목소리 <> 아 더빙.. 진짜 짜증나네요 목소리 
Score: 1.0000
656. 아 더빙.. 진짜 짜증나네요 목소리 <> 완전 감동이다 ㅠㅠ 박신혜 짱이뻐요 ㅎㅎ 
Score: 0.9541
367. 아 더빙.. 진짜 짜증나네요 목소리 <> 진 짜 리얼 개 쓰레기 영화 . 다 보고 나면 정말 찝찝해지는 영화 절대보지마 
Score: 0.9505
31. 아 더빙.. 진짜 짜증나네요 목소리 <> 졸쓰레기 진부하고말도안됌ㅋㅋ 아..시간아까워 
Score: 0.9488
548. 아 더빙.. 진짜 짜증나네요 목소리 <> 영상미가 역시 최고네요 
Score: 0.9475
59. 아 더빙.. 진짜 짜증나네요 목소리 <> 백봉기 언제나오나요? 
Score: 0.9474
707. 아 더빙.. 진짜 짜증나네요 목소리 <> 김민종 최고! 더 잘되시요 
Score: 0.9472
526. 아 더빙.. 진짜 짜증나네요 목소리 <> 박얘쁜 빠수니 죄다 OO 없어져버려 
Score: 0.9462
895. 아 더빙.. 진짜 짜증나네요 목소리 <> 너무 구식으로 웃길려고 함. 보는 내내 지겨움 
Score: 0.9459
89. 아 더빙.. 진짜 짜증나네요 목소리 <> ㅋㅋㅋ 진짜 골깜..ㅋㅋ 눈 부라릴때 쓰러짐..ㅋㅋ 
Score: 0.9446

결과는 앞의 두 경우보다 더 좋지 않았다. pretrain 모델이 여러가지가 있어, 한국어에 잘 맞느 모델을 선택하는 것이 중요할 듯 하다.

 

 

오늘은 이렇게 파이썬으로 문장 유사도를 쉽게 구하는 방법에 대해서 알아보았다. sentence transformer를 이용하면 미리 정의되고, 사전훈련되 모델을 쉽게 가져다 쓸 수 있다. 문장 유사도를 구하고자 한다면, 한 번 사용해보기 바란다.

반응형
  • 네이버 블러그 공유하기
  • 네이버 밴드에 공유하기
  • 페이스북 공유하기
  • 카카오스토리 공유하기

댓글을 달아 주세요

">
  1. thumbnail
    문의
    2021.08.03 15:37

    안녕하세요~ 글을 보고 따라해봤는데, 모델이 한글용이 아니라서 차이가 많이 나나요? 전혀 유사도가 없는 문장도 정확도가 90퍼센트가 나와서요

    • thumbnail
      tariat
      2021.08.09 06:25 신고

      pretrain모델이 다른 것도 있는데, 다른 모델로 한 번 해보시는 것을 추천 드립니다. ㅎ