본문 바로가기

Study/AI

[모두의 딥러닝] #5. 딥러닝을 이용한 자연어 처리(NLP)

반응형

본 포스트는 '모두의 딥러닝' 개정 2판을 바탕으로 공부한 것을 정리한 글입니다.

17장: 딥러닝을 이용한 자연어 처리

자연어 처리(natural Language Processing, NLP)란 컴퓨터 혹은 기계가 사람의 언어(음성, 텍스트)를 인식하고 처리할 수 있도록 하는 작업을 말합니다. 그리고 자연어 처리를 위해서는 인간의 텍스트를 컴퓨터가 이해할 수 있는 수치로 정제하는 전처리 과정이 필요합니다.

 

지금부터 자연어 처리를 위하여 데이터를 전처리하는 과정을 다뤄보겠습니다.

 

1) 텍스트의 토큰화

텍스트의 단어, 문장, 형태소처럼 작게 나누어진 하나의 단위를 토큰(token)이라고 하고 텍스트를 토큰으로 만드는 과정을 토큰화(tokenization)라고 합니다.

 

# 1. 문장 to 단어

keras 라이브러리 text 모듈의 text_to_word_sequence() 함수를 통해서 문장을 단어 별로 쪼개어 리스트로 저장할 수 있습니다. 참고로 느낌표와 마침표 등의 구두점은 인코딩에 포함되지 않습니다.

from tensorflow.keras.preprocessing.text import text_to_word_sequence

text = "해보지 않으면 해낼 수 없다"
result = text_to_word_sequence(text)

print(result)
>>> ['해보지', '않으면', '해낼', '수', '없다']

 

텍스트를 이렇게 단어 단위로 쪼개 놓으면 여러가지 분석이 가능합니다. 

대표적으로 같은 단어끼리 다른 칸에 분류하여 단어의 빈도를 카운트하는 것이 있는데, 이 방법을 'Bag-of-Words'라고 합니다. keras의 Tokenizer() 함수의 word_counts를 이용해 단어의 빈도 수를 알아낼 수 있습니다. 이때 word_counts의 반환값은 순서를 기억하는 OrderedDict 클래스에 담겨 있는 형태로 출력됩니다. 즉 딕셔너리 자료형입니다.

word_docs() 함수를 이용하면 각 단어가 몇 개의 문장에 등장하는 지를 셀 수 있습니다.

word_index() 함수는 각 단어에 매겨진 인덱스 값을 알려줍니다.

document_count() 함수를 이용하면 문장의 개수도 셀 수 있습니다.

from tensorflow.keras.preprocessing.text import Tokenizer

docs = ['먼저 텍스트의 각 단어를 나누어 토큰화합니다.',
        '텍스트의 단어로 토큰화해야 딥러닝에서 인식됩니다.',
        '토큰화한 결과는 딥러닝에서 사용할 수 있습니다.',
        ]

token = Tokenizer()
token.fit_on_texts(docs) # 토큰화 함수에 문장 적용 -> list로 만들음
print(token.word_counts) # 단어의 빈도 수 
print(token.word_docs) # 각 단어가 몇 개의 문장에 나오는지
print(token.word_index) # 각 단어에 매겨진 인덱스 값
print(token.document_count) # 총 몇 개의 문장이 들어있는지

>>> OrderedDict([('먼저', 1), ('텍스트의', 2), ('각', 1), ('단어를', 1), ('나누어', 1), ('토큰화합니다', 1), ('단어로', 1), ('토큰화해야', 1), ('딥러닝에서', 2), ('인식됩니다', 1), ('토큰화한', 1), ('결과는', 1), ('사용할', 1), ('수', 1), ('있습니다', 1)])
>>> {'단어를': 1, '텍스트의': 2, '각': 1, '나누어': 1, '토큰화합니다': 1, '먼저': 1, '단어로': 1, '인식됩니다': 1, '딥러닝에서': 2, '토큰화해야': 1, '수': 1, '있습니다': 1, '사용할': 1, '결과는': 1, '토큰화한': 1})
>>> {'텍스트의': 1, '딥러닝에서': 2, '먼저': 3, '각': 4, '단어를': 5, '나누어': 6, '토큰화합니다': 7, '단어로': 8, '토큰화해야': 9, '인식됩니다': 10, '토큰화한': 11, '결과는': 12, '사용할': 13, '수': 14, '있습니다': 15}
>>> 3

 

2) 단어의 원-핫 인코딩

앞서서 토큰화를 통해 문장을 잘게 쪼개보았습니다. 이제부터 토큰화한 단어가 다른 요소들과 어떤 관계를 가지는지 알아보기 위한 방법인 원-핫 인코딩(one-hot encoding)을 진행해보겠습니다.

 

다중분류의 class를 이진화 하기 위해 사용한 것과 같은 원-핫 인코딩이지만 이번에는 그걸 단어의 배열에 적용합니다. 

 

각 단어를 모두 0으로 바꾸고 원하는 단어만 1로 바꿔주는 것입니다.

우선 단어의 수만큼 0으로 채워진 벡터를 준비합니다.

그리고 각 단어가 배열에서 해당하는 위치를 1로 바궈서 벡터화합니다.

 

이 과정을 keras에서 실습해보겠습니다.

토큰화 함수로 텍스트를 토큰화하고 각 단어의 인덱스 값을 확인합니다. 그리고 각 단어를 원-핫 인코딩 방식으로 표현합니다.

from tensorflow.keras.preprocessing.text import Tokenizer

text = "오랫동안 꿈꾸는 이는 그 꿈을 닮아간다"

token = Tokenizer()
token.fit_on_texts([text]) # 각 단어 토큰화 
print(token.word_index)
>>> {'오랫동안': 1, '꿈꾸는': 2, '이는': 3, '그': 4, '꿈을': 5, '닮아간다': 6}

x = token.texts_to_sequences([text]) # text의 토큰 index로 채워진 배열을 x에 저장
print(x)
>>> [[1, 2, 3, 4, 5, 6]]

to_categorical() 함수를 이용하여 1~6의 인덱스를 0과 1로만 이루어진 배열로 바꾸는 원-핫 인코딩을 합니다.

from tensorflow.keras.utils import to_categorical

# 인덱스 수에 하나를 추가해서 원-핫 인코딩 배열 만들기(배열 맨 앞에 0 추가되므로)
word_size = len(token.word_index) + 1
x = to_categorical(x, num_classes=word_size)

print(x)
>>> [[[0. 1. 0. 0. 0. 0. 0.]
  [0. 0. 1. 0. 0. 0. 0.]
  [0. 0. 0. 1. 0. 0. 0.]
  [0. 0. 0. 0. 1. 0. 0.]
  [0. 0. 0. 0. 0. 1. 0.]
  [0. 0. 0. 0. 0. 0. 1.]]]

이런 과정을 통해 단어들을 벡터화 했습니다.

 

3) 단어 임베딩

원-핫 인코딩의 결과 단어들을 벡터화할 수 있었습니다. 그러나 아쉬운 점은 벡터의 길이가 너무 길다는 점입니다. 이를 해결하기 위하여 단어 임베딩(word embedding)을 사용합니다.

 

단어 임베딩은 단어 간의 유사도를 계산하여 주어진 배열을 정해진 길이로 압축해줍니다.

 

이때 단어 간의 유사도를 계산하는 방법은 무엇일까요? 적절한 크기의 배열로 압축하기 위해서 optimal한 유사도를 구하는 과정에서 오차 역전파가 사용됩니다. keras에서는 Embedding() 함수를 제공해서 간편하게 사용할 수 있습니다.

from keras.layers import Embedding
from keras.models import Sequential

model = Sequential()
model.add(Embedding(16,4))
# model.add(Embedding(16,4,input_lenth=2))

Embedding() 함수는 '입력의 크기'와 '출력의 크기'라는 최소 2개의 매개변수가 필요합니다. 위의 코드에서는 총 16개의 단어가 입력되고 임베딩이 끝난 후 크기 4의 벡터를 출력하겠다는 의미입니다. 여기에 input_length라는 파라미터를 사용하면 단어를 매번 얼마나 입력할지도 지정할 수 있습니다. Embedding(16,4,input_length=2)는 매번 2개씩의 단어만 넣는다는 의미입니다.

 

4) 텍스트를 읽고 긍정, 부정 예측하기

그럼 위에서 배운 것들을 바탕으로 텍스트를 읽고 긍정 또는 부정의 감정을 예측하는 모델을 만들어보겠습니다.

 

docs 변수에 리뷰 데이터를 넣어줍니다. 그리고 classes 변수에 docs의 리뷰가 긍정인지, 부정인지 라벨링을 해줍니다. 그리고 docs를 토큰화해주고 토큰에 지정된 인덱스로 새로운 배열을 만들어줍니다.

import numpy
import tensorflow as tf
from numpy import array
from tensorflow.keras.preprocessing.text  import Tokenizer
from tensorflow.keras.preprocessing.sequence  import pad_sequences
from tensorflow.keras.models  import Sequential
from tensorflow.keras.layers import Dense, Flatten, Embedding

# 텍스트 리뷰 자료 지정
docs = ['너무 재밌네요', '최고에요', '참 잘 만든 영화에요', '추천하고 싶은 영화입니다.','한 번 더 보고싶네요', '글쎄요','별로에요','생각보다 지루하네요','연기가 어색해요','재미없어요']

# 긍정 리뷰: 1, 부정 리뷰: 0으로 클래스 지정
classes = array([1,1,1,1,1,0,0,0,0,0])

# 토큰화
token = Tokenizer()
token.fit_on_texts(docs)
print(token.word_index)
>>> {'너무': 1, '재밌네요': 2, '최고에요': 3, '참': 4, '잘': 5, '만든': 6, '영화에요': 7, '추천하고': 8, '싶은': 9, '영화입니다': 10, '한': 11, '번': 12, '더': 13, '보고싶네요': 14, '글쎄요': 15, '별로에요': 16, '생각보다': 17, '지루하네요': 18, '연기가': 19, '어색해요': 20, '재미없어요': 21}

x = token.texts_to_sequences(docs)
print("\n리뷰 텍스트, 토큰화 결과: \n",x)
>>> 리뷰 텍스트, 토큰화 결과: 
 [[1, 2], [3], [4, 5, 6, 7], [8, 9, 10], [11, 12, 13, 14], [15], [16], [17, 18], [19, 20], [21]]

 

패딩(padding)

출력값을 보니 단어가 잘 토큰화 되었습니다. 그런데 각 문장 마다 토큰 수가 다릅니다. 딥러닝 모델에 들어가는 학습 데이터는 길이가 동일해야 하기 때문에 문장 마다 길이를 맞추어주는 작업이 필요합니다. 그런 작업을 패딩이라고 합니다.

keras의 pad_sequence() 함수를 통해 패딩을 할 수 있습니다. 지정한 길이보다 짧은 문장의 경우는 앞에서부터 빈자리를 0으로 채웁니다.

# 패딩. 서로 다른 길이의 데이터를 4로 맞춤
padded_x = pad_sequences(x,4)
"\n패딩 결과\n", print(padded_x)
>>> [[ 0  0  1  2]
 [ 0  0  0  3]
 [ 4  5  6  7]
 [ 0  8  9 10]
 [11 12 13 14]
 [ 0  0  0 15]
 [ 0  0  0 16]
 [ 0  0 17 18]
 [ 0  0 19 20]
 [ 0  0  0 21]]

 

토큰화와 패딩까지 마쳤으니 이제 딥러닝 모델을 만들 차례입니다. 

앞서서는 to_categorical() 함수를 이용해서 원-핫 벡터를 만들고 embedding을 했지만 사실 이 둘을 같이 쓸 필요는 없습니다.

출처: https://wikidocs.net/33520

to_categorical() 함수는 sparse vector를 만드는 방법이지만 embedding은 dense vector를 만드는 방법입니다. 우리는 dense vector를 만들면 되기 때문에 embedding을 이용하겠습니다.

 

앞서서 임베딩 함수에 사용되는 파라미터 3가지를 소개했습니다. 바로 입력, 출력, 단어 수입니다. 입력은 총 몇 개의 단어 집합을 사용하는지를, 출력은 총 몇 개의 임베딩 결과를 사용하는지, 단어 수는 매번 입력될 단어 수를 의미합니다.

 

그리고 최적화 함수로 adam()을, 오차 함수로 binary_crossentropy()를 사용하여 모델을 돌리면 됩니다.

 

결과는 이렇습니다. 결과적으로 90%의 정확도를 얻었습니다.

반응형