27_텍스트 유사도
텍스트 유사도
● 챗봇 엔진에 입력되는 문장과 시스템에서 해당 주제의 답변과 연관되어 있는 질문이 얼마나 유사한지 계산할 수 있어야 적절한 답변을 출력할 수 있음
● 두 문장 간의 유사도를 계산하기 위해서는 문장 내에 존재하는 단어들을 수치화 해야 함
■ 이 때 언어 모델에 따라 통계를 이용하는 방법과 인공신경망을 이용하는 방법으로 나눌 수 있음
■ word2vec은 인공 신경망 방식
■ 인공 신경망 방식이 절대적으로 좋은 것은 아니며 상황에 따라 통계적인 방식이 더 적절할 수 있음
01. n-gram 유사도(텍스트 데이터 처리의 기본)
● 주어진 문장에서 n개의 연속적인 단어 시퀀스(단어 나열)을 의미
● n개의 단어를 토큰으로 사용
● 이웃한 단어의 출현 횟수를 통계적으로 표현해 텍스트의 유사도를 계산하는 방법
● n-gram예시
■ 문장: 1661년 6월 뉴턴은 선생님의 제안으로 트리니티에 입학하였다.
■ n = 1: 1661년 / 6월 / 뉴턴 / 선생님 / 제안 / 트리니티 / 입학
■ n = 2: 1661년 6월 / 6월 뉴턴 / 뉴턴 선생님 / 선생님 제안 / 제안 트리니티 / 트리니티 입학
■ n = 3: 1661년 6월 뉴턴 / 6월 뉴턴 선생님 / 뉴턴 선생님 제안 / 선생님 제안 트리니티 / 제안 트리니티 입학
n-gram을 이용한 문장 간 유사도 계산
● 문장을 n-gram으로 토큰을 분리한 후 단어 문서 행렬을 만든 후 두 문장을 서로 비교해 동일한 단어의 출현 빈도를 확률로 계산해 유사도를 구함
● 수식: 두 문장 A와 B에서 동일한 토큰의 출현 빈도 / A문장의 전체 토큰 수
■ 기준이 되는 문장 A에서 나온 전체 토큰 중에서 A와 B에 동일한 토큰이 얼마나 있는지 비율로 표현
■ 1에 가까울수록 B가 A에 유사함
from konlpy.tag import Komoran
import numpy as np
from numpy import dot
from numpy.linalg import norm
# 어절 단위 n-gram
def word_ngram(bow, num_gram):
text = tuple(bow)
ngrams = [text[x:x + num_gram] for x in range(len(text)) if len(text[x:]) >= num_gram]
return tuple(ngrams)
# 음절 n-gram 분석
def phoneme_ngram(bow, num_gram):
sentence = " ".join(bow)
text = tuple(sentence)
slen = len(text)
ngrams = [text[x:x + num_gram] for x in range(slen) if len(text[x:]) >= num_gram]
return ngrams
# 유사도 계산
def similarity(doc1, doc2):
cnt = 0
for token in doc1:
if token in doc2:
cnt = cnt + 1
return cnt / len(doc1)
● doc1의 토큰이 doc2의 토큰과 얼마나 동일한지 횟수를 카운트
● 카운트된 값을 doc1의 전체 토큰 수로 나누면 유사도가 계산됨
# 문장 정의
sentence1 = "6월에 뉴턴은 선생님의 제안으로 트리니티에 입학하였다"
sentence2 = "6월에 뉴턴은 선생님의 제안으로 대학교에 입학하였다"
sentence3 = "나는 맛있는 밥을 뉴턴 선생님과 함께 먹었습니다"
# 형태소 분석기에서 명사 추출
komoran = Komoran()
bow1 = komoran.nouns(sentence1) # bow: bag of word
bow2 = komoran.nouns(sentence2)
bow3 = komoran.nouns(sentence3)
# 단어 n-gram 토큰 추출
doc1 = word_ngram(bow1, 2)
doc2 = word_ngram(bow2, 2)
doc3 = word_ngram(bow3, 2)
# 추출된 n-gram 토큰 출력
print(doc1)
print(doc2)
print(doc3)
(('6월', '뉴턴'), ('뉴턴', '선생님'), ('선생님', '제안'), ('제안', '트리니티'), ('트리니티', '입학'))
(('6월', '뉴턴'), ('뉴턴', '선생님'), ('선생님', '제안'), ('제안', '대학교'), ('대학교', '입학'))
(('밥', '뉴턴'), ('뉴턴', '선생'), ('선생', '님과 함께'))
# 유사도 계산
r1 = similarity(doc1, doc2)
r2 = similarity(doc3, doc1)
print(r1)
print(r2)
0.6
0.0
● n-gram은 문장에 존재하는 모든 단어의 출현 빈도를 확인하는 것이 아니라 연속되는 문장에서 일부 단어만 확인
● 전체 문장을 고려한 언어 모델보다 정확도가 떨어질 수 있음
● n을 크게 잡을수록 비교 문장의 토큰과 비교할 때 카운트를 놓칠 확률이 커짐
● n을 작게 잡을 수록 카운트 확률은 높아지지만 문맥을 파악하는 정확도는 떨어질 수 밖에 없음
● 일반적으로 n은 1~5 사이의 값을 사용
코사인 유사도(cosine similarity)
● 단어나 문장을 벡터로 표현하여 벡터 간 거리나 각도를 이용해 유사성을 파악할 수 있음
● 코사인 유사도는 두 벡터 간 코사인 각도를 이용해 유사도를 측정하는 방법
● 일반적으로 벡터의 크기가 중요하지 않을 때 거리를 측정하기 위해 사용
■ 예) 단어의 출현 빈도를 통해 유사도를 계산한다면 동일한 단어가 많이 포함되어 있을 수록 벡터의 크기가 커짐
■ 이 때 코사인 유사도는 벡터의 크기와 상관없이 결과가 안정적임
■ 앞서 배운 n-gram은 동일한 단어가 문서 내에 자주 등장하면 유사도 결과에 안 좋은 영향을 미침
# 코사인 유사도 계산
def cos_sim(vec1, vec2):
return dot(vec1, vec2) / (norm(vec1) * norm(vec2))
● dot: 인자로 들어온 2개의 넘파이 배열을 내적곱
● norm: 벡터의 크기를 측정
■ 코사인 유사도에서는 L2노름(유클리드 노름)을 주로 사용
# TDM 만들기
def make_term_doc_mat(sentence_bow, word_dics):
freq_mat = dict()
for word in word_dics:
freq_mat[word] = 0
for word in word_dics:
if word in sentence_bow:
freq_mat[word] += 1
return freq_mat
● 비교 문장에서 추출한 단어 사전을 기준으로 문장에 해당 단어들이 얼마나 포함되어 있는지 나타내는 단어 문서 행렬을 만들어 주는 함수
# 단어 벡터 만들기
def make_vector(tdm):
vec = []
for key in tdm:
vec.append(tdm[key])
return vec
● 단어 문서 행렬에서 표현된 토큰들의 출현 빈도 데이터를 벡터로 만들어주는 함수
bow = bow1 + bow2 + bow3
# 단어 묶음에서 중복제거해 단어 사전 구축
word_dics = []
for token in bow:
if token not in word_dics:
word_dics.append(token)
word_dics
['6월', '뉴턴', '선생님', '제안', '트리니티', '입학', '대학교', '밥', '선생', '님과 함께']
# 문장 별 단어 문서 행렬 계산
freq_list1 = make_term_doc_mat(bow1, word_dics)
freq_list2 = make_term_doc_mat(bow2, word_dics)
freq_list3 = make_term_doc_mat(bow3, word_dics)
print(freq_list1)
print(freq_list2)
print(freq_list3)
{'6월': 1, '뉴턴': 1, '선생님': 1, '제안': 1, '트리니티': 1, '입학': 1, '대학교': 0, '밥': 0, '선생': 0, '님과 함께': 0}
{'6월': 1, '뉴턴': 1, '선생님': 1, '제안': 1, '트리니티': 0, '입학': 1, '대학교': 1, '밥': 0, '선생': 0, '님과 함께': 0}
{'6월': 0, '뉴턴': 1, '선생님': 0, '제안': 0, '트리니티': 0, '입학': 0, '대학교': 0, '밥': 1, '선생': 1, '님과 함께': 1}
# 코사인 유사도 계산(가장 널리 사용되는 방법)
doc1 = np.array(make_vector(freq_list1))
doc2 = np.array(make_vector(freq_list2))
doc3 = np.array(make_vector(freq_list3))
doc1
array([1, 1, 1, 1, 1, 1, 0, 0, 0, 0])
doc2
array([1, 1, 1, 1, 0, 1, 1, 0, 0, 0])
doc3
array([0, 1, 0, 0, 0, 0, 0, 1, 1, 1])
r1 = cos_sim(doc1, doc2)
r2 = cos_sim(doc3, doc1)
print(r1)
print(r2)
0.8333333333333335
0.20412414523193154