NLP

NLP 실습 텍스트 유사도 - 02 (XGBoost, 1D-CNN, MaLSTM)

  • 모델은 총 3가지를 종류를 만들어 볼 것이다.
    • XGBoost
    • CNN
    • MaLSTM

XGBoost

  • 앙상블 모델 중 하나인 XGBoost 모델은 ‘eXtream Gradient Boosting’의 약자로 캐글 사용자에 큰 인기를 얻은 모델 중 하나이다. 앙상블 기법이란 여러 개의 학습 알고즘을 사용해 더 좋은 성능을 얻는 방법을 뜻한다. 앙상블 기법에는 Bagging과 Boosting이라는 방법이 있다.

    • Bagging은 여러 개의 학습 알고리즘, 모델을 통해 각각 결과를 예측하고 모든 결과를 동등하게 보고 취합해서 결과를 얻는 방식이다. Random Forest도 여러개의 decision tree 결과값의 평균을 통해 결과를 얻는 Bagging의 일종이다.

    • Boosting은 여러 알고리즘, 모델의 결과를 순차적으로 취합하는데, 단순히 하나씩 취하는 방법이 아니라 이전 알고리즘, 모델이 학습 후 잘못 예측한 부분에 가중치를 줘서 다시 모델로 가서 학습하는 방식이다.

bagging과 boosting의 차이

  • XGBoost는 Boosting 기법 중 Tree Bossting 기법을 활용한 모델이다. 쉽게 말해 Random Forest와 비슷한 원리에 Boosting 기법을 적용했다고 생각하면된다. 여러개의 Decision Tree를 사용하지만 단순히 결과를 평균내는 것이 아니라 결과를 보고 오답에 대해 가중치를 부여한다. 그리고 가중치가 적용된 오답에 대해서는 관심을 가지고 정답이 될 수 있도록 결과를 만들고 해당 결과에 대한 다른 오답을 찾아 다시 똑같은 작업을 반복적으로 진행하는 것이다.

  • 최종적으로는 XGBoost란 이러한 Tree Boosting 방식에 경사하강법을 통해 optimization을 하는 방법이다. 그리고 연산량을 줄이기 위해 Decision Tree를 구성할 때 병렬 처리를 사용해 빠른 시간에 학습이 가능하다.

1
2
3
4
5
6
7
TRAIN_Q1_DATA_FILE = 'q1_train.npy'
TRAIN_Q2_DATA_FILE = 'q2_train.npy'
TRAIN_LABEL_DATA_FILE = 'label_train.npy'

train_q1_data = np.load(open(DATA_IN_PATH + TRAIN_Q1_DATA_FILE, 'rb'))
train_q2_data = np.load(open(DATA_IN_PATH + TRAIN_Q2_DATA_FILE, 'rb'))
train_labels = np.load(open(DATA_IN_PATH + TRAIN_LABEL_DATA_FILE, 'rb'))
  • numpy의 stack 함수를 사용해 두 질문을 하나의 쌍으로 만들었다. 예를 들어, 질문 [A]와 질문 [B]가 있을 때 이 질문을 하나로 묶어 [[A], [B]] 형태로 만들었다. 이와 같은 형태는 다음과 같이 여러가지 방법으로 구현할 수 있다.
1
2
train_input_expand = np.concatenate((np.expand_dims(train_q1_data, 1), np.expand_dims(train_q2_data, 1)), axis=1)
train_input_expand.shape
결과
1
(298526, 2, 31)
1
2
train_input_concate = np.concatenate((train_q1_data[:,np.newaxis,:], train_q2_data[:,np.newaxis,:]), axis=1)
train_input_concate.shape
결과
1
(298526, 2, 31)
1
2
train_input_stack = np.stack((train_q1_data, train_q2_data), axis=1)
train_input_stack.shape
결과
1
(298526, 2, 31)
1
(train_input_concate ==  train_input_stack).all() and (train_input_stack == train_input_expand).all() and (train_input_concate == train_input_expand).all()
결과
1
True
  • 전체 29만개 정도의 데이터에 대해 두 질문이 각각 31개의 질문 길이를 가지고 있음을 확인 할 수 있다. 두 질문 쌍이 하나로 묶여 있는 것도 확인할 수 있다. 이제 학습 데이터의 20%를 모델 검증을 위한 validation set으로 만들어 둘 것이다.
1
2
3
from sklearn.model_selection import train_test_split

train_input, eval_input, train_label, eval_label = train_test_split(train_input_stack, train_labels, test_size=0.2, random_state=4242)
  • XGBoost를 사용하려면 입력값을 xgb 라이브러리의 데이터 형식인 DMatrix 형태로 만들어야 한다. 학습 데이터와 검증 데이터 모두 적용해서 해당 데이터 형식으로 만든다. 적용 과정에서 각 데이터에 대해 sum 함수를 사용하는데 이는 각 데이터의 두 질문을 하나의 값으로 만들어 주기 위해서이다. 그리고 두 개의 데이터를 묶어서 하나의 리스트로 만든다. 이때 학습 데이터와 검증 데이터는 각 상태의 문자열과 함께 tuple형태로 구성한다.

  • 참고로 XGBoost와 sklearn의 ensemble.GradientBoostingClassifier은 동일하게 Tree Boosting 모델을 가지고 있지만 속도면에서 XGBoost가 훨씬 빠르다. (사용법도 조금 다름)

1
2
3
4
5
6
import xgboost as xgb

train_data = xgb.DMatrix(train_input.sum(axis=1), label=train_label)
eval_data = xgb.DMatrix(eval_input.sum(axis=1), label=eval_label)

data_list = [(train_data, 'train'), (eval_data, 'valid')]
  • 우선 모델을 만들고 학습하기 위해 몇 가지 선택해야 하는 옵션은 dictionary를 만들어 넣으면 된다. 이때 dictionary에는 모델의 objective(loss) function와 평가 지표를 정해서 넣어야 하는데 여기서는 우선 objective(loss) function의 경우 이진 로지스틱 함수를 사용한다. 평가 지표의 경우 RMSE를 사용한다. 이렇게 만든 인자와 학습 데이터, 데이터를 반복하는 횟수인 num_boost_round, 모델 검증 시 사용할 전체 데이터 쌍, 그리고 early stopping을 위한 횟수를 정한다. 데이터를 반복하는 횟수, 즉 Epoch을 의미하는 값으로는 1000을 설정했다. 전체 데이터를 1000번 반복해야 끝나도록 설정한 것이다. 그리고 early stopping을 위한 횟수값으로는 10을 설정해서 만약 10 epoch 동안 error값이 크게 줄지 않는다면 학습을 종료시키도록 하였다.
1
2
3
4
5
params = {}
params['objective'] = 'binary:logistic'
params['eval_metric'] = 'rmse'

bst = xgb.train(params, train_data, num_boost_round = 1000, evals = data_list, early_stopping_rounds=10)

예측하기

1
2
3
4
5
6
7
TEST_Q1_DATA_FILE = 'test_q1.npy'
TEST_Q2_DATA_FILE = 'test_q2.npy'
TEST_ID_DATA_FILE = 'test_id.npy'

test_q1_data = np.load(open(DATA_IN_PATH + TEST_Q1_DATA_FILE, 'rb'))
test_q2_data = np.load(open(DATA_IN_PATH + TEST_Q2_DATA_FILE, 'rb'))
test_id_data = np.load(open(DATA_IN_PATH + TEST_ID_DATA, 'rb'))
1
2
3
test_input = np.stack((test_q1_data, test_q2_data), axis=1)
test_data = xgb.DMatrix(test_input.sum(axis=1))
test_predict = bst.predict(test_data)
1
2
3
4
5
6
7
DATA_OUT_PATH = '/content/'

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

output = pd.DataFrame({'test_id': test_id_data, 'is_duplicate': test_predict})
output.to_csv(DATA_OUT_PATH + 'sample_xgb.csv', index=False)
  • kaggle API를 통해서 바로 파일 올려주었다.
1
!kaggle competitions submit quora-question-pairs -f "sample_xgb.csv" -m "XGBoost Model"
  • 3294팀 중 2714등이다. 물론, 임베딩 벡터라든지 아무런 조치를 취하지 않았기 때문에 score가 안좋을 수 밖에 없다. 추후에 TF-IDF 행렬을 사용하거나 더 좋은 임베딩 기법을 사용해서 다시 올려볼 것이다. 지금은 유사도를 구하는 방법에 대한 기본 튜토리얼이므로 이 정도에서 그치겠다.

결과

CNN 텍스트 유사도 분석 모델

  • 합성곱 신경망 구조를 활용해 텍스트 유사도를 측정하는 모델을 만들어 보겠다. 기본적인 구조는 이전 장의 합성곱 모델과 유사하지만 이번 경우에는 각 데이터가 두 개의 텍스트 문장으로 돼 있기 때문에 병렬적인 구조를 가진 모델을 만들어야 한다.
  • 모델에 입력하고자 하는 데이터는 문장 2개다. 문장에 대한 유사도를 보기 위해서는 기준이 되는 문장이 필요하다. 이를 ‘기준 문장’이라 정의한다. 그리고 ‘기준 문장’에 대해 비교해야 하는 문장이 있는데 이를 ‘대상문장’이라 한다. 만약 모델에 입력하고자 하는 기준 문장이 ‘I love deep NLP’이고 이를 비교할 대상 문장이 ‘Deep NLP is awesome’이라 하자. 이 두 문장은 의미가 상당히 유사하다. 만약 학습이 진행된 후에 두 문장에 대한 유사도를 측정하고하 한다마녀 아마도 높은 유사도 점수를 보일 것이다. 이처럼 문장이 의미적으로 가까우면 유사도 점수는 높게 표현 될 것이고 그렇지 않을 경우에는 낮게 표현될 것이다.
  • 전반적인 유사도 분석 모델 구조에 대한 흐름을 보자. 모델에 데이터를 입력하기 전에 기준 문장과 대상 문장에 대해서 인덱싱을 거쳐 문자열 형태의 문장을 인덱스 벡터 형태로 구성한다. 인덱스 벡터로 구성된 문장 정보는 임베딩 과정을 통해 각 단어들이 임베딩 벡터로 바뀐 행렬로 구성 될 것이다. 임베딩 과정을 통해 나온 문장 행렬은 기준 문장과 대상 문장 각각에 해당하는 CNN 블록을 거치게 한다. CNN 블록은 Convolution 층과 Max Pooling층을 합친 하나의 신경망을 의미한다. 두 블록을 거쳐 나온 벡터는 문장에 대한 의미 벡터가 된다. 두 문장에 대한 의미 벡터를 가지고 여러 방식으로 유사도를 구할 수 있다. 여기서는 FC layer를 거친 후 최종적으로 logistic regression 방법을 통해 문자 유사도 점수를 측정할 것이다. 이렇게 측정한 점수에 따라 두 문장의 유사 여부를 판단할 것이다.

모델 구현 준비

1
2
3
4
5
6
7
import tensorflow as tf
import numpy as np
import os

from sklearn.model_selection import train_test_split

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

TRAIN_Q1_DATA_FILE = 'q1_train.npy'
TRAIN_Q2_DATA_FILE = 'q2_train.npy'
TRAIN_LABEL_DATA_FILE = 'label_train.npy'
DATA_CONFIGS = 'data_configs.json'

TEST_SPLIT = 0.1
RNG_SEED = 13371447
  • 모델 파라미터 설정
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
EPOCH=10
BATCH_SIZE=1024

MAX_SEQUENCE_LENGTH = 26 # 31

WORD_EMBEDDING_DIM = 100
CONV_FEATURE_DIM = 300
CONV_OUTPUT_DIM = 128
CONV_WINDOW_SIZE = 3
SIMILARITY_DENSE_FEATURE_DIM = 200

prepro_configs = None

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

VOCAB_SIZE = prepro_configs['vocab_size'] #76464개
1
2
3
q1_data = np.load(open(DATA_IN_PATH + TRAIN_Q1_DATA_FILE, 'rb'))
q2_data = np.load(open(DATA_IN_PATH + TRAIN_Q2_DATA_FILE, 'rb'))
labels = np.load(open(DATA_IN_PATH + TRAIN_LABEL_DATA_FILE, 'rb'))
1
2
3
4
5
6
7
8
9
X = np.stack((q1_data, q2_data), axis=1)
y = labels

train_X, eval_X, train_y, eval_y = train_test_split(X, y, test_size=TEST_SPLIT, random_state=RNG_SEED)

train_Q1 = train_X[:, 0]
train_Q2 = train_X[:, 1]
eval_Q1 = eval_X[:,0]
eval_Q2 = eval_X[:,1]
  • estimator에 활용할 데이터 입력 함수를 만들 것이다.

    • map 함수
    • 학습 입력 함수
    • 검증 입력 함수
  • 우선 map 함수로 정의한 rearrange 함수부터 설명하면 3개의 값이 인자로 들어오는데, 각각 기준 질문, 대상 질문, 라벨값이다. 이렇게 들어온 인자 값을 통해 2개의 질문을 하나의 dictionary 형태의 입력값으로 만든다. 그리고 이렇게 만든 dictionary와 label을 return하는 구조로 돼 있다. 이 함수를 학습 입력함수와 검증 입력 함수에 적용할 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def rearrange(base, hypothesis, label):
features = {'x1' : base, 'x2' : hypothesis}
return features, label

def train_input_fn():
dataset = tf.data.Dataset.from_tensor_slices((train_Q1, train_Q2, train_y))
dataset = dataset.shuffle(buffer_size=100)
dataset = dataset.batch(16)
dataset = dataset.map(rearrange)
dataset = dataset.repeat(EPOCH)
iterator = dataset.make_one_shot_iterator()

return iterator.get_next()

def eval_input_fn():
dataset = tf.data.Dataset.from_tensor_slices((eval_Q1, eval_Q2, eval_y))
dataset = dataset.shuffle(buffer_size=100)
dataset = dataset.batch(16)
dataset = dataset.map(rearrange)
iterator = dataset.make_one_shot_iterator()

return iterator.get_next()

모델 구현

  • CNN 블록 함수를 먼저 정의할 것이다. CNN 블록 함수는 convolution layer와 Pooling, Dense를 하나로 합친 형태로 정의할 것이다.
  • 이 함수는 2개의 인자값을 받는데, 각각 입력값과 이름을 의미한다. 이 함수에서 합성곱의 경우 이전 장의 CNN 모델을 구성할 때와 동일하게 Conv1D를 사용할 것이다. Max Pooling도 마찬사지로 MaxPooling1D 객체를 활용한다. 그리고 이렇게 합성곱과 Max Pooling을 적용한 값에 대해 차원을 바꾸기 위해 Dense 층을 통과 시킨다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
def basic_conv_sementic_network(inputs, name):
conv_layer = tf.keras.layers.Conv1D(CONV_FEATURE_DIM,
CONV_WINDOW_SIZE,
activation=tf.nn.relu,
name=name + 'conv_1d',
padding='same')(inputs) #1024 X 26 X 300

max_pool_layer = tf.keras.layers.MaxPool1D(MAX_SEQUENCE_LENGTH, 1)(conv_layer) # 1024 X 1 X 300

output_layer = tf.keras.layers.Dense(CONV_OUTPUT_DIM, activation=tf.nn.relu, name=name + 'dense')(max_pool_layer) #1024 X 1 X 128

output_layer = tf.squeeze(output_layer, 1) # 1024 X 128

return output_layer
  • 이제 모델 함수를 설명 할 것이다. 먼저 현재 튜토리얼은 임베딩 벡터에 크게 신경쓰지 않고 모델의 구조에 대해 집중하는 튜토리얼이므로 특별한 기법 없이 tf.keras.layers.Embedding으로 임베딩 벡터를 만들어준 뒤 Conv1D 구조를 3번 거쳐 최종적으로 dense layer를 통해 1개의 노드로 맞춰준 logit 값을 sigmoid함수를 통해 마치 로지스틱 회귀와 같은 구조를 만들어 줄 것이다.
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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
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 = tf.keras.layers.Embedding(VOCAB_SIZE, WORD_EMBEDDING_DIM)

base_embedded_matrix = embedding(features['x1']) # 1024 X 31 X 100
hypothesis_embedded_matrix = embedding(features['x2']) # 1024 X 31 X 100

base_embedded_matrix = tf.keras.layers.Dropout(0.2)(base_embedded_matrix)
hypothesis_embedded_matrix = tf.keras.layers.Dropout(0.2)(hypothesis_embedded_matrix)

conv_layer_base_first = tf.keras.layers.Conv1D(CONV_FEATURE_DIM, CONV_WINDOW_SIZE, activation=tf.nn.relu, padding='same')(base_embedded_matrix) #1024 X 31 X 300
max_pool_layer_base_first = tf.keras.layers.MaxPool1D(2, 1)(conv_layer_base_first) # 1024 X 30 X 300

conv_layer_hypothesis_first = tf.keras.layers.Conv1D(CONV_FEATURE_DIM, CONV_WINDOW_SIZE, activation=tf.nn.relu, padding='same')(hypothesis_embedded_matrix) #1024 X 31 X 300
max_pool_layer_hypothesis_first = tf.keras.layers.MaxPool1D(2, 1)(conv_layer_hypothesis_first) # 1024 X 30 X 300


conv_layer_base_second = tf.keras.layers.Conv1D(CONV_FEATURE_DIM, 5, activation=tf.nn.relu, padding='same')(max_pool_layer_base_first) #1024 X 30 X 300
max_pool_layer_base_second = tf.keras.layers.MaxPool1D(5, 1)(conv_layer_base_second) # 1024 X 26 X 300

conv_layer_hypothesis_second = tf.keras.layers.Conv1D(CONV_FEATURE_DIM, 5, activation=tf.nn.relu, padding='same')(max_pool_layer_hypothesis_first) #1024 X 30 X 300
max_pool_layer_hypothesis_second = tf.keras.layers.MaxPool1D(5, 1)(conv_layer_hypothesis_second) # 1024 X 26 X 300

base_sementic_matrix = basic_conv_sementic_network(max_pool_layer_base_second, 'base') # 1024 X 128
hypothesis_sementic_matrix = basic_conv_sementic_network(max_pool_layer_hypothesis_second, 'hypothesis') # 1024 X 128

merged_matrix = tf.concat([base_sementic_matrix, hypothesis_sementic_matrix], -1) # 1024 X 256

similarity_dense_layer = tf.keras.layers.Dense(SIMILARITY_DENSE_FEATURE_DIM, activation=tf.nn.relu)(merged_matrix) # 1024 X 200

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

logit_layer = tf.keras.layers.Dense(1)(similarity_dense_layer) # 1024 X 1
logit_layer = tf.squeeze(logit_layer, 1) # (1024, )
similarity = tf.nn.sigmoid(logit_layer)

if PREDICT:
return tf.estimator.EstimatorSpec(
mode=mode,
predictions={
'is_duplicate':similarity
})

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

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

if TRAIN:
global_step = tf.train.get_global_step()
train_op = tf.train.AdamOptimizer(1e-3).minimize(loss, global_step)

return tf.estimator.EstimatorSpec(
mode=mode,
train_op=train_op,
loss=loss)
  • 먼저, 변수값 등 모델과 관련된 내용을 담은 체크포인트 파일을 저장할 경로를 설정해야한다. 경로를 지정한 후 해당 경로가 없다면 생성하고 Estimator 객체를 생성할 때 해당 경로를 설정한다.
1
2
3
4
5
DATA_OUT_PATH = '/content/'
if not os.path.exists(DATA_OUT_PATH):
os.akedirs(DATA_OUT_PATH)

est = tf.estimator.Estimator(model_fn, model_dir=DATA_OUT_PATH + 'checkpoint')
1
est.train(train_input_fn)
1
est.evaluate(eval_input_fn)

데이터 제출

1
2
3
4
5
6
7
TEST_Q1_DATA_FILE = 'test_q1.npy'
TEST_Q2_DATA_FILE = 'test_q2.npy'
TEST_ID_DATA_FILE = 'test_id.npy'

test_q1_data = np.load(open(DATA_IN_PATH + TEST_Q1_DATA_FILE, 'rb'), allow_pickle=True)
test_q2_data = np.load(open(DATA_IN_PATH + TEST_Q2_DATA_FILE, 'rb'), allow_pickle=True)
test_id_data = np.load(open(DATA_IN_PATH + TEST_ID_DATA_FILE, 'rb'), allow_pickle=True)
  • 입력 함수나 검증 함수처럼 별도의 함수로 정의하지 않고 Estimator의 기본 numpy_input_fn 함수를 사용한다. 입력 형태는 앞서 다른 입력 함수와 마찬가지로 두 질문을 dictionary 형태로 만들었다. 그리고 shffle=False로 설정하는데 이는 두 개의 질문쌍이 같은 순서로 입력돼야 하기 때문이다. 이제 이 함수를 활용해 Estimator의 predict 함수를 실행할 것이다.
  • predict 함수를 활용하고 반복문을 통해 데이터에 대한 예측값을 받을 수 있게 한다. 이때 받고자 하는 예측값에 대해서는 is_duplicate라는 key 값으로 정의했기 때문에 아래와 같이 유사도 예측값만을 받을 것이다.
1
2
3
4
5
6
7
8
predict_input_fn = tf.estimator.inputs.numpy_input_fn(x={"x1":test_q1_data,
"x2":test_q2_data},
shuffle=False)

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

output = pd.DataFrame( data={"test_id":test_id_data, "is_duplicate": list(predictions)} )
output.to_csv("cnn_predict.csv", index=False, quoting=3)
kaggle 제출
1
!kaggle competitions submit quora-question-pairs -f "cnn_predict.csv" -m "cnn conv1d 3layer 10 Epoches"

CNN 3 layer를 통한 결과

MaLSTM

  • 마지막으로 텍스트 유사도 측정을 위해 사용할 모델은 MaLSTM 모델이다. 순서가 있는 입력 데이터에 적합하다는 평을 받는 RNN 모델을 통해 유사도를 측정한다.
  • 유사도를 구하기 위해 활용하는 대표적인 모델인 MaLSTM 모델은 2016년 MIT에서 Jonas Mueller가 쓴 “Siamese Recurrent Architectures for Learning Sentence Similarity”라는 논문에서 처음 소개 되었다. MaLSTM이란 Manhattan Distance + LSTM 의 줄임말로써, 일반적으로 문장의 유사도를 계산할 때 코사인 유사도를 사용하는 대신 맨하탄 거리를 사용하는 모델이다.

  • 이전의 합성곱 신경망 모델에서도 두 개의 문장 입력값에 대해 각각 합성곱 층을 적용한 후 최종적으로 각 문장에 대해 의미 벡터를 각각 뽑아내서 concatenate한 후 dese layer를 통해 선형 변환 해준 뒤 로지스틱 모형과 같이 값을 구해 두 문장의 유사도를 구했다. 이번에는 맨하탄 거리로 비교하는 형태의 모델로서, LSTM의 마지막 스텝의 LSTM hidden state는 문장의 모든 단어에 대한 정보가 반영된 값으로 전체 문장을 대표하는 벡터가 된다. 이렇게 뽑은 두 벡터에 대해 맨하탄 거리를 계산해서 두 문장 사이의 유사도를 측정 할 것이다. 그리고 이렇게 계산한 유사도를 실제 라벨과 비교해서 학습하는 방식으로 모델을 설계할 것이다.

MaLSTM의 모델 구조

1
2
3
4
5
6
7
8
9
import sys
import tensorflow as tf
import numpy as np
import os
import pandas as pd

from sklearn.model_selection import train_test_split

import json

모델 구현

  • 미리 Global 변수를 지정하자. 파일 명, 파일 위치, 디렉토리 등이 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
DATA_IN_PATH = '/content/'
DATA_OUT_PATH = '/content/'

TRAIN_Q1_DATA_FILE = 'q1_train.npy'
TRAIN_Q2_DATA_FILE = 'q2_train.npy'
TRAIN_LABEL_DATA_FILE = 'label_train.npy'
NB_WORDS_DATA_FILE = 'data_configs.json'

## 학습에 필요한 파라메터들에 대해서 지정하는 부분이다.
## CPU에서는 Epoch 크기를 줄이는 걸 권장한다.
BATCH_SIZE = 4096
EPOCH = 50
HIDDEN = 64

NUM_LAYERS = 3
DROPOUT_RATIO = 0.2

TEST_SPLIT = 0.1
RNG_SEED = 13371447
EMBEDDING_DIM = 128
MAX_SEQ_LEN = 31

데이터 불러오기

  • 데이터를 불러오는 부분이다. 효과적인 데이터 불러오기를 위해, 미리 넘파이 형태로 저장시킨 데이터를 로드한다.
1
2
3
4
5
6
7
q1_data = np.load(open(DATA_IN_PATH + TRAIN_Q1_DATA_FILE, 'rb'))
q2_data = np.load(open(DATA_IN_PATH + TRAIN_Q2_DATA_FILE, 'rb'))
labels = np.load(open(DATA_IN_PATH + TRAIN_LABEL_DATA_FILE, 'rb'))
prepro_configs = None

with open(DATA_IN_PATH + NB_WORDS_DATA_FILE, 'r') as f:
prepro_configs = json.load(f)
1
2
VOCAB_SIZE = prepro_configs['vocab_size']
BUFFER_SIZE = len(labels)

테스트 및 검증 데이터 나누기

  • 데이터를 나누어 저장하자. sklearn의 train_test_split을 사용하면 유용하다. 하지만, 쿼라 데이터의 경우는 입력이 1개가 아니라 2개이다. 따라서, np.stack을 사용하여 두개를 하나로 쌓은다음 활용하여 분류한다.
1
2
3
4
5
6
7
8
X = np.stack((q1_data, q2_data), axis=1)
y = labels
train_X, test_X, train_y, test_y = train_test_split(X, y, test_size=TEST_SPLIT, random_state=RNG_SEED)

train_Q1 = train_X[:,0]
train_Q2 = train_X[:,1]
test_Q1 = test_X[:,0]
test_Q2 = test_X[:,1]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def rearrange(base, hypothesis, labels):
features = {"base": base, "hypothesis": hypothesis}
return features, labels

def train_input_fn():
dataset = tf.data.Dataset.from_tensor_slices((train_Q1, train_Q2, train_y))
dataset = dataset.shuffle(buffer_size=len(train_Q1))
dataset = dataset.batch(BATCH_SIZE)
dataset = dataset.map(rearrange)
dataset = dataset.repeat(EPOCH)
iterator = dataset.make_one_shot_iterator()

return iterator.get_next()

def eval_input_fn():
dataset = tf.data.Dataset.from_tensor_slices((test_Q1, test_Q2, test_y))
dataset = dataset.batch(BATCH_SIZE)
dataset = dataset.map(rearrange)
iterator = dataset.make_one_shot_iterator()

return iterator.get_next()

모델 설계

  • 양방향 LSTM을 사용할 것이다. 즉, 2개의 LSTM을 먼저 정의해야 한다. 정방향 LSTM층과 역방향 LSTM 층을 먼저 정의할 것이다. 그리고 나선 이 2개의 LSTM 층에 데이터를 적용한 후 결과값을 하나로 concatenate한다.

  • 양방향 순환 신경망 함수의 경우 2개의 return 값이 있는데, 하나는 순환 신경망의 출력 값이고, 나머지 하나는 순환 신경망 마지막 스텝의 hidden state 벡터 값이다. 사용해야 할 것은 마지막 hidden state 벡터이므로 각각 q_output_states와 sim_output_states로 할당한다. 이렇게 뽑은 hidden state 벡터의 경우 해당 모델이 양방향 순환 신경망을 활용해 2개의 hidden state 값을 concatenate하여 하나의 벡터로 만든다. 이는 순환 신경망이 문장의 순방향과 역방향 모두 학습함으로써 성능 개선에 도움을 준다.

  • 맨하탄 거리의 경우 두 벡터를 뺀 후 절대값을 취하면 된다. 이렇게 뺀 값의 경우 벡터 형태이기 때문에 하나의 상수, 즉 scalar값으로 만들기 위해 reduce_sum 함수를 이용한다. 이렇게 되면 구한 값이 0~1사이의 값을 갖게 될 것이다.

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
def Malstm(features, labels, mode):

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

def basic_bilstm_network(inputs, name):
with tf.variable_scope(name, reuse=tf.AUTO_REUSE):
lstm_fw = [
tf.nn.rnn_cell.DropoutWrapper(tf.nn.rnn_cell.LSTMCell(HIDDEN), output_keep_prob=DROPOUT_RATIO)
for layer in range(NUM_LAYERS)
]
lstm_bw = [
tf.nn.rnn_cell.DropoutWrapper(tf.nn.rnn_cell.LSTMCell(HIDDEN), output_keep_prob=DROPOUT_RATIO)
for layer in range(NUM_LAYERS)
]

multi_lstm_fw = tf.nn.rnn_cell.MultiRNNCell(lstm_fw)
multi_lstm_bw = tf.nn.rnn_cell.MultiRNNCell(lstm_bw)

(fw_outputs, bw_outputs), _ = tf.nn.bidirectional_dynamic_rnn(cell_fw=multi_lstm_fw,
cell_bw=multi_lstm_bw,
inputs=inputs,
dtype=tf.float32)

outputs = tf.concat([fw_outputs, bw_outputs], 2)

return outputs[:,-1,:]

embedding = tf.keras.layers.Embedding(VOCAB_SIZE, EMBEDDING_DIM)

base_embedded_matrix = embedding(features['base'])
hypothesis_embedded_matrix = embedding(features['hypothesis'])

base_sementic_matrix = basic_bilstm_network(base_embedded_matrix, 'base')
hypothesis_sementic_matrix = basic_bilstm_network(hypothesis_embedded_matrix, 'hypothesis')

base_sementic_matrix = tf.keras.layers.Dropout(DROPOUT_RATIO)(base_sementic_matrix)
hypothesis_sementic_matrix = tf.keras.layers.Dropout(DROPOUT_RATIO)(hypothesis_sementic_matrix)

# merged_matrix = tf.concat([base_sementic_matrix, hypothesis_sementic_matrix], -1)
# logit_layer = tf.keras.layers.dot([base_sementic_matrix, hypothesis_sementic_matrix], axes=1, normalize=True)
# logit_layer = K.exp(-K.sum(K.abs(base_sementic_matrix - hypothesis_sementic_matrix), axis=1, keepdims=True))

logit_layer = tf.exp(-tf.reduce_sum(tf.abs(base_sementic_matrix - hypothesis_sementic_matrix), axis=1, keepdims=True))
logit_layer = tf.squeeze(logit_layer, axis=-1)

if PREDICT:
return tf.estimator.EstimatorSpec(
mode=mode,
predictions={
'is_duplicate':logit_layer
})

#prediction 진행 시, None
if labels is not None:
labels = tf.to_float(labels)

# loss = tf.reduce_mean(tf.keras.metrics.binary_crossentropy(y_true=labels, y_pred=logit_layer))
loss = tf.losses.mean_squared_error(labels=labels, predictions=logit_layer)
# loss = tf.reduce_mean(tf.losses.sigmoid_cross_entropy(labels, logit_layer))

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

elif TRAIN:

global_step = tf.train.get_global_step()
train_op = tf.train.AdamOptimizer(1e-3).minimize(loss, global_step)

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

학습 및 평가

1
2
3
4
5
6
7
8
# os.environ["CUDA_VISIBLE_DEVICES"]="0" #For GPU

model_dir = os.path.join(os.getcwd(), DATA_OUT_PATH + "checkpoint/rnn2/")
os.makedirs(model_dir, exist_ok=True)

config_tf = tf.estimator.RunConfig()

lstm_est = tf.estimator.Estimator(Malstm, model_dir=model_dir)
1
lstm_est.train(train_input_fn)
1
lstm_est.evaluate(eval_input_fn)

테스트 데이터 예측 및 캐글 제출하기

1
2
3
4
5
6
7
TEST_Q1_DATA_FILE = 'test_q1.npy'
TEST_Q2_DATA_FILE = 'test_q2.npy'
TEST_ID_DATA_FILE = 'test_id.npy'

test_q1_data = np.load(open(DATA_IN_PATH + TEST_Q1_DATA_FILE, 'rb'), allow_pickle=True)
test_q2_data = np.load(open(DATA_IN_PATH + TEST_Q2_DATA_FILE, 'rb'), allow_pickle=True)
test_id_data = np.load(open(DATA_IN_PATH + TEST_ID_DATA_FILE, 'rb'), allow_pickle=True)
1
2
3
4
5
predict_input_fn = tf.estimator.inputs.numpy_input_fn(x={"base":test_q1_data,
"hypothesis":test_q2_data},
shuffle=False)

predictions = np.array([p['is_duplicate'] for p in lstm_est.predict(input_fn=predict_input_fn)])
1
2
3
4
print(len(predictions)) #2345796

output = pd.DataFrame( data={"test_id":test_id_data, "is_duplicate": list(predictions)} )
output.to_csv( "rnn_predict.csv", index=False, quoting=3 )
1
!kaggle competitions submit quora-question-pairs -f "rnn_predict.csv" -m "MaLSTM Model with 5layers BiLSTM 50 Epoches"

MaLSTM 결과

구조에 대한 튜토리얼형식으로 만들다보니 임베딩의 질이 떨어진다면 머신러닝 기법이 딥러닝 방식보다 결과가 더 좋을 수 있다는 사실을 다시 한번 체감할 수 있는 작업이었다. 추후에 TF-IDF행렬, Word2Vec과 문장 단위 LSA를 시행해 얻은 임베딩을 사용하여 다시 한번 결과를 비교할 것이다.