인공지능/자연어 처리

자연어 처리 python 실습 - BERT 모델의 임베딩 간 유사도 측정

이게될까 2024. 3. 13. 17:28
728x90
728x90

BERT 모델의 임베딩 간 유사도 측정

Introduction

Chapter 3. 컴퓨터는 자연어를 어떻게 이해하는가 강의의 BERT 모델의 임베딩 간 유사도 측정 실습 강의입니다.

강의에서 배웠던 여러 가설 기반 임베딩을 직접 구축해보고, 구축한 임베딩을 활용하여 문장 간 유사도를 계산합니다.

추가로, 구축 방법 별 유사도 경향을 분석하여 임베딩에 대한 이해도를 높입니다.

1. 문서 집합 구축

테스트할 다양한 문장(문서)들에 대한 문서 집합을 구축합니다.
자연어의 특성인 유사성과 모호성을 잘 설명할 수 있도록 문장 예시들을 구성합니다.

  • 의미가 유사한 문장 간 유사도 계산 (조사 생략): (sen_1, sen_2)
  • 의미가 유사한 문장 간 유사도 계산 (순서 변경): (sen_1, sen_3)
  • 문장 내 단어를 임의의 단어로 치환한 문장과 원본 문장 간 유사도 계산: (sen_2, sen_4)
  • 의미는 다르지만 비슷한 주제를 가지는 문장 간 유사도 계산: (sen_1, sen_5)
  • 의미가 서로 다른 문장 간 유사도 계산: (sen_1, sen_6)
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]

2. 단어의 출현 빈도 기반 임베딩

  1. Bag of Word
  2. TF-IDF

단어 문서 집합을 구성할 때 더 다양한 단어들을 넣기 위해 뉴스 코퍼스를 수집합니다.
너무 적은 양의 문서는 학습이 제대로 되지 않기 때문에 더 위키 뉴스와 같은 곳에서 가져온다.

!pip install newspaper3k

앞선 실습에서 사용했던 것처럼, newspaper 라이브러리를 활용하여 뉴스를 크롤링해볼게요.

이번 실습에서도 저작권에서 비교적 자유로운 위키트리 뉴스 데이터를 사용하도록 하겠습니다.

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)

title: [오늘은머니] CG없이 이걸 찍었다고? 핵폭발 장면에 반응 쏟아진 영화 '오펜하이머' 티저 영상
context: 현재 할리우드에서 가장 성공한 감독 중 한 명으로 꼽히고 있는 SF 영화계의 거장 '크리스토퍼 놀란'. 연출하는 영화마다 최소한의 CG 사용으로 화제를 모았던 그가 차기작에서는 핵폭발을 CG없이 구현해냈다는 놀라운 소식이 전해졌다.

이하 유니버설 픽쳐스

지난 19일, 유니버설 픽쳐스 유튜브에 2분 길이의 한 영상이 공개됐다. 이틀 만에 80만 조회수를 기록 중인 이 영상은 영화 다크나이트, 인셉션 등 수많은 명작으로 탄탄한 팬층을 지닌 크리스토퍼 놀란 감독의 차기작 '오펜하이머'의 첫 예고편이다.

티저 형식으로 공개된 영상에는 ‘줄리어스 로버트 오펜하이머’ 역을 맡은 배우 킬리언 머피의 고뇌하는 독백이 주로 담겼다. 또한 제2차 세계대전 당시 벌어졌던 핵무기 개발 실험 '맨해튼 프로젝트'가 진행되는 모습이 그려졌다.

웅장한 스케일의 예고편 중 가장 주목해야 할 부분은 바로 핵폭발 장면이다. 지난 12일, 크리스토퍼 놀란 감독이 영국 영화 잡지 ‘토털 필름’과의 인터뷰에서 “CG를 사용하지 않고 핵폭발을 재현했다”고 언급했기 때문. 예고편을 통해 폭발 장면을 처음 접한 누리꾼들은 연신 놀랍다는 반응을 보이며, 그래픽 없이 어떻게 구현했을 지에 대해 의견이 분분하다.

주인공 역을 맡은 킬리언 머피 외에도 에밀리 블런트, 맷 데이먼, 로버트 다우니 주니어 등 유명 할리우드 스타들이 총출동할 예정으로 알려졌다. 놀란 감독의 2년 만의 차기작으로, 내년 여름 극장가를 강타할 영화 ‘오펜하이머’는 2023년 7월 국내 개봉 예정이다.

크롤링한 뉴스 데이터를 문장 단위로 분절하여 기존 문서 집합에 추가합니다.

문장을 잘 분리하기 위해, 간단한 전처리를 진행합니다.

news_context = article.text.split('\n') 

for text in news_context:
  print(text)

현재 할리우드에서 가장 성공한 감독 중 한 명으로 꼽히고 있는 SF 영화계의 거장 '크리스토퍼 놀란'. 연출하는 영화마다 최소한의 CG 사용으로 화제를 모았던 그가 차기작에서는 핵폭발을 CG없이 구현해냈다는 놀라운 소식이 전해졌다.

이하 유니버설 픽쳐스

지난 19일, 유니버설 픽쳐스 유튜브에 2분 길이의 한 영상이 공개됐다. 이틀 만에 80만 조회수를 기록 중인 이 영상은 영화 다크나이트, 인셉션 등 수많은 명작으로 탄탄한 팬층을 지닌 크리스토퍼 놀란 감독의 차기작 '오펜하이머'의 첫 예고편이다.

티저 형식으로 공개된 영상에는 ‘줄리어스 로버트 오펜하이머’ 역을 맡은 배우 킬리언 머피의 고뇌하는 독백이 주로 담겼다. 또한 제2차 세계대전 당시 벌어졌던 핵무기 개발 실험 '맨해튼 프로젝트'가 진행되는 모습이 그려졌다.

웅장한 스케일의 예고편 중 가장 주목해야 할 부분은 바로 핵폭발 장면이다. 지난 12일, 크리스토퍼 놀란 감독이 영국 영화 잡지 ‘토털 필름’과의 인터뷰에서 “CG를 사용하지 않고 핵폭발을 재현했다”고 언급했기 때문. 예고편을 통해 폭발 장면을 처음 접한 누리꾼들은 연신 놀랍다는 반응을 보이며, 그래픽 없이 어떻게 구현했을 지에 대해 의견이 분분하다.

주인공 역을 맡은 킬리언 머피 외에도 에밀리 블런트, 맷 데이먼, 로버트 다우니 주니어 등 유명 할리우드 스타들이 총출동할 예정으로 알려졌다. 놀란 감독의 2년 만의 차기작으로, 내년 여름 극장가를 강타할 영화 ‘오펜하이머’는 2023년 7월 국내 개봉 예정이다.

문장을 잘 분리하기 위해서 한국어 문장분리기인 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)

WARNING:root:Oh! You have mecab in your environment. Kss will take this as a backend! :D

(0, "현재 할리우드에서 가장 성공한 감독 중 한 명으로 꼽히고 있는 SF 영화계의 거장 '크리스토퍼 놀란'.")
(1, '연출하는 영화마다 최소한의 CG 사용으로 화제를 모았던 그가 차기작에서는 핵폭발을 CG없이 구현해냈다는 놀라운 소식이 전해졌다.')
(2, '이하 유니버설 픽쳐스')
(3, '지난 19일, 유니버설 픽쳐스 유튜브에 2분 길이의 한 영상이 공개됐다.')
(4, "이틀 만에 80만 조회수를 기록 중인 이 영상은 영화 다크나이트, 인셉션 등 수많은 명작으로 탄탄한 팬층을 지닌 크리스토퍼 놀란 감독의 차기작 '오펜하이머'의 첫 예고편이다.")
(5, '티저 형식으로 공개된 영상에는 ‘줄리어스 로버트 오펜하이머’ 역을 맡은 배우 킬리언 머피의 고뇌하는 독백이 주로 담겼다.')
(6, "또한 제2차 세계대전 당시 벌어졌던 핵무기 개발 실험 '맨해튼 프로젝트'가 진행되는 모습이 그려졌다.")
(7, '웅장한 스케일의 예고편 중 가장 주목해야 할 부분은 바로 핵폭발 장면이다.')
(8, '지난 12일, 크리스토퍼 놀란 감독이 영국 영화 잡지 ‘토털 필름’과의 인터뷰에서 “CG를 사용하지 않고 핵폭발을 재현했다”고 언급했기 때문.')
(9, '예고편을 통해 폭발 장면을 처음 접한 누리꾼들은 연신 놀랍다는 반응을 보이며, 그래픽 없이 어떻게 구현했을 지에 대해 의견이 분분하다.')
(10, '주인공 역을 맡은 킬리언 머피 외에도 에밀리 블런트, 맷 데이먼, 로버트 다우니 주니어 등 유명 할리우드 스타들이 총출동할 예정으로 알려졌다.')
(11, '놀란 감독의 2년 만의 차기작으로, 내년 여름 극장가를 강타할 영화 ‘오펜하이머’는 2023년 7월 국내 개봉 예정이다.')

augmented_training_documents = training_documents + splited_context # 내가 만든 이전 데이터를 합친다.

for text in augmented_training_documents:
  print(text)

오늘 점심에 배가 너무 고파서 밥을 너무 많이 먹었다.
오늘 점심에 배가 고파서 밥을 많이 먹었다.
오늘 배가 너무 고파서 점심에 밥을 너무 많이 먹었다.
오늘 점심에 배가 고파서 비행기를 많이 먹었다.
어제 저녁에 밥을 너무 많이 먹었더니 배가 부르다.
이따가 오후 7시에 출발하는 비행기가 3시간 연착 되었다고 하네요.
현재 할리우드에서 가장 성공한 감독 중 한 명으로 꼽히고 있는 SF 영화계의 거장 '크리스토퍼 놀란'.
연출하는 영화마다 최소한의 CG 사용으로 화제를 모았던 그가 차기작에서는 핵폭발을 CG없이 구현해냈다는 놀라운 소식이 전해졌다.
이하 유니버설 픽쳐스
지난 19일, 유니버설 픽쳐스 유튜브에 2분 길이의 한 영상이 공개됐다.
이틀 만에 80만 조회수를 기록 중인 이 영상은 영화 다크나이트, 인셉션 등 수많은 명작으로 탄탄한 팬층을 지닌 크리스토퍼 놀란 감독의 차기작 '오펜하이머'의 첫 예고편이다.
티저 형식으로 공개된 영상에는 ‘줄리어스 로버트 오펜하이머’ 역을 맡은 배우 킬리언 머피의 고뇌하는 독백이 주로 담겼다.
또한 제2차 세계대전 당시 벌어졌던 핵무기 개발 실험 '맨해튼 프로젝트'가 진행되는 모습이 그려졌다.
웅장한 스케일의 예고편 중 가장 주목해야 할 부분은 바로 핵폭발 장면이다.
지난 12일, 크리스토퍼 놀란 감독이 영국 영화 잡지 ‘토털 필름’과의 인터뷰에서 “CG를 사용하지 않고 핵폭발을 재현했다”고 언급했기 때문.
예고편을 통해 폭발 장면을 처음 접한 누리꾼들은 연신 놀랍다는 반응을 보이며, 그래픽 없이 어떻게 구현했을 지에 대해 의견이 분분하다.
주인공 역을 맡은 킬리언 머피 외에도 에밀리 블런트, 맷 데이먼, 로버트 다우니 주니어 등 유명 할리우드 스타들이 총출동할 예정으로 알려졌다.
놀란 감독의 2년 만의 차기작으로, 내년 여름 극장가를 강타할 영화 ‘오펜하이머’는 2023년 7월 국내 개봉 예정이다.

2.1. 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}") # 173개의 단어로 되어있다.

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_ # 모든 단어들이 열에 나열되고, 행이 문서가 되며, 문서마다 단어가 나온 횟수가 나온다.


import pandas as pd

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)
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)

[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0
2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 0 1 1 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 0 1 1 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0
2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 0 1 1 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0
0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 0 1 1 0 0 0 1
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
...
0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 1 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0]

코사인 기반 유사도 계산을 위해 함수를 정의합니다.

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))

Bag of words로 만든 문서-단어 행렬 임베딩을 활용하여 단어 간 유사도를 계산합니다.
sen_1 = "오늘 점심에 배가 너무 고파서 밥을 너무 많이 먹었다."

sen_2 = "오늘 점심에 배가 고파서 밥을 많이 먹었다."

sen_3 = "오늘 배가 너무 고파서 점심에 밥을 너무 많이 먹었다."

sen_4 = "오늘 점심에 배가 고파서 비행기를 많이 먹었다."

sen_5 = "어제 저녁에 밥을 너무 많이 먹었더니 배가 부르다."

sen_6 = "이따가 오후 7시에 출발하는 비행기가 3시간 연착 되었다고 하네요."

  • 의미가 유사한 문장 간 유사도 계산 (조사 생략): (sen_1, sen_2)
  • 의미가 유사한 문장 간 유사도 계산 (순서 변경): (sen_1, sen_3)
  • 문장 내 단어를 임의의 단어로 치환한 문장과 원본 문장 간 유사도 계산: (sen_2, sen_4)
  • 의미는 다르지만 비슷한 주제를 가지는 문장 간 유사도 계산: (sen_1, sen_5)
  • 의미가 서로 다른 문장 간 유사도 계산: (sen_1, sen_6)
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)}")

의미가 유사한 문장 간 유사도 계산 (조사 생략): (sen_1, sen_2) = 0.7977240352174656
의미가 유사한 문장 간 유사도 계산 (순서 변경): (sen_1, sen_3) = 1.0
문장 내 단어를 임의의 단어로 치환한 문장과 원본 문장 간 유사도 계산: (sen_2, sen_4) = 0.857142857142857
밥 -> 비행기로만 바뀐 것으로 여기선 의미가 들어간게 아니기 때문에 유사도가 높게 나온다.
의미는 다르지만 비슷한 주제를 가지는 문장 간 유사도 계산: (sen_1, sen_5) = 0.5330017908890261
의미가 서로 다른 문장 간 유사도 계산: (sen_1, sen_6) = 0.0
단어가 하나도 겹치지 않아서 유사도가 0으로 나온다.

2.2. 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}")

12일: 0
19일: 1
2023년: 2
2년: 3
2분: 4
3시간: 5
7시에: 6
7월: 7
80만: 8
cg: 9
cg를: 10
cg없이: 11
sf: 12
가장: 13
감독: 14
감독의: 15
감독이: 16
강타할: 17
개발: 18
개봉: 19
거장: 20
고뇌하는: 21
고파서: 22
공개됐다: 23
공개된: 24
...
핵폭발을: 170
현재: 171
형식으로: 172
화제를: 173

이건 띄어쓰기를 기반으로 만드는 거기 때문에 똑같다.

sk_tf_idf = tfidfv.transform(augmented_training_documents).toarray()
print(sk_tf_idf)

[[0. 0. 0. ... 0. 0. 0. ]
[0. 0. 0. ... 0. 0. 0. ]
[0. 0. 0. ... 0. 0. 0. ]
...
[0. 0. 0. ... 0. 0. 0. ]
[0. 0. 0. ... 0. 0. 0. ]
[0. 0. 0.26243151 ... 0. 0. 0. ]]

중요도가 고려된 값이 들어가 있다.

TF-IDF 행렬에서 얻어지는 유사도의 값을 0~1로 scaling하기 위해 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)

tfidf_norm_l1

<18x174 sparse matrix of type '<class 'numpy.float64'>'
with 218 stored elements in Compressed Sparse Row format>

sparse matrix 형태로 되어있는 것을 알 수 있다.

이제 TF-IDF 행렬을 활용하여 문장 간 유사도를 측정해볼게요!

이번에는 크게 다음과 같이 여러가지 방법으로 유사도를 구해보겠습니다.
sen_1 = "오늘 점심에 배가 너무 고파서 밥을 너무 많이 먹었다."

sen_2 = "오늘 점심에 배가 고파서 밥을 많이 먹었다."

sen_3 = "오늘 배가 너무 고파서 점심에 밥을 너무 많이 먹었다."

sen_4 = "오늘 점심에 배가 고파서 비행기를 많이 먹었다."

sen_5 = "어제 저녁에 밥을 너무 많이 먹었더니 배가 부르다."

sen_6 = "이따가 오후 7시에 출발하는 비행기가 3시간 연착 되었다고 하네요."
일단 가독성을 위해서 TF-IDF 행렬 내 문장 각각을 변수로 선언해줄게요!

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_1

<1x174 sparse matrix of type '<class 'numpy.float64'>'
with 8 stored elements in Compressed Sparse Row format>

tf_sen_1.toarray()

어레이 형식으로 변환해준다.

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

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)

sen_1 = "오늘 점심에 배가 너무 고파서 밥을 너무 많이 먹었다."

sen_2 = "오늘 점심에 배가 고파서 밥을 많이 먹었다."

sen_3 = "오늘 배가 너무 고파서 점심에 밥을 너무 많이 먹었다."

sen_4 = "오늘 점심에 배가 고파서 비행기를 많이 먹었다."

sen_5 = "어제 저녁에 밥을 너무 많이 먹었더니 배가 부르다."

sen_6 = "이따가 오후 7시에 출발하는 비행기가 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)}")

의미가 유사한 문장 간 유사도 계산 (조사 생략): (sen_1, sen_2) = 0.011
의미가 유사한 문장 간 유사도 계산 (순서 변경): (sen_1, sen_3) = 0.0
문장 내 단어를 임의의 단어로 치환한 문장과 원본 문장 간 유사도 계산: (sen_2, sen_4) = 0.011
의미는 다르지만 비슷한 주제를 가지는 문장 간 유사도 계산: (sen_1, sen_5) = 0.017
의미가 서로 다른 문장 간 유사도 계산: (sen_1, sen_6) = 0.023

아직도 문제가 해결되진 않았지만 그래도 조금은 해결되었다.

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

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. 코사인 유사도 측정

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)}")

의미가 유사한 문장 간 유사도 계산 (조사 생략): (sen_1, sen_2) = 0.763
의미가 유사한 문장 간 유사도 계산 (순서 변경): (sen_1, sen_3) = 1.0
문장 내 단어를 임의의 단어로 치환한 문장과 원본 문장 간 유사도 계산: (sen_2, sen_4) = 0.797
의미는 다르지만 비슷한 주제를 가지는 문장 간 유사도 계산: (sen_1, sen_5) = 0.441
의미가 서로 다른 문장 간 유사도 계산: (sen_1, sen_6) = 0.0
단어 빈도를 사용하는 것 자체의 문제라서 어쩔수 없다.

Bag of Words의 결과와 비교해봅시다!


의미가 유사한 문장 간 유사도 계산 (조사 생략): (sen_1, sen_2) = 0.797

의미가 유사한 문장 간 유사도 계산 (순서 변경): (sen_1, sen_3) = 1.0

문장 내 단어를 임의의 단어로 치환한 문장과 원본 문장 간 유사도 계산: (sen_2, sen_4) = 0.857

의미는 다르지만 비슷한 주제를 가지는 문장 간 유사도 계산: (sen_1, sen_5) = 0.533

의미가 서로 다른 문장 간 유사도 계산: (sen_1, sen_6) = 0.0

실습1. 증강하지 않은 TF-IDF 행렬에 대한 문장 유사도 계산

그렇다면 과연, 문장을 증강하지 않은 단어-문서 행렬 (training documents)에 대한 문장 간 유사도는 어떻게 계산될까요?

한 번 직접 확인해보세요!

Free Trial 1

2. 언어 모델을 활용한 문장 간 유사도 측정

다음은 언어 모델 기반의 임베딩을 활용하여 문장 간 유사도를 측정해볼게요!

아직 강의에서 다루진 않았지만, 언어 모델 기반의 임베딩은 학습한 자연어 코퍼스를 언어모델링하여, 입력 문장을 하나의 벡터로 압축할 수 있습니다.
이번 실습에서, 우리는 BERT 모델을 활용할 건데요, BERT 모델은 입력 문장의 정보들을 [CLS] 토큰에 압축할 수 있습니다.
따라서, 아래 그림처럼 문장 임베딩이라 할 수 있는 [CLS] 토큰의 임베딩을 활용하여 문장 간 유사도를 계산할 수 있어요!

image.png

이전 Huggingface 실습에서 했던 것처럼, 한국어 문장 유사도 측정을 위해

Huggingface 라이브러리에서 multi-lingual BERT를 불러와서 사용해보겠습니다.

!pip install transformers  

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)

 

3.1. multi-lingual BERT를 활용한 문장 유사도 계산

다음과 같이 문장의 [CLS] 토큰을 활용하여 유사도를 측정할 수 있습니다.
먼저, 토크나이저를 사용하여 앞서 사용한 문장들을 입력 형태로 변환해줄게요

sen_1 = "오늘 점심에 배가 너무 고파서 밥을 너무 많이 먹었다."

sen_2 = "오늘 점심에 배가 고파서 밥을 많이 먹었다."

sen_3 = "오늘 배가 너무 고파서 점심에 밥을 너무 많이 먹었다."

sen_4 = "오늘 점심에 배가 고파서 비행기를 많이 먹었다."

sen_5 = "어제 저녁에 밥을 너무 많이 먹었더니 배가 부르다."

sen_6 = "이따가 오후 7시에 출발하는 비행기가 3시간 연착 되었다고 하네요."

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")  

각각의 문장에 대한 [CLS] 토큰 벡터를 추출하기 위해서는 다음과 같은 과정이 필요합니다.

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)  

sen_1 = "오늘 점심에 배가 너무 고파서 밥을 너무 많이 먹었다."

sen_2 = "오늘 점심에 배가 고파서 밥을 많이 먹었다."

sen_3 = "오늘 배가 너무 고파서 점심에 밥을 너무 많이 먹었다."

sen_4 = "오늘 점심에 배가 고파서 비행기를 많이 먹었다."

sen_5 = "어제 저녁에 밥을 너무 많이 먹었더니 배가 부르다."

sen_6 = "이따가 오후 7시에 출발하는 비행기가 3시간 연착 되었다고 하네요."

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)}")

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

다양한 언어를 학습했기 때문에 한국어라는 이유로 많이 몰려있기도 하다.

728x90