본문 바로가기
Deep Learning/Hands On Machine Learning

16.2 감성 분석

by 대소기 2021. 11. 23.

IMDB 데이터를 통한 감성분석

 

데이터셋 로드

(X_train, y_train), (X_test, y_test) = keras.datasets.imdb.load_data()
X_train[0][:10]

#[1, 14, 22, 16, 43, 530, 973, 1622, 1385, 65]

* IMDB의 training set은 문장의 구두점을 제거하고 소문자로 변환하여 빈도에 따라 인덱스가 부여되어있다.

* 인덱스가 낮을 수록 자주 등장하는 단어이다.

* 또한 0은 <pad>, 1은 <sos>, 2는 <unk> 즉, 알 수 없는 단어를 의미한다. 만약 데이터셋을 문장으로 디코딩하고 싶다면 아래와 같이 디코딩해야 한다.

 

#인덱스를 단어로 디코딩하는 법
word_index=keras.datasets.imdb.get_word_index()
#id 값마다 3을 더해서 0,1,2 자리를 만들었음
id_to_word = {id_ + 3: word for word, id_ in word_index.items()}
for id_, token in enumerate(("<pad>", "<sos>", "<unk>")):
    id_to_word[id_] = token #딕셔너리에 key, value 추가
print(sorted(id_to_word.items()))
" ".join([id_to_word[id_] for id_ in X_train[0][:10]]) #단어들을 string으로 조인

* imdb 데이터는 미리 단어들이 토큰화 되어 있지만, 우리가 실제 프로젝트를 진행할 때는 토큰화에 대한 작업을 직접 해줘야 한다. 토큰화에 대한 종류는 아래와 같다.

 

1) 공백을 기준으로 단어를 구분, 토큰화 : 이 방법은 모든 언어에 적용되지 않는다는 단점이 존재한다. 영어의 경우 공백을 통한 구분이 가능하지만, 베트남어의 경우 단어 사이에도 공백이 있다.

2) Google SentencePiece project : 공백도 하나의 문자로 취급하고 이전에 본 적 없는 단어도 접미사 등을 구분해 뜻 유추 가능.

3) Byte pair encoding

4) TF.Text 라이브러리 : 바이트 페어 인코딩의 변종인 워드피스를 포함하는 다양한 토큰화 전략 구현

 

전처리 모델에 포함시키기

* 모바일 기기나 웹 브라우저 등에 배포할 때 매번 다른 형식으로 전처리함수를 작성하는 것은 비효율적이다. 이를 방지하기 위해 텐서플로 연산만을 사용해 전처리 자체를 모델에 포함시킬 수 있다. 이를 위해서 먼저 데이터셋을 byte string으로 적재해야 한다.

* byte string으로 적재하기 위해 tensorflow datasets API를 활용한다.

import tensorflow_datasets as tfds

datasets, info = tfds.load('imdb_reviews', as_supervised=True, with_info=True)
train_size=info.splits['train'].num_examples

* 그 다음으로 전처리 함수를 작성해보겠다.

def preprocess(X_batch, y_batch):
  X_batch = tf.strings.substr(X_batch, 0, 300) #
  X_batch = tf.strings.regex_replace(X_batch, b"<br\\s*/?>", b" ") #<br> tag 공백으로
  X_batch = tf.strings.regex_replace(X_batch, b"[^a-zA-Z]", b" ") #문자와 작은 따옴표 제외 공백으로
  X_batch = tf.strings.split(X_batch)
  return X_batch.to_tensor(default_value=b"<pad>"), y_batch

* split()을 통해 공백을 기준으로 나눠진 텐서들은 래그드 텐서 형태이므로, 밀집 텐서 형태로 바꾸기 위해 default value를 <pad>로 주어서 동일한 길이로 맞춰준다.

 

어휘사전 구축

* Counter 객체를 할당해서 어휘사전을 생성한다.

from collections import Counter
vocabulary = Counter()
for X_batch, y_batch in datasets['train'].batch(32).map(preprocess):
  for review in X_batch:
    vocabulary.update(list(review.numpy()))

 

 

* 어휘 사전 중에 가장 많이 사용되는 10000개의 단어만 남기도록 한다.

vocab_size=10000
truncated_vocabulary = [
  word for word, count in vocabulary.most_common()[:vocab_size]]

* 1000개의 oov bucket을 사용하는 룩업테이블 생성

words=tf.constant(truncated_vocabulary)
word_ids=tf.range(len(truncated_vocabulary), dtype=tf.int64)
vocab_init=tf.lookup.KeyValueTensorInitializer(words, word_ids)
num_oov_buckets=1000
table=tf.lookup.StaticVocabularyTable(vocab_init, num_oov_buckets)

 

* 최종 훈련 세트 생성

def encode_words(X_batch, y_batch):
  return table.lookup(X_batch), y_batch

train_set=datasets['train'].batch(32).map(preprocess)
train_set=train_set.map(encode_words).prefetch(1)

 

* 모델 훈련

embed_size = 128
model = keras.models.Sequential([
    keras.layers.Embedding(vocab_size + num_oov_buckets, embed_size,
                           mask_zero=True, # not shown in the book
                           input_shape=[None]),
    keras.layers.GRU(128, return_sequences=True),
    keras.layers.GRU(128),
    keras.layers.Dense(1, activation="sigmoid")
])
model.compile(loss="binary_crossentropy", optimizer="adam", metrics=["accuracy"])
history = model.fit(train_set, epochs=5)

# Epoch 1/5
# 782/782 [==============================] - 18s 16ms/step - loss: 0.5305 - accuracy: 0.7281
# Epoch 2/5
# 782/782 [==============================] - 12s 16ms/step - loss: 0.3459 - accuracy: 0.8549
# Epoch 3/5
# 782/782 [==============================] - 12s 16ms/step - loss: 0.1934 - accuracy: 0.9313
# Epoch 4/5
# 782/782 [==============================] - 12s 16ms/step - loss: 0.1361 - accuracy: 0.9503
# Epoch 5/5
# 782/782 [==============================] - 12s 16ms/step - loss: 0.1032 - accuracy: 0.9634

마스킹

* 모델이 훈련할 때는 우리가 추가해 놓은 zero padding을 무시한 채로 실제 데이터에만 집중해야 한다. 이를 위해서는  Embedding layer의 masking_zero 매개변수를 True로 지정해줘야 한다. 이 과정은 '마스킹'을 통해 이뤄지고 자세한 내용은 아래와 같다.

* Embedding 층은 keras.backend.not_equal(inputs, 0)과 같은 mask tensor를 만든다. 이를 층마다 전파하여 zero padding들을 무시하게 된다.

 

K = keras.backend
embed_size = 128
inputs = keras.layers.Input(shape=[None])
mask = keras.layers.Lambda(lambda inputs: K.not_equal(inputs, 0))(inputs)
z = keras.layers.Embedding(vocab_size + num_oov_buckets, embed_size)(inputs)
z = keras.layers.GRU(128, return_sequences=True)(z, mask=mask)
z = keras.layers.GRU(128)(z, mask=mask)
outputs = keras.layers.Dense(1, activation="sigmoid")(z)
model = keras.models.Model(inputs=[inputs], outputs=[outputs])
model.compile(loss="binary_crossentropy", optimizer="adam", metrics=["accuracy"])
history = model.fit(train_set, epochs=5)

Epoch 1/5
782/782 [==============================] - 18s 15ms/step - loss: 0.5426 - accuracy: 0.7156
Epoch 2/5
782/782 [==============================] - 12s 16ms/step - loss: 0.3469 - accuracy: 0.8572
Epoch 3/5
782/782 [==============================] - 12s 16ms/step - loss: 0.1753 - accuracy: 0.9384
Epoch 4/5
782/782 [==============================] - 12s 16ms/step - loss: 0.1274 - accuracy: 0.9542
Epoch 5/5
782/782 [==============================] - 12s 16ms/step - loss: 0.1131 - accuracy: 0.9577

 

사전 훈련된 임베딩 재사용하기

* tensorflow hub 프로젝트를 사용하여 사전 훈련된 모델 컴포넌트를 모델에 추가해 보겠다. 우리가 사용하는 것은 nnlm-en-dim50 문장 임베딩 모듈 버전 1이다. 이를 통해 감성 분석을 실시해보겠다.

import tensorflow_hub as hub

model=keras.Sequential([
  hub.KerasLayer('https://tfhub.dev/google/tf2-preview/nnlm-en-dim50/1',
    dtype=tf.string, input_shape=[], output_shape=[50]),
  keras.layers.Dense(128, activation='relu'),
  keras.layers.Dense(1, activation='sigmoid')
])
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])

* hub.KerasLayer 층은 주어진 URL에서 문장 인코더 모듈을 다운로드 한다.  기본적으로 KerasLayer는 훈련되지 않지만 trainable=True로 설정하면 훈련 가능하게 조정할 수 있다.

 

import tensorflow_datasets as tfds

datasets, info = tfds.load("imdb_reviews", as_supervised=True, with_info=True)
train_size = info.splits["train"].num_examples
batch_size = 32
train_set = datasets["train"].batch(batch_size).prefetch(1)
history = model.fit(train_set, epochs=5)

#Epoch 1/5
#782/782 [==============================] - 5s 5ms/step - loss: 0.5461 - accuracy: 0.7267
#Epoch 2/5
#782/782 [==============================] - 4s 5ms/step - loss: 0.5130 - accuracy: 0.7495
#Epoch 3/5
#782/782 [==============================] - 4s 5ms/step - loss: 0.5081 - accuracy: 0.7532
#Epoch 4/5
#782/782 [==============================] - 4s 5ms/step - loss: 0.5047 - accuracy: 0.7540
#Epoch 5/5
#782/782 [==============================] - 4s 5ms/step - loss: 0.5018 - accuracy: 0.7566

* 다시 tensorflow dataset API를 통해 데이터를 적재하였다. tensorflow hub의 사전 훈련된 임베딩을 사용하였기 때문에 전처리를 할 필요가 없어졌다.

* TF허브는 기본적으로 다운로드한 파일을 시스템의 임시 디렉토리에 캐싱하게 된다. 이를 고정 디렉토리로 바꾸기 위해서는 TFHUB_CACHE_DIR 환경변수에 원하는 디렉토리를 지정하면 된다(ex- os.environ["TFHUB_CACHE_DIR"] = "./my_tfhub_cache").