Skip to main content

거리와 유사도

벡터

우리가 문서-단어 행렬이 있으면, 가로 방향은 문서를 나타내고 세로 방향은 단어를 나타낸다. 가로 방향의 수 하나하나를 문서 벡터라고 부르고, 세로 방향을 단어 벡터라고 부른다.

수학에서 수가 여러 개 모여있으면 그걸 그냥 벡터라고 부른다. 문서 벡터는 문서를 여러 개의 수로 표현한 것이고, 단어 벡터는 단어를 여러 개의 수로 표현한 것이다. 벡터는 그냥 여러 개의 수 모임이다.

여기서 벡터를 이루는 수들 하나 하나는 구체적인 의미를 가질 필요가 없다. 벡터를 이용해서 계산을 했을 때, 그것이 잘 작동하면 그 수 하나 하나가 무슨 의미인지는 상관이 없다.

그렇기 때문에 매우 많은 것을 벡터로 다룰 수 있다. 간단히 말하면 컴퓨터 파일로 저장할 수 있는 것은 모두 벡터다. 컴퓨터 파일이라는 건 결국 10110011 같은 수로 표시된 데이터이기 때문이다. 영상도 벡터가 되고, 사람 목소리도 벡터가 되고, 텍스트도 벡터가 된다.

컴퓨터에 저장할 수 있는 건 다 벡터면, 벡터에 적용되는 수학을 사람 목소리, 텍스트, 이미지에 모두 적용할 수 있다.

딥러닝 같은 경우도 텍스트에 적용하는 기법이 이미지에도 적용되고, 이미지에 적용되는 기법이 음성에도 적용된다. 음성 인식할 때 쓰는 Whisper와 텍스트를 처리하는 ChatGPT가 사용하는 딥러닝 모델이 거의 똑같다.

유사도의 활용

유사도는 다양한 곳에서 활용된다. 예를 들어:

  1. 신문 기사에서 관련 기사 추천
  2. 유튜브에서 관련 영상 추천
  3. 쇼핑몰에서 비슷한 상품 추천

이런 추천 시스템들은 모두 비슷한 원리로 작동한다. 텍스트, 영상, 상품 등 다양한 대상을 벡터로 표현하고, 이 벡터들 간의 유사도를 계산하는 것이다.

벡터는 그저 형식만 있는 추상적인 개념이기 때문에, 텍스트 분석에서 사용하는 유사도 계산 방법을 다른 영역(영상, 상품 등)에도 똑같이 적용할 수 있다.

거리

유사도를 계산하기 전에 먼저 거리의 개념을 생각해 볼 것이다. 두 개가 비슷하다는 것은 거리가 가깝다는 의미로 볼 수 있다.

현대 수학에서는 거리를 추상적으로 정의한다. 거리는 특정한 성질을 만족하는 관계로 정의되며, 그 성질을 만족하면 모두 거리라고 부른다.

수학에서 거리의 세 가지 성질은 다음과 같다:

  1. 거리는 0보다 크거나 같아야 한다. 두 대상이 같을 때만 거리가 0이 된다.
  2. X에서 Y까지의 거리와 Y에서 X까지의 거리는 같아야 한다.
  3. X에서 Y까지 직접 가는 거리가 Z를 경유해서 가는 거리보다 짧거나 같아야 한다.

이 세 가지 성질을 만족하면 무엇이든 거리라고 부를 수 있다. 이는 거리의 개념이 다양할 수 있음을 의미한다. 따라서 우리는 필요에 따라 거리를 다양하게 정의할 수 있다.

유사도는 수학적인 정의가 따로 없다. 보통 거리가 가까우면 유사도가 높다고 본다.

유사도

민코우스키 거리

민코우스키 거리는 여러 거리 개념을 포함하는 일반화된 거리 측정 방법이다. 이 중 가장 잘 알려진 것이 유클리드 거리다.

유클리드 거리는 일반적으로 우리가 알고 있는 직선 거리를 말한다. 두 점 사이의 거리를 계산할 때, 각 차원의 차이를 제곱하고 더한 후 제곱근을 취하는 방식으로 구한다.

맨해튼 거리는 뉴욕 맨해튼의 격자 모양 도로 구조에서 유래했다. 이는 두 점 사이의 거리를 각 차원의 차이의 절대값의 합으로 계산한다. 예를 들어, A에서 B까지 가로 3칸, 세로 3칸이면 총 6칸의 거리가 된다.

민코우스키 거리는 거리 계산 방식을 일반화한 개념이다. 이는 p값에 따라 다양한 거리 측정 방법을 포함한다. p가 1일 때는 맨해튼 거리, 2일 때는 유클리드 거리가 되며, 3, 4 등 다른 값일 때는 또 다른 형태의 거리가 된다.

이렇게 거리의 개념을 다양하게 정의할 수 있으며, 상황에 따라 적절한 거리 측정 방법을 선택할 수 있다.

레벤슈타인 거리 & 해밍 거리

문자열의 거리 측정 방법에는 레벤슈타인 거리와 해밍 거리가 있다.

레벤슈타인 거리는 한 문자열을 다른 문자열로 변환하는 데 필요한 최소 편집 횟수이다. 예를 들면, "cat"을 "data"로 바꾸는 데 필요한 키보드 입력 횟수는 3번이므로 레벤슈타인 거리도 3이 된다. 주로 오타 교정에 사용한다.

해밍 거리는 같은 길이의 두 문자열에서 서로 다른 문자의 개수이다. "AYC"와 "AXC"의 해밍 거리는 1이다. DNA 분석 등에 사용한다.

자카드 유사도

자카드 유사도는 데이터 분석이나 머신러닝에서 자주 사용되는 개념으로, 두 집합이 얼마나 겹치는지를 계산하는 방법이다. 이 유사도는 교집합의 크기를 합집합의 크기로 나누어 계산하며, 겹치는 부분이 많을수록 유사도가 높아진다.

자카드 유사도는 다양한 분야에서 활용될 수 있다. 예를 들어, 유튜브 구독자 추천 시스템에서는 사용자들의 구독 목록을 비교해 취향이 비슷한 사람을 찾고, 그 사람이 구독하는 채널 중 아직 구독하지 않은 채널을 추천할 수 있다. 또한 신문 기사 추천 시스템에서는 현재 보고 있는 기사와 단어가 많이 겹치는 기사를 추천하는 데 사용될 수 있다.

그러나 자카드 유사도에는 한계도 있다. 이 방법은 명확하게 겹치는 요소를 셀 수 있을 때만 사용 가능하다. 연속적인 값이나 아주 작은 차이를 가진 값들을 비교할 때는 적합하지 않다. 예를 들어, 3.7과 3.70001은 거의 같은 값이지만, 자카드 유사도로는 다른 것으로 간주된다.

따라서 자카드 유사도는 명확히 구분되는 요소들로 이루어진 집합을 비교할 때 유용하지만, 연속적인 값이나 미세한 차이를 다룰 때는 한계가 있다. 이러한 특성을 고려하여 적절한 상황에서 사용해야 한다.

유클리드 거리의 문제점

텍스트 분석에서 유클리드 거리를 사용할 때 몇 가지 문제점이 발생할 수 있다. 이는 문서-단어 행렬에서 단어의 출현 빈도를 벡터로 표현할 때 특히 두드러진다.

첫째, 같은 내용을 반복하는 경우 문제가 생길 수 있다. 예를 들어, "오늘 수업이 너무 좋았어요. 오늘 수업 진짜 재밌지 않냐? 오늘 수업만큼 좋은 수업은..."과 같은 문장에서 "오늘"과 "수업" 같은 특정 단어의 빈도가 높아진다. 이를 유클리드 거리로 계산하면 다른 문서와의 거리가 멀어지게 된다.

둘째, 짧은 문서와 긴 문서를 비교할 때 문제가 발생한다. 짧은 문서들은 원점 근처에 모여 있어 서로 비슷해 보이는 반면, 긴 문서들은 원점에서 멀리 떨어져 있어 서로 다르게 보이게 된다.

셋째, 내용의 유사성과 거리의 불일치가 일어날 수 있다. 같은 주제에 대해 길게 쓴 문서와 짧게 쓴 문서가 유클리드 거리상으로는 멀리 떨어져 보일 수 있다. 즉, 실제로는 유사한 내용이지만 거리 측정상으로는 다르게 나타날 수 있다.

이러한 이유로 텍스트 분석에서 유클리드 거리는 문서의 유사성을 측정하는 데 적합하지 않다고 볼 수 있다. 이 방법은 문서의 길이나 특정 단어의 반복에 지나치게 영향을 받기 때문에, 실제 내용의 유사성을 정확히 반영하지 못하는 경우가 많다.

코사인 유사도

텍스트 분석에서는 주로 코사인 유사도를 사용한다. 코사인 유사도는 원점에서 벡터의 방향을 보는 방식으로, 벡터의 길이와 상관없이 방향만을 고려한다. 이는 두 벡터 사이의 각도를 측정하여 유사도를 계산하는 방식이다. 방향이 같으면 각도가 0도, 완전히 다르면 180도가 된다.

코사인 유사도는 각도를 코사인 함수에 넣어 계산한다. 같은 방향일 때는 1(최대 유사도), 수직일 때는 0, 반대 방향일 때는 -1(최소 유사도)이 된다. 이 방법의 장점은 360도와 0도를 구분할 수 있고, 90도와 270도를 동일하게 처리하며, 문서의 길이에 상관없이 내용의 유사성을 측정할 수 있다는 점이다.

결과적으로 코사인 유사도는 문서나 단어의 방향성이 같으면 길이와 상관없이 유사도가 높고, 방향성이 다르면 유사도가 낮아진다. 이러한 특성 때문에 텍스트 분석에서 문서나 단어의 유사성을 측정하는 데 적합한 방법으로 널리 사용된다.

점곱

수학에서 점곱이라는 개념이 있다. 점곱은 두 벡터의 같은 위치에 있는 요소들을 서로 곱한 후 모두 더하는 것이다. 예를 들어, 벡터 A(A1, A2, A3)와 벡터 B(B1, B2, B3)의 점곱은 A1B1 + A2B2 + A3*B3이다.

점곱도 유사도 계산에 많이 사용된다. 방향성이 같은 두 벡터는 같은 위치에 있는 숫자값이 둘 다 크게 나타난다. 예를 들어, 두 사람이 동쪽에 있다면 둘 다 동쪽 좌표값이 크다는 의미이다. 이런 점에서 점곱은 코사인 유사도와 비슷한 성질을 가진다.

수학적으로 점곱과 코사인 유사도 사이에는 관계가 있다. 두 벡터 A와 B의 점곱은 A벡터의 길이, B벡터의 길이, 그리고 두 벡터의 코사인 유사도를 곱한 것과 같다.

따라서 점곱과 코사인 유사도는 거의 비례한다고 볼 수 있다. 달리 말하면, 코사인 유사도는 점곱에서 벡터의 길이를 제외한 것과 같다. 실제 사용할 때는 상황에 따라 점곱을 사용하기도 하고 코사인 유사도를 사용하기도 하는데, 점곱을 사용하면 벡터의 길이도 어느 정도 반영된다는 차이가 있다.

노말라이제이션

노멀라이제이션(normalization, 정규화)은 데이터 분석에서 여러 가지 의미를 가지고 있지만, 기본적으로 길이를 맞춰주는 작업을 말한다. 노멀라이제이션의 종류는 다양하지만, 모든 노멀라이제이션의 공통점은 뭔가의 길이를 맞춰주는 작업이라는 것이다.

예를 들어, 카메라로 사진을 찍을 때 조명에 따라 밝기가 달라지는 경우, 이를 조절하여 실제 밝기와 비슷하게 맞추는 것도 일종의 노멀라이제이션이다.

텍스트 분석에서는 문서들의 길이가 다른 경우, 각 단어의 빈도를 총 단어 수로 나누어 노멀라이제이션을 할 수 있다. 예를 들어, "오늘 수업 진짜 재밌다. 오늘 수업"이라는 문장과 "오늜 수업 재밌지"라는 문장이 있다면, 전자는 총 4단어, 후자는 총 3단어이다. 이를 각각의 총 단어 수로 나누면 두 문장의 '오늘'과 '수업'의 빈도가 0.5로 동일해진다.

이렇게 노멀라이제이션을 하면 문서의 길이 차이가 없어진다. 이는 점곱과 코사인 유사도의 관계에도 영향을 미친다. 점곱은 A의 길이, B의 길이, 코사인 유사도를 곱한 것인데, 길이를 모두 1로 맞추면 점곱과 코사인 유사도가 같아지게 된다.

코사인 유사도와 상관계수

코사인 유사도는 상관계수와 밀접한 관련이 있다. 상관계수는 두 변수 간의 관계를 나타내는데, 하나가 클 때 다른 하나도 크고, 하나가 작을 때 다른 하나도 작으면 양의 상관관계가 있다고 한다. 반대로 움직이면 음의 상관관계, 특별한 관계가 없으면 상관계수가 0에 가깝다.

코사인 유사도도 이와 비슷한 개념이다. 예를 들어, A라는 문서에서 두 단어가 함께 나타나고, B라는 문서에서 두 단어가 함께 나타나지 않으면 이 두 단어의 코사인 유사도는 높다.

상관관계의 관점에서 보면, 두 변수가 함께 증가하거나 감소하는 패턴을 보일 때 유사도가 높다고 할 수 있다. 실생활의 예시로는 학생들의 과목별 성적 패턴이 비슷할 때 유사도가 높다고 볼 수 있다. 또한, 아버지와 아들이 전체적인 크기는 다르지만 얼굴 특징의 비율이 비슷할 때 닮았다고 하는 것과 유사한 개념으로 이해할 수 있다.

실습: 유사 특허 찾기

import pandas as pd
df = pd.read_excel('patents.xlsx')

이번 실습에서는 실제 데이터를 사용하여 특허 분석을 할 예정이다. 샴푸와 관련된 약 450개의 특허 파일을 대상으로 한다.

특허 출원 시 중요한 점 중 하나는 자신의 특허와 비슷한 특허가 있는지 알아내는 것이다. 하지만 두 특허가 비슷하다는 것은 매우 주관적인 판단이다.

실습에서는 이런 특허의 유사성을 어떻게 객관적으로 계산하고, 비슷한 특허를 어떻게 찾아낼 수 있는지를 실제로 해볼 것이다. 이를 통해 텍스트 분석 기법을 실제 데이터에 적용하는 방법을 배울 수 있을 것이다.

문서 단어 행렬

from sklearn.feature_extraction.text import CountVectorizer
cv = CountVectorizer(tokenizer=extract_nouns, min_df=10)
dtm = cv.fit_transform(df.abstract)

min_df=10은 최소 문서 빈도(minimum document frequency)를 의미한다. 이는 최소한 10개의 문서에 나온 단어만 포함하라는 뜻이다.

문서 유사도를 계산할 때, A라는 문서와 B라는 문서를 비교하려면 두 문서에 공통적으로 나오는 단어가 필요하다. 단순히 많이 나온 단어가 아니라 여러 문서에서 공통적으로 나오는 단어를 사용해야 한다. 이렇게 공통 단어를 기준으로 문서 간 유사도를 비교하게 된다.

word_count = pd.DataFrame({
'word': cv.get_feature_names_out(),
'count': dtm.sum(axis=0).flat
})
word_count.sort_values('count', ascending=False).head(20)

이 코드는 단어의 빈도수를 계산하고 가장 많이 나오는 20개의 단어를 보여준다.

샴푸 관련 특허에 나오는 고빈도 단어들을 살펴보면 다음과 같다:

  1. 샴푸: 특허의 주제이므로 가장 많이 나온다.
  2. 물: 샴푸의 주요 성분이므로 자주 언급된다.
  3. 발명: 특허가 발명에 관한 것이므로 많이 나온다.
  4. 조성: 샴푸의 구성 성분을 설명할 때 사용된다.
  5. 중량: 성분의 양을 나타낼 때 사용된다.
  6. 상기: '위에서 기재한'이라는 뜻으로, 특허 문서에서 자주 사용되는 표현이다.
  7. 모발: 샴푸를 적용하는 대상이므로 자주 언급된다.
  8. 추출물, 포함: 샴푸의 성분을 설명할 때 사용되는 단어들이다.

이러한 단어들은 샴푸 관련 특허의 특성을 잘 보여주고 있다.

문서 벡터

dtm[0].A

문서-단어 행렬(DTM)에서 행은 문서를, 열은 단어를 나타낸다. DTM에서 특정 행을 뽑아내려면 dtm[0]이나 dtm[0, :]와 같이 표현할 수 있다. 파이썬은 숫자를 0부터 세기 때문에, 0은 첫 번째 행을 의미한다.

dtm[0]만 실행하면 결과가 '1 x 362 sparse matrix'와 같이 나온다. 이는 대부분의 값이 0인 압축된 형태의 행렬이기 때문이다. 'Sparse'는 대부분의 숫자가 0이라는 뜻이며, 이는 메모리 효율을 위해 압축되어 있다.

압축을 풀어서 실제 값을 보려면 .A를 붙여 dtm[0].A와 같이 사용한다. 이렇게 하면 대부분 0인 배열이 나온다. 특허 문서에는 모든 단어가 다 포함되지 않기 때문에 대부분의 값이 0이 된다.

예를 들어, 특정 특허 문서에서 어떤 단어는 한 번, 다른 단어는 두 번 나오는 식으로 표현된다. 이러한 방식으로 각 특허 문서의 단어 빈도를 나타낸다.

단어 벡터

words = cv.get_feature_names_out().tolist()
j = words.index('추출')
dtm[:, j].A.flatten()

이 코드는 단어 벡터를 보는 방법을 설명한다. '추출'이라는 단어를 예로 들어 그 단어의 벡터를 살펴본다.

먼저 get_feature_names_out()으로 단어 목록을 가져오고, 이를 리스트로 변환한다. 리스트로 변환하는 이유는 index() 함수를 사용하기 위해서다.

words.index('추출')로 '추출'이라는 단어의 인덱스 번호를 찾는다. 이 예에서는 280번이다.

DTM에서 이 단어의 벡터를 추출하기 위해 dtm[:, j]를 사용한다. 여기서 :는 모든 행을, j는 '추출' 단어의 열을 의미한다.

.A를 붙여 압축을 풀고, .flatten()으로 세로로 나열된 벡터를 가로로 평평하게 만든다. 이렇게 하면 각 문서에서 '추출'이라는 단어가 몇 번 사용되었는지를 보여주는 벡터가 나온다.

단어 문서 행렬

코사인 유사도를 계산하기 전에 dtm.T를 사용한다. 여기서 대문자 T는 전치(transpose) 행렬을 의미한다.

전치 행렬은 원래 행렬의 행과 열을 뒤집는 것이다. 이는 처음 들으면 헷갈릴 수 있는 개념이다.

문서-단어 행렬(DTM)에서 행은 문서를, 열은 단어를 나타낸다. 이 행렬을 전치시키면 행과 열이 바뀌어 단어-문서 행렬이 된다. 즉:

  • 원래 DTM: 행 = 문서, 열 = 단어
  • 전치 후: 행 = 단어, 열 = 문서

이렇게 전치를 하는 이유는 코사인 유사도 계산 함수의 입력 형식에 맞추기 위해서이다.

코사인 유사도

from sklearn.metrics.pairwise import cosine_similarity
word_similarity = cosine_similarity(dtm.T)
word_similarity.shape

이 코드는 코사인 유사도를 계산하는 방법을 설명한다. 코사인 유사도 계산 함수는 기본적으로 행과 행의 유사도를 계산한다.

DTM을 그대로 넣으면 문서와 문서의 유사도를 구하게 된다. 하지만 우리는 단어와 단어의 유사도를 구하고 싶기 때문에 dtm.T를 사용하여 전치 행렬을 만든다. 이렇게 하면 단어-문서 행렬이 되어, 행과 행의 유사도를 구하면 단어와 단어의 유사도가 계산된다.

word_similarity.shape를 보면 362x362가 나온다. 이는 DTM의 shape가 439x362(439개 문서, 362개 단어)였던 것과 비교된다. 단어와 단어를 비교했기 때문에 가로 362, 세로 362의 크기를 가진 행렬이 만들어진 것이다.

유사 단어 찾기

import numpy as np
for k in np.argsort(word_similarity[j])[-2:-11:-1]:
print(words[k])

이 코드는 '추출'이라는 단어와 유사한 단어들을 유사도 순으로 보여주는 것이다.

word_similarity[j]는 '추출' 단어와 다른 모든 단어들 사이의 유사도를 나타낸다. 여기서 우리는 유사도가 높은 순서대로 단어를 보고 싶다.

np.argsort() 함수는 단순히 정렬하는 것이 아니라, 정렬 후 각 요소의 원래 인덱스를 반환한다. 이는 유사도 값뿐만 아니라 해당 단어도 함께 볼 수 있게 해준다.

[-2:-11:-1]는 파이썬의 슬라이싱 문법이다:

  • -2: 뒤에서 두 번째부터 시작 (자기 자신을 제외한 가장 유사한 단어)
  • -11: 뒤에서 11번째까지 (파이썬은 마지막 숫자를 포함하지 않으므로 실제로는 10번째까지)
  • -1: 역순으로 진행

따라서 이 코드는 '추출'과 가장 유사한 9개의 단어를 유사도가 높은 순서대로 출력한다.

유사 특허 찾기

doc_similarity = cosine_similarity(dtm) # .T를 하지 않음
for k in np.argsort(doc_similarity[i])[-2:-11:-1]:
print(k, df.ko_title[k])