오늘은 BERT 모델을 이용하여 임베딩 및 유사도 측정 실습을 진행하였다.
1. 문서 집합 구축
우선 이후에 유사도를 이용하여 유사성과 모호성을 구별해 보기 위해 적절한 문장 예시들을 생성하였다.
sen_1 = "오늘 점심에 배가 너무 고파서 밥을 너무 많이 먹었다."
sen_2 = "오늘 점심에 배가 고파서 밥을 많이 먹었다."
sen_3 = "오늘 배가 너무 고파서 점심에 밥을 너무 많이 먹었다."
sen_4 = "오늘 점심에 배가 고파서 비행기를 많이 먹었다."
sen_5 = "어제 저녁에 밥을 너무 많이 먹었더니 배가 부르다."
sen_6 = "이따가 오후 7시에 출발하는 비행기가 3시간 연착 되었다고 하네요."
training_documents = [sen_1, sen_2, sen_3, sen_4, sen_5, sen_6]
각각의 문장예시는 아래의 내용들을 비교하기 위해 만들어 졌다.
`sen_1 ` 과 ` sen_2 ` 는 의미가 유사한 문장.(부사 생략)
`sen_1 ` 과 ` sen_3 ` 는 의미가 유사한 문장 (순서 바꿈)
` sen_2 ` 와 ` sen_4 ` 는 단어를 랜덤으로 변경한 문장
` sen_1 ` 과 ` sen_5 ` 는 의미는 다르지만 비슷한 주제를 가진 문장
` sen_1 ` 과 ` sen_6 ` 은 의미가 서로 다른 문장
이렇게 만든 문서 집합을 이용하여 단어 집합을 만들어야 하는데 문장이 6개밖에 안돼서 너무 크기가 작기 때문에 다양한 단어들을 포함하기 위하여 임의의 뉴스 corpus를 수집할 것이다.
!pip install newspaper3k
from newspaper import Article
URL = "https://www.wikitree.co.kr/articles/817443"
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)
이제 이렇게 얻은 뉴스 corpus를 기존 문서 집합에 문장 단위로 집어넣어야 한다.
이번 실습에서는 문장을 분리하기 위하여 한국어 문장분리기인 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)
위와 같이 전체 corpus에서 한 줄씩 불러와서 kss를 이용하여 문장을 분리한 뒤 분절된 문장들을 `splited_context`에 넣어줬다.
이후 제일 처음에 인위적으로 생성한 문장들과 방금 뉴스 corpus로부터 처리한 문장들을 합쳐준다.
augmented_training_documents = training_documents + splited_context
2. Bag of Word 기반
우선 BoW(Bag of Word) 기반 문서-단어 행렬을 활용하여 문장의 유사도를 측정해 보자.
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()):
print(f"{key}: {idx}")
import pandas as pd
result = []
vocab = list(word_idxes.keys())
for i in range(len(training_documents)):
result.append([])
d = training_documents[i]
for j in range(len(vocab)):
target = vocab[j]
result[-1].append(d.count(target))
tf_ = pd.DataFrame(result, columns = vocab)
tf_
위에서 뉴스 corpus와 임의로 생성한 문장들에 있는 단어들을 이용하여 단어 vocab을 생성하였고, 각 문장들을 순회하면서 어떤 단어가 몇 번 들어가 있는지를 count 하면서 문서-단어 행렬을 만든다. 우리는 임의로 만든(본 글 젤 위에 있는 예시) 문장들의 유사도를 비교하는 것이 목적이므로, 해당 단어들의 문서-단어 행렬을 위의 코드를 통해 만들면 아래와 같은 결과가 나온다.
이거는 우리가 눈으로 확인할 수 있게 임의로 만든 방법이고, 이제 아까 fit 한 countvectorizer을 이용하여 각 문장들을 transform 해주면 각 문장들이 원-핫 인코딩 된 벡터들로 임베딩 된 것을 볼 수 있다.
bow_vector_sen_1 = bow_vectorizer.transform([sen_1]).toarray()[0]
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]
2-1. Cosine similarity
이제 각 문장들이 임베딩이 되었으므로, cosine유사도를 이용하여 각 문장의 유사도를 계산해 보자.
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"의미가 유사한 문장 간 유사도 계산 (부사 생략): (sen_1, sen_2) = {cos_sim(bow_vector_sen_1, bow_vector_sen_2)}")
print(f"의미가 유사한 문장 간 유사도 계산 (순서 변경): (sen_1, sen_3) = {cos_sim(bow_vector_sen_1, bow_vector_sen_3)}")
print(f"문장 내 단어를 임의의 단어로 치환한 문장과 원본 문장 간 유사도 계산: (sen_2, sen_4) = {cos_sim(bow_vector_sen_2, bow_vector_sen_4)}")
print(f"의미는 다르지만 비슷한 주제를 가지는 문장 간 유사도 계산: (sen_1, sen_5) = {cos_sim(bow_vector_sen_1, bow_vector_sen_5)}")
print(f"의미가 서로 다른 문장 간 유사도 계산: (sen_1, sen_6) = {cos_sim(bow_vector_sen_1, bow_vector_sen_6)}")
위와 같이 유사도가 의미 있게 나온 것을 볼 수 있다.
3. TF-IDF 기반
다음으로는 TF-IDF 기반 문서-단어 행렬을 만들어 유사도를 측정해 보도록 하겠다.
from sklearn.feature_extraction.text import TfidfVectorizer
tfidfv = TfidfVectorizer().fit(augmented_training_documents)
for key, idx in sorted(tfidfv.vocabulary_.items()):
print(f"{key}: {idx}")
sk_tf_idf = tfidfv.transform(augmented_training_documents).toarray()
print(sk_tf_idf)
이전 BoW는 단순 count만 세서 표현했는데, tf-idf의 경우 빈도수를 고려하기 때문에 위와 같은 숫자들이 나온다. 이 임베딩 값들을 0~1로 정규화하기 위하여 `L1 정규화`를 진행한다.
def l1_normalize(v):
norm = np.sum(v)
return v / norm
tfidf_vectorizer = TfidfVectorizer()
tfidf_matrix_l1 = tfidf_vectorizer.fit_transform(augmented_training_documents)
tfidf_norm_l1 = l1_normalize(tfidf_matrix_l1)
tf_sen_1 = tfidf_norm_l1[0:1]
tf_sen_2 = tfidf_norm_l1[1:2]
tf_sen_3 = tfidf_norm_l1[2:3]
tf_sen_4 = tfidf_norm_l1[3:4]
tf_sen_5 = tfidf_norm_l1[4:5]
tf_sen_6 = tfidf_norm_l1[5:6]
위의 tf_sen들은 모두 sparse matrix 형태로 되어있기 때문에 값을 확인하기 위해서는 toarray()를 써야 한다.
이제 위와 같이 임베딩된 벡터들을 이용하여 다양한 유사도를 계산해 보자.
3-1. Euclidean distance
from sklearn.metrics.pairwise import euclidean_distances
def euclidean_distances_value(vec_1, vec_2):
return round(euclidean_distances(vec_1, vec_2)[0][0], 3)
print(f"의미가 유사한 문장 간 유사도 계산 (부사 생략): (sen_1, sen_2) = {euclidean_distances_value(tf_sen_1, tf_sen_2)}")
print(f"의미가 유사한 문장 간 유사도 계산 (순서 변경): (sen_1, sen_3) = {euclidean_distances_value(tf_sen_1, tf_sen_3)}")
print(f"문장 내 단어를 임의의 단어로 치환한 문장과 원본 문장 간 유사도 계산: (sen_2, sen_4) = {euclidean_distances_value(tf_sen_2, tf_sen_4)}")
print(f"의미는 다르지만 비슷한 주제를 가지는 문장 간 유사도 계산: (sen_1, sen_5) = {euclidean_distances_value(tf_sen_1, tf_sen_5)}")
print(f"의미가 서로 다른 문장 간 유사도 계산: (sen_1, sen_6) = {euclidean_distances_value(tf_sen_1, tf_sen_6)}")
우선 matrix가 아니라 스칼라값으로 나오게 하기 위해 round를 이용하여 반올림처리를 해주는 유클리드 거리를 이용하였고, 계산결과는 위와 같다. 유클리드 거리는 거리가 작을수록 유사한 것이다.
3-2. Manhattan distance
맨해튼 거리도 동일하게 스칼라값으로 결과값이 나오도록 round함수를 적용해준다음 유사도를 계산하였다.
from sklearn.metrics.pairwise import manhattan_distances
def manhattan_distances_value(vec_1, vec_2):
return round(manhattan_distances(vec_1, vec_2)[0][0], 3)
print(f"의미가 유사한 문장 간 유사도 계산 (조사 생략): (sen_1, sen_2) = {manhattan_distances_value(tf_sen_1, tf_sen_2)}")
print(f"의미가 유사한 문장 간 유사도 계산 (순서 변경): (sen_1, sen_3) = {manhattan_distances_value(tf_sen_1, tf_sen_3)}")
print(f"문장 내 단어를 임의의 단어로 치환한 문장과 원본 문장 간 유사도 계산: (sen_2, sen_4) = {manhattan_distances_value(tf_sen_2, tf_sen_4)}")
print(f"의미는 다르지만 비슷한 주제를 가지는 문장 간 유사도 계산: (sen_1, sen_5) = {manhattan_distances_value(tf_sen_1, tf_sen_5)}")
print(f"의미가 서로 다른 문장 간 유사도 계산: (sen_1, sen_6) = {manhattan_distances_value(tf_sen_1, tf_sen_6)}")
맨하튼 거리도 대체로 올바르게 측정된 것 같으며 유클리디안 거리보다 당연히 조금씩 크게 나온 것을 확인할 수 있다.
3-3. Cosine similarity
마지막으로 코사인 유사도이다.
from sklearn.metrics.pairwise import cosine_similarity
def cosine_similarity_value(vec_1, vec_2):
return round(cosine_similarity(vec_1, vec_2)[0][0], 3)
print(f"의미가 유사한 문장 간 유사도 계산 (부사 생략): (sen_1, sen_2) = {cosine_similarity_value(tf_sen_1, tf_sen_2)}")
print(f"의미가 유사한 문장 간 유사도 계산 (순서 변경): (sen_1, sen_3) = {cosine_similarity_value(tf_sen_1, tf_sen_3)}")
print(f"문장 내 단어를 임의의 단어로 치환한 문장과 원본 문장 간 유사도 계산: (sen_2, sen_4) = {cosine_similarity_value(tf_sen_2, tf_sen_4)}")
print(f"의미는 다르지만 비슷한 주제를 가지는 문장 간 유사도 계산: (sen_1, sen_5) = {cosine_similarity_value(tf_sen_1, tf_sen_5)}")
print(f"의미가 서로 다른 문장 간 유사도 계산: (sen_1, sen_6) = {cosine_similarity_value(tf_sen_1, tf_sen_6)}")
4. 언어모델을 활용한 문장 간 유사도 측정
다음으로 언어모델 BERT를 이용하여 유사도를 측정해 볼 것이다.
언어모델기반의 임베딩은 학습한 자연어 코퍼스를 언어 모델링하여 , 입력 문장을 하나의 벡터로 압축한다.
BERT 모델은 입력 문장의 정보를 [CLS]라는 토큰에 압축한다.
우선 Hugging Face에서 다중언어 BERT모델을 불러온다.
from transformers import AutoModel, AutoTokenizer, BertTokenizer
# Store the model we want to use
MODEL_NAME = "bert-base-multilingual-cased"
# We need to create the model and tokenizer
model = AutoModel.from_pretrained(MODEL_NAME)
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
다음으로 우리가 테스트할 문장을 BERT에 맞는 tokenizer을 이용하여 tokenizing 해준다.
그리고 BERT 모델에 들어가기 위해서는 [CLS]라는 토큰이 필요하기 때문에 아래의 pooler_output을 뽑아내는 과정을 수행한다.
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")
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
그리고 해당 모델의 임베딩 된 결과는 이전까지 처럼 numpy형태가 아니라 BERT모델의 출력값 형태로 되어있기 때문에 pytorch의 코사인 유사도를 구하는 함수를 이용하여 유사도를 계산해 준다.
from torch import nn
cos_sim = nn.CosineSimilarity(dim=1, eps=1e-6)
print(f"의미가 유사한 문장 간 유사도 계산 (부사 생략): (sen_1, sen_2) = {cos_sim(sen_1_pooler_output, sen_2_pooler_output)}")
print(f"의미가 유사한 문장 간 유사도 계산 (순서 변경): (sen_1, sen_3) = {cos_sim(sen_1_pooler_output, sen_3_pooler_output)}")
print(f"문장 내 단어를 임의의 단어로 치환한 문장과 원본 문장 간 유사도 계산: (sen_2, sen_4) = {cos_sim(sen_2_pooler_output, sen_4_pooler_output)}")
print(f"의미는 다르지만 비슷한 주제를 가지는 문장 간 유사도 계산: (sen_1, sen_5) = {cos_sim(sen_1_pooler_output, sen_5_pooler_output)}")
print(f"의미가 서로 다른 문장 간 유사도 계산: (sen_1, sen_6) = {cos_sim(sen_1_pooler_output, sen_6_pooler_output)}")
우선 결과는 위와 같이 다른 문장 간 유사도도 높게 나왔지만, 위 모델은 multi-lingual BERT이긴 하지만 한국어에 대한 학습이 많이 부족하고 엄청나게 많은 언어들에 대한 정보가 학습되어 있는 문장에서 한국어 자체들끼리는 매우 넓은 공간에서 보면 가깝게 있을 수도 있기 때문에 위의 문장들이 모두 한국어여서 조금씩 비슷하게 나온 것 같다.
'ML & DL > NLP' 카테고리의 다른 글
[패캠/NLP] 임베딩 기법(Word2Vec, FastText, GloVe) (1) | 2023.12.20 |
---|---|
[패캠/NLP] 워드 임베딩 (1) | 2023.12.20 |
[패캠/NLP] 자연어 특성과 임베딩 (0) | 2023.12.14 |
형태소 분석과 전처리 실습 (0) | 2023.12.07 |
코퍼스 전처리 및 토큰화 (1) | 2023.12.07 |