본문 바로가기
Python/NLP

[파이썬, Python] 자연어처리 - 유사도 측정 실습

by coding-choonsik 2023. 7. 7.
728x90
반응형
SMALL

✅ 예시 문장

sen_1 = '오늘 점심에 배가 너무 고파서 밥을 너무 많이 먹었다.'
sen_2 = '오늘 점심에 배가 고파서 밥을 많이 먹었다.'
sen_3 = '오늘 배가 너무 고파서  점심에 밥을 너무 많이 먹었다.'
sen_4 = '오늘 점심에 배가 고파서 비행기를 많이 먹었다.'
sen_5 = '어제 저녁에 밥을 너무 많이 먹었더니 배가 부르다.'
sen_6 = '이따 오후 9시에 출발하는 비행기가 3시간 연착 되었다고 하네요.'
sen_1, sen_2: 의미가 유사한 문장 간 유사도 계산(조사를 생략)
sen_1, sen_3: 의미가 유사한 문장 간 유사도 계산(순서 변경)
sen_2, sen_4: 문장 내 단어를 임의의 단어로 치환한 문장과 원본 문장간의 유사도 계산
sen_1, sen_5: 의미는 다르지만 비슷한 주제를 가지는 문장 간 유사도 계산
sen_1, sen_6: 의미가 서로 다른 문장 간 유사도 계산

 

✅ 학습 데이터로서 리스트로 묶어줌

training_documents = [sen_1, sen_2, sen_3, sen_4, sen_5, sen_6]

 

✅ 뉴스기사를 가져와 파싱하는 라이브러리

!pip install newspaper3k
from newspaper import Article

# 가져올 기사의 URL
URL = 'https://v.daum.net/v/20230628102530481'
article = Article(URL, language='ko')

# 기사 다운로드 및 파싱
article.download()
article.parse()

news_title = article.title
news_context = article.text

print('title', news_title)
print('context', news_context)

 

✅ 기사의 내용을 "\n"을 기준으로 split

news_context = article.text.split('\n') # 엔터를 기준으로 split

for text in news_context:
  print(text)

 

 

✅ 한국어 문장 분리를 위해 kss 라이브러리 설치 및 임포트

!pip install kss

import kss

 

✅ 한 줄 단위로 단어를 분리하는 함수 정의

def sentence_seperator(processed_context):
  splited_context = []
  for text in processed_context:
    text = text.strip()  # 공백 제거
    if text:
      splited_text = kss.split_sentences(text)
      splited_context.extend(splited_text)  #  리스트에 다른 리스트를 덧붙일 때 사용(다른 시퀀스 형식(리스트, 튜플, 문자열 등)의 요소를 추가)
  return splited_context

 

✅ 함수를 이용하여 뉴스기사를 한 줄 단위로 문장 분리

splited_context = sentence_seperator(news_context)

for text in enumerate(splited_context):
  print(text)

▲ 한 줄 별로 인덱스와 각 문장 분리

 

✅ 학습 데이터 만들기(training_documents + splited_context)

augmented_training_documents = training_documents + splited_context

for text in augmented_training_documents:
  print(text)

 

✅ Bag of Words 기반으로 문장 간 유사도 측정

# CountVectorizer: 단어의 출현 빈도를 기반으로 문서를 벡터화
from sklearn.feature_extraction.text import CountVectorizer

# 객체 생성
bow_vectorizer = CountVectorizer()
bow_vectorizer.fit(augmented_training_documents)  #  벡터화 모델을 학습

 

✅ 단어와 인덱스 사이의 매핑 정보를 담고 있는 딕셔너리 생성

word_idxes = bow_vectorizer.vocabulary_  # 단어와 해당 인덱스 사이의 매핑 정보를 담고 있는 딕셔너리

for key, idx in sorted(word_idxes.items()):  # items(): (키, 값) 쌍(tuple)으로 반환
  print(f'{key}:{idx}')

▲ 단어:인덱스 순으로 나타남

 

✅ word_idxes의 key를 column으로 하고 training 6개의 문장을 index로 하는 dataframe 생성,  training_document에 있는 문장들을 분리하여 해당 개수(빈도)만큼 저장

result = []
vocab= list (word_idxes.keys())

for i in range(len(augmented_training_documents)):
  result.append([])
  d = augmented_training_documents[i]
  for j in range(len(vocab)):
    target = vocab[j]
    result[-1].append(d.count(target))

tf =pd.DataFrame(result, columns=vocab, index=augmented_training_documents)
tf

▲ 해당 문장에 단어 빈도수만큼 찍힌 데이터프레임 생성

 

✅ 유사도를 측정할 문장들을 문장-단어 행렬 기반 임베딩으로 변환

bow_vector_sen_1 = bow_vectorizer.transform([sen_1]).toarray()[0]     # 벡터화 하여 array화
bow_vector_sen_2 = bow_vectorizer.transform([sen_2]).toarray()[0]
bow_vector_sen_3 = bow_vectorizer.transform([sen_3]).toarray()[0]
bow_vector_sen_4 = bow_vectorizer.transform([sen_4]).toarray()[0]
bow_vector_sen_5 = bow_vectorizer.transform([sen_5]).toarray()[0]
bow_vector_sen_6 = bow_vectorizer.transform([sen_6]).toarray()[0]


print(bow_vector_sen_1)
print(bow_vector_sen_2)
print(bow_vector_sen_3)
print(bow_vector_sen_4)
print(bow_vector_sen_5)
print(bow_vector_sen_6)

 

 

 

✅ 코사인 기반 유사도를 계산하기 위해 함수 정의

▲ 코사인 유사도 수식

 

import numpy as np
from numpy import dot
from numpy.linalg import norm

def cos_sim(A, B):
  return dot(A, B) / (norm(A) * norm(B))

print(f'의미가 유사한 문장 간 유사도 계산(조사 생략): {cos_sim(bow_vector_sen_1, bow_vector_sen_2)}')  # 0.7977, 1에 가까울수록 유사하다고 판단
print(f'의미가 유사한 문장 간 유사도 계산(순서 변경): {cos_sim(bow_vector_sen_1, bow_vector_sen_3)}')
print(f'문장 내 단어를 임의의 단어로 치환한 문장과 원본 문장간의 유사도 계산: {cos_sim(bow_vector_sen_2, bow_vector_sen_4)}')    # 0.8571
print(f'의미는 다르지만 비슷한 주제를 가지는 문장 간 유사도 계산: {cos_sim(bow_vector_sen_1, bow_vector_sen_5)}')
print(f'의미가 서로 다른 문장 간 유사도 계산: {cos_sim(bow_vector_sen_1, bow_vector_sen_6)}')

>>>
의미가 유사한 문장 간 유사도 계산(조사 생략): 0.7977240352174656
의미가 유사한 문장 간 유사도 계산(순서 변경): 1.0
문장 내 단어를 임의의 단어로 치환한 문장과 원본 문장간의 유사도 계산: 0.857142857142857
의미는 다르지만 비슷한 주제를 가지는 문장 간 유사도 계산: 0.5330017908890261
의미가 서로 다른 문장 간 유사도 계산: 0.0

 

✅ TF-IDF 기반 문서-단어 행렬을 활용하여 문장 간 유사도 측정

from sklearn.feature_extraction.text import TfidfVectorizer

# 객체생성 및 학습
tfidfv = TfidfVectorizer().fit(augmented_training_documents)

# 단어사전에서 단어와 idx값 
for key, idx in sorted(tfidfv.vocabulary_.items()):
  print(f'{key}: {idx}')

 

# TF-IDF 변환기인 tfidfv를 사용하여 주어진 문서들을 TF-IDF 벡터로 변환 후 array형태로 변환
sk_tf_idf = tfidfv.transform(augmented_training_documents).toarray()

print(sk_tf_idf) # 0~ 1 사이의 값보다 클 수 있음

# TF-IDF 행렬에서 얻어지는 유사도의 값을  0 ~ 1로 스케일링하기 위해 L1 정규화를 진행
def l1_normalize(v):
  norm = np.sum(v)
  return v / norm
  

fidf_vectorizer = TfidfVectorizer()  # 객체생성
fidf_matrix_l1 = fidf_vectorizer.fit_transform(augmented_training_documents) # 학습시키고 벡터화
fidf_norm_1 = l1_normalize(fidf_matrix_l1)  # 정규화
fidf_norm_1  # 데이터가 있는  객체
>>> <18x147 sparse matrix of type '<class 'numpy.float64'>'
	with 212 stored elements in Compressed Sparse Row format>
    
    
tf_sen_1 = fidf_norm_1[0:1]
tf_sen_2 = fidf_norm_1[1:2]
tf_sen_3 = fidf_norm_1[2:3]
tf_sen_4 = fidf_norm_1[3:4]
tf_sen_5 = fidf_norm_1[4:5]
tf_sen_6 = fidf_norm_1[5:6]
tf_sen_1  # 객체
>>> <1x147 sparse matrix of type '<class 'numpy.float64'>'
	with 8 stored elements in Compressed Sparse Row format>
    
# array형태로 보기
tf_sen_1.toarray()

 

 

✅ 유클리디안 거리 기반 유사도 측정

from sklearn.metrics.pairwise import euclidean_distances

euclidean_distances(tf_sen_1, tf_sen_2)
>>> array([[0.01090025]])

print(f'의미가 유사한 문장 간 유사도 계산(조사 생략): {euclidean_distances_value(tf_sen_1, tf_sen_2)}')
print(f'의미가 유사한 문장 간 유사도 계산(순서 변경): {euclidean_distances_value(tf_sen_1, tf_sen_3)}')
print(f'문장 내 단어를 임의의 단어로 치환한 문장과 원본 문장간의 유사도 계산: {euclidean_distances_value(tf_sen_2, tf_sen_4)}')
print(f'의미는 다르지만 비슷한 주제를 가지는 문장 간 유사도 계산: {euclidean_distances_value(tf_sen_1, tf_sen_5)}')
print(f'의미가 서로 다른 문장 간 유사도 계산: {euclidean_distances_value(tf_sen_1, tf_sen_6)}')

>>> 의미가 유사한 문장 간 유사도 계산(조사 생략): 0.011
의미가 유사한 문장 간 유사도 계산(순서 변경): 0.0
문장 내 단어를 임의의 단어로 치환한 문장과 원본 문장간의 유사도 계산: 0.011
의미는 다르지만 비슷한 주제를 가지는 문장 간 유사도 계산: 0.018
의미가 서로 다른 문장 간 유사도 계산: 0.024

 

 

✅ 맨해튼 거리 기반 유사도 측정

from sklearn.metrics.pairwise import manhattan_distances, cosine_similarity

manhattan_distances(tf_sen_1, tf_sen_2)
>>> array([[0.01962577]])

# 맨해튼 거리 유사도를 나타내는 함수
def manhattan_distances_value(vec_1, vec_2):
  return round(manhattan_distances(vec_1, vec_2)[0][0], 3)
  
print(f'의미가 유사한 문장 간 유사도 계산(조사 생략): {manhattan_distances_value(tf_sen_1, tf_sen_2)}')
print(f'의미가 유사한 문장 간 유사도 계산(순서 변경): {manhattan_distances_value(tf_sen_1, tf_sen_3)}')
print(f'문장 내 단어를 임의의 단어로 치환한 문장과 원본 문장간의 유사도 계산: {manhattan_distances_value(tf_sen_2, tf_sen_4)}')
print(f'의미는 다르지만 비슷한 주제를 가지는 문장 간 유사도 계산: {manhattan_distances_value(tf_sen_1, tf_sen_5)}')
print(f'의미가 서로 다른 문장 간 유사도 계산: {manhattan_distances_value(tf_sen_1, tf_sen_6)}')

>>> 의미가 유사한 문장 간 유사도 계산(조사 생략): 0.02
의미가 유사한 문장 간 유사도 계산(순서 변경): 0.0
문장 내 단어를 임의의 단어로 치환한 문장과 원본 문장간의 유사도 계산: 0.017
의미는 다르지만 비슷한 주제를 가지는 문장 간 유사도 계산: 0.054
의미가 서로 다른 문장 간 유사도 계산: 0.096

 

✅ 코사인 유사도 측정

cosine_similarity(tf_sen_1, tf_sen_2)

def cosine_similarity_value(vec_1, vec_2):
  return round(cosine_similarity(vec_1, vec_2)[0][0], 3)
  
print(f'의미가 유사한 문장 간 유사도 계산(조사 생략): {cosine_similarity_value(tf_sen_1, tf_sen_2)}')
print(f'의미가 유사한 문장 간 유사도 계산(순서 변경): {cosine_similarity_value(tf_sen_1, tf_sen_3)}')
print(f'문장 내 단어를 임의의 단어로 치환한 문장과 원본 문장간의 유사도 계산: {cosine_similarity_value(tf_sen_2, tf_sen_4)}')
print(f'의미는 다르지만 비슷한 주제를 가지는 문장 간 유사도 계산: {cosine_similarity_value(tf_sen_1, tf_sen_5)}')
print(f'의미가 서로 다른 문장 간 유사도 계산: {cosine_similarity_value(tf_sen_1, tf_sen_6)}')

>>> 의미가 유사한 문장 간 유사도 계산(조사 생략): 0.791
의미가 유사한 문장 간 유사도 계산(순서 변경): 1.0
문장 내 단어를 임의의 단어로 치환한 문장과 원본 문장간의 유사도 계산: 0.797
의미는 다르지만 비슷한 주제를 가지는 문장 간 유사도 계산: 0.425
의미가 서로 다른 문장 간 유사도 계산: 0.0

 

 

✅ 언어 모델을 활용하여 문장 간 유사도 측정하기(BERT모델)

!pip install transformers

from transformers import AutoModel, AutoTokenizer, BertTokenizer

# BERT 모델
MODEL_NAME = 'bert-base-multilingual-cased'

model = AutoModel.from_pretrained(MODEL_NAME)

# 사전학습한 토크나이저 생성
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)

bert_sen_1 = tokenizer(sen_1, return_tensors= 'pt')  # 파이토치 텐서로
bert_sen_2 = tokenizer(sen_2, return_tensors= 'pt')
bert_sen_3 = tokenizer(sen_3, return_tensors= 'pt')
bert_sen_4 = tokenizer(sen_4, return_tensors= 'pt')
bert_sen_5 = tokenizer(sen_5, return_tensors= 'pt')
bert_sen_6 = tokenizer(sen_6, return_tensors= 'pt')


# 문장 임베딩
# pooler_output: 문장 임베딩을 나타내는 벡터
sen_1_outputs = model(**bert_sen_1)  #**:  딕셔너리 형태로 매개변수를 집어넣음
sen_1_pooler_output = sen_1_outputs.pooler_output

sen_2_outputs = model(**bert_sen_2)
sen_2_pooler_output = sen_2_outputs.pooler_output

sen_3_outputs = model(**bert_sen_3)
sen_3_pooler_output = sen_3_outputs.pooler_output

sen_4_outputs = model(**bert_sen_4)
sen_4_pooler_output = sen_4_outputs.pooler_output

sen_5_outputs = model(**bert_sen_5)
sen_5_pooler_output = sen_5_outputs.pooler_output

sen_6_outputs = model(**bert_sen_6)
sen_6_pooler_output = sen_6_outputs.pooler_output

 

from torch import nn
cos_sim = nn.CosineSimilarity(dim=1, eps=1e-6)   # eps: 분모 자체가 0이되는것을 방지하기 위해 작은 값을 입력

print(f'의미가 유사한 문장 간 유사도 계산(조사 생략): {cos_sim(sen_1_pooler_output, sen_2_pooler_output)}')
print(f'의미가 유사한 문장 간 유사도 계산(순서 변경): {cos_sim(sen_1_pooler_output, sen_3_pooler_output)}')
print(f'문장 내 단어를 임의의 단어로 치환한 문장과 원본 문장간의 유사도 계산: {cos_sim(sen_2_pooler_output, sen_4_pooler_output)}')
print(f'의미는 다르지만 비슷한 주제를 가지는 문장 간 유사도 계산: {cos_sim(sen_1_pooler_output, sen_5_pooler_output)}')
print(f'의미가 서로 다른 문장 간 유사도 계산: {cos_sim(sen_1_pooler_output, sen_6_pooler_output)}')

>>> 의미가 유사한 문장 간 유사도 계산(조사 생략): tensor([0.9901], grad_fn=<SumBackward1>)
의미가 유사한 문장 간 유사도 계산(순서 변경): tensor([0.9972], grad_fn=<SumBackward1>)
문장 내 단어를 임의의 단어로 치환한 문장과 원본 문장간의 유사도 계산: tensor([0.9916], grad_fn=<SumBackward1>)
의미는 다르지만 비슷한 주제를 가지는 문장 간 유사도 계산: tensor([0.9744], grad_fn=<SumBackward1>)
의미가 서로 다른 문장 간 유사도 계산: tensor([0.9619], grad_fn=<SumBackward1>)


# 1,3번의 유사도가 더 높다. -> BERT 모델에 한국어를 지원하지만 완벽하지 않기 때문에

728x90
반응형
LIST