NLP

NLP 실습 텍스트 분류(Conv1d CNN, LSTM) -03

순환신경망 분류 모델

  • 앞선 모델들과 달리 이미 주어진 단어 특징 벡터를 활용해 모델을 학습하지 않고 텍스트 정보를 입력해서 문장에 대한 특징 정보를 추출한다.

  • RNN은 현재 정보는 이전 정보가 점층적으로 쌓이면서 정보를 표현할 수 있는 모델이다. 따라서 시간에 의존적인 또는 순차적인 데이터에 대한 문제에 활용된다. 이 모델은 한단에 대한 정보를 입력하면 이 단어 다음에 나올 단어를 맞추는 모델이라 순차적인 데이터에 대한 모델링이 가능한 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
DATA_IN_PATH = '/content/'
DATA_OUT_PATH = '/content/'

INPUT_TRAIN_DATA_FILE_NAME = 'train_input.npy'
LABEL_TRAIN_DATA_FILE_NAME = 'train_label.npy'
DATA_CONFIGS_FILE_NAME = 'data_configs.json'

train_input = np.load(open(DATA_IN_PATH + INPUT_TRAIN_DATA_FILE_NAME, 'rb'))
train_label = np.load(open(DATA_IN_PATH + LABEL_TRAIN_DATA_FILE_NAME, 'rb'))

prepro_configs = None

with open(DATA_IN_PATH + DATA_CONFIGS_FILE_NAME, 'r') as f:
prepro_configs = json.load(f)

학습과 검증 데이터셋 분리

1
2
3
4
5
from sklearn.model_selection import train_test_split
TEST_SPLIT=0.1
RANDOM_SEED=13371447

input_train, input_eval, label_train, label_eval = train_test_split(train_input, train_label, test_size=TEST_SPLIT, random_state=RANDOM_SEED)

데이터 입력 함수

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
import tensorflow as tf

BATCH_SIZE = 16
NUM_EPOCHS = 20

def mapping_fn(X, Y):
inputs, labels = {'x' : X}, Y
return inputs, labels

def train_input_fn():
dataset = tf.data.Dataset.from_tensor_slices((input_train, label_train))
dataset = dataset.shuffle(buffer_size=50000)
dataset = dataset.batch(BATCH_SIZE)
dataset = dataset.repeat(count=NUM_EPOCHS)
dataset = dataset.map(mapping_fn)
iterator = dataset.make_one_shot_iterator()

return iterator.get_next()

def eval_input_fn():
dataset = tf.data.Dataset.from_tensor_slices((input_eval, label_eval))
dataset = dataset.map(mapping_fn)
dataset = dataset.batch(BATCH_SIZE)
iterator = dataset.make_one_shot_iterator()

return iterator.get_next()

모델 함수

모델 하이퍼파라미터 정의
1
2
3
4
5
6
7
8
9
VOCAB_SIZE = prepro_configs['vocab_size']

WORD_EMBEDDING_DIM = 100

HIDDEN_STATE_DIM = 150

DENSE_FEATURE_DIM = 150

learning_rate = 0.001
모델 구현
  • 먼저 모델에서 배치 데이터를 받게 된다면 단어 인덱스로 구성된 Sequence 형태로 입력이 들어온다. 데이터 입력 함수에서 정의했듯이 모델 함수의 입력 인자인 features는 Python dictionary 형태로 구성돼 있다.

  • 모델에 들어온 입력 데이터는 보통 Embedding Layer를 거친다. 구현하고자 하는 모델에서는 tf.keras.Embedding함수가 이 같은 역할을 수행한다.

  • Embedding Layer를 거쳐 나온 데이터는 순환 신경망 층을 거쳐 문자의 벡터를 출력한다. 여기서는 간단한 심층 순환 신경망 모델로 LSTM 모델을 통해 구현한다. 순환 신경망을 구현하기 위해서는 RNNCell이란 객체를 활용함ㄴ다. RNNCell은 순환 신경망 객체라 보면된다. LSTM으로 순환 신경망을 구현하기 위해 tf.nn.rnn_cell.LSTMCell객체를 생성하며, 이 객체는 하나의 LSTM Cell을 의미한다. 따라서 해당 Cell 객체를 여러개 생성해서 하나의 리스트로 만들어 준다. LSTMCell을 생성할 때는 은닉 상태 벡터(Hidden state vector)에 대한 차원만 정의하면 된다.

  • 여러 LSTMCell을 쌀게 되면 이를 하나의 MultiRNN으로 묶어야, 즉 wrapping해야한다. tf.nn.rnn_cell.MultiRNNCell을 생성함으로써 Stack 구조의 LSTM 신경망을 구현할 수 있다. 단순히 RNNCell 만으로 구성해 모델 연산 그래프를 만들 수 있다. RNNCell 객체는 Sequence 한 스텝에 대한 연산만 가능하다. 따라서 여러 스텝에 대한 연산을 하기 위해서는 for 문을 활용해 연산을 할 수 있게 구현해야한다. 하지만 이보다 더 간단하게 구현할 수 있는 방법은 tf.nn.dynamic_rnn 함수를 사용하는 것이다. 이 함수는 for 문 없이 자동으로 순환 신경망을 만들어 주는 역할을 한다.

  • dynamic_rnn 함수에 필요한 입력 인자는 2개다. 첫 번째 순환 신경망 객체인 MultiRNNCell 객체이고, 나머지 하나는 입력값을 넣어주면된다.

  • Dense에 적용시키는 입력값은 LSTM 신경망의 마지막 출력값을 넣어준다. 출력값에 [:, -1, :]로 마지막 값만 뽑아낸 후 Dense에 적용시킨다.

  • 마지막으로 감정이 긍정인지 부정인지 판단할 수 있도록 출력값을 하나로 만들어야 한다. 보통 선형변환을 통해 입력 벡터에 대한 차원수를 바꾼다.

모델 학습, 검정 및 테스트를 위한 구현

  • 앞서 모델에서 구현한 값과 정답 label을 가지고 loss 값을 구해 Adam optimizer를 활용해 모델 parameter를 최적화 해 볼 것이다.

  • 모델 예측 loss값은 모델에서 구한 logits 변수의 경우 아직 Logistic 함수를 통해 0~1 사이의 값으로 스케일을 맞춰두지 않았다. 물론 앞서 dense 층에서 activation 인자를 tf.nn.sigmoid로 설정해둘 수 있다. 하지만 여기서는 tf.losses.sigmoid_cross_entropy 함수를 활용해 손실값을 구할 수 있기 때문에 dense 층에서 설정하지 않았다.

  • 예측 loss값을 구하고 나면 이제 parameter optimization을 하고자 SGD를 진행한다. 여기서는 tf.train.AdamOptimizer클래스를 활용할 것이다. tf.train.AdamOptimizer.minimize 함수를 선언 할 때 전체 학습에 대한 global step값을 넣어야 한다. tf.train.get_global_step을 선언하면 현재 학습 global step을 얻을 수 있다.

  • 보통 직접 모델 함수를 구현하게 되면 tf.estimator.EstimatorSpec 객체를 생성해서 반환하게 한다. 이 객체는 현재 함수가 어느 모드에서 실행되고 있는지 확인한다. 그리고 각 모드에 따라 필요한 입력 인자가 다르다.

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
31
32
33
34
35
36
37
38
39
40
41
42
43
def model_fn(features, labels, mode):
TRAIN = mode == tf.estimator.ModeKeys.TRAIN
EVAL = mode == tf.estimator.ModeKeys.EVAL
PREDICT = mode == tf.estimator.ModeKeys.PREDICT

embedding_layer = tf.keras.layers.Embedding(VOCAB_SIZE, WORD_EMBEDDING_DIM)(features['x'])

embedding_layer = tf.keras.layers.Dropout(0.2)(embedding_layer)

rnn_layers = [tf.nn.rnn_cell.LSTMCell(size) for size in [HIDDEN_STATE_DIM, HIDDEN_STATE_DIM]]

multi_rnn_cell = tf.nn.rnn_cell.MultiRNNCell(rnn_layers)

outputs, state = tf.nn.dynamic_rnn(cell=multi_rnn_cell, inputs=embedding_layer, dtype=tf.float32)

outputs = tf.keras.layers.Dropout(0.2)(outputs)

hidden_layer = tf.keras.layers.Dense(DENSE_FEATURE_DIM, activation=tf.nn.tanh)(outputs[:, -1, :])
hidden_layer = tf.keras.layers.Dropout(0.2)(hidden_layer)

logits = tf.keras.layers.Dense(1)(hidden_layer)
logits = tf.squeeze(logits, axis=-1)

sigmoid_logits = tf.nn.sigmoid(logits)

if PREDICT:
predictions = {'sentiment': sigmoid_logits}

return tf.estimator.EstimatorSpec(mode=mode, predictions=predictions)

loss = tf.losses.sigmoid_cross_entropy(labels, logits)

if EVAL:
accuracy = tf.metrics.accuracy(labels, tf.round(sigmoid_logits))
eval_metric_ops = {'acc':accuracy}

return tf.estimator.EstimatorSpec(mode, loss=loss, eval_metric_ops=eval_metric_ops)

if TRAIN:
global_step = tf.train.get_global_step()
train_op = tf.train.AdamOptimizer(learning_rate).minimize(loss, global_step)

return tf.estimator.EstimatorSpec(mode=mode, train_op=train_op, loss=loss)

TF Estimator 활용한 모델 학습 및 성능 검증

1
2
3
4
5
6
7
8
9
10
DATA_OUT_PATH = '/content/'

if not os.path.exists(DATA_OUT_PATH):
os.makedirs(DATA_OUT_PATH)

est = tf.estimator.Estimator(model_fn, model_dir=DATA_OUT_PATH + 'checkpoint')

os.environ["CUDA_VISIBLE_DEVICES"]="4"

est.train(train_input_fn)
  • validation data에 대한 성능이 약 85%정도였다. 오히려 앞의 머신러닝 기법들 중 어떤 기법보다는 성능이 떨어진다는 것을 볼 수 있었지만, test data에 대한 성능을 한번 체크해 보아야 할 것 같다.
1
2
3
4
est.evaluate(eval_input_fn)

# 결과
# {'acc': 0.8472, 'global_step': 18291, 'loss': 0.6007853}

데이터 제출

1
2
3
4
DATA_OUT_PATH = '/content/'
TEST_INPUT_DATA = 'test_input.npy'

test_input_data = np.load(open(DATA_IN_PATH + TEST_INPUT_DATA, 'rb'))
  • estimator를 통해 예측하기 위해서는 데이터 입력 함수를 정의해야 했다. 이 경우는 tf.estimator.inputs.numpy_input_fn 함수를 활용해 데이터 입력 함수를 생성한다.
1
2
3
predict_input_fn = tf.estimator.inputs.numpy_input_fn(x={"x": test_input_data}, shuffle=False)

predictions = np.array([p['sentiment'] for p in est.predict(input_fn=predict_input_fn)])
1
2
3
4
TEST_ID_DATA = 'test_id.npy'
test_id = np.load(open(DATA_IN_PATH + TEST_ID_DATA, 'rb'), allow_pickle='True')
output = pd.DataFrame({'id': test_id, 'sentiment': list(predictions)})
output.to_csv(DATA_OUT_PATH + "rnn_predic.csv", index=False, quoting=3)
1
!kaggle competitions submit word2vec-nlp-tutorial -f "rnn_predic.csv" -m "LSTM Model with Epoch 10"

LSTM의 성능

CNN을 이용한 문장 분류

  • CNN은 보통 image에서 많이 사용된다고 생각들지만, 텍스트에서도 좋은 효과를 낼 수 있다는 점을 Yoon Kimm(2014) 박사가 쓴 “Convolutional Neural Network for Sentence Classification”을 통해 입증되었다. RNN이 단어의 입력 순서를 중요하게 반영한다면 CNN은 문장의 지역정보를 보존하면서 각 문장 성분의 등장 정보를 학습에 반영하는 구조로 풀어가고 있다. 학습할 때 각 필터 크기를 조절하면서 언어의 특징 값을 추출하게 되는데, 기존의 n-gram(2그램, 3그램) 방식과 유사하다고 볼 수 있다.

합성곱 신경망

2-D 합성곱 신경망

1-D 합성곱 신경망

CNN을 이용한 문장 분류

모델 구현

1
2
3
4
5
6
7
8
9
# 기본적인 라이브러리들을 불러온다
import sys
import os
import numpy as np
import json
from sklearn.model_selection import train_test_split

import tensorflow as tf
from tensorflow import keras
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 이전에 저장했던 학습에 필요한 디렉터리 설정 및 학습/평가 데이터를 불러온다.

DATA_IN_PATH = '/content/'
DATA_OUT_PATH = '/content/'
INPUT_TRAIN_DATA_FILE_NAME = 'train_input.npy'
LABEL_TRAIN_DATA_FILE_NAME = 'train_label.npy'
INPUT_TEST_DATA_FILE_NAME = 'test_input.npy'

DATA_CONFIGS_FILE_NAME = 'data_configs.json'

train_input_data = np.load(open(DATA_IN_PATH + INPUT_TRAIN_DATA_FILE_NAME, 'rb'))
train_label_data = np.load(open(DATA_IN_PATH + LABEL_TRAIN_DATA_FILE_NAME, 'rb'))
test_input_data = np.load(open(DATA_IN_PATH + INPUT_TEST_DATA_FILE_NAME, 'rb'))

with open(DATA_IN_PATH + DATA_CONFIGS_FILE_NAME, 'r') as f:
prepro_configs = json.load(f)
print(prepro_configs.keys())

학습과 검증 데이터셋 분리

1
2
3
4
5
6
7
8
9
10
# 파라미터 변수
RNG_SEED = 1234
BATCH_SIZE = 16
NUM_EPOCHS = 10
VOCAB_SIZE = prepro_configs['vocab_size']
EMB_SIZE = 128
VALID_SPLIT = 0.2

# 학습 데이터와 검증 데이터를 train_test_split 함수를 활용해 나눈다.
train_input, eval_input, train_label, eval_label = train_test_split(train_input_data, train_label_data, test_size=VALID_SPLIT, random_state=RNG_SEED)

데이터 입력 함수

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
# 전처리 학습을 위해 tf.data를 설정한다.
def mapping_fn(X, Y=None):
input, label = {'x': X}, Y
return input, label

def train_input_fn():
dataset = tf.data.Dataset.from_tensor_slices((train_input, train_label))
dataset = dataset.shuffle(buffer_size=len(train_input))
dataset = dataset.batch(BATCH_SIZE)
dataset = dataset.map(mapping_fn)
dataset = dataset.repeat(count=NUM_EPOCHS)

iterator = dataset.make_one_shot_iterator()

return iterator.get_next()


def eval_input_fn():
dataset = tf.data.Dataset.from_tensor_slices((eval_input, eval_label))
dataset = dataset.shuffle(buffer_size=len(eval_input))
dataset = dataset.batch(BATCH_SIZE)
dataset = dataset.map(mapping_fn)

iterator = dataset.make_one_shot_iterator()

return iterator.get_next()

모델 구현

  • 합성곱 연산의 경우 케라스 모듈 중 Conv1D를 활용해 진행한다. 총 3개의 합성곱 층을 사용하는데, 각각 필터의 크기를 다르게 해서 적용한다. 즉, kernel_size를 3,4,5로 설정할 것이다. 그리고 이렇게 각각 다른 필터의 크기로 적용한 합성곱 층 출력값을 하나로 합칠 것이다. 그리고 추가로 각 합성곱 신경망 이후에 max pooling 층을 적용한다.
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
def model_fn(features, labels, mode):

TRAIN = mode == tf.estimator.ModeKeys.TRAIN
EVAL = mode == tf.estimator.ModeKeys.EVAL
PREDICT = mode == tf.estimator.ModeKeys.PREDICT

# embedding layer를 선언
embedding_layer = keras.layers.Embedding(VOCAB_SIZE, EMB_SIZE)(features['x'])

# embedding layer에 대한 output에 대해 dropout을 취한다.
dropout_emb = keras.layers.Dropout(0.5)(embedding_layer)

## filters = 128이고 kernel_size = 3,4,5이다.
## 길이기ㅏ 3, 4, 5인 128개의 다른 필터를 생성한다. 3, 4, 5 gram의 효과처럼 다양한 각도에서 문장을 보는 효과가 있다.
## conv1d는 (배치 크기, 길이, 채널)로 입력값을 받는데, 배치 사이즈 : 문장 숫자 | 길이 : 각 문장의 단어의 개수 | 채널 : 임베딩 출력 차원수

conv1 = keras.layers.Conv1D(filters=128, kernel_size=3, padding='valid', activation=tf.nn.relu)(dropout_emb)
pool1 = keras.layers.GlobalMaxPool1D()(conv1)

conv2 = keras.layers.Conv1D(filters=128, kernel_size=4, padding='valid', activation=tf.nn.relu)(dropout_emb)
pool2 = keras.layers.GlobalMaxPool1D()(conv2)

conv3 = keras.layers.Conv1D(filters=128, kernel_size=5, padding='valid', activation=tf.nn.relu)(dropout_emb)
pool3 = keras.layers.GlobalMaxPool1D()(conv3)

# 3,4,5 gram이후 모아주기
concat = keras.layers.concatenate([pool1, pool2, pool3])

hidden = keras.layers.Dense(250, activation=tf.nn.relu)(concat)
dropout_hidden = keras.layers.Dropout(0.5)(hidden)
logits = keras.layers.Dense(1, name='logits')(dropout_hidden)
logits = tf.squeeze(logits, axis=-1)

# 최종적으로 학습, 검증, 평가의 단계로 나누어 활용
if PREDICT:
return tf.estimator.EstimatorSpec(mode=mode, predictions={'prob': tf.nn.sigmoid(logits)})

loss = tf.losses.sigmoid_cross_entropy(labels, logits)

if EVAL:
pred = tf.nn.sigmoid(logits)
accuracy = tf.metrics.accuracy(labels, tf.round(pred))
return tf.estimator.EstimatorSpec(mode=mode, loss=loss, eval_metric_ops={'acc':accuracy})

if TRAIN:
global_step = tf.train.get_global_step()
train_op = tf.train.AdamOptimizer(0.001).minimize(loss, global_step)

return tf.estimator.EstimatorSpec(mode=mode, train_op=train_op, loss=loss)

모델 학습

1
2
3
4
5
6
7
8
9
model_dir = os.path.join(os.getcwd(), "data_out/checkpoint/cnn")
os.makedirs(model_dir, exist_ok=True)

config_tf = tf.estimator.RunConfig(save_checkpoints_steps=200, keep_checkpoint_max=2,
log_step_count_steps=400)

# Estimator 객체 생성
cnn_est = tf.estimator.Estimator(model_fn, model_dir=model_dir)
cnn_est.train(train_input_fn)

검증 데이터 평가

  • 검증 데이터에 대한 정확도가 약 88%정도로 측정되었다. 지금껏 간단한 모델들 중 제일 높은 성능을 보이고 있어 필자는 약간 기대하고 있었다. 이에따른 test data의 성능을 알아보기 위해 캐글에 test data의 예측값을 제출해 볼 것이다.
1
2
3
4
cnn_est.evaluate(eval_input_fn)

# 결과
# {'acc': 0.8774, 'global_step': 94200, 'loss': 1.3248637}
1
2
3
4
5
6
7
DATA_IN_PATH = '/content/'
DATA_OUT_PATH = '/content/'
TEST_INPUT_DATA = 'test_input.npy'
TEST_ID_DATA = 'test_id.npy'

test_input_data = np.load(open(DATA_IN_PATH + TEST_INPUT_DATA, 'rb'))
ids = np.load(open(DATA_IN_PATH + TEST_ID_DATA, 'rb'), allow_pickle=True)
1
2
3
4
5
6
7
predict_input_fn = tf.estimator.inputs.numpy_input_fn(x={'x':test_input_data}, shuffle=False)

predictions = np.array([p['prob'] for p in cnn_est.predict(input_fn=predict_input_fn)])

output = pd.DataFrame({"id": list(ids), "sentiment":list(predictions)})

output.to_csv(DATA_OUT_PATH + "Bag_of_Words_model_test.csv", index=False, quoting=3)
1
!kaggle competitions submit word2vec-nlp-tutorial -f "Bag_of_Words_model_test.csv" -m "CNN 1d Model with EPOCHS 10"

3,4,5-gram 을 활용한 CNN의 성능