본문 바로가기

Machine Learning/챗봇 만들기

RNN 그리고 LSTM 그리고 문장만들

이전 마르코프성질이라는 확률론적 방법을 사용하였다면 이번에는 딥러닝을 이용해 문장을 구현 할 것이다.


RNN의 기본구조 

 

 

 RNN은 히든 노드가 방향을 가진 엣지로 연결돼 순환구조를 이루는(directed cycle) 인공신경망의 한 종류이다. 음성, 문자 등 순차적으로 등장하는 데이터 처리에 적합한 모델로 알려져 있다. 위 그림에서도 알 수 있듯이 시퀸스 길이에 관계없이 인풋과 아웃풋을 받아들일 수 있는 네트워크 구조이기 때문에 필요에 따라 다양하고 유연하게 구조를 만들 수 있다는 점이 RNN의 가장 큰 장점이다.

 

 

 

위 그림이 RNN의 기본구조이다 녹색박스는 히든 state h_t 를 의미하고 빨간 박스는 인풋 x, 파란 박스는 아웃풋 y이다. 현재 상태의 히든 state h_t 는 직전 시점의 히든 state h_(t-1) 를 받아 갱신된다 

 

현재상태의 아웃풋 y현재 상태 h 를 전달받아 갱신되는 구조이다 이때 수식에서도 알 수 있듯 히든 state의 활성화 함수는 하이퍼볼릭탄젠트(tanh)이다.


RNN의 순전파

 

 

 

딥러닝에 관하여 순전파와 역전파는 중요한 개념으로 작용하는데 공부하는데 있어 아래의 사이트에서 공부및 참고 하였음을 알린다. https://ratsgo.github.io/deep%20learning/2017/05/14/backprop/ 위에는 순전파의 수식을 그래프로 옮겨 놓은 것이다. 순전파를 통하여 y가 출력된다.

 

RNN의 역전파 

이제부터가 중요한데 우선 foward pass(순전파)를 통하여 최종 출력되는 결과는 y_t이다 최종 loss에 대한 y_t의 그래디언트 (dL/dy_t)가 RNN의 역전파 연산에서 가장 먼저 등장한다이를 편의상 dy_t라고 표기하였다.

 

dy_t는 덧셈 그래프를 타고 양방향에 모두 분배가 된다.  dW_hy는 흘러들어온 그래디언트 dy_t에 local gradient h_t를 곱해 구한다. dh_t는 흘러들어온 그래디언트 dy_t에 W_hy를 곱한 값이다. dh_raw는 흘러들어온 그래디언트인 dh_t에 local gradient인 1-tan^2(h_raw)을 곱해 구한다. 나머지도 동일한 방식이다.


 

LSTM의 기본구조

RNN은 관련정보와 그 정보를 사용하는 지점 사이 거리가 멀 경우 역전파시 gradient가 점차 줄어 학습 능력이 크게 저하되는 것으로 알려져 있다. 이를 vanishing gradient problem 이라고 한다.

 

이 문제를 극복하기 위해서 고안된 것이 LSTM 이다, LSTM 은 RNN의 히든 state에 cell_state를 추가한 구조이다. LSTM을 가장 쉽게 시각화한 포스트를 기본으로 해서 설명을 이어나가겠다.

 

cell state는 일종의 컨베이어 벨트 역할을 한다. 덕분에  state가 꽤 오래 경과하더라도 그래디언트가 비교적 전파가 잘 된다. LSTM 셀의 수식은 아래와 같다. ⊙는 요소별 곱셈을 뜻하는 Hadamard product 연산자이다

 

 

forget gate  f_t는 ‘과거 정보를 잊기’를 위한 게이트이다. ht_1 x_t를 받아 시그모이드를 취해준 값이 바로 forget gate가 내보내는 값이 됩니다. 시그모이드 함수의 출력 범위는 0에서 1 사이이기 때문에 그 값이 0이라면 이전 상태의 정보는 잊고, 1이라면 이전 상태의 정보를 온전히 기억하게 됩니다.

 

input gate i_tg_t 는 ‘현재 정보를 기억하기’ 위한 게이트입니다. ht_1 x_t를 받아 시그모이드를 취하고, 또 같은 입력으로 하이퍼볼릭탄젠트를 취해준 다음 Hadamard product 연산을 한 값이 바로 input gate가 내보내는 값이 된다. 개인적으로 i_t의 범위는 0~1, g_t의 범위는 -1~1이기 때문에 각각 강도와 방향을 나타낸다고 생각하자

 

 

 


LSTM의 순전파

 

주목해야 할 점은 H_t인데 이 행렬을 행 기준으로 4등분해 i,f,o,g 각각에 해당하는 활성함수를 적용하는 방식으로 그 값을 구한다.

 

LSTM의 역전파

이제부터 나열한 그림은 위 움짤과 내용이 같습니다. 우선 df_t, di_t, dg_t, do_t 를 구하기까지 backward pass는 RNN과 유사하다. dH_t를 구하는 과정이 LSTM backward pass의 핵심이라고 할 수 있다. H_ti,g,o,f 로 구성된 행렬이다. 바꿔 말하면 각각에 해당하는 그래디언트를 이를 합치면 dH_t를 만들 수 있다는 의미다. i, f, o의 활성함수는 시그모이드이고, g만 하이퍼볼릭탄젠트이다. 각각의 활성함수에 대한 로컬 그래디언트를 구해 흘러들어온 그래디언트를 곱해주면 된다. 


 

LSTM으로 문장 만들기

 

import codecs
from bs4 import BeautifulSoup
from keras.models import Sequential
from keras.layers import Dense, Activation, Dropout
from keras.layers import LSTM
from keras.optimizers import RMSprop
from keras.utils.data_utils import get_file
import numpy as np
import random, sys

fp = codecs.open("C:\doit\stiens.txt", "r", encoding="ANSI")
text = fp.readlines()
words = []


for line in text:
    line.strip()
    words.append(line.split(" "))



print('말뭉치의 길이: ', len(text)) 

# 문자를 하나하나 읽어 들이고 ID 붙이기
chars = []
for i in words:
    for char in i:
        char = char.strip()
        chars.append(char)


print('사용되고 있는 문자의 수:', len(chars))
char_indices = dict((c, i) for i, c in enumerate(chars)) # 문자 → ID
indices_char = dict((i, c) for i, c in enumerate(chars)) # ID → 문자
#사전을 종류별로 
print(char_indices)

# 텍스트를 maxlen개의 문자로 자르고 다음에 오는 문자 등록하기
maxlen = 20
step = 3
sentences = []#만들어지는 문장
next_chars = []

for i in range(0, len(text) - maxlen, step):
    sentences.append(text[i: i + maxlen])
    next_chars.append(text[i + maxlen])

print('학습할 구문의 수:', len(sentences))
print('텍스트를 ID 벡터로 변환합니다...')

X = np.zeros((len(sentences), maxlen, len(chars)), dtype=np.bool)
y = np.zeros((len(sentences), len(chars)), dtype=np.bool)

for i, sentence in enumerate(sentences):

    for t, char in enumerate(sentence):
        X[i, t, char_indices[char]] = 1
    y[i, char_indices[next_chars[i]]] = 1

# 모델 구축하기(LSTM)
print('모델을 구축합니다...')
model = Sequential()
model.add(LSTM(128, input_shape=(maxlen, len(chars))))
model.add(Dense(len(chars)))
model.add(Activation('softmax'))
optimizer = RMSprop(lr=0.01)
model.compile(loss='categorical_crossentropy', optimizer=optimizer)

# 후보를 배열에서 꺼내기
def sample(preds, temperature=1.0):
    preds = np.asarray(preds).astype('float64')
    preds = np.log(preds) / temperature
    exp_preds = np.exp(preds)
    preds = exp_preds / np.sum(exp_preds)
    probas = np.random.multinomial(1, preds, 1)
    return np.argmax(probas)

# 학습시키고 텍스트 생성하기 반복
for iteration in range(1, 60):
    print()
    print('-' * 50)
    print('반복 =', iteration)
    model.fit(X, y, batch_size=128, nb_epoch=1) 

    # 임의의 시작 텍스트 선택하기
    start_index = random.randint(0, len(text) - maxlen - 1)

    # 다양한 다양성의 문장 생성
    for diversity in [0.2, 0.5, 1.0, 1.2]:
        print()
        print('--- 다양성 = ', diversity)
        generated = ''
        sentence = text[start_index: start_index + maxlen]
        generated += sentence
        print('--- 시드 = "' + sentence + '"')
        sys.stdout.write(generated)

        # 시드를 기반으로 텍스트 자동 생성
        for i in range(400):
            x = np.zeros((1, maxlen, len(chars)))
            for t, char in enumerate(sentence):
                x[0, t, char_indices[char]] = 1.

            # 다음에 올 문자를 예측하기
            preds = model.predict(x, verbose=0)[0]
            next_index = sample(preds, diversity)
            next_char = indices_char[next_index]

            # 출력하기
            generated += next_char
            sentence = sentence[1:] + next_char
            sys.stdout.write(next_char)
            sys.stdout.flush()

        print()