NLP-writing

텍스트 유사도-벡터 유사도

summerorange 2023. 6. 4. 23:48
반응형

서론

ChatGPT는 일상 생활에 들어와서 없으면 안 될 존재가 되셨습니다. 저의 사수 같은 chatGPT....😂 가끔 사용하는 언어가 바뀔 일이 있어도 gpt 분이 계시기 때문에 괜찮습니다. 생산성과 효율성이 확실히 빨라졌다고 느낍니다. chatGPT를 사용해서 텍스트 요약이나 키워드 추출, 표 설명, 텍스트 생성 등을 하는 경우도 있습니다. GPT-4를 사용하고 있는데 플러그인을 잘 활용하면 문서 분류도 자동으로 해줍니다. 하지만 외부로 알려지면 안되는 개인정보나 중요한 정보의 경우엔 프롬프트를 사용하는 것이 금지되어 있죠. 

이 분은 저의 선배님이심. 비동기 처리할 때 코드 많이 배웠습니다.

최근엔 문서 분류와 관련해서 현업에서 활용할 것인지 고민하고 있습니다.  텍스트 유사도은 이런 문서 분류, 정보 검색, 기계 번역 등의 자연어 처리 분야에서 중요한 개념입니다. 오늘 포스팅은 텍스트 유사도에 대해서 기본적인 특징을 다루고 어떻게 활용할 수 있는지 작성하려고 합니다.


텍스트 유의도의 기본 개념

- 단어의 형태와 의미

'오늘 울릉도로 가는 를 타러가면서 과일을 먹었는데 가 참 맛있었어.'

라고 할 때 '배'와 '배'가 다르다는 건 사람은 압니다. 이런 단어의 경우 동형어라고 하는데 생긴 건 같은데 의미가 완전히 다른 말을 의미합니다. 반대로 다의어는 비슷한 여러 의미를 지니는 단어를 뜻합니다. Time flies like a arrow.  Bird fly the sky. 할 때의 fly는 다른 의미로 쓰이지만 움직임을 나타내죠.

뒤에 설명할 tf-idf에도 나오듯이, 중요하고 빈번하게 자주 언급되는 단어들은 주로 동형어와 다의어가 많습니다. 사람의 뇌는 

수십만개에서 수백만개의 단어를 모두 활용할 만큼의 자원을 쓰지 않습니다. 

최소한의 에너지를 쓰면서 최대한의 효과를 보고 싶어하는 심리가 뇌에 있습니다😆 같아보이는 단어지만 다른 의미를 지니고 있어서 문맥에 따라서 적절하게 활용하는 게 더 적은 단어로 많은 의미를 전달할 수 있는 효율성이 있죠. 줄임말도 그렇구요.

- 문장의 형태와 의미

마찬가지로 문장도 그렇죠.

'엄마는 나를 사랑한다.'

'어머니는 나를 사랑한다'

'부모님은 나를 사랑한다'

이 문장의 유사도를 측정할 때 원 핫 인코딩(One-Hot Encoding) 처럼 엄마 단어, 어머니 단어, 부모님 단어 벡터 각각 따로 만들면 저 문장들이 어느 정도로 유사하게 나타날까요? 문장 모두 다른 의미라는 0의 값이 나올 수도 있습니다. 하지만, 사람은 저 문장이 같은 의미를 지니고 있다는 것을 알죠. 차이점이 없는데 이러한 유사성을 어떻게 하면 잘 측정할 지가 NLP의 과제 중 하나입니다.

- 희소 벡터 

엄마 1, 0, 0

어머니 0,1,0

부모님 0,0,1

로 각 단어 마다 벡터 값을 매길 경우 벡터의 차원이 너무 커질 우려가 있습니다. 1은 너무 적고 0이 많아질 수  있죠. 대부분이 0인 경우를 희소 벡터 sparse vector라고 합니다. 

이럴 경우 어떤 연산을 해도 유사성이 0이 됩니다. 0이란 단어간 관련이 없다는 의미입니다. 벡터 결과가 서로 직교하는 경우가 많아지기 때문입니다.


텍스트 유의성의 측정

-TF-IDF

TF-IDF(w, d) = TF(w, d) / DF(w)

문서 내에 출현하는 단어 빈도를 활용해서 유의성을 계산하는 방법이 tf-idf입니다 영어로는 Term Frequency-Inverse Document Frequency로 , TF(w, d)는 document에 해당 word의 빈도 값이고, DF(w)는 해당 단어가 나타난 문서의 수를 뜻합니다. 숫자가 크면 클수록 중요한 단어, 많이 쓰이는 단어일 확률이 높습니다.

해당 코드는 직접 짜는 방법도 있고,

import math
from collections import Counter

# 예시 문서들
documents = [
    'sen1.',
    'sen2.',
    'sen3.'
]

# 전처리 (소문자화 및 토큰화)
documents = [doc.lower().split() for doc in documents]

# 단어 빈도(TF) 계산
tf = [Counter(doc) for doc in documents]

# 역 문서 빈도(IDF) 계산
idf = Counter(word for doc in documents for word in set(doc))

# IDF 값 로그 스케일링
idf = {word: math.log(len(documents) / count) for word, count in idf.items()}

# TF-IDF 계산
tf_idf = [{word: tf_count * idf[word] for word, tf_count in doc.items()} for doc in tf]

# 결과 출력
for idx, doc in enumerate(tf_idf):
    print(f"Document {idx + 1}: {doc}")

sklearn 라이브러리를 활용해서 계산하는 방법이 있습니다.

from sklearn.feature_extraction.text import TfidfVectorizer

# 예시 문서들
documents = [
'sentence1',
'sentence2',
'sentence3'
]

# TfidfVectorizer 객체 생성
vectorizer = TfidfVectorizer()

# 문서들에 대한 TF-IDF 계산
X = vectorizer.fit_transform(documents)

# 계산된 TF-IDF 출력
print(X.toarray())

실제로는 좀 더 복잡한 전처리 과정이 필요합니다!

tf-idf 단점,

이 걸 실무에서 얼마나 활용할지는 모르겠지만... 문서가 지나치게 많으면 벡터의 차원이 지나치게 커질 수 있어서, tf-idf로 추출했을 때 대부분 0으로 채워진 경우가 많을 것입니다. 문서가 10,000 개만 되어도... 의미 있는 값이 제대로 추출이 되지 않을 경우가 높겠습니다.


- 코사인 유사성(Cosine Similarity)

두 벡터 간의 코사인 각도를 이용해서 거리를 구하는 방법이 있습니다.

두 벡터의 방향이 얼마나 유사한지를 측정하기 때문에 벡터의 크기와는 무관합니다.

cosine_similarity(A, B) = dot_product(A, B) / (norm(A) * norm(B))

다음과 같이 구할 수 있습니다. 함수로 짜면 pytorch로 활용해서 다음과 같이 짤 수 있습니다.

def cosine_similarity(A, B):
	return (A * B).sum() / ((A**2).sum()**.5 * (x2**2).sum()**.5)

 

혹은 sklearn 라이브러리를 활용할 수도 있습니다.

from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity

def cosine_similarity(sentences):
    tf = TfidfVectorizer()
    tfidf_matrix = tf.fit_transform(sentences)
    cos_similar = cosine_similarity(tfidf_matrix[0:1], tfidf_matrix[1:2])
	return cos_similar[0][0]

벡터 A와 벡터 B의 내적과 벡터 A와 B의 크기를 활용해서 값을 도출하면,

해당 값은 -1에서 1 사이의 값이 나옵니다.

1에 가까우면 두 사이는 유사하고, 그렇지 않으면 서로 다르다는 것을 의미합니다.

장점은 벡터 사이의 각도를 측정하기 때문에 문서의 길이에 무관한 장점이 있지만,

동일한 단어 빈도를 가진 단어에 대해서 동일한 가중치를 부여해서, 특정 문서에 전문 용어나 드문 단어가 등장해도 일반적인 단어와 동일하게 취급하는 단점이 있죠.


- 자카드 유사도 (Jaccard Similarity)

자카드 유사도는 두 집합 간의 유사도를 구하는 방법입니다. 두 문장의 교집합과 합집합을 통해서 유사도를 구할 수 있습니다. 

Jaccard(A, B) = |Intersection(A, B)| / |Union(A, B)|

계산이 굉장히 간단하고 직관적입니다. 문서의 크기에 민감하지 않은 장점이 있습니다.

단점은, 단어의 순서와 빈도를 고려하지 않아서 문맥이 중요한 경우에는 유사성의 판단이 잘못될 경우가 있습니다. 그리고 문장 1과 문장2를 비교할 때 문장 1은 길이가 길고 문장 2는 길이가 작다면 해당 문장에 대한 실제 유사성을 정확히 반영하지 못합니다.

파이썬 함수 코드로 정리하면 다음과 같이 직접 짜볼 수 있고, 

def jaccard_similarity(sent1, sent2):
    intersection = len(set.intersection(*[set(sent1), set(sent2)]))
    union = len(set.union(*[set(sent1), set(sent2)]))
    similarity = intersection / float(union)
    return similarity

pytorch를 활용할 경우: 

def jaccard_similarity(sent1, sent2):
    return torch.stack([sent1, sent2]).min(dim=0)[0].sum() / torch.stack([sent1, sent2]).max(dim=0)[0].sum()

I have a pen

I have a apple 

관사 an 그냥 제쳐두고 해당 2 문장을 해당 함수를 통해 자카드 유사도 측정하면 3/5 = 0.6 입니다. 

공통되는 단어 i, have, a 3개, 그리고 나온 단어 i, have, a, pen, apple 5개 해서 3/5 죠.

import torch
from collections import Counter

def jaccard_similarity(sent1, sent2):
    return torch.stack([sent1, sent2]).min(dim=0)[0].sum() / torch.stack([sent1, sent2]).max(dim=0)[0].sum()

# 토큰화
tokens1 = Counter("i have a pen".split())
tokens2 = Counter("i have a apple".split())

# 단어 집합 생성
vocab = set(tokens1.keys()).union(set(tokens2.keys()))

# BoW 벡터 생성
bow1 = torch.tensor([tokens1.get(token, 0) for token in vocab])
bow2 = torch.tensor([tokens2.get(token, 0) for token in vocab])

# 자카드 유사도 계산
similarity = jaccard_similarity(bow1, bow2)

print(similarity)

 

이렇게 수정해서 결과를 보면 0.600000


마무리

이 외에도 편집 거리 (Levenshtein distance) 가 있습니다.

일명 레벤슈타인 거리는 두 문자열 사이의 거리를 측정하는 문자열 matrix입니다. 

kitten 이라는 단어를 sitting이라는 문자열로 바꾼다면,

1. k를 s로 대체 - 레벤슈타인 거리 1

2. e를 i로 대체 - 레벤슈타인 거리 2

3. g를 삽입 - 레벤슈타인 거리 3

kitten -> sitting이 됩니다. 거리는 3.

경우에 따라선 레벤슈타인의 거리를 쓰는 게 코사인 유사도, 자카드 유사도보다 더 정확할 수 있습니다.

라이브러리는 difflib가 있죠.

from difflib import SequenceMatcher

matcher = SequenceMatcher(None, 'kitten', 'sitting')
print(matcher.ratio())  # 출력: 0.6153846153846154

일치하는 문자는 4개 i, t, t, n

전체 문자는 13

2.0 * M / T 이기 때문에

2.0 * 4 / 13 = 0.615 

입니다.

마무리라고 하고 레벤슈타인 거리까지 작성했는데,

유사도를 어떻게 측정하는지가 중요합니다🥲 그럼 전처리가 안된 일반 문서에서도

오늘은 날시가 좋아.

오늘은 날씨가 좋네

등에서도 유사도를 좀 더 보정해서 측정할 수 있죠...

 

오늘은 여기까지. 끝.

반응형