반응형
Notice
Recent Posts
Recent Comments
Link
«   2025/04   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30
Tags
more
Archives
Today
Total
관리 메뉴

Ethan's Values

파이썬 데이터 주무르기 8장 자연어 처리(5~7/8절) 본문

Python

파이썬 데이터 주무르기 8장 자연어 처리(5~7/8절)

Ethan_hyk 2023. 11. 6. 16:57
반응형

https://hykethan.tistory.com/24

 

파이썬 데이터 주무르기 8장 자연어 처리 시작하기(~4/8절)

1. 한글 자연어 처리 기초 - KoNLPy 및 필요 모듈의 설치 먼저 설치와 주의해야 되는 것들이 있습니다. **먼저 파이썬 버젼을 확인해야 합니다. 2023년 11월 2일 기준으로 3.12버전 까지 나왔습니다. 하

hykethan.tistory.com

8장의 4절에 이어서 진행하겠습니다.

5. Naive Bayes Classifier의 이해 - 영문

유명한 Bayes법치에 기반한 분류기이다.

그 특징은 서로 확률적으로 독립이라는 가정이 있다.

특정 경찰서에 있는 경찰관의 이름이 나열되어 있을때 drew라는 이름의 경찰관은 남자일까/여자일까

P(male | drew) = 1/3 * 3/8 = 0.125

P(female | drew) = 2/5 * 5/8 = 0.25

=> 여성 쪽이 높은 값이 나오기 때문에 여자라고 분류된다.

 

일단 필요한 모듈을 import 합니다.

from nltk.tokenize import word_tokenize
import nltk

그리고 나서, 연습용 데이터를 만듭니다.

train = [('i like you', 'pos'),
         ('i hate you', 'neg'),
         ('you like me', 'neg'),
         ('i like her', 'pos')]

train 문장에서 사용된 전체 단어를 찾습니다. 

코드해석은 아래 링크로 들어가서 보시면 됩니다. 내포함수들이 나오기 때문에 내용을 정리했습니다.

all_words = set(word.lower() for sentence in train  for word in word_tokenize(sentence[0]))
all_words

https://hykethan.tistory.com/23

 

Python 리스트, 집합, 딕셔너리 내포 정리

내포란? 내포는 간결하고 효율적인 방식으로 리스트, 집합, 딕셔너리를 생성할 수 있는 기능입니다. for문과 조건문 if문을 사용한 코드를 더 간결하게 쓸 수 있습니다. 하지만, 내포 코드가 너무

hykethan.tistory.com

{'hate', 'her', 'i', 'like', 'me', 'you'}

위와 같이 집합(set) 안에 있기 때문에 {}인 집합으로 도출되며 unique한 값만 도출되게 됩니다.

우리는 이것을 '말뭉치'라고 해두겠습니다.

그리고 말뭉치 기준으로 train문장에 속한 단어인지 아닌지를 기록합니다.

t = [({word: (word in word_tokenize(x[0])) for word in all_words}, x[1])
                                                        for x in train]

=> 첫번째 문장은 말뭉치에서 you, like, i가 들어있습니다.

이제 이를 이용해서 Naive Bayes 분류기를 동작시키도록 합니다.

classifier = nltk.NaiveBayesClassifier.train(t)
classifier.show_most_informative_features()

=> train 문장에 붙은 pos/neg (긍정/부정) 태그를 이용해서 분류한 결과 hate라는 단어가  Fasle 일때 즉, hate가 없을때  긍정일 비율이 1.7:1.0이라는 의미입니다.

이제, 분류하는 분류기를 만들었습니다. 이걸 가지고 테스트 문장을 통과시켜 보겠습니다.

test_sentence = 'i like MeRui'
test_sent_features = {word.lower(): (word in word_tokenize(test_sentence.lower()))  for word in all_words}
test_sent_features

=> i와 like만 일치하네요. 

이에 따라서 긍정일지 부정일지 확인해보니 긍정이라고 나왔습니다. 

classifier.classify(test_sent_features)
'pos'

6. Naive Bayes Classifier의 이해 - 한글

한글에서는 형태소 분석을 이용해야 합니다. 형태소 분석을 하지 않으면 분류기의 동작을 장담하기 어렵고, 신뢰도가 매우 낮기 때문에 꼭 형태소 분석을 거쳐 신뢰도 높은 분류기를 만들어 사용해야 합니다. 우리는 Okt 형태소 분석기를 사용하겠습니다.

from konlpy.tag import Okt
pos_tagger = Okt()

학습할 데이터를 정의해줍니다.

train = [('메리가 좋아', 'pos'),
         ('고양이도 좋아', 'pos'),
         ('난 수업이 지루해', 'neg'),
         ('메리는 이쁜 고양이야', 'pos'),
         ('난 마치고 메리랑 놀거야', 'pos')]

형태소 분석이 필요하기 때문에 형태소에 태그를 붙여주는 tokenize 함수를 정의합니다.

def tokenize(doc):
    return [word for word in pos_tagger.pos(doc, join=True)]

정의한 함수를 train에 적용시키면 아래와 같이 도출됩니다.

이제 문장만 들어있는 항목을 tokens에 넣어주어 전체 말뭉치를 만들어 줍니다.

tokens = [t for d in train_docs for t in d[0]]
tokens

=>

말뭉치에 들어있는 단어가 있는지 아닌지를 구분하는 함수를 만들어서 train 문장에 적용합니다.

train_xy = [(term_exists(d),c) for d,c in train_docs]
train_xy

=> 

이제 분류기에 학습을 시켜줍니다. 그리고 이 학습된 분류기에 적용할 테스트할 문장은 아래와 같습니다.

classifier = nltk.NaiveBayesClassifier.train(train_xy)
test_sentence = [("난 수업이 마치면 메리랑 놀거야")]

테스트 문장을 형태소 분석해줍니다.

test_docs = pos_tagger.pos(test_sentence[0])
test_docs

=>

있는지 없는지 확인합니다.

test_sent_features = {word: (word in tokens) for word in test_docs}
test_sent_features

=>

마지막으로 분류기를 거친 테스트 문장에 대한 결과를 보니 pos(긍정)이 도출되었습니다. 의미가 잘 드러난 것 같습니다.

classifier.classify(test_sent_features)
#'pos'

7. 문장의 유사도 측정하기

유사한 문장을 찾아내는 방법에 대해 설명하겠습니다.

먼저 문장을 컴퓨터가 인식하기 쉽게 벡터화를 하고 벡터 간 거리를 구하는 방법으로 유사도를 측정해보겠습니다.

먼저 scikit-learn에서 텍스트의 특징(feature)을 추출하는 모듈에서 CountVectorizer라는 함수를 import 합니다.

from sklearn.feature_extraction.text import CountVectorizer
vectorizer = CountVectorizer(min_df = 1) #scikit-learn 텍스트 특징(feature) 추출 모듈

연습 문장을 정의해줍니다.

contents = ['메리랑 놀러가고 싶지만 바쁜데 어떻하죠?',
                   '메리는 공원에서 산책하고 노는 것을 싫어해요',
                   '메리는 공원에서 노는 것도 싫어해요. 이상해요.',
                   '먼 곳으로 여행을 떠나고 싶은데 너무 바빠서 그러질 못하고 있어요']

문장에서 특징(feature)을 추출해 X에 저장합니다.

X = vectorizer.fit_transform(contents) #추출 첫번째 문장만 예로들면 단어들을 feature로 잡음
vectorizer.get_feature_names_out() # 추출결과는 벡터화의 기준이 된다.

특징(feature)을 기반으로 벡터화된 결과를 확인해봅시다.

X.toarray().transpose()

=> 문장이 4개이기 때문에, 4개 열로 나오고, 각 단어의 위치가 1의 값을 가집니다. 
'놀러가고'는 위 추출 결과에서 8번째에, 메리랑은 11번째에 있으므로, 아래 결과에서 1열에 8번째, 11번쨰 숫자 1
문제는 메리랑과 메리는을 분리해서 같은 단어로 봐야함.

한글 문장에 대한 벡터화를 형태소 분석 사용해서 합리적으로 진행해보겠습니다.

Twitter를 사용해서 형태소 분석한 결과를 token으로 두겠습니다.

from konlpy.tag import Twitter #형태소 분석기
t = Twitter()
contents_tokens = [t.morphs(row) for row in contents] #contents 리스트에서 한 요소 씩 row가 가져와서 형태소 분석기에 대입 형태소 분석을 한 결과를 token으로 둔다.
contents_tokens

그리고 형태소 분석을 한 후 띄어쓰기로 구분하고 그것 자체를 하나의 문장(sentence)으로 만들어서 scikit learn의 vectorizer 함수에서 사용하기 편하게 편집합니다.

#형태소 분석을 한 후 띄어쓰기로 구분하고 그것 자체를 하난의 문자을로 만들어서 vectorizer에서 사용

contents_for_vectorize = []

for content in contents_tokens: # 문장마다 content로 할당
    sentence = '' #변수 정의
    for word in content: # 할당 받은 content에서 word마다(메리, 랑, 놀러 ..) 사이에 ' ' (띄어쓰기) 를 넣어 sentence 생성 후
        sentence = sentence + ' ' + word
    contents_for_vectorize.append(sentence) # 위에서 만든 sentence를 contents_for_vectorize라는 리스트에 값으로 추가
   
contents_for_vectorize
[' 메리 랑 놀러 가고 싶지만 바쁜데 어떻하죠 ?',
 ' 메리 는 공원 에서 산책 하고 노 는 것 을 싫어해요',
 ' 메리 는 공원 에서 노 는 것 도 싫어해요 . 이상해요 .',
 ' 먼 곳 으로 여행 을 떠나고 싶은데 너무 바빠서 그러질 못 하고 있어요']

그리고 feature를 찾도록 합니다.

X = vectorizer.fit_transform(contents_for_vectorize) # 벡터화
num_samples, num_features = X.shape # 4개의 samples(문장) 과 20개의 feature(말뭉치)가 있다.
num_samples, num_features #각 변수에 담아준다

feature를 확인해보겠습니다.

vectorizer.get_feature_names_out() #말뭉치 값들. 아까 '메리랑'과 '메리는' 이 메리로 합쳐진것을 볼 수 있음.
array(['가고', '공원', '그러질', '너무', '놀러', '떠나고', '메리', '바빠서', '바쁜데', '산책',
       '싫어해요', '싶은데', '싶지만', '어떻하죠', '에서', '여행', '으로', '이상해요', '있어요',
       '하고'], dtype=object)

이제 새로운 문장을 동일한 과정으로 벡터화해서 각 벡터들 사이의 거리를 구해봅시다.

#test_text 생성 똑같이 벡터화 시켜주기 위해 먼저 형태소 분석기에 넣어서 형태소 단위 별로 띄어쓰기 해줌
new_post = ['메리랑 공원에서 산책하고 놀고 싶어요']
new_post_tokens = [t.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)과 비교해야 할 문장(contents)들 각각에 대해 거리를 구해봅시다. 먼저 거리를 구하기 위해 sp 모듈을 호출하고 벡터의 거리를 계산해서 정규화시켜주는 함수를 정의합니다.

import scipy as sp #거리를 구하기 위한 모듈 호출
#새로운 문장(new_post)과 비교해야할 문장(contents)의 거리를 구하는 함수 생성
def dist_raw(v1,v2):
    delta = v1 - v2
    return sp.linalg.norm(delta.toarray())

이제 각 문장과 새로운 문장의 거리를 구해봅시다.

best_doc = None
best_dist = 65535
best_i = None

for i in range(0, num_samples):
    post_vec = X.getrow(i) # 벡터화된 0~3번째 문장 가져오기
    d = dist_raw(post_vec, new_post_vec) #벡터화된 0~3번째 문장과 벡터화된 새로운 문장과의 거리
   
    print("== Post %i with dist=%.2f   : %s" %(i,d,contents[i])) # %i 에는 i가, %.2f 에는 d가 %s에는 contents[i]가 각각 대입됨. =>  i는 몇번째 문장과 비교인지, d는 해당 문장과의 거리, s는 문장 내용
   
    if d<best_dist:
        best_dist = d
        best_i = i
== Post 0 with dist=3.00   : 메리랑 놀러가고 싶지만 바쁜데 어떻하죠?
== Post 1 with dist=1.00   : 메리는 공원에서 산책하고 노는 것을 싫어해요
== Post 2 with dist=2.00   : 메리는 공원에서 노는 것도 싫어해요. 이상해요.
== Post 3 with dist=3.46   : 먼 곳으로 여행을 떠나고 싶은데 너무 바빠서 그러질 못하고 있어요

가장 유사한 문장을 추출해보겠습니다.

print("Best post is %i, dist = %.2f" %(best_i, best_dist))
print('-->',new_post)
print('---->',contents[best_i])
Best post is 1, dist = 1.00
--> ['메리랑 공원에서 산책하고 놀고 싶어요']
----> 메리는 공원에서 산책하고 노는 것을 싫어해요

거리가 1일 때가 가장 가까운 거리였고, 그 문장은 '메리랑 공원에서 산책하고 놀고 싶어요' 입니다. 문장의 의미는 반대지만 단어들의 조합을 보면 비슷해보입니다.

벡터화된 결과를 보겠습니다.

#벡터화된 결과를 확인해보기.
for i in range(0,len(contents)):
    print("%i 번째 문장의 벡터결과 -> " %(i) + str(X.getrow(i).toarray()))

print('-----------------------------------------------------------------')
print("새로운 문장의 벡터결과 -> " + str(new_post_vec.toarray()))
0 번째 문장의 벡터결과 -> [[1 0 0 0 1 0 1 0 1 0 0 0 1 1 0 0 0 0 0 0]]
1 번째 문장의 벡터결과 -> [[0 1 0 0 0 0 1 0 0 1 1 0 0 0 1 0 0 0 0 1]]
2 번째 문장의 벡터결과 -> [[0 1 0 0 0 0 1 0 0 0 1 0 0 0 1 0 0 1 0 0]]
3 번째 문장의 벡터결과 -> [[0 0 1 1 0 1 0 1 0 0 0 1 0 0 0 1 1 0 1 1]]
-----------------------------------------------------------------
새로운 문장의 벡터결과 -> [[0 1 0 0 0 0 1 0 0 1 0 0 0 0 1 0 0 0 0 1]]

7.1. 문장의 유사도 측정하기(TF - IDF)

TF-IDF 개념 적용
tf(term frequency) 와 idf(inverse document frequency) 는 일종의 단어별로 부과하는 가중치.
-> tf는 어떤 단어가 문서 내에서 자주 등장할수록 중요도가 높을 것으로 보는것.
-> idf는 비교하는 모든 문서에 만약 같은 단어가 있다면 이 단어는 핵심 어휘일지는 모르지만 문서 간의 비교에서는 중요한 단어가 아니라는 뜻으로 보는 것
-> TF_IDF는 TF와 IDF를 곱한 값을 의미함. 특정 문서에서만 자주 등장하는 단어는 중요도가 높다고 판단. 
-> TF_IDF 값이 낮으면 중요도가 낮은것, 크면 중요도 큰 것.

먼저 직접 TF-IDF 함수를 생성해보겠습니다.

#TF-IDF 함수 생성
#idf는 log()문서 n이 너무 커질 수 있기 때문
def tfidf(t, d, D):
    tf = float(d.count(t)) / sum(d.count(w) for w in set(d))
    idf = sp.log(float(len(D)) / len([doc for doc in D if t in doc]))
    return tf, idf

테스트를 해보겠습니다.

a, abb, abc = ['a'], ['a','b','b'], ['a','b','c']
D = [a,abb,abc]

print(tfidf('a', a, D)) # 'a'는  a에 1/1번 == tf=1, idf == log(3/3) = 0
print(tfidf('b', abb, D)) # 'b'는  abb에 2/3번 == tf=0.666, log(3/2) = 0.4054651081081644
print(tfidf('a', abc, D))
print(tfidf('b', abc, D))
print(tfidf('c', abc, D)) # 'c'는  abc에 1/3번 == tf=0.333, log(3/1) = 1.0986122886681098

이후 tf*idf 값을 취하면 되지만, scikit-learn의 TfidfVectorizer를 import 하겠습니다. 벡터화하는 모듈을 CounterVectorizer 대신 TfidVectorizer를 사용하는 것입니다.

# scikit-learn 의 TfidfVectorizer import
from sklearn.feature_extraction.text import TfidfVectorizer
vectorizer = TfidfVectorizer(min_df=1, decode_error='ignore')

이후 동일하게 contents 문장들을 다듬는 작업을 진행합니다.

contents_tokens = [t.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)
   
X = vectorizer.fit_transform(contents_for_vectorize)
num_samples, num_features = X.shape
num_samples, num_features
(4, 20)

추출된 특징(feature)를 확인해보겠습니다.

vectorizer.get_feature_names_out()
array(['가고', '공원', '그러질', '너무', '놀러', '떠나고', '메리', '바빠서', '바쁜데', '산책',
       '싫어해요', '싶은데', '싶지만', '어떻하죠', '에서', '여행', '으로', '이상해요', '있어요',
       '하고'], dtype=object)

이제, 테스트 문장을 벡터화 하는 작업을 하겠습니다.

# test text 벡터화
# 형태소 분석 후 띄어쓰기로 구분하여 하나의 문장으로 만들기

new_post = ['근처 공원에 메리랑 놀러가고 싶네요.']
new_post_tokens = [t.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
# transform
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):
    post_vec = X.getrow(i)
   
    # 함수호출
    d = dist_raw(post_vec, new_post_vec)
   
    print("== Post %i with dist=%.2f   : %s" %(i,d,contents[i]))
   
    if d<best_dist:
        best_dist = d
        best_i = i

print()
print("Best post is %i, dist = %.2f" % (best_i, best_dist))
print('-->', new_post)
print('---->', contents[best_i])
== Post 0 with dist=0.90   : 메리랑 놀러가고 싶지만 바쁜데 어떻하죠?
== Post 1 with dist=1.18   : 메리는 공원에서 산책하고 노는 것을 싫어해요
== Post 2 with dist=1.16   : 메리는 공원에서 노는 것도 싫어해요. 이상해요.
== Post 3 with dist=1.41   : 먼 곳으로 여행을 떠나고 싶은데 너무 바빠서 그러질 못하고 있어요

Best post is 0, dist = 0.90
--> ['근처 공원에 메리랑 놀러가고 싶네요.']
----> 메리랑 놀러가고 싶지만 바쁜데 어떻하죠?

이렇게 재미있는 결과를 얻을 수 있습니다.

 

본 내용은 파이썬으로 데이터 주무르기-민형기 책에서 공부한 내용을 바탕으로 작성한 글입니다.

Comments