IT/AI\ML

[python/Tensorflow2.0] RNN(Recurrent Neural Network) ; many to one stacking

개발자 두더지 2020. 4. 21. 00:23
728x90

1. Stacking이란?

CNN에서 convolution layer를 여러 개를 썼듯 ,RNN도 마찬가지로 여러 개를 쌓을 수 있다. 이를 multi layered RNN 또는 stacked RNN이라고 얘기한다.

CNN에서 convolution layer를 여러 개 쌓았을 때, 인풋 이미지에 가까운 convolution layer는 edge와 같은 글로벌한 feature을 뽑을 수 있고 아웃풋에 가까운 convolution layer는 좀 더 abstract한 feature을 뽑을 수 있듯이 RNN에서도 stacked RNN를 활용하여 비슷한 효과를 얻을 수 있다. 이는 이론적으로 증명된 것은 아니지만 다양한 문제를 풀 때 stacked RNN 구조가 shallow RNN보다 더 좋은 성능을 보여왔기 때문에 경험적으로 받아들여지고 있는 것이다.

자연어 처리 분야의 stacked RNN관련 여러 논문에서 인풋에 가까운 RNN의 hidden states가 semantic information(;의미적 정보)보다 syntatic information(;문법적 정보)을 상대적으로 더욱 잘 인코딩을 하고 있으며, 반대로 아웃풋에 가까운 RNN의 hidden states는 semantic information을 syntatic information보다 더욱 달 인코딩하고 있음을 실증적으로 파악하기 때문에 자연어 처리에 stacked RNN이 다양하게 활용되고 있다.


2. Stacked RNN 구현

이전의 many to one 구조를 활용하는 방식과 크게 다르지 않으며 차이점은 RNN을 여러 개 활용하는 stacked RNN을 사용한다는 점이다.이번에는 문장을 분류해 볼 것이다.

1) Importing libraries

# setup
import numpy as np
import tensorflow as tf
import matplotlib.pyplot as plt
from tensorflow import keras
from tensorflow.keras import layers
from tensorflow.keras import Sequential, Model
from tensorflow.keras.preprocessing.sequence import pad_sequences
from pprint import pprint
%matplotlib inline

print(tf.__version__)

 

2) Preparing dataset

# example data
sentences = ['What I cannot create, I do not understand.',
             'Intellecuals solve problems, geniuses prevent them',
             'A person who never made a mistake never tied anything new.',
             'The same equations have the same solutions.']
y_data = [1,0,0,1] # 1인 경우 richard feynman이 했던 말, 0인 경우 albert einstein가 생전에 했던 말
# creating a token dictionary
char_set = ['<pad>'] + sorted(list(set(''.join(sentences))))
idx2char = {idx : char for idx, char in enumerate(char_set)}
char2idx = {char : idx for idx, char in enumerate(char_set)}

print(char_set)
print(idx2char)
print(char2idx)

['<pad>', ' ', ',', '.', 'A', 'I', 'T', 'W', 'a', 'b', 'c', 'd', 'e', 'g', 'h', 'i', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'y']

{0: '<pad>', 1: ' ', 2: ',', 3: '.', 4: 'A', 5: 'I', 6: 'T', 7: 'W', 8: 'a', 9: 'b', 10: 'c', 11: 'd', 12: 'e', 13: 'g', 14: 'h', 15: 'i', 16: 'k', 17: 'l', 18: 'm', 19: 'n', 20: 'o', 21: 'p', 22: 'q', 23: 'r', 24: 's', 25: 't', 26: 'u', 27: 'v', 28: 'w', 29: 'y'}

{'<pad>': 0, ' ': 1, ',': 2, '.': 3, 'A': 4, 'I': 5, 'T': 6, 'W': 7, 'a': 8, 'b': 9, 'c': 10, 'd': 11, 'e': 12, 'g': 13, 'h': 14, 'i': 15, 'k': 16, 'l': 17, 'm': 18, 'n': 19, 'o': 20, 'p': 21, 'q': 22, 'r': 23, 's': 24, 't': 25, 'u': 26, 'v': 27, 'w': 28, 'y': 29}

# converting sequence of tokens to sequence of indices
x_data = list(map(lambda sentence : [char2idx.get(char) for char in sentence], sentences))
x_data_len = list(map(lambda sentence : len(sentence), sentences))

print(x_data)
print(x_data_len)
print(y_data)

[[7, 14, 8, 25, 1, 5, 1, 10, 8, 19, 19, 20, 25, 1, 10, 23, 12, 8, 25, 12, 2, 1, 5, 1, 11, 20, 1, 19, 20, 25, 1, 26, 19, 11, 12, 23, 24, 25, 8, 19, 11, 3], [5, 19, 25, 12, 17, 17, 12, 10, 26, 8, 17, 24, 1, 24, 20, 17, 27, 12, 1, 21, 23, 20, 9, 17, 12, 18, 24, 2, 1, 13, 12, 19, 15, 26, 24, 12, 24, 1, 21, 23, 12, 27, 12, 19, 25, 1, 25, 14, 12, 18], [4, 1, 21, 12, 23, 24, 20, 19, 1, 28, 14, 20, 1, 19, 12, 27, 12, 23, 1, 18, 8, 11, 12, 1, 8, 1, 18, 15, 24, 25, 8, 16, 12, 1, 19, 12, 27, 12, 23, 1, 25, 15, 12, 11, 1, 8, 19, 29, 25, 14, 15, 19, 13, 1, 19, 12, 28, 3], [6, 14, 12, 1, 24, 8, 18, 12, 1, 12, 22, 26, 8, 25, 15, 20, 19, 24, 1, 14, 8, 27, 12, 1, 25, 14, 12, 1, 24, 8, 18, 12, 1, 24, 20, 17, 26, 25, 15, 20, 19, 24, 3]]

▶ 이전 보다 시퀀스의 길이가 훨씬 길어졌음을 알 수 있다. 참고로 이렇게 길이가 긴 시퀀스를 다룰 때에는 단순 RNN보다는 Long Short - Tenrm Memory Network 또는 Gated Recurrent Unit 등을 활용하는 것이 좋다. 여기서는 stacked RNN구조로 작성하였다.

[42, 50, 58, 43]

[1, 0, 0, 1]

# padding the sequence of indices
max_sequence = 58
x_data = pad_sequences(sequences = x_data, maxlen = max_sequence,
                       padding = 'post', truncating = 'post')

# checking data
print(x_data)
print(x_data_len)
print(y_data)

[[ 7 14 8 25 1 5 1 10 8 19 19 20 25 1 10 23 12 8 25 12 2 1 5 1 11 20 1 19 20 25 1 26 19 11 12 23 24 25 8 19 11 3 0 0 0 0 0 0 0 0 0 0 0 0 0]

[ 5 19 25 12 17 17 12 10 26 8 17 24 1 24 20 17 27 12 1 21 23 20 9 17 12 18 24 2 1 13 12 19 15 26 24 12 24 1 21 23 12 27 12 19 25 1 25 14 12 18 0 0 0 0 0]

[ 4 1 21 12 23 24 20 19 1 28 14 20 1 19 12 27 12 23 1 18 8 11 12 1 8 1 18 15 24 25 8 16 12 1 19 12 27 12 23 1 25 15 12 11 1 8 19 29 25 14 15 19 13 1 19]

[ 6 14 12 1 24 8 18 12 1 12 22 26 8 25 15 20 19 24 1 14 8 27 12 1 25 14 12 1 24 8 18 12 1 24 20 17 26 25 15 20 19 24 3 0 0 0 0 0 0 0 0 0 0 0 0]]

[42, 50, 58, 43]

[1, 0, 0, 1]

3) Creating model

# creating stacked rnn for "many to one" classification with dropout
num_classes = 2
hidden_dims = [10,10]

input_dim = len(char2idx)
output_dim = len(char2idx)
one_hot = np.eye(len(char2idx))

model = Sequential()
# 앞의 포스팅의 설명과 마찬가지로 mask_zero=True옵셥을 이용하여 시퀀스 중 0으로 padding된 부분을 연산에 포함하지 않을 수 있다.
# trainable=False옵션으로 one hot vector를 트레이닝하지 않을 수 있다.
model.add(layers.Embedding(input_dim=input_dim, output_dim=output_dim,
                           trainable=False, mask_zero=True, input_length=max_sequence,
                           embeddings_initializer=keras.initializers.Constant(one_hot)))
# return_sequences=True옵션은 두 번째 RNN이 필요한 형태, 즉 (data dimension, max sequneces, input dimentsion)의 형태로 데이터를 리턴한다. 
model.add(layers.SimpleRNN(units=hidden_dims[0], return_sequences=True))
# 다음으로 layers.TimeDistributed과 layers.Dropout을 이용하는데, 
# 그 이유는 stacked RNN은 shallow RNN에 비해서 모델의 capacity가 높은 구조이므로 over fitting될 가능성이 크기 때문이다.
# 따라서 RNN이 각각의 토큰을 처리한 hidden states에 Drop out을 걸어 over fitting을 방지해준다.
model.add(layers.TimeDistributed(layers.Dropout(rate = .2)))
# 마찬가지로 두 번째 layer에서도 over fitting 방지를 위해 Drop out을 해준다.
model.add(layers.SimpleRNN(units=hidden_dims[1]))
model.add(layers.Dropout(rate = .2))
model.add(layers.Dense(units=num_classes))
model.summary()

Model: "sequential"

_________________________________________________________________

Layer (type) Output Shape Param #

=================================================================

embedding (Embedding) (None, 55, 30) 900

_________________________________________________________________

simple_rnn (SimpleRNN) (None, 55, 10) 410

_________________________________________________________________

time_distributed (TimeDistri (None, 55, 10) 0

_________________________________________________________________

simple_rnn_1 (SimpleRNN) (None, 10) 210

_________________________________________________________________

dropout_1 (Dropout) (None, 10) 0

_________________________________________________________________

dense (Dense) (None, 2) 22

=================================================================

Total params: 1,542

Trainable params: 642

Non-trainable params: 900

_________________________________________________________________

4) Training model

# creating loss function
# Dropout은 트레닝할 때는 활용하지만, inference 단계에서는 사용하지 않으므로
# 이를 컨트롤 하기 위해 y_pred=model(x, training)옵션에 training argument를 둔다.
def loss_fn(model, x, y, training):    
    return tf.reduce_mean(tf.keras.losses.sparse_categorical_crossentropy(
        y_true=y, y_pred=model(x, training), from_logits=True))

# creating and optimizer
lr = .01
epochs = 30
batch_size = 2
opt = tf.keras.optimizers.Adam(learning_rate = lr)

▶ 위의 오차함수는 2.0 버전이므로 2.1버전으로 바꿔 작성하면 다음과 같다.

def loss_fun(model, x, y, training):

return tf.losses.spares_sofmax_cross_entropy(labels=y, logits=model(x,training))

▶ Adam또한 2.1버전으로 작성하면 아래와 같다.

opt = tf.train.AdamOptimizer(learning_rate = lr)

# generating data pipeline
tr_dataset = tf.data.Dataset.from_tensor_slices((x_data, y_data))
tr_dataset = tr_dataset.shuffle(buffer_size=4)
tr_dataset = tr_dataset.batch(batch_size=batch_size)

print(tr_dataset)

<BatchDataset shapes: ((None, 55), (None,)), types: (tf.int32, tf.int32)>

# training
tr_loss_hist = []

for epoch in range(epochs):
    avg_tr_loss = 0
    tr_step = 0
    
    for x_mb, y_mb in tr_dataset:
        with tf.GradientTape() as tape:
            tr_loss = loss_fn(model, x=x_mb, y=y_mb, training=True)
        grads = tape.gradient(target=tr_loss, sources=model.variables)
        opt.apply_gradients(grads_and_vars=zip(grads, model.variables))
        avg_tr_loss += tr_loss
        tr_step += 1
    else:
        avg_tr_loss /= tr_step
        tr_loss_hist.append(avg_tr_loss)
    
    if (epoch + 1) % 5 ==0:
        print('epoch : {:3}, tr_loss : {:.3f}'.format(epoch + 1, avg_tr_loss.numpy()))

epoch : 5, tr_loss : 0.056

epoch : 10, tr_loss : 0.007

epoch : 15, tr_loss : 0.004

epoch : 20, tr_loss : 0.003

epoch : 25, tr_loss : 0.003

epoch : 30, tr_loss : 0.002

5) Checking performance

yhat = model.predict(x_data)
yhat = np.argmax(yhat, axis=-1)
print('accuracy : {:.2%}'.format(np.mean(yhat == y_data)))

accuracy : 100.00%

plt.plot(tr_loss_hist)

[<matplotlib.lines.Line2D at 0x29d124f9a08>]


참고자료

https://www.edwith.org/boostcourse-dl-tensorflow/lecture/43752/

https://github.com/deeplearningzerotoall/TensorFlow/blob/master/tf_2.x/lab-12-2-many-to-one-stacking-keras-eager.ipynb

728x90