TOP
class="layout-aside-left paging-number">
본문 바로가기
[파이썬 Projects]/<파이썬 데이터 분석>

[파이썬] 자연어 처리(NLP) 시작하기 - 8

by 기록자_Recordian 2024. 5. 14.
728x90
반응형
시작에 앞서
해당 내용은 <파이썬으로 데이터 주무르기> -민형기 저, BJPUBLIC 출판사 의 내용을 토대로 작성되었습니다.
보다 자세한 내용은 해당 교재를 확인하여 주시기 바랍니다.

지난 챕터
 

[파이썬] 자연어 처리(NLP) 시작하기 - 7

시작에 앞서해당 내용은 -민형기 저, BJPUBLIC 출판사 의 내용을 토대로 작성되었습니다. 보다 자세한 내용은 해당 교재를 확인하여 주시기 바랍니다.지난 챕터 [파이썬] 자연어 처리(NLP) 시작하기

puppy-foot-it.tistory.com


문장의 유사도 측정하기

 

분류는 지도학습이라 미리 정답을 알고 있어야 하는데, 이번 챕터에서는 많은 문장 혹은 문서들 중에서 유사한 문장을 찾아내는 방법에 대해 진행해보려고 한다.

 

만약 어떤 문장을 벡터로 표현할 수 있다면 벡터 간 거리를 구하는 방법으로 손쉽게 해결할 수 있다.

 

먼저 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' 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

okt 불러오고 token 화 하기
형태소 분석 결과

 

그리고 형태소 분석을 한 후 띄어쓰기로 구분하고 그것 자체를 하나의 문장(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))

tf-idf 연습용 코드

모든 문장에 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

contents 문장 수정

 

만들어진 말뭉치를 확인해보면

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 리스트에서 가장 유사한 문서의 내용을 출력

각 문장간 거리 비교하여 가장 유사한 문장 출력하기


전체코드

NLP_Basic.ipynb
1.39MB

728x90
반응형