시작에 앞서
해당 내용은 <파이썬으로 데이터 주무르기> -민형기 저, BJPUBLIC 출판사 의 내용을 토대로 작성되었습니다.
보다 자세한 내용은 해당 교재를 확인하여 주시기 바랍니다.
지난 챕터
문장의 유사도 측정하기
분류는 지도학습이라 미리 정답을 알고 있어야 하는데, 이번 챕터에서는 많은 문장 혹은 문서들 중에서 유사한 문장을 찾아내는 방법에 대해 진행해보려고 한다.
만약 어떤 문장을 벡터로 표현할 수 있다면 벡터 간 거리를 구하는 방법으로 손쉽게 해결할 수 있다.
먼저 scikit-learn 에서 텍스트의 특징(feature)을 추출하는 모듈에서 CountVectorizer 라는 함수를 import
※ 스킷런(scikit-learn)
스킷런은 파이썬에서 머신러닝을 위한 오픈 소스 라이브러리이며, 다양한 머신러닝 모델을 간편하게 구현하고 테스트할 수 있도록 지원.
※ CountVectorizer 함수
CountVectorizer는 문서 집합에서 단어 토큰을 생성하고 각 단어의 수를 세어 Bag of Words(BOW) 인코딩 벡터를 만든다.
즉, 각 문서에서 단어가 등장한 빈도를 카운트하여 문서를 벡터화한다.
from sklearn.feature_extraction.text import CountVectorizer
vectorizer = CountVectorizer(min_df = 1) #단어가 나타나는 최소 문서 빈도수는 1
※ min_df = 1은 단어가 나타나는 최소 문서 빈도수를 지정하는 매개변수이다.
여기서는 단어가 적어도 한 번 이상 문서에 나타나야 한다는 조건을 설정하였다. 이렇게 설정하면 너무 드물게 나타나는 단어는 무시되고, 자주 등장하는 단어만을 고려하여 문서 단어 행렬을 만들게 된다.
연습용 문장 만들고(유사한 문장) 벡터화하기
contents = ['메리랑 놀러가고 싶지만 바쁜데 어떡하죠?',
'메리는 공원에서 산책하고 노는 것을 싫어해요',
'메리는 공원에서 노는 것도 싫어해요. 이상해요.',
'먼 곳으로 여행을 떠나고 싶은데 너무 바빠서 그러질 못하고 있어요']
상단의 연습용 문장이 어떻게 벡터로 표현되는지 확인해보기
# 텍스트 데이터를 입력으로 받아, 각 문서에서 단어의 빈도수를 계산하고 문서 단어 행렬로 변환
X = vectorizer.fit_transform(contents)
#vectorizer를 사용하여 텍스트 데이터인 contents를 처리 vectorizer.get_feature_names()
그런데, 상단의 코드를 입력하면
AttributeError: 'CountVectorizer' object has no attribute 'get_feature_names'
라는 에러 메시지가 뜨는데, 이는 CountVectorizer 객체가 get_feature_names 속성을 가지고 있지 않기 때문에 발생하며,. 이 속성은 이전 버전의 scikit-learn에서는 사용되었지만 현재 버전에서는 사용되지 않는다고 한다.
Chat GPT를 통해 에러 분석 및 해결 코드 생성을 요청하여 하단의 코드를 받아 입력해보았다.
# Fit and transform the documents
X = vectorizer.fit_transform(contents)
# Get the feature names from the vocabulary_
feature_names = vectorizer.get_feature_names_out()
# Print the feature names
print(feature_names)
그리고 이를 좀 더 벡터화를 시켜보는 작업을 해보겠다.
먼저 KoNPLy 의 Okt (구.Twitter) 를 이용하여 형태소 분석을 한 결과를 token으로 두고
from konlpy.tag import Okt
o = Okt()
contents_tokens = [o.morphs(row) for row in contents]
contents_tokens
그리고 형태소 분석을 한 후 띄어쓰기로 구분하고 그것 자체를 하나의 문장(sentence)으로 만들어서 scikit learn의 vectorizer 함수에서 사용하기 편하게 편집
# 텍스트 데이터 벡터화하기
contents_for_vectorize = []
for content in contents_tokens:
sentence = ''
for word in content:
sentence = sentence + ' ' + word
contents_for_vectorize.append(sentence)
contents_for_vectorize
<코드 설명 with Chat GPT> 이 코드는 텍스트 데이터를 벡터화하기 위해 사용됩니다. 먼저, contents_tokens라는 리스트에 있는 각 문서를 반복합니다. 그런 다음, 각 문서에 있는 단어를 반복하면서 문장을 형성합니다. 각 단어 사이에는 공백을 넣어줍니다. 이렇게 형성된 문장들은 contents_for_vectorize라는 새로운 리스트에 추가됩니다. 마지막으로, contents_for_vectorize를 출력하여 문장들이 올바르게 형성되었는지 확인할 수 있습니다. 이러한 과정을 거치면 텍스트 데이터를 CountVectorizer나 TfidfVectorizer와 같은 벡터화 모델에 입력할 수 있는 형태로 변환할 수 있습니다. |
그리고 특징(feature)를 찾는다
# 텍스트 데이터를 CountVectorizer에 입력할 수 있는 형태로 변환
# fit_transform() 메서드를 사용하여 각 문서를 벡터화하고, 이를 변수 X에 할당
X = vectorizer.fit_transform(contents_for_vectorize)
# X의 형태를 통해 벡터화된 데이터셋의 샘플 수와 특징 수를 확인
# 텍스트 데이터를 수치 데이터로 변환하여 머신러닝 모델에 입력
num_samples, num_features = X.shape
num_samples, num_features
그리고 리스트를 받고
vectorizer.get_feature_names_out()
다시 벡터화
import numpy as np #numpy 모듈 import 가 안 되어 있으면 필요
X.toarray().transpose()
<코드 설명 with Chat GPT> 이 코드는 NumPy 배열을 전치하는 작업을 수행합니다. 먼저, X.toarray()는 희소 행렬인 X를 밀집 배열로 변환합니다. 그런 다음 .transpose() 메서드를 사용하여 이 배열을 전치시킵니다. 이것은 행과 열을 바꾸어주는 작업을 의미합니다. 결과적으로, 행렬의 행이 열이 되고 열이 행이 됩니다. 이 코드는 데이터를 분석하거나 변형할 때 주로 사용되며, 데이터의 구조를 변경하여 분석이나 처리를 용이하게 합니다. |
새로운 문장을 벡터화시키기
새로운 문장을 위와 동일한 과정을 거쳐 벡터화를 시킨 뒤, 각 벡터들 사이의 거리를 구해본다.
먼저 새로운 문장을 만들고, 형태소 단위로 분해한 뒤 문장 형태로 만들어본다.
# new_post 리스트를 정의
new_post = ['메리랑 공원에서 산책하고 놀고 싶어요']
# o.morphs 함수를 사용하여 문장을 형태소(단어) 단위로 분해
# 이 과정의 결과는 new_post_tokens에 저장
new_post_tokens = [o.morphs(row) for row in new_post]
# 벡터화할 문장을 담게 될 리스트를 생성.
new_post_for_vectorize = []
# 각 단어 리스트를 하나의 문자열로 합치고, 이때 단어 사이에 공백을 추가하여 문장 형태로 만듦
for content in new_post_tokens:
sentence = ''
for word in content:
sentence = sentence + ' ' + word
new_post_for_vectorize.append(sentence)
new_post_for_vectorize
그런 다음, 새 문장을 벡터화 시키고, 밀집 배열로 변환 시켜본다.
new_post_vec = vectorizer.transform(new_post_for_vectorize)
new_post_vec.toarray()
이제 새로운 문장(new_post_vec)과 비교해야 할 문장(contents)들 각각에 대해 거리를 구해본다.
먼저 두 벡터의 차를 구하고 난 결과의 norm을 구하는 함수를 만들어주고
# SciPy 라이브러리 불러오기
import scipy as sp
# 주어진 두 벡터 v1과 v2 사이의 유클리드 거리를 계산하여 반환
def dist_raw(v1, v2):
delta = v1 - v2
return sp.linalg.norm(delta.toarray())
<코드 설명 with Chat GPT> SciPy는 과학 및 기술 계산을 위한 파이썬 라이브러리입니다. v1과 v2는 입력 벡터입니다. delta는 두 벡터의 차이를 계산합니다. delta.toarray()는 희소 행렬(sparse matrix)을 일반 배열로 변환합니다. sp.linalg.norm 함수는 벡터의 유클리드 노름(Euclidean norm, 벡터의 길이)을 계산합니다. 이 함수는 delta의 크기를 반환합니다. 따라서, 이 코드는 주어진 두 벡터 v1과 v2 사이의 유클리드 거리를 계산하여 반환합니다. |
각 문장과 새로운 문장의 거리를 구해준다.
먼저 최적의 값을 저장하기 위해 변수를 초기화 해주는 코드를 생성하고
best_doc = None # 최적의 문서 저장
best_dist = 65535 # 최적의 거리(또는 가장 작은 거리)를 저장
best_i = None # 최적의 문서의 인덱스를 저장
<코드 설명 with Chat GPT> 위의 코드는 세 가지 변수를 초기화하는 부분입니다. 각 변수의 의미는 다음과 같습니다: 1) best_doc = None: best_doc 변수는 최적의 문서를 저장하기 위해 초기화된 것입니다. 초기 값으로 None을 설정하여 아직 최적의 문서가 선택되지 않았음을 나타냅니다. 2) best_dist = 65535: best_dist 변수는 최적의 거리(또는 가장 작은 거리)를 저장하기 위해 사용됩니다. 초기 값으로 65535라는 매우 큰 값을 설정하여 이후에 계산된 거리 값들이 이 값보다 작게 설정될 수 있도록 합니다. 이는 일반적으로 임의의 큰 수를 설정하여 최소값을 찾기 위한 기법입니다. 3) best_i = None: best_i 변수는 최적의 문서의 인덱스를 저장하기 위해 초기화된 것입니다. 초기 값으로 None을 설정하여 아직 최적의 문서 인덱스가 선택되지 않았음을 나타냅니다. 이 코드는 일반적으로 문서 간의 거리를 계산하여 가장 유사한 문서를 찾는 알고리즘의 일부로 사용됩니다. 초기화 후, 루프나 비교 과정을 통해 각 문서의 거리와 비교하면서 가장 작은 거리와 그에 해당하는 문서를 찾게 됩니다. |
여러 개의 문서 중에서 새로운 문서와 가장 유사한 문서를 찾는 코드를 입력한다.
for i in range(0, num_samples): # 0부터 num_samples(전체 문서)까지 반복
post_vec = X.getrow(i) #X: 문서 벡터들을 포함하는 행렬, getrow(i): i번째 문서의 벡터 가져옴
d = dist_raw(post_vec, new_post_vec) # 두 벡터 사이의 거리 계산
#현재 문서의 인덱스 i, 거리 d, 그리고 문서의 내용을 출력.
# contents[i]는 i번째 문서의 내용을 나타냄.
print("== Post %i with dist=%.2f : %s" %(i,d,contents[i]))
# 가장 유사한 문서 찾기
if d<best_dist:
best_dist = d
best_i = i
<코드 설명 with Chat GPT> 1) for i in range(0, num_samples): 0부터 num_samples까지 반복합니다. num_samples는 전체 문서의 개수를 나타냅니다. 2) post_vec = X.getrow(i): X는 문서 벡터들을 포함하는 행렬입니다. getrow(i)는 i번째 문서의 벡터를 가져옵니다. 3) d = dist_raw(post_vec, new_post_vec): dist_raw 함수는 두 벡터 사이의 거리를 계산합니다. post_vec는 현재 문서의 벡터이고, new_post_vec는 새로운 문서의 벡터입니다. d는 두 문서 사이의 거리를 저장합니다. 4) print("== Post %i with dist=%.2f : %s" %(i,d,contents[i])): 현재 문서의 인덱스 i, 거리 d, 그리고 문서의 내용을 출력합니다. contents[i]는 i번째 문서의 내용을 나타냅니다. 5) if d < best_dist: 현재 계산된 거리가 best_dist보다 작으면, 즉 더 유사하면, 6) best_dist = d best_i = i best_dist를 현재 거리 d로 업데이트하고, best_i를 현재 문서의 인덱스 i로 업데이트합니다. 이 과정은 가장 유사한 문서를 찾기 위해 사용됩니다. 이 루프는 모든 문서에 대해 반복되며, 최종적으로 새로운 문서와 가장 유사한 문서의 인덱스 best_i와 그 거리를 best_dist에 저장하게 됩니다. |
새로운 문장(new_post)와 가장 흡사한 문장(contents)을 조회해보면
print("Best post is %i, dist = %.2f" % (best_i, best_dist)) #best_i와 best_dist를 출력.
print('-->', new_post) #새로운 문서 new_post를 출력
print('--->', contents[best_i]) #contents 리스트에서 가장 유사한 문서의 내용을 출력
※ best_i는 가장 유사한 문서의 인덱스.
※ best_dist는 가장 유사한 문서와의 거리.
※ 포맷 문자열을 사용하여 출력. (%i는 정수, %.2f는 소수점 둘째 자리까지의 실수)
새로운 문장 '메리랑 공원에서 산책하고 놀고 싶어요' 와 가장 유사한 문장은
'메리는 공원에서 산책하고 노는 것을 싫어해요' 로 출력된다.
문장의 의미는 반대지만 소속된 단어들의 조합(메리, 공원, 산책, 놀다 등)을 보면 타당해 보인다.
두 문장의 벡터화된 결과 보기
for i in range(0, len(contents)): # contents 리스트의 길이만큼 반복문을 실행
# i는 0부터 contents 리스트의 길이 - 1까지의 값
print(X.getrow(i).toarray()) # X 행렬의 각 문서 벡터를 배열 형태로 출력
print('------------------')
print(new_post_vec.toarray()) # 새로운 문서의 벡터 표현을 배열 형태로 변환하여 출력
벡터화된 결과를 보면 4개의 contents 변수에 저장된 문장과 새로운 문장이 형태소 분석 후 벡터화된 결과를 볼 수 있다.
거리 구하기
먼저 각 벡터의 norm을 나눠준 후 거리를 구하도록 함수를 생성하고
#두 벡터 사이의 거리(유클리드 거리) 계산(유사도 측정)
def dist_norm(v1, v2):
# 벡터를 정규화하고 크기를 계산한 뒤, 이를 벡터로 나눔
v1_normalized = v1 / sp.linalg.norm(v1.toarray()) # v1 벡터 정규화. 크기(벡터 길이, 유클리드 노름)를 계산
v2_normalized = v2 / sp.linalg.norm(v2.toarray()) # v2 벡터 정규화. 크기(벡터 길이, 유클리드 노름)를 계산
# 정규화된 벡터 v1과 v2의 차이 계산
delta = v1_normalized - v2_normalized
# delta 벡터의 유클리드 노름 계산하여 반환 (두 벡터 사이의 거리)
return sp.linalg.norm(delta.toarray())
거리를 다시 구해보면
# 최적의 값을 저장하기 위해 변수 초기화
best_doc = None
best_dist = 65535
best_i = None
# 여러 개의 문서 중에서 새로운 문서와 가장 유사한 문서를 찾는 코드
for i in range(0, num_samples): # 0부터 num_samples(전체 문서)까지 반복
post_vec = X.getrow(i) #X: 문서 벡터들을 포함하는 행렬, getrow(i): i번째 문서의 벡터 가져옴
d = dist_norm(post_vec, new_post_vec) # 정규화된 두 벡터 사이의 거리 계산
#현재 문서의 인덱스 i, 거리 d, 그리고 문서의 내용을 출력.
# contents[i]는 i번째 문서의 내용을 나타냄.
print("== Post %i with dist=%.2f : %s" %(i,d,contents[i]))
# 가장 유사한 문서 찾기
if d<best_dist:
best_dist = d
best_i = i
거리를 구한 결과가 앞서 구한 dist_raw 와는 달라져 있는 것을 알 수 있다.
그러나 가장 가까운 문장을 찾는 것에 대한 결과는 크게 다르지 않다.
print("Best post is %i, dist = %.2f" % (best_i, best_dist)) #best_i와 best_dist를 출력.
print('-->', new_post) #새로운 문서 new_post를 출력
print('--->', contents[best_i]) #contents 리스트에서 가장 유사한 문서의 내용을 출력
TF-IDF 개념 적용해보기
tfidf 는 텍스트 마이닝에서 사용하는 일종의 단어별로 부과하는 가중치
- tf(term frequency): 어떤 단어가 문서 내에서 자주 등장할수록 중요도가 높을 것으로 보는 것
- idf(inverse document frequency): 비교하는 모든 문서에 만약 같은 단어가 있다면 이 단어는 핵심 어휘일지는 모르지만 문서간의 비교에서는 중요한 단어가 아니라는 뜻으로 보는 것
tfidf 원리로 tfidf 함수를 만들고
# 텍스트 마이닝에서 사용하는 TF-IDF 값을 계산하는 함수
# t는 단어, d는 문서, D는 문서 집합
def tfidf(t, d, D):
# 단어의 tf 계산(문서에서 단어의 등장 횟수의 합을 구한 뒤, 횟수로 나눔)
tf = float(d.count(t)) / sum(d.count(w) for w in set(d))
# 전체 문서 수를 t가 포함된 문서 수로 나눈 뒤 나눈 값을 로그로 변환
idf = sp.log( float(len(D))/len([doc for doc in D if t in doc]))
# 계산된 TF와 IDF 값 반환
return tf, idf
연습용 코드를 만들어 돌려보면,
# 연습용 코드
a, abb, abc = ['a'], ['a','b','b'], ['a','b','c']
D = [a,abb,abc]
print(tfidf('a', a, D))
print(tfidf('b', abb, D))
print(tfidf('a', abc, D))
print(tfidf('b', abc, D))
print(tfidf('c', abc, D))
모든 문장에 a가 있기 때문에 idf의 결과는 0
이제 이 두 값을 곱한 것을 tfidf 라고 하는 함수로 수정해서 사용하면 되지만
맨 처음 scikit-learn 의 countVectorizer 클래스를 import 할 때,
scikit-learn 의 TfidfVectorizer 클래스를 같이 import 해서 사용해도 된다.
# scikit-learn 라이브러리의 TfidfVectorizer 클래스를 사용하여 텍스트 데이터를 TF-IDF 형식으로 변환
from sklearn.feature_extraction.text import TfidfVectorizer
# fidfVectorizer의 인스턴스를 생성하고 vectorizer 변수에 할당
vectorizer = TfidfVectorizer(min_df=1, decode_error='ignore')
※ TfidfVectorizer: 텍스트 데이터를 TF-IDF 벡터로 변환하는 도구
※ min_df=1: 단어가 최소 한 문서에 등장해야 벡터화에 포함. 이 값이 높으면 드물게 등장하는 단어들이 무시.
※ decode_error='ignore': 디코딩 오류가 발생할 경우 무시하도록 설정. 이는 텍스트 데이터에 인코딩 문제가 있을 때 유용.
그리고나서 동일하게 contents 문장들을 다듬고,
contents_tokens = [o.morphs(row) for row in contents]
# 벡터화할 문장을 담게 될 리스트를 생성.
contents_for_vectorize = []
# 각 단어 리스트를 하나의 문자열로 합치고, 이때 단어 사이에 공백을 추가하여 문장 형태로 만듦
for content in contents_tokens:
sentence = ''
for word in content:
sentence = sentence + ' ' + word
contents_for_vectorize.append(sentence)
# 텍스트 데이터를 CountVectorizer에 입력할 수 있는 형태로 변환
# fit_transform() 메서드를 사용하여 각 문서를 벡터화하고, 이를 변수 X에 할당
X = vectorizer.fit_transform(contents_for_vectorize)
num_samples, num_features = X.shape
num_samples, num_features
만들어진 말뭉치를 확인해보면
vectorizer.get_feature_names_out()
테스트용 문장과 비교도 해본다.
먼저 새로운 테스트용 문장을 만들고
# new_post 리스트를 정의
new_post = ['근처 공원에 메리랑 놀러가고 싶네요']
# o.morphs 함수를 사용하여 문장을 형태소(단어) 단위로 분해
# 이 과정의 결과는 new_post_tokens에 저장
new_post_tokens = [o.morphs(row) for row in new_post]
# 벡터화할 문장을 담게 될 리스트를 생성.
new_post_for_vectorize = []
# 각 단어 리스트를 하나의 문자열로 합치고, 이때 단어 사이에 공백을 추가하여 문장 형태로 만듦
for content in new_post_tokens:
sentence = ''
for word in content:
sentence = sentence + ' ' + word
new_post_for_vectorize.append(sentence)
new_post_for_vectorize
벡터화를 시키고
new_post_vec = vectorizer.transform(new_post_for_vectorize)
다른 결과와 비교해보면
# 최적의 값을 저장하기 위해 변수 초기화
best_doc = None
best_dist = 65535
best_i = None
# 여러 개의 문서 중에서 새로운 문서와 가장 유사한 문서를 찾는 코드
for i in range(0, num_samples): # 0부터 num_samples(전체 문서)까지 반복
post_vec = X.getrow(i) #X: 문서 벡터들을 포함하는 행렬, getrow(i): i번째 문서의 벡터 가져옴
d = dist_norm(post_vec, new_post_vec) # 정규화된 두 벡터 사이의 거리 계산
#현재 문서의 인덱스 i, 거리 d, 그리고 문서의 내용을 출력.
# contents[i]는 i번째 문서의 내용을 나타냄.
print("== Post %i with dist=%.2f : %s" %(i,d,contents[i]))
# 가장 유사한 문서 찾기
if d<best_dist:
best_dist = d
best_i = i
print("Best post is %i, dist = %.2f" % (best_i, best_dist)) #best_i와 best_dist를 출력.
print('-->', new_post) #새로운 문서 new_post를 출력
print('--->', contents[best_i]) #contents 리스트에서 가장 유사한 문서의 내용을 출력
전체코드
'[파이썬 Projects] > <파이썬 데이터 분석>' 카테고리의 다른 글
[파이썬] 자연어 처리(NLP) - 여자친구 선물 고르기 : 2(실패) (0) | 2024.05.15 |
---|---|
[파이썬] 자연어 처리(NLP) - 여자친구 선물 고르기 : 1 (1) | 2024.05.15 |
[파이썬] 자연어 처리(NLP) 시작하기 - 7 (0) | 2024.05.13 |
[파이썬] 자연어 처리(NLP) 시작하기 - 6 (0) | 2024.05.13 |
[파이썬] 자연어 처리(NLP) 시작하기 - 5 (0) | 2024.05.12 |