[인공지능 기초] 9. RNN

2022. 2. 19. 02:04인공지능/인공지능 기초

Traditional Neural Network나 CNN은 Sequence Data에 대해서 좋은 성능을 내지 못하였다. 이러한 데이터들은 시간이나 순서에 중요한 정보를 가지고 있기 때문에 이러한 특성을 같이 포함시킬 수 있는 네트워크가 필요하였다. 그리고 그것이 바로 RNN이다.


1. RNN

 

RNN은 시간이나 순서에 따른 처리를 해주기 위하여 등장하였다. 기본적으로 ANN의 형태를 가지지만, 이전 state 값 $s_{i-1}$가 현재 state 값 $s_i$를 계산할 때 사용된다는 점이 다르다. 각 state는 이전 정보를 함축하고 있고, 따라서 제일 마지막 $s_n$은 모든 정보를 포함하게 된다.

$$ h_t = f_W(h_{t-1}, x_t)$$

 

1.1 RNN Cell

 

기본적으로 RNN Cell은 위와 같은 형태를 가진다. 즉, 입력 $x_t$를 선형 변환($W_{xh}$)한 값과 이전 state값인 $h_{t-1}$을 선형 변환($W_{hh}$)한 값을 더하고, 여기에 hyperbolic-tangent를 non-linear function으로써 적용하여 hidden state $h_t$를 구한다. 또한, 이 $h_t$를 또 한번의 linear function($w_{hy}$)로 매핑하여 output을 출력한다.

 

Why Tanh?

sigmoid를 사용하면 초반에 에러가 생길 시, layer를 거칠 수록 그 에러가 증폭된다고 한다. tanh를 사용하면 에러 보정 효과도 주기 때문에 많이 사용된다.

**sigmoid는 0~1값을 가지기 때문에 항상 양의 값이므로 에러가 더해지기만 하고, tanh는 -1 ~ 1값을 가지므로 음수 값을 통해 에러를 보정해줄 수 있게 된다.

 

1.2 RNN Flexibility

 

기본적으로 입력을 받아 출력을 만들고, 그 출력을 다시 입력으로 받는다. RNN은 시퀀스 길이에 상관없이 input과 output을 받아들일 수 있기 때문에 필요에 따라 다양하고 유연하게 구조를 만들 수 있다. 그리고 그 형태에 따라서 적용되는 분야도 다르다.

 

  • one to many: Image Captioning
  • many to one: Sentiment Classification
  • many to many 1: Machine Translation
  • many to many 2: Video Classification on frame level

2. Example

RNN이 실제로 어떻게 동작하는지 tensorflow를 이용하 간단한 예제로 살펴보자. 

 

위 예제는 hihello에 대해서 각 알파벳의 다음으로 나올 알파벳을 예측하는 RNN이다. 이 때, 각 형태소(h, e, l, o, i)는 one-hot encoding으로 나타낸다.

 

import tensorflow as tf
import numpy as np

idx2char = ['h','i','e','l','o']

x_data = [[0,1,0,2,3,3]] # hihell
y_data = [[1,0,2,3,3,4]] # ihello
x_onehot = [[[1,0,0,0,0],
             [0,1,0,0,0],
             [1,0,0,0,0],
             [0,0,1,0,0],
             [0,0,0,1,0],
             [0,0,0,1,0]]]
num_classes = 5
input_dim = 5
hidden_size = 10 # hidden vector(적절히 지정) : cell의 output 크기
batch_size = 1 # 문장의 갯수 : 여기서는 1 문장
seq_length = 6

X = tf.placeholder(tf.float32, [None, seq_length, input_dim])
Y = tf.placeholder(tf.int32, [None, seq_length])

#model, cost, train
# 출력의 크기 hidden size를 정해준다.
# 1. 셀을 만들어준다. num_units(출력의 크기)
cell = tf.contrib.rnn.BasicLSTMCell(num_units=hidden_size, state_is_tuple=True)
initial_state = cell.zero_state(batch_size, tf.float32) # 초기 state는 0

# 2. cell을 만든 것을 실제로 구동시켜 입력을 주고 model(출력값) 을 얻는다.
model, _states = tf.nn.dynamic_rnn(cell, X, initial_state=initial_state, dtype=tf.float32)

# fully connected : model을 1차 배열로 변환시켜주어야한다.
X_for_fc = tf.reshape(model, [-1,hidden_size])

# cell의 output을 input과 같은 크기로 바꾸어 줌.
model = tf.contrib.layers.fully_connected(inputs=X_for_fc,
                                         num_outputs=num_classes,
                                         activation_fn=None)

# batch_size는 단어가 몇개 이냐에 따른다.
model = tf.reshape(model, [batch_size, seq_length, num_classes])

# 초기 weight는 다 1로 초기화. 단, 여기서 weight는 기존 NN의 가중치를 나타내는 것이 아닌
# 노드에 대한 중요도를 나타내는 가중치이다.
weights = tf.ones([batch_size, seq_length]) 

## cost function
cost = tf.reduce_mean(tf.contrib.seq2seq.sequence_loss(logits=model,
                                                      targets=Y,
                                                      weights=weights))
train = tf.train.AdamOptimizer(0.1).minimize(cost)
pred = tf.argmax(model, axis=2)

with tf.Session() as sess:
    sess.run(tf.global_variables_initializer())
    for i in range(200):
        c, _ = sess.run([cost, train], feed_dict={X:x_onehot,Y:y_data})
        result = sess.run(pred, feed_dict = {X:x_onehot})
        result_str = [idx2char[c] for c in np.squeeze(result)]
        if i % 10 ==0:
            print(i, "cost: ", c, "pred:", result)
            print("prediction string: ", ''.join(result_str))
0 cost:  1.6130934 pred: [[3 3 3 3 3 3]]
prediction string:  llllll
10 cost:  0.27073646 pred: [[1 0 2 3 3 4]]
prediction string:  ihello
20 cost:  0.026551021 pred: [[1 0 2 3 3 4]]
prediction string:  ihello

위 결과에서 알 수 있듯이 hihell을 input으로 넣었을 때, 각 알파벳의 다음 글자를 잘 예측한다(ex. h->i, l->0). 이번 예제는 학습 데이터만을 사용하여 테스트를 했기 때문에 결과가 잘 나왔지만 실제로는 다양한 변수가 존재한다. 

**hello의 경우에도 l->l, l->0, l에서 2가지의 예측이 가능하기 때문에 hel을 Input으로 주었을 때 hell이 나올지 helo가 나올지 알 수 없다.

 

3. Deep RNN

CNN과 마찬가지로 wide하고 deep하게 층을 만들어주는 것이 더 좋은 결과를 얻을 수 있다. 또한, RNN Cell output을 fully connected layer를 이용하여 한 번 더 mapping한 후에 softmax layer를 거쳐 output을 낼 수도 있다. 이는 long sentence일 수록 큰 효과를 발휘한다.

 

3.1 Example

import tensorflow as tf
import numpy as np
from tensorflow.contrib import rnn

tf.set_random_seed(777) 

sentence = ("if you want to build a ship, don't drum up people together to "
            "collect wood and don't assign them tasks and work, but rather "
            "teach them to long for the endless immensity of the sea.")

# 중복되지 않게, unique한 알파벳들을 뽑아서 list로 만듬.
char_set = list(set(sentence)) 

# enumerate를 이용해 딕셔너리를 만듦.
# ex) i:0
char_dic = {w:i for i, w in enumerate(char_set)}

hidden_size = len(char_set) # 출력의 크기
num_classes = len(char_set) # 입력의 차원, 클래스의 개수
sequence_length = 10 # 시퀀스의 개수. 원하는 숫자를 줄 수 있다.
learning_rate = 0.1

dataX=[]
dataY=[]

# sentence를 여러 문장으로(sequence_lenght 간격으로) 나누는 파트.
for i in range(0, len(sentence) - sequence_length):
    x_str = sentence[i:i+sequence_length]
    y_str = sentence[i+1:i+sequence_length+1]
    #print(i, x_str, '->',y_str)
    
    # 잘라낸 sentence의 char들을 index로 바꾸어주는 파트
    x = [char_dic[c] for c in x_str]
    y = [char_dic[c] for c in y_str]
    
    dataX.append(x)
    dataY.append(y)
    
batch_size = len(dataX)

X = tf.placeholder(tf.int32, [None, sequence_length])
Y = tf.placeholder(tf.int32, [None, sequence_length])

# one-hot encoding
X_one_hot = tf.one_hot(X, num_classes)
print(X_one_hot)

# LSTM 셀을 만드는 메서드
def lstm_cell():
    cell = rnn.BasicLSTMCell(hidden_size, state_is_tuple=True)
    return cell


## 다층 RNN 
# cell에 * (쌓을 층 수) 만으로 다층 RNN을 만들 수 있다.
multi_cells = rnn.MultiRNNCell([lstm_cell() for _ in range(2)], state_is_tuple=True)
# 다른 방식:                   ([cell]*2)

outputs, _ = tf.nn.dynamic_rnn(multi_cells, X_one_hot, dtype=tf.float32)

# FC layer  : 여기서는 따로 hidden layer를 쌓지는 않았다.
X_for_fc = tf.reshape(outputs, [-1, hidden_size])
outputs = tf.contrib.layers.fully_connected(X_for_fc, num_classes, activation_fn=None)

# reshape out for sequence_loss

outputs = tf.reshape(outputs, [batch_size, sequence_length, num_classes])


# 모든 weight의 초기 값 1
weights = tf.ones([batch_size, sequence_length])

sequence_loss = tf.contrib.seq2seq.sequence_loss(
logits=outputs, targets=Y, weights=weights)
mean_loss = tf.reduce_mean(sequence_loss)
train_op = tf.train.AdamOptimizer(learning_rate=learning_rate).minimize(mean_loss)

with tf.Session() as sess:
    sess.run(tf.global_variables_initializer())
    
    # train
    for i in range(500):
        _, l, results= sess.run(
            [train_op, mean_loss, outputs],feed_dict={X:dataX, Y:dataY})
        for j, result in enumerate(results):
            index = np.argmax(result,axis=1)
            if i%50==0 and j%50 == 0:
                print(i,j,''.join([char_set[t] for t in index]),l)
        
    
    # test
    results = sess.run(outputs, feed_dict={X:dataX})
    
    # results : shape=(batch_size, sequence_length, num_classes)
    # 한 문장씩 가져와서 각 sequence의 출력값마다 argmax를 해주어
    # 최종 출력 문장을 print한다.
    for j, result in enumerate(results):
        index = np.argmax(result, axis=1)
        # 첫 result만 확인하고, 그 뒤에거는 그냥 붙여줌.
        if j is 0: 
            print(''.join([char_set[t] for t in index]), end='')
        else:
            print(char_set[index[-1]], end='')
0 0 o nprrreoo 3.220966
0 50 tohgpaggpp 3.220966
0 100 egnn''npog 3.220966
0 150 pp'nppkkpp 3.220966
50 0 p you want 0.25509247
50 50 h ether to 0.25509247
50 100 , and work 0.25509247
50 150 tndless im 0.25509247
100 0 p you want 0.23295718
100 50   ether to 0.23295718
100 100 , and work 0.23295718
100 150 tndless im 0.23295718
150 0 g you want 0.2305699
150 50 h ether to 0.2305699
150 100 , and work 0.2305699
150 150 tndless im 0.2305699
200 0 f you want 0.22989985
200 50   ether to 0.22989985
200 100 , and work 0.22989985
200 150 tndless im 0.22989985

f you want to build a ship, don't drum up people together to 
collect wood and don't assign them tasks and work, 
but rather teach them to long for the endless immensity of the sea.

이상 RNN에 대해서 간단히 알아보았다. RNN은 자연어처리의 기본이 되는 만큼 제대로 이해하기를 권장한다. 필자는 컴퓨터비전을 주로 공부하지만 자연어처리에도 어느정도 관심이 있어 수업도 듣고 여러 논문도 보는 편이다. 이 정리는 옛날에 작성한 것이기 때문에 내용이 부족할 수 있다. 하지만 어디까지나 인공지능 기초를 다루는 글이기 때문에 이 정도로 충분하다 보고 나중에 기회가 되면 자연어처리 게시글에 더 자세히 알아보도록 하겠다. (근데 어차피 기본 자연어처리 개념은 정리글이 너무 많기 때문에...뭐)